Developers

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.

Edit this page on GitHub