AffiliateRail docs
API

Webhooks

Signed HTTP delivery for every event in your program, with verification, retries and partner postbacks.

AffiliateRail sends a signed HTTP POST to your endpoint when something happens in your program: a partner is approved, a referral converts, a commission is earned, a payout settles or fails. Partners get the same mechanism for their own events from the portal (postbacks). Everything below applies to both.

The machine-readable version of this page, with the same examples, is published at /webhooks/llms.txt on the app.

Setting up an endpoint

Merchant webhooks: Settings → Webhooks → Add endpoint. Tick the event types you want (nothing ticked means everything), and copy the signing secret when it is shown. It is shown exactly once; if you lose it, rotate it.

Partner postbacks: in the partner portal, Settings → Postbacks. Partners can subscribe to the referral, commission and payout families for their own activity only. The request, the signature and the retry schedule are identical to merchant webhooks.

Endpoint URLs must be public https:// (or http://) addresses. URLs that point at private, loopback, link-local or internal hostnames are refused when saved and again at send time, and redirects are never followed.

The request

POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: AffiliateRail-Webhooks/1.0
Rail-Signature: t=1756728000,v1=5f1c6c...e2a9
Rail-Event: commission.created
Rail-Event-Id: evt_4Kp2Qw9eRt7YuIoP1aSdFg
Rail-Delivery-Id: whd_7HgFdSaQwErTyUiOpLkJh
Rail-Attempt: 1

{"id":"evt_4Kp2Qw9eRt7YuIoP1aSdFg","type":"commission.created","created_at":"2026-09-01T12:00:00.000Z","data":{...}}
HeaderMeaning
Rail-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>. See Verifying the signature.
Rail-EventThe event type, same as type in the body. Handy for routing before you parse.
Rail-Event-IdThe event id, same as id in the body. The same event keeps the same id across retries and replays.
Rail-Delivery-IdThis particular delivery. A replay gets a new delivery id with the old event id.
Rail-Attempt1 for the first try, up to 8.

The body is always the same four keys:

KeyTypeMeaning
idstringEvent id, evt_.... Test events from the dashboard start with evt_test_.
typestringOne of the types in the catalogue.
created_atstringISO 8601, UTC, when the event happened.
dataobjectThe event's fields. Shapes are per type, below.

Conventions inside data: ids are prefixed strings (part_, sale_, com_, pyt_...); money is an integer in minor units in a *_minor field (4999 is 49.99) with a currency field beside it; times are ISO 8601 UTC; partner_id is on every event that belongs to a partner.

Verifying the signature

Every delivery is signed with the endpoint's secret. Verify before you trust anything in the body.

  1. Read the Rail-Signature header and split it on commas. Take the t value (a unix timestamp in seconds) and every v1 value (there is normally one).
  2. Build the signed string: the timestamp, a literal ., and the raw request body exactly as received. Do not re-serialise the JSON; parsers reorder keys.
  3. Compute HMAC-SHA256(secret, signed_string) and hex-encode it. The key is the whole secret as shown in the dashboard, whsec_ prefix included.
  4. Compare it to each v1 value with a constant-time comparison. Any match is valid.
  5. Reject if t is more than five minutes from your clock. That closes replay of a captured request.

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyRailSignature(secret, header, rawBody, toleranceSeconds = 300) {
  const parts = header.split(",").map((p) => p.trim());
  const t = parts.find((p) => p.startsWith("t="))?.slice(2);
  const sigs = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3));
  if (!t || !/^\d+$/.test(t) || sigs.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return sigs.some((s) => s.length === expected.length && timingSafeEqual(Buffer.from(s, "hex"), Buffer.from(expected, "hex")));
}

// Express: keep the raw body. express.json() would re-serialise it.
app.post("/webhooks/affiliaterail", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyRailSignature(process.env.RAIL_WEBHOOK_SECRET, req.get("Rail-Signature") ?? "", req.body.toString("utf8"))) {
    return res.status(400).send("bad signature");
  }
  const event = JSON.parse(req.body.toString("utf8"));
  // handle event.type / event.data, then answer quickly
  res.sendStatus(200);
});

Python

import hmac, hashlib, time

def verify_rail_signature(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    parts = [p.strip() for p in header.split(",")]
    t = next((p[2:] for p in parts if p.startswith("t=")), None)
    sigs = [p[3:] for p in parts if p.startswith("v1=")]
    if t is None or not t.isdigit() or not sigs:
        return False
    if abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in sigs)

