# Guide to Treezor Webhooks: Features and Best Practices

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

| Feature | Details |
| --- | --- |
| Configuration | Separate host: `POST /settings/hooks` on `webhook.api.treezor.co` (prod) / `webhook.sandbox.treezor.co` |
| Verification | `object_payload_signature` field in the body |
| Signature scheme | base64 HMAC-SHA256 over the prepared `object_payload`, keyed with the webhook secret |
| Payload prep | Flatten the JSON and convert UTF-8 chars to unicode escapes (accented chars become `\u00e9`-style sequences) before hashing |
| MIME | `text/plain` |
| Retries | Every minute, up to 30 attempts (return 5xx to trigger) |
| Duplicates | Possible (same `webhook_id`) |
| Subscription state | Starts `PENDING`; may require Treezor activation |
| SDK | None |

## Common events

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

| Event | Fires when |
| --- | --- |
| `payin.create` | A pay-in is created |
| `payout.update` | A payout updates |
| `cardtransaction.create` | A card transaction occurs |
| `wallet.create` | A wallet is created |
| `user.kycreview` | A 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:

```php
$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:

```javascript
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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Subscribe on the webhook host and confirm activation

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

## 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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle Treezor webhooks reliably in minutes.