Skip to content

Webhook endpoints

POST /v1/webhook_endpoints

{
"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.

EventFires when
checkout_link.completedA checkout link’s donation is approved
payment.approvedA payment is approved
payment.refundedA payment is refunded
vaki.state_changedA cause changes state
withdrawal.state_changedA withdrawal moves through its state machine
bank_account.verifiedA bank account passes verification
creator.verification_completedA creator’s KYC resolves

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 as Vaki-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.id and do not assume a payment.approved arrives before the checkout_link.completed it 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.