Product: BabyFX Pay
Webhooks
Webhooks are how BabyFX Pay tells your server that something happened. Every event is signed with HMAC-SHA256 over the raw body using your webhook_secret.
Always verify the signature. Never trust the payload until you have checked
x-babyfx-signature. The endpoint is public, so anyone can POST to it.Event types
| Event | Triggered when | Idempotency key |
|---|---|---|
payment.detected |
Paying tx in a block | invoice.id + ":detected" |
payment.confirmed |
Confirmations pass threshold | invoice.id + ":confirmed" |
payment.swept |
Sweep tx confirmed | invoice.id + ":swept" |
payment.failed |
Sweep permanently failed | invoice.id + ":failed" |
payment.expired |
Invoice passed expiry unpaid | invoice.id + ":expired" |
Headers
| Header | Value |
|---|---|
Content-Type |
application/json |
x-babyfx-event |
event type, e.g. payment.detected |
x-babyfx-signature |
t=<unix_ts>,v1=<hex_hmac> |
x-babyfx-delivery |
UUID per attempt, useful for dedupe |
User-Agent |
BabyFX-Pay-Webhooks/1.0 |
Payload shape (all events)
{
"id": "evt_01HXY...",
"type": "payment.confirmed",
"created_at": "2026-06-22T12:55:31Z",
"data": {
"invoice_id": "inv_a1b2c3d4",
"merchant_id": 1,
"deposit_address": "0x8e74c9d4...",
"baby_amount_wei": "4500000000000000000",
"fiat_amount": "4.50",
"fiat_currency": "USD",
"tx_hash": "0x9f3a...",
"confirmations": 12,
"sweep_status": "PENDING"
}
}
Verifying the signature
import crypto from "node:crypto";
export function verifyWebhook(rawBody, header, secret) {
// header: "t=1719063331,v1=abcdef..."
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const expected = crypto.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
// timing-safe compare
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Why a timestamp in the signed string? Replay protection. Reject any signature whose
t is more than 5 minutes away from your server clock.Retry policy
| Attempt | Delay |
|---|---|
| 1 | immediate |
| 2 | +30s |
| 3 | +2m |
| 4 | +10m |
| 5 | +1h |
| 6 | +6h |
| 7 | +12h |
| 8 | +24h (final) |
A delivery is marked delivered=TRUE only on a 2xx response. 4xx is treated as a permanent failure (your bug, we will not retry). 5xx and timeouts trigger the retry ladder.
