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:
| Field | Type | Meaning |
|---|---|---|
entityType | string | What changed — a user or an order |
entityId | string | The userId or orderId |
status | string | The new status |
user | object | User details, when relevant |
order | object | Order 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 '', 401Mac 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.
Updated 28 days ago