BTC$68,420▲ 0.82% / ETH$3,841▼ 0.31% / LTC$94.05▲ 1.14% / SOL$221.60▲ 3.02% / USDT·TRC20$1.0000 / USDC·ERC20$0.9999 / XMR$168.42▼ 0.18% / TON$6.02▲ 0.47% / XRP$2.41▼ 0.09% / GAS·ETH18 gwei / Indicative prices · illustrative ticker / No-KYC · non-pooled · 0.4% flat / BTC$68,420▲ 0.82% / ETH$3,841▼ 0.31% / LTC$94.05▲ 1.14% / SOL$221.60▲ 3.02% / USDT·TRC20$1.0000 / USDC·ERC20$0.9999 / XMR$168.42▼ 0.18% / TON$6.02▲ 0.47% / XRP$2.41▼ 0.09% / GAS·ETH18 gwei / Indicative prices · illustrative ticker / No-KYC · non-pooled · 0.4% flat
OrbChain
UTC 18:47:02 Get started →
Feature · Deterministic Webhooks ▓ HMAC-SHA512 · DET. EVENT_ID Idempotent by construction

Webhooks
that dedupe.

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.

Signed · Deterministic · Replayable
§ Part I / V · The contract

What we promise.

Every guarantee
Load-bearing for integrators
SymGuaranteeHowStatus
SIGSignedHMAC-SHA512 over raw body, keyed on your webhook_secret. Header HMAC.REQUIRED
DETDeterministic event_idSHA-256(event_type, entity_id) → first 128 bits → 26-char Crockford-base32. Retry = same id.REQUIRED
AT-LEAST-ONCEAt-least-once delivery5 retries, 2s/4s/8s/16s/32s backoff. Every attempt logged to WebhookLog.GUARANTEED
REQ-IDX-Request-IdSame request id flows API → worker → webhook body + header — full tracing across our stack.PROVIDED
SSRFSSRF-hardenedPublic-IP check before every dispatch. maxRedirects: 0 — no 302-to-private gotcha.DEFENSIVE
RPLReplayableFailed deliveries listed in /dashboard/webhooks with one-click replay.UI
§ Part II / V · Event types

Nine event types.

Every lifecycle
Subscribe in the dashboard
Symevent_typeWhen it firesRetryable
PAYpayment.paidInvoice reached merchant's confirmation threshold. Balance credited. Most-subscribed event.YES
EXPpayment.expiredInvoice expires_at passed without full payment.YES
REFpayment.refundedRefund broadcast + confirmed. Balance re-debited.YES
PO-Cpayout.confirmedPayout broadcast + confirmed on-chain (or Lightning settled).YES
PO-Fpayout.failedBroadcast rejected or confirmations never reached. Balance re-credited atomically.YES
SUB-Isubscription.invoicedBilling tick issued a fresh invoice. Tells you to notify the customer.YES
SUB-Psubscription.paidCurrent-period invoice PAID. next_invoice_at advanced.YES
SUB-Fsubscription.failedDunning ladder exhausted. Subscription cancelled.YES
DEPdeposit.confirmed (V1)Static-address deposit credited. Includes external_user_id. V1-shape (HMAC-SHA256).YES
§ Part III / V · Payload

What you receive.

Example: payment.paid
Every field documented
POST your-webhook-urlsigned · hmac-sha512
// 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"
}
§ Part IV / V · Verifier

Verify + dedupe.
Eight lines.

Constant-time compare
event_id for dedupe · raw body for HMAC
node express webhook handlerreference · copy-paste
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);
  });
§ Part V / V · Questions

Things integrators
ask first.

Short answers
Links to code
Q.01What makes the event_id deterministic?

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.

Q.02Why HMAC-SHA512 and not SHA-256?

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.

Q.03What happens after 5 retries fail?

The delivery is marked failed in WebhookLog. Dashboard shows the failed deliveries list; one click replays. No event is ever permanently lost.

Q.04Can I filter which events I receive?

Yes — per-endpoint event-type subscriptions in /dashboard/settings/webhooks. Noise-free integration.

Q.05How do I test webhook handling locally?

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.

Webhooks that
actually dedupe.

Idempotent by construction. No fresh DB column. No random UUIDs to dedupe against.