# Flask
@app.post("/webhooks/affiliaterail")
def rail_webhook():
    if not verify_rail_signature(os.environ["RAIL_WEBHOOK_SECRET"], request.headers.get("Rail-Signature", ""), request.get_data()):
        abort(400)
    event = request.get_json()
    return "", 200

PHP

function verifyRailSignature(string $secret, string $header, string $rawBody, int $tolerance = 300): bool
{
    $t = null;
    $sigs = [];
    foreach (explode(',', $header) as $part) {
        $part = trim($part);
        if (str_starts_with($part, 't=')) { $t = substr($part, 2); }
        if (str_starts_with($part, 'v1=')) { $sigs[] = substr($part, 3); }
    }
    if ($t === null || !ctype_digit($t) || $sigs === []) { return false; }
    if (abs(time() - (int) $t) > $tolerance) { return false; }
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    foreach ($sigs as $sig) {
        if (hash_equals($expected, $sig)) { return true; }
    }
    return false;
}

$raw = file_get_contents('php://input');
if (!verifyRailSignature(getenv('RAIL_WEBHOOK_SECRET'), $_SERVER['HTTP_RAIL_SIGNATURE'] ?? '', $raw)) {
    http_response_code(400);
    exit;
}
$event = json_decode($raw, true);
http_response_code(200);

Rotating a secret

Rotating gives you a new secret and keeps the old one signing alongside it for 24 hours. During that day every delivery's Rail-Signature carries two v1 entries, the new secret's first; the verifier above accepts any one of them, so a handler still holding the old secret keeps working while you swap. After the day, deliveries carry one signature and the old secret verifies nothing. Rotating again inside the day replaces the old secret: at most two are ever live.

Responding, retries and failures

  • Answer with any 2xx within 10 seconds to acknowledge. Do the work after you respond if it is slow; a timeout counts as a failure.

  • Anything else (a 3xx, 4xx or 5xx, a timeout, a refused connection, a name that stops resolving) is a failed attempt. Redirects are not followed.

  • Failed attempts retry on this schedule, measured from the previous attempt:

    AttemptWaits
    21 minute
    35 minutes
    430 minutes
    52 hours
    612 hours
    724 hours
    824 hours

    After the eighth failure the delivery is marked failed and stays in the log. That is about 63 hours of retries, which covers a weekend.

  • The delivery log (Settings → Webhooks → Delivery log) shows every attempt's status code and response body, and the exact request body. Test events appear there too.

Replay

From the log you can replay one delivery, replay everything that failed since a point in time, or replay everything in a time window of up to 30 days. A replay creates a new delivery with the original payload and the original event id; the old entry stays in the log unchanged.

Delivery guarantees

  • At least once. A delivery is retried until you acknowledge it or the schedule runs out, and a replay can send it again on purpose. If a worker dies between your 200 and our bookkeeping, you may see the same event twice. De-duplicate on id.
  • In order, per endpoint, as far as the network allows. Events are emitted in a strict sequence (a sale.created always precedes the commission.created it produced) and each endpoint is delivered one request at a time in that sequence. Retries move a failed event later, so do not assume your endpoint saw everything before a given event; use created_at and the ids inside data to reconcile.
  • Events are written in the same database transaction as the change they describe. There is no event without the change and no change without the event.

Test events

Settings → Webhooks → Send a test sends one catalogue example, signed with the endpoint's secret, and shows you the status code your endpoint returned. The event id starts with evt_test_ so your handler can tell it apart. Test deliveries are not retried.

Partner postbacks

Partners can register their own URLs in the portal. They receive only events whose partner_id is theirs, only from the referral.*, commission.* and payout.* families, and never a customer's email address or other identity. Everything else, including the signature and the header names, is identical to merchant webhooks, so the same verifier works for both.

Event catalogue

Every event type, with the data shape it carries. Event names follow the vocabulary most widely used in affiliate software, so handlers you already have for another tool keep working when you point them here.

Partners

affiliate.created

A partner record exists: signed up through the portal, invited, created in the dashboard or imported.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "affiliate.created",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "email": "alice@example.com",
    "handle": "alice",
    "status": "pending",
    "group_id": null
  }
}

affiliate.updated

A partner's profile, group or status changed. status carries the new value and reason the note if one was given.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "affiliate.updated",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "status": "declined",
    "reason": "Outside our market"
  }
}

affiliate.confirmed

