Skip to content

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

EventFired when
payment.succeededThe provider confirmed the money moved
payment.failedThe provider definitively declined
payment.expiredThe customer never acted
payment.unresolved24 hours of unknown; a human is investigating
refund.succeededA refund completed
refund.failedA refund could not be completed
settlement.paidA 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=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t is a unix timestamp; v1 is HMAC-SHA256(secret, "{t}.{raw_body}") in hex. To verify:

  1. Split the header into t and v1.
  2. Reject if |now − t| is more than 300 seconds (replay protection).
  3. Compute HMAC-SHA256 of "{t}.{raw_body}" — the raw request body, before any JSON parsing — with your endpoint’s signing secret.
  4. Compare with v1 using 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.