When WiseYield delivers a webhook to your application, it is signed using the Standard Webhooks specification (the same scheme used by Svix, Resend, Dodo Payments, and Clerk). This guide shows you how to verify the signature and what to expect in the payload.Documentation Index
Fetch the complete documentation index at: https://docs.wiseyield.co/llms.txt
Use this file to discover all available pages before exploring further.
Outbound webhooks are part of the versioned API expansion roadmap. The signature contract documented here is what integrators should align against today so the wire format does not change at GA.
The contract
WiseYield sends three headers with every webhook delivery:| Header | Format | Purpose |
|---|---|---|
webhook-id | UUID | Stable identifier for this delivery (for idempotency) |
webhook-timestamp | Unix seconds | When the webhook was signed (reject if too old) |
webhook-signature | v1,<base64-signature> (space-separated for multiple) | HMAC-SHA256 of {id}.{timestamp}.{body} using your endpoint secret |
Header fallback (Svix parity)
WiseYield readswebhook-* first and falls back to svix-* on the way in, and emits both header sets on the way out for downstream compatibility. Your handler should do the same:
Verifying the signature
The canonical approach is to use the officialstandardwebhooks (or svix) library — it handles timing-safe comparison, multi-signature parsing, and replay-window validation correctly.
Why raw body matters
The signature is computed over the raw request body bytes, not the JSON-parsed object. Most web frameworks parse JSON by default — you must read the raw body before parsing, or signature verification will fail intermittently when whitespace or key ordering differs.- Express: use
express.raw({ type: 'application/json' })on the webhook route. - Next.js App Router: use
request.text()beforeJSON.parse(). - Flask: use
request.data(notrequest.json). - FastAPI: use
await request.body().
Idempotency
Webhooks can be delivered more than once — retries on network blips, processing failures on your end, etc. Usewebhook-id as the dedupe key:
webhook_id plus ON CONFLICT DO NOTHING is the simplest form. WiseYield uses exactly this pattern internally for the webhooks it consumes.
Replay window
webhook-timestamp is signed alongside the body. Reject deliveries older than ~5 minutes to prevent replay attacks:
standardwebhooks and svix libraries enforce this automatically.
Retry behavior
When your endpoint returns a non-2xx response (or fails to respond within 30 seconds), WiseYield retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 5 seconds |
| 3 | 30 seconds |
| 4 | 5 minutes |
| 5 | 30 minutes |
| 6 | 2 hours |
| 7 | 5 hours |
| 8 | 10 hours |
| 9 | 24 hours |
200 as soon as you’ve verified the signature and queued the event for processing; do any heavy work asynchronously.
What to log on verification failure
When verification fails, log loudly enough to diagnose:- Body was JSON-parsed before signature check (always read raw bytes first).
- Wrong endpoint secret (rotate, store in env, never commit).
- Reverse-proxy stripping or re-casing headers (check for
Webhook-Idvswebhook-id). - Reading the wrong header set (you sent
svix-*, your code only looks forwebhook-*, or vice versa).
See also
- Standard Webhooks specification — the canonical reference
- Errors & status codes — return shapes from your endpoint