A partner was approved and is active. For programs with an application step this follows application.approved.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "affiliate.confirmed",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "email": "alice@example.com",
    "handle": "alice",
    "group_id": "grp_2PqRsTuVwXyZ0123456789"
  }
}

affiliate.deleted

A partner was deleted. Their links stop attributing and their open commissions are voided separately.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "affiliate.deleted",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A"
  }
}

Applications

application.submitted

A prospective partner answered your application questions. Review it in the dashboard or approve over the API.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "application.submitted",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "application_id": "app_4RfVbGtYhNmJuKiLoPqWe",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "email": "alice@example.com",
    "handle": "alice",
    "answers": {
      "website": "https://alice.example",
      "audience": "Indie SaaS founders"
    }
  }
}

application.approved

An application was approved. affiliate.confirmed follows in the same moment.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "application.approved",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "application_id": "app_4RfVbGtYhNmJuKiLoPqWe",
    "email": "alice@example.com",
    "handle": "alice",
    "reviewed_by": "usr_8UjMkIoLpNbVcXzAsDfGh",
    "reason": null
  }
}

application.rejected

An application was rejected, with the reason the reviewer gave.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "application.rejected",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "application_id": "app_4RfVbGtYhNmJuKiLoPqWe",
    "email": "alice@example.com",
    "handle": "alice",
    "reviewed_by": "usr_8UjMkIoLpNbVcXzAsDfGh",
    "reason": "No relevant audience"
  }
}

affiliate_link.created

A referral link was created for a partner, by them or by you.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "affiliate_link.created",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "link_id": "lnk_7HgFdSaQwErTyUiOpLkJh",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "url": "https://acme.com/?ref=alice",
    "param": "ref",
    "value": "alice",
    "destination_url": "https://acme.com/"
  }
}

affiliate_link.updated

A link's destination, label or short slug changed. Old links keep working; this is the new shape.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "affiliate_link.updated",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "link_id": "lnk_7HgFdSaQwErTyUiOpLkJh",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "url": "https://acme.com/pricing?ref=alice",
    "destination_url": "https://acme.com/pricing"
  }
}

affiliate_link.deleted

A link was deleted. Clicks on it no longer attribute.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "affiliate_link.deleted",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "link_id": "lnk_7HgFdSaQwErTyUiOpLkJh",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A"
  }
}

Coupons

affiliate_coupon.created

A coupon code was attached to a partner. The code itself is created in your payment processor and read by us.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "affiliate_coupon.created",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "coupon_id": "cpn_3KlMnOpQrStUvWxYz01234",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "code": "ALICE20",
    "active": true
  }
}

affiliate_coupon.updated

A coupon's partner or code mapping changed.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "affiliate_coupon.updated",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "coupon_id": "cpn_3KlMnOpQrStUvWxYz01234",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "code": "ALICE25",
    "active": true
  }
}

affiliate_coupon.activated

A coupon was switched on: purchases using it attribute to the partner again.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "affiliate_coupon.activated",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "coupon_id": "cpn_3KlMnOpQrStUvWxYz01234",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "code": "ALICE20",
    "active": true
  }
}

affiliate_coupon.deactivated

A coupon was switched off: purchases using it no longer attribute.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "affiliate_coupon.deactivated",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "coupon_id": "cpn_3KlMnOpQrStUvWxYz01234",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "code": "ALICE20",
    "active": false
  }
}

affiliate_coupon.deleted

A coupon mapping was removed.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "affiliate_coupon.deleted",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "coupon_id": "cpn_3KlMnOpQrStUvWxYz01234",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A"
  }
}

Referrals

referral.created

A new visitor arrived through a partner's link. A referral is a visit identity, not a person; repeat visits by the same browser attach to the existing referral and do not fire again. Available to partner postbacks.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "referral.created",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "referral_id": "ref_8QwErTyUiOpAsDfGhJkLz",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "link_id": "lnk_7HgFdSaQwErTyUiOpLkJh",
    "landing_url": "https://acme.com/?ref=alice",
    "tracked_by": "link",
    "expires_at": "2026-10-20T10:00:00.000Z"
  }
}

referral.lead

The visitor signed up. A customer record now exists against the referral. Partner postbacks receive this event without the email field. Available to partner postbacks.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "referral.lead",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "referral_id": "ref_8QwErTyUiOpAsDfGhJkLz",
    "customer_id": "cus_9aBcDeFgHiJkLmNoPqRsT",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "email": "buyer@example.com",
    "tracked_by": "link"
  }
}

