Every outbound webhook carries event_id = SHA-256(event_type, entity_id). Retry 1,000 times; the id is identical. HMAC-SHA512 over the raw body. 5 retries with exponential backoff. Log table, replayable from dashboard, SSRF-hardened.
webhook_secret. Header HMAC.REQUIREDSHA-256(event_type, entity_id) → first 128 bits → 26-char Crockford-base32. Retry = same id.REQUIREDWebhookLog.GUARANTEEDmaxRedirects: 0 — no 302-to-private gotcha.DEFENSIVE/dashboard/webhooks with one-click replay.UIexpires_at passed without full payment.YESnext_invoice_at advanced.YESexternal_user_id. V1-shape (HMAC-SHA256).YES// Headers HMAC: 5f3a…f8 // hex, HMAC-SHA512 of raw body X-Event-Id: 01H8KP9M3FRA8YTVKX6ZBN4AX2 X-Event-Type: payment.paid X-Event-Timestamp: 2026-04-20T18:45:12Z X-Request-Id: req_7f8a2e Content-Type: application/json // Body (signed) { "event_id": "01H8KP9M3FRA8YTVKX6ZBN4AX2", "event_type": "payment.paid", "event_timestamp": "2026-04-20T18:45:12Z", "request_id": "req_7f8a2e", "track_id": "pay_01JR3K9TFR", "order_id": "order_7f92a1", "status": "PAID", "coin": "BTC_LN", "amount": "0.00213", "amount_usd": "142.00", "net_usd": "141.43", "fee_usd": "0.57", "tx_hash": "ln_inv_abc…", "confirmations": 1, "confirmed_at": "2026-04-20T18:45:11Z", "receipt_url": "https://orbchain.io/v1/payment/pay_01JR3K9TFR/receipt" }
const crypto = require("crypto"); const SECRET = process.env.ORB_WEBHOOK_SECRET; function verify(rawBody, signatureHex) { const expected = crypto.createHmac("sha512", SECRET) .update(rawBody).digest("hex"); return crypto.timingSafeEqual( Buffer.from(expected, "hex"), Buffer.from(signatureHex, "hex") ); } app.post("/webhooks/orbchain", express.raw({ type: "application/json" }), // raw body required for HMAC (req, res) => { if (!verify(req.body, req.header("HMAC"))) return res.sendStatus(401); const event = JSON.parse(req.body.toString()); // Dedupe with the deterministic event_id — skip if already seen. if (await seen(event.event_id)) return res.sendStatus(200); await markSeen(event.event_id); // Your business logic here if (event.event_type === "payment.paid") { await markOrderPaid(event.order_id, event.amount_usd); } res.sendStatus(200); });
It's computed as Crockford-base32(SHA-256(event_type || entity_id)[:16]). Same inputs = same id. Retries carry the same id. You dedupe with one SET-membership check.
No technical reason — SHA-256 would be fine too. SHA-512 is faster on 64-bit hardware (which is everywhere in 2026) and provides the same security in practice. We chose it once and standardized.
The delivery is marked failed in WebhookLog. Dashboard shows the failed deliveries list; one click replays. No event is ever permanently lost.
Yes — per-endpoint event-type subscriptions in /dashboard/settings/webhooks. Noise-free integration.
Expose your localhost via ngrok / Cloudflare Tunnel, set the URL in dashboard, test mode fires realistic payloads. Or use /dashboard/webhooks/test which dispatches a canned event to your endpoint on demand.
Idempotent by construction. No fresh DB column. No random UUIDs to dedupe against.