Skip to content

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.

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:

  1. You subscribe to the lead/create topic and give us your URL.
  2. A new lead gets submitted in your portal.
  3. We POST a notification to your URL such as { id, topic, timestamp }.
  4. Your server verifies the request is genuinely from us using the signature header.
  5. Your server calls our REST API to fetch the full lead by id.
  6. 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.

You can subscribe to:

TopicWhat it means
lead/createA new lead was submitted
lead/updateAn existing lead was modified
lead/deleteA 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.

All webhook management calls are sent to:

https://api.partnerportal.io/1.0/webhooks/companies/YOUR_COMPANY_ID/subscriptions

Every request needs the same Authorization: Bearer YOUR_ACCESS_TOKEN header used by the REST API.

Terminal window
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:

  • id so you can delete the subscription later
  • signingSecret so you can verify incoming webhook deliveries

Treat the signingSecret like a password.

Terminal window
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.

Terminal window
curl -X DELETE "https://api.partnerportal.io/1.0/webhooks/companies/YOUR_COMPANY_ID/subscriptions/SUBSCRIPTION_ID" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

When a matching event happens, we POST to the URL you registered.

HeaderValue
Content-Typeapplication/json
User-Agentppio-webhooks/<version>
x-ppio-signature-v2Recommended signature header computed over the raw request body
x-ppio-signatureLegacy signature header being deprecated
{
"id": "69a212f5d084ce2b8ee77a59",
"topic": "lead/create",
"timestamp": "2026-02-27T21:56:05Z"
}
FieldMeaning
idThe PartnerPortal ID of the affected record
topicWhich event happened
timestampWhen 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.

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 update may 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

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-v2 is the recommended HMAC-SHA256 signature computed over the exact raw request body bytes
  • x-ppio-signature is a legacy signature over a canonicalized payload that is being deprecated

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 hmac
import hashlib
from 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 "", 200

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.

Once you receive a webhook and verify the signature, fetch the full lead through the REST API:

Terminal window
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.

  • 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.

1. Get API credentials -> Portal Setup -> Other -> Developer / API Keys
2. Subscribe to lead/create -> POST /1.0/webhooks/companies/{companyId}/subscriptions
3. PartnerPortal POSTs to your URL -> { id, topic, timestamp }
4. Your server verifies the signature -> using signingSecret
5. 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 system

That is the standard pattern for real-time lead sync into another system.

For credentials, custom field schemas, or webhook troubleshooting, contact [email protected].