> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.trevex.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.trevex.io/_mcp/server.

# Webhooks

When a TREVEX admin approves a KYB request, a `POST` request is sent to the
`webhook_url` configured on your Project.

## Delivery

| Property     | Value                                                                |
| ------------ | -------------------------------------------------------------------- |
| Method       | `POST`                                                               |
| Content-Type | `application/json`                                                   |
| Signing      | `X-Trevex-Timestamp` + `X-Trevex-Signature` headers on every request |
| Timeout      | 10 seconds                                                           |
| Retries      | 3 attempts at 60 s -> 300 s -> 900 s (exponential backoff)           |
| Success      | Any `2xx` HTTP status                                                |
| Audit        | Every attempt is logged in `WebhookDeliveryLog`                      |

## Payload

#### claim_approved

```json
{
  "event": "claim_approved",
  "request_id": "REQ-ABC123XYZ789",
  "trevex_company_id": "TRXAE0000042",
  "company": {
    "name": "Acme Corporation LLC",
    "country_code": "AE",
    "trade_license_url": "https://...",
    "industry": {
      "sector": "Technology",
      "group": "Software",
      "industry": "SaaS",
      "sub_industries": ["Cloud Computing", "Artificial Intelligence"]
    },
    "address": {
      "line_1": "123 Sheikh Zayed Road",
      "city": "Dubai",
      "country": "UAE"
    },
    "authorized_signatory": {
      "name": "John Doe",
      "designation": "CEO",
      "email": "john@acmecorp.com",
      "mobile": "+971501234567",
      "nationality": "AE"
    }
  },
  "contact_email": "john.doe@example.com"
}
```

## Verifying signatures

Every webhook request is signed. You **must** verify the signature before
trusting the payload - without this, an attacker who discovers your webhook
URL can forge approval events.

### How the signature is built

On every delivery, TREVEX computes:

```
signature = HMAC_SHA256(
  secret = your_webhook_signing_secret,
  message = "{timestamp}.{raw_request_body}"
)
```

and sends it hex-encoded in the `X-Trevex-Signature` header alongside the
`X-Trevex-Timestamp` header (Unix epoch seconds).

### Headers

| Header               | Example                                                    |
| -------------------- | ---------------------------------------------------------- |
| `X-Trevex-Timestamp` | `1745600123`                                               |
| `X-Trevex-Signature` | `7c3f2a1b9e4d...` (hex-encoded HMAC-SHA256, 64 characters) |

### Verification steps

#### Check the timestamp

Reject the request if `abs(now - timestamp) > 300` seconds. This prevents
replay attacks where an attacker captures a valid request and resends it
later.

#### Recompute the signature

Concatenate `{timestamp}.{raw_body}` and compute HMAC-SHA256 using your
webhook signing secret.

#### Constant-time compare

Compare your computed signature to `X-Trevex-Signature` using a
constant-time comparison function (not `==`). This prevents timing attacks.

#### Only then, parse the body

Never parse or act on the payload before verification succeeds.

> **Warn**
>
> Use the **raw request body** - the exact bytes sent over the wire, not a
> re-serialised version. Most frameworks let you access this via something like
> `request.body` or `request.rawBody`. If you use a parsed JSON object and
> `JSON.stringify` it, whitespace and key ordering will differ and verification
> will fail.

### Example implementations

#### Python (Flask)

```python
import hmac
import hashlib
import time
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = b"your_webhook_signing_secret"
TOLERANCE_SECONDS = 300

def verify_trevex_webhook(raw_body: bytes, timestamp: str, signature: str) -> bool:
    # 1. Timestamp window
    try:
        ts = int(timestamp)
    except (TypeError, ValueError):
        return False
    if abs(time.time() - ts) > TOLERANCE_SECONDS:
        return False

    # 2. Recompute
    message = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(WEBHOOK_SECRET, message, hashlib.sha256).hexdigest()

    # 3. Constant-time compare
    return hmac.compare_digest(expected, signature)

@app.post("/webhooks/trevex")
def trevex_webhook():
    raw = request.get_data()  # raw bytes, not request.json
    ts = request.headers.get("X-Trevex-Timestamp", "")
    sig = request.headers.get("X-Trevex-Signature", "")

    if not verify_trevex_webhook(raw, ts, sig):
        abort(401)

    event = request.get_json()
    # handle event["request_id"], event["trevex_company_id"], ...
    return "", 200
```

#### Node.js (Express)

```javascript
import express from "express";
import crypto from "crypto";

const app = express();
const WEBHOOK_SECRET = "your_webhook_signing_secret";
const TOLERANCE_SECONDS = 300;

// IMPORTANT: capture the raw body
app.use("/webhooks/trevex", express.raw({ type: "application/json" }));

function verifyTrevexWebhook(rawBody, timestamp, signature) {
  const ts = parseInt(timestamp, 10);
  if (Number.isNaN(ts)) return false;
  if (Math.abs(Date.now() / 1000 - ts) > TOLERANCE_SECONDS) return false;

  const message = Buffer.concat([Buffer.from(`${timestamp}.`), rawBody]);
  const expected = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(message)
    .digest("hex");

  // Lengths must match or timingSafeEqual throws
  const sigBuf = Buffer.from(signature, "hex");
  const expBuf = Buffer.from(expected, "hex");
  if (sigBuf.length !== expBuf.length) return false;
  return crypto.timingSafeEqual(sigBuf, expBuf);
}

app.post("/webhooks/trevex", (req, res) => {
  const ts = req.headers["x-trevex-timestamp"];
  const sig = req.headers["x-trevex-signature"];

  if (!verifyTrevexWebhook(req.body, ts, sig)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString("utf8"));
  // handle event.request_id, event.trevex_company_id, ...
  res.sendStatus(200);
});
```

#### PHP

```php
<?php
$WEBHOOK_SECRET = 'your_webhook_signing_secret';
$TOLERANCE_SECONDS = 300;

$raw       = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_TREVEX_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_TREVEX_SIGNATURE'] ?? '';

if (!ctype_digit($timestamp) || abs(time() - (int)$timestamp) > $TOLERANCE_SECONDS) {
    http_response_code(401);
    exit;
}

$expected = hash_hmac('sha256', $timestamp . '.' . $raw, $WEBHOOK_SECRET);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$event = json_decode($raw, true);
// handle $event['request_id'], $event['trevex_company_id'], ...
http_response_code(200);
```

### Secret rotation

If you need to rotate your webhook signing secret, contact your TREVEX account
manager. During the rotation window, TREVEX can sign with both the old and new
secrets so you can switch without dropping events. Accept either signature
until you've fully deployed the new secret, then ask TREVEX to retire the old
one.

> **Warn**
>
> Webhook signing secrets are different from API keys. Losing or rotating one
> doesn't affect the other. Store them separately in your secret manager.

## Matching webhooks to your records

Use `request_id` to match an incoming webhook to the KYB request you submitted.
It's the same value returned from `create_claim`.

`trevex_company_id` is the canonical TREVEX ID for the company. Store it if you
plan to call [`update_claim`](/api-reference/kyb-requests/update-claim) later.

## Handling retries

Your endpoint should be **idempotent** - the same `request_id` may arrive more
than once if the first attempt fails or times out. A common pattern:

1. Look up the `request_id` in your database.
2. If already processed, return `200` without doing any work.
3. Otherwise, process and store.

> **Warn**
>
> Return a `2xx` status quickly (within the 10-second timeout). If you need to
> do slow downstream work, queue it and return `200` immediately.