ExchainDocs
DocsAPI

APIWebhooks

Webhooks

Signed event deliveries for partners: events, payload, signature check and retries.

Get a signed POST when a payment lands, a budget is returned or an alarm fires. Webhooks are delivered to partners for the nodes they answer for. The signing secret comes with your partner credentials.

The decision.* events cover Cardano wallets: decision.landed and decision.released come from the Cardano watcher, and decision.alarm from POST /v1/cosign. Guarded Safes on EVM chains send no decision events.

Events

Eventdata
decision.landeddecisionId, walletId, nodeId, network, txHash, slot
decision.releaseddecisionId, walletId, nodeId, network, txHash, invalidHereafterSlot
decision.alarmdecisionId, walletId, nodeId, network, code, txBodyHash
wallet.state_changedwalletId, nodeId, state (paused, resumed, revoked, restored), reason
signing.state_changedscope, stopped, reason
quorum.state_changedavailable, reachable, threshold
webhook.testpartnerId, message
Payload
{
  "event_type": "decision.landed",
  "service_name": "exchain-cosign",
  "timestamp": "2026-11-05T10:04:11Z",
  "webhook_id": "whd_3f2a9c…",
  "data": { "decisionId": "dec_01J9XK3M4N5P6Q7R8S9T0V1W2X", "walletId": "wal_01J9XK3M4N5P6Q7R8S9T0V1W2Z",
            "nodeId": "node_1", "network": "preprod", "txHash": "…", "slot": 138150155 }
}

Verify the signature

Every delivery carries these headers:

HeaderValue
X-Exchain-EventThe event type
X-Exchain-DeliveryEqual to webhook_id
X-Exchain-TimestampUnix seconds
X-Exchain-Signaturev1=<hex>

The signature is HMAC-SHA256 over "<timestamp>.<raw body>", keyed with the hex-decoded signing secret.

verify.ts
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyExchainWebhook(rawBody: string, headers: Headers, secretHex: string) {
  const ts = headers.get('x-exchain-timestamp') ?? '';
  const sig = (headers.get('x-exchain-signature') ?? '').replace(/^v1=/, '');
  const expected = createHmac('sha256', Buffer.from(secretHex, 'hex')).update(`${ts}.${rawBody}`).digest();
  const given = Buffer.from(sig, 'hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  return fresh && given.length === expected.length && timingSafeEqual(given, expected);
}

Delivery

  • Any 2xx acknowledges.
  • Failed deliveries are retried after 10 s, 30 s, 1 min, 5 min, 15 min, 1 h and 3 h (8 attempts in total), then marked failed.
  • Deliveries can repeat. Deduplicate on webhook_id.

On this page