Outbound webhooks send signed HTTP POST requests from AffiliateBase to your application when affiliate, referral, commission, or payout activity changes. This reference is for developers building a webhook receiver.
Use webhooks for event-driven updates. Use the REST API to query current state or backfill historical data.
Endpoint setup
Manage webhook endpoints in the AffiliateBase app:
- Go to Settings -> Webhooks.
- Enter an Endpoint name and Endpoint URL. In production the URL must use HTTPS.
- Select the Events to subscribe to, then choose Create endpoint.
- Copy the signing secret shown after creation.
- Open the endpoint and choose Send test before relying on production events. The test delivery uses the endpoint’s first subscribed event type (or
affiliate.createdif none) and adataobject oftest: true,endpoint_id, andaccount_id.
Each endpoint has its own signing secret and event subscriptions. Send test works only while the endpoint is active.
Event catalog
| Event | Subject | Description |
|---|---|---|
affiliate.created | Affiliate | An affiliate record was created |
affiliate.approved | Affiliate | An affiliate moved to active state |
affiliate.banned | Affiliate | An affiliate was banned. Sent once for each affiliate record of the person |
affiliate.unbanned | Affiliate | A ban was lifted and the affiliate moved to disabled state. Sent once for each record |
referral.created | Referral | A referral was created |
referral.converted | Referral | A referral reached conversion state |
commission.created | Commission | A commission was created |
commission.due | Commission | A commission moved to due state |
commission.voided | Commission | A commission was voided |
payout.paid | Payout | A payout was marked paid. Sent for each payment, so a payout that Stripe returned and that is paid again sends a new event |
payout.failed | Payout | Stripe failed, canceled, returned, or fully reversed a payout. The payout is due again and can be retried |
payout.failed includes the payout fields (id, state, amount_cents, currency, affiliate_id, paid_at, created_at) plus provider (stripe), provider_attempt_id, provider_reference (the Stripe payment ID, the Stripe transfer ID tr_... for Stripe payouts, or null if Stripe rejected the request), failure_status (failed, canceled, or returned, or for Stripe payouts also reversed or not_found), failure_code, failure_reason, failed_at, and previously_paid. state is due. previously_paid is true when Stripe returned a payout after it was marked paid. AffiliateBase sends one event for each failed attempt, so a retry that fails again sends a new event. PayPal and Wise payouts do not send this event.
AffiliateBase creates one delivery per subscribed active endpoint for each event.
Event payload fields
The data object contains these fields for each subject. Empty commission fields are omitted.
| Subject | data fields |
|---|---|
| Affiliate | id, email, state, campaign_id, created_at |
| Referral | id, state (the conversion state), affiliate_id, campaign_id, customer_id, created_at, became_lead_at, became_conversion_at |
| Commission | id, state, amount_cents, currency, affiliate_id, campaign_id, sale_id, sale_customer_email, sale_customer_name, sale_customer_first_name, sale_customer_last_name, sale (with id and customer), due_at, paid_at, voided_at, created_at |
| Payout | id, state, amount_cents, currency, affiliate_id, paid_at, created_at |
Request
AffiliateBase sends a POST request to your endpoint URL.
Headers:
Content-Type: application/json
User-Agent: AffiliateBase-Webhooks/1.0
svix-id: webhook_delivery_id
svix-timestamp: unix_timestamp
svix-signature: v1,base64_hmac_sha256_signatureBody:
{
"id": "webhook_event_id",
"type": "affiliate.created",
"occurred_at": "2026-05-29T14:21:12Z",
"data": {
"id": "affiliate_id",
"email": "partner@example.com",
"state": "active",
"campaign_id": "campaign_id",
"created_at": "2026-05-29T14:21:12Z"
}
}The body id identifies the outbound event and stays the same across retries of that event. Store it and process it idempotently. The svix-id header identifies the delivery, so a manual retry of the same delivery keeps the same svix-id.
affiliate.banned and affiliate.unbanned add ban_reason (fraud, fake_traffic, self_referral, tos_violation, brand_abuse, spam, or other) and banned_at to the affiliate fields above. For affiliate.unbanned they describe the ban that was lifted. The merchant’s ban note is never included. A preview (dry_run) or a repeated ban sends nothing. Lifting a ban never sends affiliate.approved, because the affiliate moves to disabled, not active.
Webhook plus API enrichment
Webhook events contain the IDs you need to look up full resource details. Some events also include common CRM fields. For example, commission.created includes sale_customer_email, sale_customer_name, sale_customer_first_name, and sale_customer_last_name when AffiliateBase has received them from Stripe or the conversion flow.
Use the webhook as the real-time trigger. Call the REST API when you need current state or related records.
For commission.created, the commission ID is data.id. To get the related sale, referral, customer, and affiliate details:
GET https://app.affiliatebase.io/api/v1/commissions/{commission_id}?account_id={account_id}
Authorization: Bearer {api_key}GET /api/v1/commissions/:id returns the commission object directly. It expands the campaign and the sale; the sale includes its customer, referral, and affiliate. Common fields for CRM enrichment include:
sale.referral.customer.emailsale.referral.customer.namesale.referral.customer.first_namesale.referral.customer.last_namesale.referral.stripe_customer_idsale.affiliate.idsale.affiliate.emailsale.affiliate.first_namesale.affiliate.last_name
Customer email and name are returned when AffiliateBase has received them from Stripe or the conversion flow. Phone is not currently a standard AffiliateBase customer field.
Signature verification
The svix-signature header is computed from:
svix-id.svix-timestamp.raw_request_bodyUse HMAC SHA-256 with your endpoint signing secret, then Base64 encode the digest.
Node.js example:
import crypto from "node:crypto";
export function verifyAffiliateBaseWebhook(req, rawBody, signingSecret) {
const messageId = req.headers["svix-id"];
const timestamp = req.headers["svix-timestamp"];
const signatureHeader = req.headers["svix-signature"] || "";
const expected = crypto
.createHmac("sha256", signingSecret)
.update(`${messageId}.${timestamp}.${rawBody}`)
.digest("base64");
const received = signatureHeader.replace(/^v1,/, "");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}Ruby example:
require "base64"
require "openssl"
def valid_affiliatebase_webhook?(headers:, raw_body:, signing_secret:)
message_id = headers.fetch("svix-id")
timestamp = headers.fetch("svix-timestamp")
signature = headers.fetch("svix-signature").delete_prefix("v1,")
signed_content = "#{message_id}.#{timestamp}.#{raw_body}"
expected = Base64.strict_encode64(
OpenSSL::HMAC.digest("SHA256", signing_secret, signed_content)
)
ActiveSupport::SecurityUtils.secure_compare(expected, signature)
endVerify against the raw request body, not parsed JSON.
Delivery behavior
A delivery is successful when your endpoint returns a 2xx status code.
AffiliateBase records:
- request headers
- request body
- response status
- response headers
- response body excerpt
- error code, if the request failed before a response
- attempt timing
Each attempt allows 10 seconds to connect and 10 seconds for a response. AffiliateBase does not retry failed deliveries automatically. You can retry a failed delivery from Settings -> Webhooks.
After 10 consecutive failed deliveries, AffiliateBase disables the endpoint and stops sending it events. A successful delivery resets the count. Enable does not reset the failure count, and Send test only delivers to active endpoints, so recover in this order:
- Fix the receiver.
- Open the endpoint in Settings -> Webhooks and click Retry on one failed delivery. Retry works while the endpoint is disabled, and a successful delivery resets the failure count.
- Click Enable.
- Retry any other failed deliveries you still need.
Events that happen while the endpoint is disabled are not recorded for it, so they can’t be retried later. After you re-enable the endpoint, use the REST API to backfill anything that changed while it was disabled.
Delivery error codes
The delivery log shows one of these codes when a delivery fails:
| Code | Meaning |
|---|---|
private_uri | The endpoint resolved to a private, loopback, link-local, or multicast address |
invalid_uri | The endpoint URL could not be parsed |
timeout | The connection or read timed out |
dns_error | DNS resolution failed |
ssl_error | TLS verification or negotiation failed |
http_<status> | The endpoint returned a non-2xx HTTP status |
Receiver best practices
- Return
2xxonly after accepting the event. - Enqueue slow work and respond quickly.
- Store processed event IDs for idempotency.
- Reject invalid signatures.
- Reject stale timestamps (compare
svix-timestampwith the current time) to reduce replay risk. - Log the event type and event ID, not the signing secret.