Webhooks and signatures
Webhooks tell your server what happened without polling. Every delivery is signed; verify the signature and you can trust the news came from Lango and wasn’t replayed.
Events
| Event | Fired when |
|---|---|
payment.succeeded | The provider confirmed the money moved |
payment.failed | The provider definitively declined |
payment.expired | The customer never acted |
payment.unresolved | 24 hours of unknown; a human is investigating |
refund.succeeded | A refund completed |
refund.failed | A refund could not be completed |
settlement.paid | A settlement batch paid out to your bank |
There is no event for unknown — it isn’t an outcome, and an event would tempt you to
treat it as one.
Verifying the signature
Every delivery carries:
Lango-Signature: t=1727286000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdt is a unix timestamp; v1 is HMAC-SHA256(secret, "{t}.{raw_body}") in hex. To verify:
- Split the header into
tandv1. - Reject if
|now − t|is more than 300 seconds (replay protection). - Compute
HMAC-SHA256of"{t}.{raw_body}"— the raw request body, before any JSON parsing — with your endpoint’s signing secret. - Compare with
v1using a constant-time comparison.
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(header, rawBody, secret) { const parts = Object.fromEntries(header.split(',').map((p) => p.split('='))) if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex') const a = Buffer.from(expected) const b = Buffer.from(parts.v1 ?? '') return a.length === b.length && timingSafeEqual(a, b)}function verify(string $header, string $rawBody, string $secret): bool { $parts = []; foreach (explode(',', $header) as $pair) { [$k, $v] = explode('=', $pair, 2); $parts[$k] = $v; } if (abs(time() - (int) $parts['t']) > 300) return false; $expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret); return hash_equals($expected, $parts['v1'] ?? '');}The official SDKs ship this as one function call.
Delivery and retries
- Respond with any 2xx within 10 seconds to acknowledge. Do the real work after responding, not before.
- On failure we retry at 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, and stop after 24 hours. An endpoint that keeps failing is marked degraded in your dashboard.
- Deliveries can arrive out of order and, rarely, more than once. Key your handling on
the event
id(store processed ids) and on the payment’s current state, not on arrival order.
Missed something?
Every event is also in the log: GET /v1/events lists them, and
POST /v1/events/{id}/replay re-queues delivery to your endpoints. After an outage on your
side, replay the window you missed instead of reconciling by hand.