Skip to content

Outbound webhooks

Cobalz emits Standard Webhooks v1 — the spec from the Standard Webhooks project that Slack, Twilio, GitHub, and Svix helped define. You can verify signatures with any svix-compatible library in Node, Python, Ruby, Go, Rust, PHP, Java, .NET, or Elixir.

Add an endpoint

Settings → Webhooks → New endpoint. Paste your URL, pick the events you care about. The signing secret is shown exactly once on creation; rotate at any time.

Signature verification

Every request includes three headers:

  • webhook-id — unique message id for idempotency.
  • webhook-timestamp — unix seconds. Reject if >5 min off your wall clock.
  • webhook-signaturev1,<base64-hmac>, possibly multiple comma-separated values during key rotation.
# Pseudocode — verify_webhook_signature(secret, headers, body)
signed = f"{headers['webhook-id']}.{headers['webhook-timestamp']}.{body}"
expected = base64(hmac_sha256(secret, signed))
for sig in headers['webhook-signature'].split(','):
    version, value = sig.split(',', 1)
    if version == 'v1' and timing_safe_eq(value, expected):
        return ok

Retries

On non-2xx responses we retry with this schedule:

  • 5 seconds → 5 minutes → 30 minutes → 2 hours → 5 hours → 10 hours → 14 hours → 20 hours → 24 hours
  • After 9 attempts, the delivery moves to dead_letter and can be replayed manually from the endpoint detail page.
  • HTTP 410 Gone auto-disables your endpoint (no further deliveries).
  • Other 4xx (except 429) are treated as permanent client errors — no retry.
  • HTTP 429 respects your Retry-After header.

Event catalog

typedata payload
affiliate.approved{ affiliate_id, slug, email }
affiliate.suspended{ affiliate_id, reason }
commission.created{ commission_id, affiliate_id, amount, currency, order_id }
commission.approved{ commission_id, affiliate_id, amount, currency }
commission.refunded{ commission_id, affiliate_id, amount, currency, refund_amount }
payout.sent{ payout_id, affiliate_id, amount, currency, rail, external_id }
payout.paid{ payout_id, affiliate_id, amount, currency, rail }
tax_form.submitted{ affiliate_id, form_type }
fraud.flagged{ affiliate_id, reason }

Replay + dead letters

Every delivery — including failed and dead-lettered ones — is visible at Settings → Webhooks → <endpoint>. Each row has a Replay button that re-emits the same payload (with a fresh webhook-id). Bulk-replay via SQL is documented in the webhook-deliveries-backlog runbook.