Outgoing webhooks and alerts
OpenPromoter webhooks: every outgoing event, the payload format, signature checks and retries with backoff. Plus Slack and Discord alerts.
OpenPromoter sends signed webhooks when things happen in your program. Add endpoints in Settings > API & webhooks and choose which events each one receives (or all of them).
Events
| Event | When |
|---|---|
partner.applied |
Someone applies to your program |
partner.approved |
A partner is approved by you, a rule or Autopilot |
referral.created |
A new referred signup or customer is recorded |
commission.created |
A commission is booked |
commission.reversed |
A commission is reversed by a refund or chargeback |
payout.paid |
A payout is sent |
fraud.flagged |
A fraud check flags a partner |
autopilot.action |
An Autopilot run finishes (with the number of suggestions created and applied) |
Payload
Every delivery is a POST with a JSON body:
{
"id": "whd_01HZX9…",
"type": "commission.created",
"created": 1793750400,
"data": { "amount": 1470, "currency": "usd", "status": "pending" }
}
commission.created carries id, kind, partnerId, amount, currency and status. A two-tier override has kind: "override" and also carries parentCommissionId (the sub-partner's commission) and subPartnerId. partner.applied and partner.approved carry recruitedBy, the id of the partner who recruited them or null.
Headers:
| Header | Value |
|---|---|
OpenPromoter-Signature |
t=<unix seconds>,v1=<hex signature> |
OpenPromoter-Event |
The event type |
OpenPromoter-Delivery |
The delivery id; use it to deduplicate |
User-Agent |
OpenPromoter-Webhooks/1 |
Verifying the signature
The signature is a hex-encoded HMAC-SHA256 of "<t>.<raw body>" with your endpoint secret:
OpenPromoter-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "t.body")>
Always verify against the raw request body before parsing JSON. With the SDK:
import { verifyWebhook } from "openpromoter";
const event = verifyWebhook(rawBody, req.headers.get("openpromoter-signature"), process.env.OPENPROMOTER_WEBHOOK_SECRET!);
// throws if the signature is wrong or older than 5 minutes
Without the SDK, in Node:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) throw new Error("Timestamp outside tolerance");
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1 ?? "", "hex");
if (a.length !== b.length || !timingSafeEqual(a, b)) throw new Error("Bad signature");
return JSON.parse(rawBody);
}
In Express, read the raw body with express.raw({ type: "application/json" }) on the webhook route. In a Next.js route handler, use await req.text().
Retries
Respond with any 2xx within 10 seconds. Any other status, a timeout or a redirect counts as a failure. Failures are retried with exponential backoff: 14 attempts in total, spread over about a day. Each delivery and its last response are listed in Settings > API & webhooks, where you can resend it.
Deliveries can arrive more than once or out of order. Deduplicate on OpenPromoter-Delivery and look up current state with the API when order matters.
Testing locally
Your laptop can't receive webhooks from Cloud. Use the CLI instead. It polls GET /api/v1/events and forwards each new event to a local URL with the same signature as a real delivery:
openpromoter listen --forward-to http://localhost:4000/webhooks/openpromoter --secret whsec_local
Verify with the same --secret in your handler. See CLI.
Event feed
GET /api/v1/events?after=<event id> returns the same payloads as webhooks with the oldest first. You can poll it instead of (or as well as) receiving webhooks.
Slack and Discord
Add a Slack or Discord incoming-webhook URL as an endpoint of that kind. OpenPromoter then posts a short message for each selected event, such as "New commission $14.70 (pending)" or "Payout sent: $240.00 via paypal". These messages are not signed.
Zapier and Make
Use a "Catch Hook" (Zapier) or "Custom webhook" (Make) URL as a regular endpoint.