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":{...}}| Header | Meaning |
|---|---|
Rail-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256>. See Verifying the signature. |
Rail-Event | The event type, same as type in the body. Handy for routing before you parse. |
Rail-Event-Id | The event id, same as id in the body. The same event keeps the same id across retries and replays. |
Rail-Delivery-Id | This particular delivery. A replay gets a new delivery id with the old event id. |
Rail-Attempt | 1 for the first try, up to 8. |
The body is always the same four keys:
| Key | Type | Meaning |
|---|---|---|
id | string | Event id, evt_.... Test events from the dashboard start with evt_test_. |
type | string | One of the types in the catalogue. |
created_at | string | ISO 8601, UTC, when the event happened. |
data | object | The 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.
- Read the
Rail-Signatureheader and split it on commas. Take thetvalue (a unix timestamp in seconds) and everyv1value (there is normally one). - 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. - Compute
HMAC-SHA256(secret, signed_string)and hex-encode it. The key is the whole secret as shown in the dashboard,whsec_prefix included. - Compare it to each
v1value with a constant-time comparison. Any match is valid. - Reject if
tis 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 "", 200PHP
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:
Attempt Waits 2 1 minute 3 5 minutes 4 30 minutes 5 2 hours 6 12 hours 7 24 hours 8 24 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.createdalways precedes thecommission.createdit 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; usecreated_atand the ids insidedatato 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"
}
}Links
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"
}
}