Outbound Webhooks
Outbound webhooks send signed AffiliateBase events to your application when affiliate, referral, commission, or payout activity changes.
Use webhooks when you need event-driven updates. Use the REST API when you need to query current state or backfill historical data.
Endpoint setup
Webhook endpoints are managed in the AffiliateBase app:
- Go to Settings -> Webhooks.
- Create an endpoint with a name and destination URL.
- Select event types.
- Copy the signing secret shown after creation.
- Send a test delivery before relying on production events.
Each endpoint has its own signing secret and event subscriptions.
Event catalog
| Event | Subject | Description |
|---|---|---|
affiliate.created | Affiliate | An affiliate record was created |
affiliate.approved | Affiliate | An affiliate moved to active state |
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 |
AffiliateBase creates one delivery per subscribed active endpoint for each event.
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:
{
"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 id field identifies the outbound event. Store it and process it idempotently.
Webhook plus API enrichment
Webhook events contain the IDs needed to look up fuller resource details. Some events include common CRM fields directly. 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, then call the REST API when you need current state or deeper related records.
For commission.created, the commission ID is data.id. To retrieve 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 includes expanded sale and campaign data. 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_body
Use 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)
end
Always verify against the raw request body. Do not verify against 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
Failed deliveries can be retried from Settings -> Webhooks.
Error codes
| 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 to reduce replay risk.
- Log the event type and event ID, not the signing secret.