ChainBridge · Webhook

Webhook signature verification

Every webhook request ChainBridge sends carries a HMAC-SHA256 signature and a timestamp. Check both the signature and the timestamp skew. That is how you know a request really came from ChainBridge and is not a replay.

Headers

HeadersExampleDescription
X-CS-Eventapproval.createdEvent type
X-CS-Deliverydlv_01J9Y7Q2Delivery ID. The same value survives retries and re-deliveries, so use it to deduplicate.
X-CS-Timestamp1761408000Unix timestamp in seconds, used for replay protection
X-CS-Signaturesha256=9f2c…HMAC-SHA256 signature:sha256= prefix followed by lowercase hex
Content-Typeapplication/jsonThe body is JSON

Signature algorithm

Algorithm definition
signed_payload = timestamp + "." + raw_body
signature      = HMAC-SHA256(webhook_secret, signed_payload)

# use a constant-time comparison (timing-safe); never compare the strings with ==
# header format: X-CS-Signature: sha256=
Always use the raw request body

The signature covers the raw bytes of the HTTP body. If you deserialize and re-serialize the JSON first, whitespace, key order, and escaping all change. The signature check will always fail. Parse the JSON only after verification succeeds.

Verification steps

1
Read the raw request bodyRead it as bytes, with no charset or format conversion.
2
Check that the timestamp skew is ≤ 300 secondsCompare the server time against X-CS-Timestamp ; reject anything beyond ±300 seconds to prevent replay attacks. Also verify NTP clock sync.
3
Compute and compare the signatureUse timestamp + "." + raw_body to compute an HMAC-SHA256 with your signing secret, and compare it with X-CS-Signature in constant time.
4
DeduplicateUse endpoint_id + event_id(or X-CS-Delivery) to tell whether the event was already handled; return 2xx for duplicate deliveries without processing the business logic again.
5
Return 2xx only after processing finishesHand the event to your queue or storage first, then return 2xx; the endpoint must respond within 5 seconds.

Code examples

Express — keep the raw body for verification
const crypto = require('crypto');
const express = require('express');

const app = express();

// key point: verify the signature against the raw body; do not run express.json() first
app.post('/chainshield/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const secret = process.env.CS_WEBHOOK_SECRET;   // whsec_xxx — server side only
    const timestamp = req.header('X-CS-Timestamp') || '';
    const signature = req.header('X-CS-Signature') || '';
    const rawBody = req.body.toString('utf8');

    // 1) timestamp skew ≤ 300 seconds
    const skew = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
    if (!timestamp || Number.isNaN(skew) || skew > 300) {
      return res.status(400).json({ ok: false, message: 'timestamp outside the allowed window (±300 seconds)' });
    }

    // 2) HMAC-SHA256 signature
    const expected = 'sha256=' + crypto
      .createHmac('sha256', secret)
      .update(`${timestamp}.${rawBody}`)
      .digest('hex');

    const a = Buffer.from(expected);
    const b = Buffer.from(signature);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.status(401).json({ ok: false, message: 'signature verification failed' });
    }

    // 3) dedupe: handle the same endpoint_id + event_id only once
    const event = JSON.parse(rawBody);
    // example: check those keys for duplicates before running business logic

    // 4) return 2xx only after processing completes
    res.status(200).json({ ok: true });
  });

app.listen(3000);

Retry backoff and dead letters

Failed deliveries retry with exponential backoff, six times in total. A delivery that still fails lands in the dead-letter queue for inspection and manual re-delivery in the console.

AttemptIntervalCumulative time (approx.)Description
11 minute1 minuteFirst retry, reusing the same X-CS-Delivery
25 minutes6 minutes—
330 minutes36 minutes—
42 hoursabout 2.6 hours—
56 hoursabout 8.6 hours—
624 hoursabout 32.6 hoursFinal attempt
———After 6 attempts it enters the dead-letter queue, kept 30 days for manual re-delivery.

When a delivery is retried

  • · 响应 5xx、连接失败、TLS 失败、响应超时(> 5 秒)→ 按退避重试
  • · 4xx response (such as 404 or 401) → permanent failure, goes straight to the dead-letter queue
  • · 3xx redirect → treated as a configuration error and sent straight to the dead-letter queue

Deduplication and billing

  • · Dedupe key:endpoint_id + event_id
  • · Retries and re-deliveries reuse the same X-CS-Delivery, which receivers use to deduplicate
  • · Only successfully delivered events are billed; retries are not charged again

Event type

EventTrigger
approval.createdA new token approval is detected
approval.changedThe allowance changes
approval.revokedA revocation completes and is confirmed onchain
revoke.failedA revocation transaction fails (with the reason)
risk.label.updatedThe risk label of an address or contract changes
incident.publishedAn incident is published
incident.correctedAn incident is corrected
monitor.alertA monitoring rule matches and raises an alert
usage.quota.warningUsage approaches the plan quota
invoice.issuedAn invoice is issued

Search this page

Enter a keyword to search headers, event types, error codes, and troubleshooting entries.

Troubleshooting

Most often, in this order: ① you hashed re-serialized JSON instead of the raw body; ② you left out the timestamp + "." prefix or the dot when concatenating; ③ you included the sha256= prefix in the hex comparison; ④ the secret is the pre-rotation value. The delivery log on the Webhooks page shows the response code and latency. That separates a verification failure from a network problem.

400601 means webhook signature verification failed (including a timestamp out of range). Check the receiving server's clock sync (NTP is recommended) and confirm no proxy rewrites the request body.

Returns 400600(webhook endpoint unreachable). Check the URL, DNS, TLS certificate, and firewall allowlist. Repeated failures raise an alert, and the event is retried with backoff until it lands in the dead-letter queue.

Yes. Use X-CS-Event to route them. Splitting endpoints by business domain (approvals, billing) keeps a slow handler from blocking the rest.

Yes: ChainBridge must be able to reach your receiving address (it must be https://). If inbound traffic is not allowed, poll the Data API instead, or use the self-hosted gateway's internal event channel.