Webhooks

How you find out an order is done

TransFi calls your endpoint when a user or an order changes status. Set this up before you go live — every flow completes asynchronously.

Set up

In the dashboard, Settings → Integration: enter your HTTPS Webhook URL, then click the regenerate icon beside Webhook secret key and copy it. You'll use the secret to verify every delivery.

Settings → Integration: Webhook URL and secret key

What arrives

A JSON POST with the same shape every time:

FieldTypeMeaning
entityTypestringWhat changed — a user or an order
entityIdstringThe userId or orderId
statusstringThe new status
userobjectUser details, when relevant
orderobjectOrder details, when relevant

Events by flow: Users · Collections · Custodial wallet · Pre-funding · Payouts · On-ramp · Off-ramp · Swaps

Verify the signature

Every delivery carries X-Transfi-Hmac-Hash: an HMAC-SHA256 of the raw request body using your webhook secret. Compute the same and compare.

const crypto = require('crypto');
// Use the raw body bytes — not a re-serialised JSON object
app.post('/webhooks/transfi', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = crypto.createHmac('sha256', process.env.TRANSFI_WEBHOOK_SECRET).update(req.body).digest('hex');
  const received = req.headers['x-transfi-hmac-hash'] || '';      // Node lower-cases header names
  const valid = expected.length === received.length &&
                crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
  if (!valid) return res.status(401).end();
  res.status(200).end();                                           // acknowledge first…
  process(JSON.parse(req.body));                                   // …then do the work
});
import hmac, hashlib, os
raw = request.get_data()                                           # raw bytes, not request.get_json()
expected = hmac.new(os.environ['TRANSFI_WEBHOOK_SECRET'].encode(), raw, hashlib.sha256).hexdigest()
received = request.headers.get('X-Transfi-Hmac-Hash', '')
if not hmac.compare_digest(expected, received):
    return '', 401
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] digest = mac.doFinal(rawBodyBytes);                         // the exact bytes received
String expected = HexFormat.of().formatHex(digest);
boolean valid = MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8), received.getBytes(StandardCharsets.UTF_8));
🛑

Hash the raw bytes. Parsing and re-serialising the JSON changes key order and whitespace, and the hash will not match. Use a constant-time comparison.

Respond

Return HTTP 200 as soon as you've verified the signature, then process. Slow handlers time out, get retried, and process twice.

Retries

If we don't get a 200, we retry 8 times at 15-minute intervals — about two hours. Retries are on by default. Because of this, the same event can arrive more than once: key your handling on entityId + status so a repeat delivery does nothing.

📘

Keep polling as a backstop. Reconcile with List Orders on a schedule. In sandbox, re-trigger a delivery with Resend Webhook.

→ Orders & Statuses


Did this page help you?