guides
Webhooks
One event format for every PSP. Register endpoints, verify signatures, and rely on retries plus a re-drivable delivery log instead of building your own.
Endpoints
POST /v1/webhook_endpoints { "url": "https://example.com/hooks", "event_types": ["payment.captured"] }
Leave event_types empty to receive everything. The signing secret (whsec_…) is returned once — store it like a password.
Event payload
{
"id": "evt_…",
"object": "event",
"type": "payment.captured",
"created_at": "2026-07-12T14:03:11Z",
"data": { "payment": { "id": "pay_…", "status": "captured", … } }
}
Event types today: payment.created, payment.authorized, payment.captured, payment.requires_action, payment.failed, payment.voided, refund.succeeded, payment_method.created.
Verifying signatures
Each delivery carries a vermilion-signature header: t=<unix>,v1=<hmac> where v1 is HMAC-SHA256 of `${t}.${rawBody}` with your endpoint secret, hex-encoded. Reject if the signature mismatches or t is more than 5 minutes old (replay protection).
// node example
const [t, v1] = header.split(',').map(p => p.split('=')[1]);
const expected = crypto.createHmac('sha256', secret)
.update(`${t}.${rawBody}`).digest('hex');
const valid = timingSafeEqual(expected, v1) && Math.abs(now - t) < 300;
Delivery & retries
- Respond with any 2xx within 5 seconds; anything else is retried up to 3 times.
- Exhausted deliveries are marked
failed— list them withGET /v1/webhook_deliveries?status=failed. - Re-drive any delivery with
POST /v1/webhook_deliveries/:id/retry(also a button in the dashboard). - Deliveries can arrive out of order or, rarely, more than once — treat the event id as your dedupe key.