# AffiliateRail webhooks > Signed HTTP POST notifications for affiliate program events. This file is the machine-readable > catalogue: every event type, its payload example, and how to verify the signature. Merchant > webhooks are configured at Settings > Webhooks; partner postbacks in the partner portal at > Settings > Postbacks. Both use exactly the same request format and signature. ## Request POST to your URL with Content-Type: application/json and these headers: - Rail-Signature: t=,v1= - Rail-Event: the event type (same as body.type) - Rail-Event-Id: the event id (same as body.id; stable across retries and replays) - Rail-Delivery-Id: this delivery (a replay gets a new one) - Rail-Attempt: 1 to 8 - User-Agent: AffiliateRail-Webhooks/1.0 Body: {"id": "evt_...", "type": "", "created_at": "", "data": {...}} Test events sent from the dashboard have ids starting evt_test_. Conventions in data: prefixed string ids (part_, sale_, com_, pyt_, ...); money as integer minor units in *_minor fields with a currency field beside them (4999 = 49.99); ISO 8601 UTC times; partner_id on every event that belongs to a partner. ## Verify the signature signed = "" + "." + expected = hex(HMAC_SHA256(key = the whole secret string incl. whsec_ prefix, message = signed)) valid if expected equals any v1 value (constant-time compare) and |now - t| <= 300 seconds. Node: createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex") Python: hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() PHP: hash_hmac('sha256', $t . '.' . $rawBody, $secret) Secret rotation is immediate; update the handler first. Verifiers should accept several v1 values. ## Respond, retries, replay - Any 2xx within 10 seconds acknowledges. Anything else, a timeout, or a redirect is a failure. - Retry waits after a failure: 1m, 5m, 30m, 2h, 12h, 24h, 24h. After 8 attempts the delivery is marked failed and stays in the log. Test events are not retried. - The delivery log shows request body, response status and response body per delivery, and lets you replay one delivery, all failed since a time, or everything in a window up to 30 days. Replays reuse the original payload and event id. - Delivery is at least once: de-duplicate on id. Per endpoint, events are sent one at a time in emission order; retries move a failed event later. - Endpoint URLs must be public http(s). Private, loopback, link-local and internal addresses are refused at save time and at send time. ## Partner postbacks Partners receive only events with their own partner_id, only from the referral.*, commission.* and payout.* families (marked "Available to partner postbacks" below), with customer email removed. Same headers, same signature, same schedule. # Event catalogue ## 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" } }