Webhooks
Lidya sends signed HTTPS POST requests to your endpoints when state changes happen. The same payload is delivered to your webhookEndpoint.url and (after the customer redirect completes) appended to your configured returnUrl.
Headers
Every webhook carries:
| Header | Purpose |
|---|---|
Lidya-Signature | HMAC-SHA256 of ${timestamp}.${rawBody} using your endpoint's secret. Hex-encoded. |
Lidya-Timestamp | Unix seconds when we signed. |
Lidya-Webhook-Id | Stable id for de-duplication. Stash for 24h. |
Lidya-Event | Event type — same as eventType in body. |
Verification
You must check three things, in this order:
- Timestamp skew — reject if
now - timestamp > 5 minutes. - Signature — recompute HMAC-SHA256 with
timingSafeEqual. - Replay — if you've seen
Lidya-Webhook-Idbefore, return 200 silently.
import { createHmac, timingSafeEqual } from "node:crypto";
const SKEW = 5 * 60;
export async function verify(req: Request, secret: string): Promise<boolean> {
const ts = Number(req.headers.get("Lidya-Timestamp"));
if (Math.abs(Date.now() / 1000 - ts) > SKEW) return false;
const raw = await req.text();
const sig = req.headers.get("Lidya-Signature")!;
const expected = createHmac("sha256", secret)
.update(`${ts}.`)
.update(raw)
.digest("hex");
return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}Event types
| Event | Trigger |
|---|---|
charge.pending | New charge created. |
charge.succeeded | Acquirer confirmed payment. |
charge.failed | Customer attempt declined. |
charge.expired | Customer never paid within window. |
charge.refunded | You issued a refund. |
payout.queued | Payout enqueued for broadcast. |
payout.broadcasted | TX hash assigned, awaiting confirmations. |
payout.succeeded | Confirmed on-chain. |
payout.failed | Broadcast failed. |
Retries
Failed deliveries (anything that isn't a 2xx within 10 seconds) are retried with exponential backoff for up to 24 hours. After that we mark the delivery failed and stop retrying. You can fetch missed deliveries from the Events API anytime.
Idempotency
Always treat webhooks as at-least-once. The Lidya-Webhook-Id header is stable across retries — keep a 24-hour set in Redis or your database and ack-without-side-effects on duplicates.