Skip to content

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

EventFires when
attribution.createdA conversion event is attributed to a partner (post-stitching).
commission.accruedA Commission row enters accrued state. Most-used integration trigger.
commission.approvedA commission is approved (manually or via auto-approve after holdback elapses).
commission.paidA commission is paid out via the payout runner.
commission.reversedA commission is reversed (refund / chargeback / fraud).
partner.createdA partner row is inserted (admin invite, self-signup, or Network federation).
partner.activatedAn invited partner consumes their magic link.
partnership.approvedA Network creator’s PartnershipRequest is approved + the local Partner row is provisioned via federation.
payout.createdA 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>/webhooks
Authorization: Bearer <key>
Content-Type: application/json
{
"url": "https://your-server.example.com/openpartner-webhook",
"events": ["commission.accrued", "payout.created"],
"label": "Production Slack notifier"
}
FieldNotes
urlHTTPS subscriber URL.
eventsArray of event types from the table above. Use ["*"] to receive everything.
labelOptional 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 endpoints
  • GET /webhooks/:id — single endpoint detail
  • PATCH /webhooks/:id — update url, events, label, active flag
  • DELETE /webhooks/:id — soft-delete (sets active=false); ?hard=1 for hard delete

Test fire

Two modes:

POST <base>/webhooks/:id/test
Authorization: Bearer <key>

Fires a generic webhook.test envelope to verify URL reachability + signature parsing.

POST <base>/webhooks/:id/test?event=commission.accrued
Authorization: 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.accrued
x-openpartner-delivery: evt_01HXX...
x-openpartner-timestamp: 1714123200
x-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 WebhookDelivery log 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...",
"email": "[email protected]",
"name": "Alice",
"activatedAt": null,
"invited": true,
"campaignIds": ["cmp_default"]
}

partner.activated

{
"partnerId": "01HXX...",
"email": "[email protected]",
"name": "Alice",
"activatedAt": "2026-04-24T12:05:00Z"
}

partnership.approved

{
"partnerId": "01HXX...",
"email": "[email protected]",
"name": "Alice",
"networkCreatorId": "crt_aaaaaaaaaaaaaaaaaaaa",
"campaignIds": ["cmp_default"]
}

payout.created

{
"payoutId": "01HXX...",
"partnerId": "01HXX...",
"amount": "120.00",
"currency": "USD",
"commissionCount": 10
}