Webhooks Guide
Webhooks let your application get notified in real time when something changes in your PartnerPortal portal.
If you are new to the API, start with Developer Documentation for the overview and the Public API Guide for credentials and authentication.
What webhooks do
Section titled “What webhooks do”Instead of polling our API every few minutes asking whether anything changed, you give us a URL and we send a small POST to it whenever a relevant event happens.
A typical setup looks like this:
- You subscribe to the
lead/createtopic and give us your URL. - A new lead gets submitted in your portal.
- We POST a notification to your URL such as
{ id, topic, timestamp }. - Your server verifies the request is genuinely from us using the signature header.
- Your server calls our REST API to fetch the full lead by
id. - Your server saves the lead in your CRM, commissions system, or another downstream tool.
You will need API credentials before you can subscribe to webhooks. If your plan includes API access, a portal admin can generate a key under Portal Setup → Other → Developer / API Keys.
Available topics
Section titled “Available topics”You can subscribe to:
| Topic | What it means |
|---|---|
lead/create | A new lead was submitted |
lead/update | An existing lead was modified |
lead/delete | A lead was deleted |
Payment topics are not active today. Contact
[email protected]if you need payment notifications and we will let you know when they are available.
Subscribing
Section titled “Subscribing”All webhook management calls are sent to:
https://api.partnerportal.io/1.0/webhooks/companies/YOUR_COMPANY_ID/subscriptionsEvery request needs the same Authorization: Bearer YOUR_ACCESS_TOKEN header used by the REST API.
Create a subscription
Section titled “Create a subscription”curl -X POST "https://api.partnerportal.io/1.0/webhooks/companies/YOUR_COMPANY_ID/subscriptions" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "topic": "lead/create", "url": "https://your-server.com/webhooks/ppio" }'You will get back a response like:
{ "status": 200, "data": { "id": "abc123...", "topic": "lead/create", "url": "https://your-server.com/webhooks/ppio", "signingSecret": "whseecret_def456..." }}Save two things from that response:
idso you can delete the subscription latersigningSecretso you can verify incoming webhook deliveries
Treat the signingSecret like a password.
List your subscriptions
Section titled “List your subscriptions”curl "https://api.partnerportal.io/1.0/webhooks/companies/YOUR_COMPANY_ID/subscriptions" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"This lists the subscriptions created by the API key you are calling with.
Delete a subscription
Section titled “Delete a subscription”curl -X DELETE "https://api.partnerportal.io/1.0/webhooks/companies/YOUR_COMPANY_ID/subscriptions/SUBSCRIPTION_ID" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"What we send to your URL
Section titled “What we send to your URL”When a matching event happens, we POST to the URL you registered.
Headers
Section titled “Headers”| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | ppio-webhooks/<version> |
x-ppio-signature-v2 | Recommended signature header computed over the raw request body |
x-ppio-signature | Legacy signature header being deprecated |
{ "id": "69a212f5d084ce2b8ee77a59", "topic": "lead/create", "timestamp": "2026-02-27T21:56:05Z"}| Field | Meaning |
|---|---|
id | The PartnerPortal ID of the affected record |
topic | Which event happened |
timestamp | When it happened, in UTC |
The body is intentionally small. It tells you what changed, not the full record. Your next step is to call the REST API to fetch the record itself.
Acknowledge the delivery
Section titled “Acknowledge the delivery”Return HTTP 200 from your endpoint to acknowledge receipt. If we get any non-200 response, or no response at all, we retry automatically.
A few things to know:
- At least once delivery: you may receive the same event twice
- No guaranteed ordering: an
updatemay arrive before or after another event in ways you do not expect - No replay: if your endpoint is down for an extended period, you should recover by listing leads from the REST API
Verify that a webhook came from us
Section titled “Verify that a webhook came from us”Anyone can POST to your webhook URL. To make sure a request actually came from PartnerPortal, verify the signature using your subscription’s signingSecret.
We currently send two signature headers:
x-ppio-signature-v2is the recommended HMAC-SHA256 signature computed over the exact raw request body bytesx-ppio-signatureis a legacy signature over a canonicalized payload that is being deprecated
Recommended: verify x-ppio-signature-v2
Section titled “Recommended: verify x-ppio-signature-v2”The key requirement is to compute the HMAC over the exact raw bytes you received before any JSON parsing or re-serialization.
Node.js
const crypto = require('crypto');const express = require('express');
const app = express();app.use('/webhooks/ppio', express.raw({ type: 'application/json' }));
function verifyPpioSignature(rawBody, signingSecret, signatureHeader) { const expected = crypto.createHmac('sha256', signingSecret).update(rawBody).digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));}
app.post('/webhooks/ppio', (req, res) => { const ok = verifyPpioSignature(req.body, process.env.PPIO_SIGNING_SECRET, req.get('x-ppio-signature-v2')); if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8')); res.sendStatus(200);});Python (Flask)
import hmacimport hashlibfrom flask import request, abort
def verify_ppio_signature(raw_body: bytes, signing_secret: str, signature_header: str) -> bool: expected = hmac.new( signing_secret.encode("utf-8"), raw_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected, signature_header or "")
@app.post("/webhooks/ppio")def ppio_webhook(): if not verify_ppio_signature( request.get_data(), PPIO_SIGNING_SECRET, request.headers.get("x-ppio-signature-v2"), ): abort(401) return "", 200Legacy: verify x-ppio-signature
Section titled “Legacy: verify x-ppio-signature”Only use this if you are still on the legacy header. It signs a canonicalized JSON payload with the keys id, topic, and timestamp sorted alphabetically.
const crypto = require('crypto');
function verifyPpioSignatureLegacy(body, signingSecret, signatureHeader) { const signed = { id: body.id, topic: body.topic, timestamp: body.timestamp }; const ordered = Object.keys(signed) .sort() .reduce((acc, k) => ((acc[k] = signed[k]), acc), {}); const expected = crypto.createHmac('sha256', signingSecret).update(JSON.stringify(ordered), 'utf8').digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));}Do not rely on IP allowlisting alone. Always verify the signature.
Fetch the full record
Section titled “Fetch the full record”Once you receive a webhook and verify the signature, fetch the full lead through the REST API:
curl "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/leads/LEAD_ID" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"The response includes all standard lead fields plus any custom fields you configured in your portal.
Things to be aware of
Section titled “Things to be aware of”- Subscriptions are owned by the API key that created them
- Each subscription is for one topic and one URL
- There is no event filtering on our end
- Webhook receivers must be HTTPS
If you revoke the key that created a subscription, its subscriptions are removed automatically and should be recreated with the replacement key.
A typical integration end to end
Section titled “A typical integration end to end”1. Get API credentials -> Portal Setup -> Other -> Developer / API Keys2. Subscribe to lead/create -> POST /1.0/webhooks/companies/{companyId}/subscriptions3. PartnerPortal POSTs to your URL -> { id, topic, timestamp }4. Your server verifies the signature -> using signingSecret5. Your server fetches the full lead -> GET /1.0/api/company/.../leads/{id}6. Your server saves the lead -> in your CRM or another downstream systemThat is the standard pattern for real-time lead sync into another system.
Need help?
Section titled “Need help?”For credentials, custom field schemas, or webhook troubleshooting, contact [email protected].