referral.converted

The referred customer made their first payment. Fires once per referral, alongside the first sale.created. Available to partner postbacks.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "referral.converted",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "referral_id": "ref_8QwErTyUiOpAsDfGhJkLz",
    "customer_id": "cus_9aBcDeFgHiJkLmNoPqRsT",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "sale_id": "sale_5ZxCvBnMaSdFgHjKlQwEr"
  }
}

referral.deleted

A referral was deleted, usually because it was flagged as self-referral or fraud. Available to partner postbacks.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "referral.deleted",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "referral_id": "ref_8QwErTyUiOpAsDfGhJkLz",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "reason": "self_referral"
  }
}

Sales

sale.created

A charge attributed to a partner was recorded. amount_minor is the charge in integer minor units (4999 = 49.99); is_first_sale distinguishes a new customer from a renewal.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "sale.created",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "sale_id": "sale_5ZxCvBnMaSdFgHjKlQwEr",
    "customer_id": "cus_9aBcDeFgHiJkLmNoPqRsT",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "amount_minor": 4999,
    "currency": "USD",
    "is_first_sale": true,
    "external_charge_id": "ch_3PqRsTuVwXyZ",
    "occurred_at": "2026-09-01T00:00:00.000Z"
  }
}

sale.updated

A sale's amount or attribution was corrected after the fact. Commissions are recalculated and emit their own events.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "sale.updated",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "sale_id": "sale_5ZxCvBnMaSdFgHjKlQwEr",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "amount_minor": 3999,
    "currency": "USD",
    "previous_amount_minor": 4999
  }
}

sale.refunded

The charge was refunded. Unpaid commissions on it are voided (commission.voided); an already-paid one gets a negative clawback commission.created instead.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "sale.refunded",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "sale_id": "sale_5ZxCvBnMaSdFgHjKlQwEr",
    "customer_id": "cus_9aBcDeFgHiJkLmNoPqRsT",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "amount_minor": 4999,
    "currency": "USD",
    "refunded_at": "2026-09-03T00:00:00.000Z"
  }
}

sale.deleted

A sale was deleted outright, for instance when an import is rolled back.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "sale.deleted",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "sale_id": "sale_5ZxCvBnMaSdFgHjKlQwEr",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A"
  }
}

Commissions

commission.created

A commission was earned. status starts at pending (inside the holding period) or due; mature_at is when it becomes payable. A clawback after a refund of a paid commission arrives here with kind: "clawback" and a negative amount. Available to partner postbacks.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "commission.created",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "commission_id": "com_6AsDfGhJkLzXcVbNmQwEr",
    "sale_id": "sale_5ZxCvBnMaSdFgHjKlQwEr",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "amount_minor": 1000,
    "currency": "USD",
    "status": "pending",
    "flow_id": "flw_1QaZxSwEdCvFrTgBnHyUj",
    "mature_at": "2026-09-15T00:00:00.000Z"
  }
}

commission.updated

A commission changed status or amount: it matured to due, was approved or rejected manually, or was edited. Available to partner postbacks.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "commission.updated",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "commission_id": "com_6AsDfGhJkLzXcVbNmQwEr",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "amount_minor": 1000,
    "currency": "USD",
    "status": "due",
    "previous_status": "approved"
  }
}

commission.paid

The commission left in a payout that settled. payout_id links it to the batch. Available to partner postbacks.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "commission.paid",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "commission_id": "com_6AsDfGhJkLzXcVbNmQwEr",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "payout_id": "pyt_2WsXcDeRfVtGbYhNuJmIk",
    "amount_minor": 1000,
    "currency": "USD",
    "paid_at": "2026-10-15T09:00:00.000Z"
  }
}

commission.voided

An unpaid commission was cancelled, almost always because its sale was refunded. reason says why. Available to partner postbacks.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "commission.voided",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "commission_id": "com_6AsDfGhJkLzXcVbNmQwEr",
    "sale_id": "sale_5ZxCvBnMaSdFgHjKlQwEr",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "amount_minor": 1000,
    "currency": "USD",
    "reason": "sale_refunded"
  }
}

commission.deleted

A commission was deleted, for instance when an import is rolled back. Available to partner postbacks.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "commission.deleted",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "commission_id": "com_6AsDfGhJkLzXcVbNmQwEr",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A"
  }
}

Payouts

payout.created

