Webhook endpoints
POST /v1/webhook_endpoints
Intended contract
Section titled “Intended contract”{ "url": "https://example.org/api/vaki/webhooks", "events": ["payment.approved", "vaki.state_changed", "withdrawal.state_changed"], "description": "Production"}201 Created:
{ "id": "whe_01J9Z4M2K7QF3B", "object": "webhook_endpoint", "url": "https://example.org/api/vaki/webhooks", "events": ["payment.approved", "vaki.state_changed", "withdrawal.state_changed"], "secret": "whsec_…", "status": "enabled", "created_at": "2026-08-17T14:00:00Z"}secret is returned once, at creation. Store it before you close the
connection; it cannot be retrieved later.
Intended event catalog
Section titled “Intended event catalog”| Event | Fires when |
|---|---|
checkout_link.completed | A checkout link’s donation is approved |
payment.approved | A payment is approved |
payment.refunded | A payment is refunded |
vaki.state_changed | A cause changes state |
withdrawal.state_changed | A withdrawal moves through its state machine |
bank_account.verified | A bank account passes verification |
creator.verification_completed | A creator’s KYC resolves |
Intended delivery semantics
Section titled “Intended delivery semantics”These are the parts most webhook integrations get wrong, so they are specified up front rather than discovered:
- Signature. HMAC-SHA256 over
{timestamp}.{raw_body}, sent asVaki-Signature: t=<iso8601>,v1=<hex>, with a tolerance window on the timestamp. Compare in constant time. - The timestamp is ISO 8601, not epoch. We have been on the receiving end of this ambiguity from a provider and it broke every signature check we had, so we are stating it before shipping rather than after.
- Sign the raw body. Compute the HMAC over the bytes as received, before any JSON parse. A framework that parses and re-serialises for you will produce a different byte string and a failing signature.
- At-least-once, unordered. Dedupe on
event.idand do not assume apayment.approvedarrives before thecheckout_link.completedit relates to. - Retries with exponential backoff over a bounded window. An endpoint that fails permanently is disabled and the technical contact is emailed.
- A notification payload is not the REST resource. Where the shapes match we will say so explicitly. Do not assume it.
What to build now so nothing changes later
Section titled “What to build now so nothing changes later”Set callback_url on every checkout link today. Handle status transitions in one
function that your polling loop calls, so that when delivery ships your webhook
handler calls the same function and the polling loop is deleted rather than
rewritten.