Skip to main content

Affiliate Webhook API Reference

Build an AffiliateBase webhook receiver with signature verification, event payloads, manual delivery retry, and idempotent processing.

Table of Contents

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:

  1. Go to Settings -> Webhooks.
  2. Enter an Endpoint name and Endpoint URL. In production the URL must use HTTPS.
  3. Select the Events to subscribe to, then choose Create endpoint.
  4. Copy the signing secret shown after creation.
  5. 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.created if none) and a data object of test: true, endpoint_id, and account_id.

Each endpoint has its own signing secret and event subscriptions. Send test works only while the endpoint is active.

Event catalog

EventSubjectDescription
affiliate.createdAffiliateAn affiliate record was created
affiliate.approvedAffiliateAn affiliate moved to active state
affiliate.bannedAffiliateAn affiliate was banned. Sent once for each affiliate record of the person
affiliate.unbannedAffiliateA ban was lifted and the affiliate moved to disabled state. Sent once for each record
referral.createdReferralA referral was created
referral.convertedReferralA referral reached conversion state
commission.createdCommissionA commission was created
commission.dueCommissionA commission moved to due state
commission.voidedCommissionA commission was voided
payout.paidPayoutA payout was marked paid. Sent for each payment, so a payout that Stripe returned and that is paid again sends a new event
payout.failedPayoutStripe 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.

Subjectdata fields
Affiliateid, email, state, campaign_id, created_at
Referralid, state (the conversion state), affiliate_id, campaign_id, customer_id, created_at, became_lead_at, became_conversion_at
Commissionid, 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
Payoutid, 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_signature

Body:

JSON
{
  "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.email
  • sale.referral.customer.name
  • sale.referral.customer.first_name
  • sale.referral.customer.last_name
  • sale.referral.stripe_customer_id
  • sale.affiliate.id
  • sale.affiliate.email
  • sale.affiliate.first_name
  • sale.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_body

Use HMAC SHA-256 with your endpoint signing secret, then Base64 encode the digest.

Node.js example:

JavaScript
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:

Ruby
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)
end

Verify 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:

  1. Fix the receiver.
  2. 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.
  3. Click Enable.
  4. 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:

CodeMeaning
private_uriThe endpoint resolved to a private, loopback, link-local, or multicast address
invalid_uriThe endpoint URL could not be parsed
timeoutThe connection or read timed out
dns_errorDNS resolution failed
ssl_errorTLS verification or negotiation failed
http_<status>The endpoint returned a non-2xx HTTP status

Receiver best practices

  • Return 2xx only after accepting the event.
  • Enqueue slow work and respond quickly.
  • Store processed event IDs for idempotency.
  • Reject invalid signatures.
  • Reject stale timestamps (compare svix-timestamp with the current time) to reduce replay risk.
  • Log the event type and event ID, not the signing secret.
Esc