The custom-signing tax
Every API that fires webhooks signs them somehow. Stripe uses Stripe-Signature: t=…,v1=… . GitHub uses X-Hub-Signature-256: sha256=…. Twilio uses X-Twilio-Signature with their own canonicalisation. PayPal, Slack, Shopify — every one of them signs differently.
For a developer integrating two or three of these, fine. For a developer integrating fifteen — say, anyone building a CRM or a workflow tool — the signing-scheme tax adds up. You write the verification code once per provider, maintain it as their docs drift, and pray you handle the edge cases (timestamp tolerance windows, key rotation, multiple active signatures during a key swap) correctly each time.
Standard Webhooks fixes this
Standard Webhooks v1.0.2 is a small, opinionated spec from the people behind Svix. It defines:
- Three lowercase headers:
webhook-id,webhook-timestamp,webhook-signature. - A canonical signed payload:
${id}.${timestamp}.${body}. - HMAC-SHA256, base64-encoded, prefixed with
v1,in the signature header. Multiple signatures space-separated to support seamless key rotation. - A timestamp tolerance (typically 5 minutes) to defeat replay attacks.
- Idempotency via the unique
webhook-id— receivers dedupe on it.
Every receiver gets the same code path:
import { Webhook } from 'svix';
app.post('/webhooks/cobalz', (req, res) => {
const wh = new Webhook(process.env.COBALZ_SECRET);
try {
const evt = wh.verify(req.rawBody, req.headers);
// evt.type === 'commission.approved' etc.
} catch {
return res.status(401).send();
}
// ... your business logic
res.status(200).send();
});That's it. Three lines for verification. Drop in svix-libs (Python, Ruby, Go, Rust, Kotlin, .NET, PHP, Elixir all supported) and you're done.
What we shipped
Cobalz signs every outbound webhook with this spec — 12 event types as of today: affiliate.approved, affiliate.suspended,affiliate.tier_promoted, commission.created,commission.approved, commission.voided,payout.sent, payout.paid, fraud.flagged,tax_form.submitted, plus two more in the pipeline.
The implementation lives in the open atpackages/shared/src/standard-webhooks.ts. Sign-and-verify in 90 lines. We use it for outbound delivery to merchants' webhook endpoints, and our Resend webhook receiver verifies inbound Resend events the same way (Resend itself uses Svix under the hood, so it just works).
The retry policy is part of the spec
The recommended retry schedule from the Standard Webhooks docs is published and stable: 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 14 hours, 20 hours, 24 hours. We follow it exactly. If your endpoint goes down for a day, we have ten attempts to deliver, spread to give you headroom to recover. After max attempts, the event lands in our dead-letter store with a one-click replay button in the dashboard.
When a receiver returns 410 Gone, we automatically disable the endpoint and dead-letter every pending delivery. When it returns 5xxor 429 we honour Retry-After. When it returns other 4xx (which means the receiver permanently rejected the payload — usually a bug on their side), we dead-letter immediately rather than retry their bug.
Why this matters for affiliates
An affiliate program is a ledger. Every event that changes that ledger (commission accrued, commission approved, refund clawed back, payout sent, payout paid) needs to flow into the merchant's downstream systems — accounting tools, business-intelligence dashboards, Slack channels, CRM workflows — exactly once and at the right time.
Custom signing schemes turn that into a special-snowflake integration project. Standard Webhooks turns it into “point svix-libs at the URL we give you and you're done.”
Spec details: /features/webhooks