Gareth Wilson Gareth Wilson

Guide to Treezor Webhooks: Features and Best Practices

Published


Treezor webhooks notify your systems about banking-as-a-service activity: a pay-in is created, a payout updates, a card transaction occurs, a KYC review changes. If you're building on Treezor's EU BaaS platform, webhooks are how you react to account and compliance events without polling.

This guide covers how Treezor webhooks work, the events you'll handle, its unusual verification model (the signature is a field in the body, with a payload-prep step), the retry behavior, and the best practices for production.

What are Treezor webhooks?

Treezor webhooks are HTTP POSTs (sent with a text/plain MIME type) delivered to an endpoint you subscribe. The verification model is unusual: there's no signature header. Each webhook body carries an object_payload_signature field, and you verify it by recomputing an HMAC over the object_payload in the same body.

Treezor webhook features

FeatureDetails
ConfigurationSeparate host: POST /settings/hooks on webhook.api.treezor.co (prod) / webhook.sandbox.treezor.co
Verificationobject_payload_signature field in the body
Signature schemebase64 HMAC-SHA256 over the prepared object_payload, keyed with the webhook secret
Payload prepFlatten the JSON and convert UTF-8 chars to unicode escapes (accented chars become \u00e9-style sequences) before hashing
MIMEtext/plain
RetriesEvery minute, up to 30 attempts (return 5xx to trigger)
DuplicatesPossible (same webhook_id)
Subscription stateStarts PENDING; may require Treezor activation
SDKNone

Common events

Treezor event names use object.action across a large BaaS surface:

EventFires when
payin.createA pay-in is created
payout.updateA payout updates
cardtransaction.createA card transaction occurs
wallet.createA wallet is created
user.kycreviewA KYC review changes

Some objects are camelCase (sca.wallet.*, qes.created). Consult Treezor's event catalog for the full list, and manage subscribed events via /settings/hooks/{uuid}/events.

Setting up Treezor webhooks

Subscribe on Treezor's dedicated webhook host: POST /settings/hooks on webhook.api.treezor.co (production) or webhook.sandbox.treezor.co (sandbox). Manage events via /settings/hooks/{uuid}/events. Subscriptions start in a PENDING state and may require Treezor to activate them. Treezor provides the webhook_secret used for verification.

Securing Treezor webhooks

The signature is the object_payload_signature field inside the body. To verify: take the object_payload object, prepare it (flatten the JSON, and convert UTF-8 characters to unicode escape sequences), compute an HMAC-SHA256 with the webhook_secret, base64-encode it, and compare to object_payload_signature.

Treezor's reference is PHP, where json_encode already produces the required unicode escapes:

$computed = base64_encode(
  hash_hmac('sha256', json_encode($object_payload), $webhook_secret, true)
);
// compare $computed to $body['object_payload_signature']

In Node.js, JSON.stringify does not escape non-ASCII to \uXXXX, so you must replicate PHP's json_encode output:

const crypto = require("crypto");

const SECRET = process.env.TREEZOR_WEBHOOK_SECRET;

// Match PHP json_encode: escape non-ASCII to \uXXXX (and forward slashes)
function phpJsonEncode(obj) {
  return JSON.stringify(obj)
    .replace(/[\u0080-\uffff]/g, (c) =>
      "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")
    )
    .replace(/\//g, "\\/");
}

function verify(body) {
  const prepared = phpJsonEncode(body.object_payload);
  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(prepared)
    .digest("base64");
  const a = Buffer.from(expected);
  const b = Buffer.from(body.object_payload_signature || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook", express.text({ type: () => true }), (req, res) => {
  const body = JSON.parse(req.body);
  if (!verify(body)) {
    return res.status(500).send(); // 5xx triggers Treezor's retry
  }

  res.sendStatus(200); // 200 on match
  processQueue.add(body); // process asynchronously
});

Getting the serialization byte-exact is the hard part. If your reproduction doesn't match, extract the object_payload substring from the raw body (the exact bytes Treezor hashed) rather than re-serializing, and confirm against Treezor's integrity-checks docs.

Treezor webhook limitations and pain points

The signature is a body field with a prep step

The Problem: There's no header to check, and the HMAC is over a normalized form of object_payload (flattened, with UTF-8 as unicode escapes). Hashing the raw sub-object, or a differently-serialized version, fails.

Why It Happens: Treezor signs a specific serialization of object_payload and carries the result in the body.

Workarounds:

  • Reproduce PHP's json_encode output (unicode escapes, escaped slashes), or extract the exact object_payload bytes from the raw body.

How Hookdeck Can Help: Hookdeck verifies against the payload as received, so your app doesn't have to reproduce Treezor's serialization quirks.

text/plain body and separate host

The Problem: Webhooks arrive as text/plain (not application/json), so a JSON-only body parser may not populate the body, and subscriptions live on a separate host (webhook.api.treezor.co), which is easy to miss.

Why It Happens: Treezor sends text/plain and manages hooks on a dedicated host.

Workarounds:

  • Read the body as text and JSON.parse it; subscribe on the webhook host.

How Hookdeck Can Help: Hookdeck ingests any content type and normalizes it, so the text/plain MIME isn't a parsing problem.

Retries and duplicates

The Problem: Treezor retries every minute up to 30 attempts (on 5xx), and duplicates are possible (same webhook_id), so the same event can arrive many times.

Why It Happens: At-least-once delivery with minute-interval retries.

Workarounds:

  • Dedupe on webhook_id and make side effects idempotent; return 5xx only when you actually want a retry.

How Hookdeck Can Help: Hookdeck deduplicates on the webhook id at the edge, so retries don't double-process. See our guide to webhook idempotency.

Subscriptions start PENDING

The Problem: A new subscription is PENDING and may need Treezor to activate it, so events won't flow immediately.

Why It Happens: Treezor gates webhook activation.

Workarounds:

  • Confirm activation with Treezor before relying on delivery.

How Hookdeck Can Help: Hookdeck's observability shows whether events are arriving, so a still-pending subscription is obvious.

Best practices

Verify the body-field signature with the prep step

Prepare object_payload (flatten + unicode-escape, matching PHP json_encode), HMAC-SHA256 with the webhook_secret, base64, and compare to object_payload_signature, or hash the exact raw substring.

Read the body as text and dedupe

Parse the text/plain body yourself, and dedupe on webhook_id.

Return 200 on match, 5xx only to retry

Return 200 when verified; return 5xx only when you genuinely want Treezor to retry. See why to process webhooks asynchronously.

Subscribe on the webhook host and confirm activation

Use webhook.api.treezor.co, and confirm the subscription is active (not PENDING).

Make Treezor webhooks production-ready

Hookdeck verifies the object_payload_signature, deduplicates, and durably queues every BaaS event

Conclusion

Treezor webhooks verify not with a header but with an object_payload_signature field, a base64 HMAC-SHA256 over a prepared (flattened, unicode-escaped) object_payload. Reproduce the serialization exactly (or hash the raw substring), read the text/plain body, dedupe on webhook_id, and confirm the subscription is active.

Hookdeck verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique BaaS events.

Get started with Hookdeck for free and handle Treezor webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.