# Set up webhooks

Receive an HTTPS POST from Pozy when a post publishes, a message arrives or an account disconnects. Create an endpoint, verify it, and handle your first event.

Webhooks tell your app when something happens instead of making you ask. Pozy sends an HTTPS `POST` with a JSON body to your URL each time an event you subscribed to occurs: a post published, a DM arrived, a review was left, an account needs reconnecting. This page takes you from nothing to a verified endpoint receiving events.

## How it works

1. You run an HTTPS endpoint that accepts `POST` requests.
2. You register it with Pozy, with a secret and the events you want.
3. Pozy signs each delivery with your secret and sends it. Your endpoint checks the signature, stores the event and answers `2xx` within 5 seconds.
4. If it does not answer `2xx` in time, Pozy tries again, up to 7 attempts in total. See [Retries](https://pozy.ai/docs/webhooks/retries).

## Build a receiver

Your handler needs the **raw** request body to verify the signature, so read the bytes before any JSON parsing.

```ts Node
// npm install express
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.POZY_WEBHOOK_SECRET!;
const queue: { id?: string; type: string; payload: unknown }[] = []; // stand-in for your job queue

app.post("/webhooks/pozy", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.get("X-Pozy-Signature") ?? "";
  const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
  const valid = signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  if (!valid) return res.status(401).send("bad signature");

  const event = JSON.parse(req.body.toString("utf8"));
  // Store it and hand it to a background job; answer quickly.
  queue.push({ id: req.get("X-Pozy-Event-Id"), type: event.event, payload: event });
  res.sendStatus(200);
});

app.listen(3001);
```
```python Python
# pip install flask
import hashlib
import hmac
import json
import os

from flask import Flask, request

app = Flask(__name__)
SECRET = os.environ["POZY_WEBHOOK_SECRET"].encode()

def enqueue(event_id, event_type, payload):
    ...  # stand-in for your job queue

@app.post("/webhooks/pozy")
def pozy_webhook():
    raw = request.get_data()  # bytes, before any parsing
    expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("X-Pozy-Signature", "")):
        return "bad signature", 401

    event = json.loads(raw)
    # Store it and hand it to a background job; answer quickly.
    enqueue(request.headers["X-Pozy-Event-Id"], event["event"], event)
    return "", 200
```

The [Signatures](https://pozy.ai/docs/webhooks/signatures) page covers the check in detail, and [Idempotency and ordering](https://pozy.ai/docs/webhooks/idempotency) shows how to make the background job safe to run twice.

## Register the endpoint

Generate a long random secret, keep it in your server's environment, and create the endpoint with `POST /v1/webhooks/settings`:

```bash Terminal
export POZY_WEBHOOK_SECRET="$(openssl rand -hex 32)"
```

```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": "Production",
    "url": "https://app.example.com/webhooks/pozy",
    "secret": "'"$POZY_WEBHOOK_SECRET"'",
    "events": ["post.published", "post.partial", "post.failed", "account.disconnected", "message.received"]
  }'
```
```ts Node
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: "Production",
    url: "https://app.example.com/webhooks/pozy",
    secret: process.env.POZY_WEBHOOK_SECRET,
    events: ["post.published", "post.partial", "post.failed", "account.disconnected", "message.received"],
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);

console.log(data.webhook._id);
```
```python Python
import os
import requests

resp = requests.post(
    "https://api.pozy.ai/v1/webhooks/settings",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "name": "Production",
        "url": "https://app.example.com/webhooks/pozy",
        "secret": os.environ["POZY_WEBHOOK_SECRET"],
        "events": ["post.published", "post.partial", "post.failed", "account.disconnected", "message.received"],
    },
    timeout=30,
)
print(resp.json()["webhook"]["_id"])
```

```json
{
  "success": true,
  "webhook": {
    "_id": "whk_4Tq9Lm2Vx8Rb6Nc1Pz7K",
    "name": "Production",
    "url": "https://app.example.com/webhooks/pozy",
    "events": ["post.published", "post.partial", "post.failed", "account.disconnected", "message.received"],
    "isActive": true,
    "profileIds": [],
    "accountIds": [],
    "customHeaders": {},
    "failureCount": 0,
    "lastFiredAt": null
  }
}
```

> **Always set a secret:**
> 
> Pozy signs deliveries with the secret you choose and never generates one for you. An endpoint without a secret receives unsigned requests that anyone who knows the URL could forge.

Then send a test event to check the whole path: `POST /v1/webhooks/test` with the `webhookId`. See [Test webhooks](https://pozy.ai/docs/webhooks/testing).

## Endpoint settings

| Field | Required | What it does |
| --- | --- | --- |
| `name` | Yes | A label for you, 1 to 50 characters. |
| `url` | Yes | Where deliveries go. Use HTTPS in production. Redirects are not followed, so give the final URL. |
| `events` | Yes | At least one event name. Unknown names are rejected with `code: "invalid_event"`. See the [events reference](https://pozy.ai/docs/webhooks/events). |
| `secret` | No, but set it | The key for the `X-Pozy-Signature` HMAC. |
| `profileIds` | No | Only send events that belong to these profiles. Empty means every profile. |
| `accountIds` | No | Only send events about these connected accounts. Empty means every account. |
| `customHeaders` | No | Extra headers added to every delivery, for example a static token your gateway expects: `{ "X-Gateway-Token": "..." }`. |
| `isActive` | No | `false` pauses deliveries without deleting the endpoint. Defaults to `true`. |

A workspace can have up to 50 endpoints. Use several when different services care about different events, for example one for publishing results and one for the inbox.

## Send events to the right place

By default an endpoint receives matching events for the whole workspace. Two filters narrow it:

- **`profileIds`** keeps events whose profile is in the list. A post event matches when any account it targets belongs to one of those profiles.
- **`accountIds`** keeps events about one of those connected accounts.

With both set, an event must match both. Events that belong to no profile (workspace-level events, for example) only reach endpoints with no `profileIds` filter.

A common use is staging: put your test accounts in their own profile and point a staging endpoint at it with `profileIds`. Prefer profile filters to account filters, because a profile id stays the same when its account is reconnected.

If you run a platform with one profile per customer, every event also carries `profiles[].externalId`, your own customer id, so one endpoint can route to every tenant without a lookup. See [Route webhooks to tenants](https://pozy.ai/docs/build/route-webhooks).

## What a delivery looks like

```http
POST /webhooks/pozy HTTP/1.1
Host: app.example.com
Content-Type: application/json
User-Agent: Pozy-Webhooks/1.0
X-Pozy-Event: post.published
X-Pozy-Event-Id: 8e0b6f8a-3c51-4b7e-9d0a-2f4c6e1b9a77
X-Pozy-Delivery-Id: dlv_Qm3Zt8Kp1Wx6Bn4Rc9Ly
X-Pozy-Attempt: 1
X-Pozy-Signature: 3f6c1d0b9a7e5c2f4d8b6a1e0c9f7d5b3a2e4c6f8d0b1a3c5e7f9d2b4a6c8e0f
```

```json
{
  "id": "8e0b6f8a-3c51-4b7e-9d0a-2f4c6e1b9a77",
  "event": "post.published",
  "timestamp": "2026-11-03T08:30:04.000Z",
  "profiles": [{ "id": "670f9b1d4c2e8a7f3b6d5e10", "externalId": "cust_4821" }],
  "post": {
    "id": "6721a8c04e9b2f1d7c3a5e88",
    "content": "Scheduled exports are live.",
    "status": "published",
    "scheduledFor": "2026-11-03T08:30:00.000Z",
    "publishedAt": "2026-11-03T08:30:03.000Z",
    "platforms": [
      {
        "platform": "linkedin",
        "status": "published",
        "accountId": "6710c3e2a9f14b0d8e5c7a21",
        "platformPostId": "urn:li:share:7259012345678901234",
        "publishedUrl": "https://www.linkedin.com/feed/update/urn:li:share:7259012345678901234"
      }
    ],
    "metadata": { "campaignId": "cmp_2041" }
  }
}
```

| Header | Meaning |
| --- | --- |
| `X-Pozy-Event` | The event name, same as `event` in the body. |
| `X-Pozy-Event-Id` | The event's id, same as `id` in the body. Identical on every retry and redelivery, so use it to deduplicate. |
| `X-Pozy-Delivery-Id` | This delivery. A redelivery of the same event gets a new delivery id. |
| `X-Pozy-Attempt` | Which attempt this is, from 1 to 7. |
| `X-Pozy-Signature` | Hex HMAC-SHA256 of the raw body, keyed with your secret. Sent when the endpoint has a secret. |

Every body has `id`, `event` and `timestamp`. Events that belong to a profile add `profiles`, and the rest of the body depends on the event: `post`, `account`, `message`, `comment` and so on. Test deliveries add `"test": true`.

## Answer fast

Pozy waits 5 seconds for a response. Do the minimum inside the request: check the signature, write the event to a table or queue, answer `200`. Do the real work (updating your database, calling other services, sending email) in a background job. An endpoint that does slow work inline will time out, get retried, and process the same event several times.

Any `2xx` status counts as delivered. The body of your response is ignored, but the first 2,000 characters are kept in the [delivery logs](https://pozy.ai/docs/webhooks/retries#read-the-delivery-logs) to help you debug.

## Manage endpoints

| Task | Call |
| --- | --- |
| List endpoints, with their secrets and failure counts | `GET /v1/webhooks/settings` |
| Change events, URL, filters or secret | `PUT /v1/webhooks/settings` with `webhookId` and the fields to change |
| Pause or resume | `PUT /v1/webhooks/settings` with `isActive` |
| Delete | `DELETE /v1/webhooks/settings?webhookId=whk_...` |

Managing endpoints needs a workspace owner or admin in the dashboard, or an API key that has not disabled the `webhooks` resource group.

## 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 |
