# Route webhooks to tenants

Use the profiles array on every event to send each webhook to the right customer, filter endpoints by profile or account, and fan out safely.

When many customers share your workspace, every event has to find its way back to the right one. Pozy makes that a field lookup: each event lists the profiles it concerns, with the `externalId` you gave them.

## The profiles array

Every event body carries `profiles`:

```json
{
  "id": "evt_5b0e9c3a71f2d846",
  "event": "message.received",
  "timestamp": "2026-10-30T16:41:09.000Z",
  "profiles": [{ "id": "6720f3b1c9e4a85d2f0b7c14", "externalId": "cus_8f3a2" }],
  "message": { "...": "..." }
}
```

- `id` is the Pozy profile id. `externalId` is yours, or `null` if you never set one.
- Most events concern one profile. A post that targets accounts in several profiles lists each of them.
- Workspace-level events can arrive with an empty array. Route those to your own operations channel.

Set `externalId` on every customer profile ([profiles](https://pozy.ai/docs/build/profiles)) and you never need a database query to route an event.

## Pattern 1: one endpoint, route in your code

Register a single endpoint for all customers and route on `externalId`. This is the simplest setup and scales to any number of customers.

```ts
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.POZY_WEBHOOK_SECRET;
const seen = new Set(); // use Redis or your database in production

function verified(raw, signature) {
  const expected = createHmac("sha256", SECRET).update(raw).digest("hex");
  return typeof signature === "string" && signature.length === expected.length &&
    timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

createServer((req, res) => {
  let raw = "";
  req.on("data", (c) => (raw += c));
  req.on("end", async () => {
    if (!verified(raw, req.headers["x-pozy-signature"])) return res.writeHead(401).end();

    const eventId = req.headers["x-pozy-event-id"];
    if (seen.has(eventId)) return res.writeHead(200).end(); // duplicate delivery
    seen.add(eventId);

    const event = JSON.parse(raw);
    const tenants = (event.profiles ?? []).map((p) => p.externalId).filter(Boolean);

    // Acknowledge fast, work later: Pozy waits 5 seconds at most.
    res.writeHead(200).end();
    if (tenants.length === 0) await queue.push("ops", event);
    for (const tenant of tenants) await queue.push(`tenant:${tenant}`, event);
  });
}).listen(8080);
```

Follow three rules:

1. **Verify first.** Reject anything whose `X-Pozy-Signature` does not match ([verify signatures](https://pozy.ai/docs/webhooks/signatures)).
2. **Dedupe on `X-Pozy-Event-Id`.** A delivery can arrive more than once, for example when your server timed out but had already processed it. The header equals the body's `id`.
3. **Answer within 5 seconds.** Push the event to a per-tenant queue and return `200`. Slow handlers cause retries.

## Pattern 2: one endpoint per customer

When a customer wants events delivered to their own URL, give them an endpoint filtered to their profile, with its own secret:

```bash cURL
curl -X POST https://api.pozy.ai/v1/webhooks/settings \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "cus_8f3a2",
    "url": "https://hooks.acme.example/pozy",
    "secret": "9c2f6b8e1d4a7f3c5e0b2d9a6c8f1e4b",
    "events": ["post.published", "post.failed", "account.disconnected"],
    "profileIds": ["6720f3b1c9e4a85d2f0b7c14"],
    "customHeaders": { "X-Tenant": "cus_8f3a2" }
  }'
```
```ts Node
import { randomBytes } from "node:crypto";
const secret = randomBytes(32).toString("hex");
const res = await fetch("https://api.pozy.ai/v1/webhooks/settings", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: customer.id,
    url: customer.webhookUrl,
    secret,
    events: ["post.published", "post.failed", "account.disconnected"],
    profileIds: [customer.pozyProfileId],
    customHeaders: { "X-Tenant": customer.id },
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
// show `secret` to the customer once so they can verify deliveries
```
```python Python
import os, secrets, requests

secret = secrets.token_hex(32)
resp = requests.post(
    "https://api.pozy.ai/v1/webhooks/settings",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "name": "cus_8f3a2",
        "url": "https://hooks.acme.example/pozy",
        "secret": secret,
        "events": ["post.published", "post.failed", "account.disconnected"],
        "profileIds": ["6720f3b1c9e4a85d2f0b7c14"],
        "customHeaders": {"X-Tenant": "cus_8f3a2"},
    },
)
```

A workspace can have up to 50 endpoints. Beyond that, use pattern 1 and forward to customer URLs from your own queue (pattern 3).

## Endpoint filters

An event is delivered to an endpoint only when it passes every filter the endpoint has:

| Filter | Empty means | Set means |
| --- | --- | --- |
| `events` | (required, at least one) | Only these event types. |
| `profileIds` | Every profile | Only events whose `profiles` include one of these ids. |
| `accountIds` | Every account | Only events about one of these connected accounts. |

`customHeaders` are added to every delivery to that endpoint. They are useful for routing at a gateway, but they are not a substitute for checking the signature.

Change filters later with `PUT /v1/webhooks/settings`, passing the endpoint's `webhookId` and the fields to change.

## Pattern 3: fan out from your own queue

For many customers with their own URLs, or for strict per-customer isolation, receive everything on one endpoint (pattern 1), then deliver to each customer from your own infrastructure. You control retries, per-customer rate limits and the signing secret your customers verify, and a slow customer endpoint can never delay events for the others.

## Account events tie it together

Two events keep your tenant records in step with what customers connect:

- `account.connected`: store `account.accountId` on the customer named by `profiles[0].externalId`.
- `account.disconnected`: mark that account as needing attention and prompt the customer to reconnect.

| Event | When it fires |
| --- | --- |
| [`account.connected`](https://pozy.ai/docs/webhooks/events#account-connected) | Account connected. |
| [`account.rate_limited`](https://pozy.ai/docs/webhooks/events#account-rate-limited) | Mailbox sending cooldown started. |
| [`account.disconnected`](https://pozy.ai/docs/webhooks/events#account-disconnected) | Account disconnected. |
| [`account.permissions_changed`](https://pozy.ai/docs/webhooks/events#account-permissions-changed) | Account permissions changed. |
| [`account.ads.initial_sync_completed`](https://pozy.ai/docs/webhooks/events#account-ads-initial-sync-completed) | Ads initial sync completed. |
| [`account.ads.sync_failed`](https://pozy.ai/docs/webhooks/events#account-ads-sync-failed) | Ads sync failed. |
| [`account.ads.sync_recovered`](https://pozy.ai/docs/webhooks/events#account-ads-sync-recovered) | Ads sync recovered. |

## Testing a route

`POST /v1/webhooks/test` sends a sample event to one endpoint so you can watch it reach the right queue. Test deliveries carry `"test": true` in the body; ignore them in production handlers. Delivery attempts, response codes and bodies are kept in `GET /v1/webhooks/logs`, filterable by endpoint, event and status ([test webhooks](https://pozy.ai/docs/webhooks/testing)).

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/webhooks/settings`](https://pozy.ai/docs/api/getWebhookSettings) | List webhooks |
| [`POST /v1/webhooks/settings`](https://pozy.ai/docs/api/createWebhookSettings) | Create webhook |
| [`PUT /v1/webhooks/settings`](https://pozy.ai/docs/api/updateWebhookSettings) | Update webhook |
| [`DELETE /v1/webhooks/settings`](https://pozy.ai/docs/api/deleteWebhookSettings) | Delete webhook |
| [`GET /v1/webhooks/logs`](https://pozy.ai/docs/api/getWebhookLogs) | List webhook delivery logs |
| [`POST /v1/webhooks/logs/redeliver`](https://pozy.ai/docs/api/redeliverWebhookEvent) | Redeliver a webhook event |
| [`POST /v1/webhooks/test`](https://pozy.ai/docs/api/testWebhook) | Send test webhook |
