Webhooks
OpenPartner posts JSON envelopes to subscriber URLs whenever named events fire. Used by the official Zapier and ActivePieces integrations and by any custom endpoint you wire up.
Event types
| Event | Fires when |
|---|---|
attribution.created | A conversion event is attributed to a partner (post-stitching). |
commission.accrued | A Commission row enters accrued state. Most-used integration trigger. |
commission.approved | A commission is approved (manually or via auto-approve after holdback elapses). |
commission.paid | A commission is paid out via the payout runner. |
commission.reversed | A commission is reversed (refund / chargeback / fraud). |
partner.created | A partner row is inserted (admin invite, self-signup, or Network federation). |
partner.activated | An invited partner consumes their magic link. |
partnership.approved | A Network creator’s PartnershipRequest is approved + the local Partner row is provisioned via federation. |
payout.created | A payout batch is created (per-partner, once per payout run). |
Plus the wildcard * for endpoint subscriptions only — subscribers using * receive every
event.
Envelope
Every delivery has the same outer shape:
{ "id": "evt_01HXX...", "event": "commission.accrued", "created": "2026-04-24T12:00:00.000Z", "data": { ... }}The data field’s shape is event-specific — see payload reference
below.
Subscribe
POST /webhooks — registers a new endpoint.
Auth: Admin API key.
POST <base>/webhooksAuthorization: Bearer <key>Content-Type: application/json
{ "url": "https://your-server.example.com/openpartner-webhook", "events": ["commission.accrued", "payout.created"], "label": "Production Slack notifier"}| Field | Notes |
|---|---|
url | HTTPS subscriber URL. |
events | Array of event types from the table above. Use ["*"] to receive everything. |
label | Optional human-readable label. |
Response
{ "endpoint": { "id": "01HXX...", "url": "...", "events": ["commission.accrued", "payout.created"], "active": true, "label": "Production Slack notifier", "createdAt": "2026-04-24T12:00:00Z" }, "secret": "whsec_xxx"}The secret is shown once — you’ll use it to verify HMAC signatures. Store it
immediately; it’s never returned again.
Manage
GET /webhooks— list endpointsGET /webhooks/:id— single endpoint detailPATCH /webhooks/:id— update url, events, label, active flagDELETE /webhooks/:id— soft-delete (setsactive=false);?hard=1for hard delete
Test fire
Two modes:
POST <base>/webhooks/:id/testAuthorization: Bearer <key>Fires a generic webhook.test envelope to verify URL reachability + signature parsing.
POST <base>/webhooks/:id/test?event=commission.accruedAuthorization: Bearer <key>Fires a synthetic event of the chosen type with a realistic-looking sample payload. Useful for end-to-end testing Zapier triggers / ActivePieces pieces / custom integrations without seeding fake conversion data. Bypasses subscription filtering — the synthetic event goes to the chosen endpoint regardless of whether it subscribes.
Returns the WebhookDelivery row inline so failures (HTTP 4xx/5xx, timeout, signature
errors) surface immediately rather than dropping into the delivery log.
Signature verification
Every delivery carries four headers:
x-openpartner-event: commission.accruedx-openpartner-delivery: evt_01HXX...x-openpartner-timestamp: 1714123200x-openpartner-signature: <hex>Verify with HMAC-SHA256 over ${timestamp}.${rawBody}:
const crypto = require('crypto');
function verify(req, secret) { const timestamp = req.header('x-openpartner-timestamp'); const signature = req.header('x-openpartner-signature'); const body = req.rawBody; // your framework's raw body buffer
// Replay protection: reject deliveries older than 5 minutes. if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto .createHmac('sha256', secret) .update(`${timestamp}.${body}`) .digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));}Reject deliveries where the signature doesn’t match OR the timestamp is more than 5 minutes off your clock (replay protection).
Retry behavior
- 10-second timeout per delivery.
- Failed deliveries are recorded in the
WebhookDeliverylog but not auto-retried. - Manual retry via
POST /webhooks/:id/deliveries/:deliveryId/retry(or the Retry button on the Webhooks admin page). - 4xx responses from your subscriber count as failures — return 2xx for “received.”
Auto-retry with backoff is on the roadmap. In the meantime, integration platforms like Zapier and ActivePieces handle their own retries on their side.
Payload reference
Each event’s data shape:
attribution.created
{ "attributionId": "01HXX...", "eventId": "01HXX...", "partnerId": "01HXX...", "campaignId": "cmp_default", "clickId": "01HXX...", "model": "last_click", "weight": 1, "eventType": "invoice_paid", "eventValue": 60.0, "commissionId": "01HXX...", "commissionAmount": "12.00", "commissionCurrency": "USD"}commission.accrued
{ "commissionId": "01HXX...", "partnerId": "01HXX...", "attributionId": "01HXX...", "campaignId": "cmp_default", "amount": "12.00", "currency": "USD", "eventType": "invoice_paid", "eventValue": 60.0}commission.approved
{ "commissionId": "01HXX...", "partnerId": "01HXX...", "amount": "12.00", "currency": "USD", "approvedAt": "2026-04-24T12:00:00Z"}commission.paid
{ "commissionId": "01HXX...", "payoutId": "01HXX...", "partnerId": "01HXX...", "amount": "12.00", "currency": "USD"}commission.reversed
{ "commissionId": "01HXX...", "partnerId": "01HXX...", "amount": "12.00", "currency": "USD", "reason": "refund"}partner.created
{ "partnerId": "01HXX...", "name": "Alice", "activatedAt": null, "invited": true, "campaignIds": ["cmp_default"]}partner.activated
{ "partnerId": "01HXX...", "name": "Alice", "activatedAt": "2026-04-24T12:05:00Z"}partnership.approved
{ "partnerId": "01HXX...", "name": "Alice", "networkCreatorId": "crt_aaaaaaaaaaaaaaaaaaaa", "campaignIds": ["cmp_default"]}payout.created
{ "payoutId": "01HXX...", "partnerId": "01HXX...", "amount": "120.00", "currency": "USD", "commissionCount": 10}