A payout for a partner was generated in a batch. Nothing has been sent yet. Available to partner postbacks.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "payout.created",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "payout_id": "pyt_2WsXcDeRfVtGbYhNuJmIk",
    "batch_id": "batch_9OkMijNuhBygVtfCrdXes",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "amount_minor": 12500,
    "currency": "USD",
    "status": "pending",
    "due_at": "2026-10-15T00:00:00.000Z"
  }
}

payout.updated

A payout changed status outside the paid and failed cases, for instance when it was picked up for processing. Available to partner postbacks.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "payout.updated",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "payout_id": "pyt_2WsXcDeRfVtGbYhNuJmIk",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "status": "processing",
    "previous_status": "pending"
  }
}

payout.due

Money is owed right now. This is the event to notify on if you pay by hand: the batch is ready and above the minimum. Available to partner postbacks.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "payout.due",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "payout_id": "pyt_2WsXcDeRfVtGbYhNuJmIk",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "amount_minor": 12500,
    "currency": "USD",
    "due_at": "2026-10-15T00:00:00.000Z"
  }
}

payout.paid

The rail confirmed settlement, or you marked the payout paid by hand. Only a terminal status from the rail counts; a 200 on submit never does. Available to partner postbacks.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "payout.paid",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "payout_id": "pyt_2WsXcDeRfVtGbYhNuJmIk",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "amount_minor": 12500,
    "currency": "USD",
    "method": "paypal",
    "external_id": "PAYOUT-ITEM-ID",
    "paid_at": "2026-10-15T09:00:00.000Z"
  }
}

payout.failed

The rail rejected or returned the payout. Rails fail asynchronously and often; treat this as first-class and act on error_code. Available to partner postbacks.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "payout.failed",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "payout_id": "pyt_2WsXcDeRfVtGbYhNuJmIk",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "amount_minor": 12500,
    "currency": "USD",
    "method": "paypal",
    "error_code": "RECEIVER_UNREGISTERED",
    "error_message": "Receiver is unregistered"
  }
}

payout.not_eligible

A partner had money due but could not be paid: no payout method, a missing tax form, or a balance under the minimum. reason is machine-readable; nothing is silently dropped. Available to partner postbacks.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "payout.not_eligible",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "payout_id": "pyt_2WsXcDeRfVtGbYhNuJmIk",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "amount_minor": 12500,
    "currency": "USD",
    "reason": "no_payout_method",
    "reason_text": "No payout method on file"
  }
}

payout.deleted

A pending payout was deleted; its commissions returned to due for the next batch. Available to partner postbacks.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "payout.deleted",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "payout_id": "pyt_2WsXcDeRfVtGbYhNuJmIk",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A"
  }
}

Risk

risk_flag.created

A monitoring rule raised a flag on a partner or customer. evidence is the rule's working; a human resolves the flag in the dashboard.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "risk_flag.created",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "risk_flag_id": "rsk_5TgBnHyUjMkIoLpQaZwSx",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "customer_id": "cus_9aBcDeFgHiJkLmNoPqRsT",
    "kind": "same_ip_cluster",
    "severity": "high",
    "evidence": {
      "ip_hash": "a1b2c3",
      "signups_last_24h": 14
    }
  }
}

risk_flag.resolved

A person closed a risk flag. status says which way it went, and note is their own words about what they found, which is required before a flag can be closed.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "risk_flag.resolved",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "risk_flag_id": "rsk_5TgBnHyUjMkIoLpQaZwSx",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "customer_id": "cus_9aBcDeFgHiJkLmNoPqRsT",
    "kind": "same_ip_cluster",
    "severity": "high",
    "status": "resolved",
    "note": "Same office, confirmed with the partner",
    "resolved_by": "usr_6HjKlMnOpQrStUvWxYz012"
  }
}

Customers

customer.enrolled_as_partner

A paying customer became a partner on their first sale, because the program's customer auto-enrol switch is on. partner_id is the new partner; customer_id the buyer it was made from. An affiliate.created for the same partner precedes it.

{
  "id": "evt_4Kp2Qw9eRt7YuIoP1aSdFg",
  "type": "customer.enrolled_as_partner",
  "created_at": "2026-09-01T12:00:00.000Z",
  "data": {
    "customer_id": "cus_9aBcDeFgHiJkLmNoPqRsT",
    "partner_id": "part_4Jk2mN8pQrS7tUvWxYz01A",
    "handle": "alice",
    "email": "alice@example.com",
    "group_id": "grp_2PqRsTuVwXyZ0123456789",
    "source": "customer"
  }
}