Webhooks
Testing Your Integration

Testing Your Integration

Fire a real, fully-signed webhook at your own endpoint whenever you want — without moving any funds. Use it to verify your handler end-to-end before going live, and any time afterwards.

The test event is byte-for-byte a production event — same fields, same units, same signature — with one extra field: test.

🚫

Your handler MUST check test and skip all real side effects — crediting a user, marking a withdrawal settled, updating balances. Acknowledge with { "error": 0 } without applying any ledger change. Production events never include this field.

Request

curl -X POST "https://api.<host>/session/<session-uuid>/test-webhook" \
  -H "Authorization: Bearer $OPERATOR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
  • <session-uuid> — the session id (sid) you received when you created the session via POST /session/deposit or POST /session/withdraw. The session's type decides which event is sent: a deposit session sends DEPOSIT, a withdraw session sends WITHDRAW_COMPLETE.
  • Auth — your OPERATOR_SECRET_KEY, the same key you create sessions with. Only your own sessions are addressable; anything else returns 404.
  • Destination — your configured webhookUrl. You cannot point a test at another URL.
  • Rate limit — 5 requests per minute.

Optional overrides

Send {} to use well-formed defaults derived from the session, or override any of:

FieldApplies toDescription
amountbothDecimal string, in your platform fiat (e.g. "500.00"). Defaults to the session's amount, or 100
currencybothFiat code (e.g. "TRY"). Defaults to your platform currency
chainIdDEPOSITChain id for the payload. Defaults to 8453 (Base)
addressbothToken contract for DEPOSIT (omit for a native transfer) / destination address for WITHDRAW_COMPLETE
-d '{ "amount": "500.00", "currency": "TRY", "chainId": 8453 }'

Response

The webhook is delivered synchronously, so you get your own endpoint's result back immediately:

{
  "status": "success",
  "data": { "delivered": true, "status": 200, "event": "DEPOSIT" }
}
FieldDescription
deliveredWhether your endpoint accepted the event (HTTP 2xx and { "error": 0 } in the body)
statusThe HTTP status your endpoint returned — null when it could not be reached (DNS, connection refused, timeout)
eventDEPOSIT or WITHDRAW_COMPLETE
reasonPresent only when delivered is false — your endpoint's error message, or the network error

A rejected or unreachable endpoint is not an error on this call: you still get 200 with delivered: false and the reason, so you can debug from the response.

400 responses mean the test could not be built: no webhookUrl or webhookSecret configured on your operator, or the session has no externalUserId.

What the payload looks like

Identical to the production event documented in Event Types — including units: amount / grossAmount / feeAmount are raw token base units, and amountInDollar / amountInLocalCurrency are decimals. Parse it exactly as you parse a real event.

The synthetic values are chosen so a test is recognisable and repeatable:

  • test"true". The one field a production event never has.
  • depositId (DEPOSIT) — a negative number. Real deposits always have a positive id, so a negative value can never collide with one.
  • idempotencyKey (DEPOSIT) — test-<session-uuid>.
  • Deterministic per session — sending twice for the same session produces the identical depositId, idempotencyKey and transactionHash, so you can exercise your deduplication logic.
  • transactionHash — well-formed but synthetic; it does not exist on-chain.
  • currentRate — always "1", and the local-currency figures equal the USD ones. A test event is not an FX snapshot.
  • No feefeeAmount is 0 and grossAmount equals amount.

Verifying the signature

The hash is computed with your webhook secret exactly like a production event — verify it the same way, with no special-casing for tests. See Signature Verification.

The body is form-urlencoded, so every value arrives as a string: check test === "true", not test === true. Fields that are null at the source arrive as the literal string "null" — see Event Types.

Handler example

app.post('/api/webhook', (req, res) => {
  if (!verifySignature(req.body, SECRET_KEY)) {
    return res.status(401).json({ error: 1, description: 'invalid signature' });
  }
 
  const isTest = req.body.test === 'true';
 
  if (isTest) {
    // Parse and validate exactly as you would a real event — but do not touch
    // the ledger. Acknowledge so the delivery is reported as successful.
    console.log('test webhook received:', req.body.event);
    return res.json({ error: 0, description: 'test acknowledged' });
  }
 
  // ... real crediting / settlement here
  return res.json({ error: 0, description: 'ok' });
});

What this does and does not cover

Covered: connectivity and TLS to your endpoint, signature verification, payload parsing, your response contract ({ "error": 0 }), and deduplication on repeat sends.

Not covered: the durable retry path. Production DEPOSIT and WITHDRAW_COMPLETE events are queued and retried up to 5× with backoff before dead-lettering; a test is a single synchronous delivery (with 2 network-level attempts). See Retries & Timeouts.