# Test webhooks

Send test events to an endpoint, receive webhooks on your laptop through a tunnel, replay real deliveries and write signed fixtures for your test suite.

You should not have to publish a real post to find out whether your webhook handler works. Pozy can send a sample of any event to your endpoint on demand, and the delivery logs give you real payloads to replay and to use as fixtures.

## Send a test event

`POST /v1/webhooks/test` sends one event to one endpoint, immediately. Without `event` it sends a minimal `webhook.test` event; with an event name it sends a realistic sample of that event:

```bash cURL
curl -X POST https://api.pozy.ai/v1/webhooks/test \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "webhookId": "whk_4Tq9Lm2Vx8Rb6Nc1Pz7K", "event": "post.published" }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/webhooks/test", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ webhookId: "whk_4Tq9Lm2Vx8Rb6Nc1Pz7K", event: "post.published" }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.success, data.message);
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/webhooks/test",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={"webhookId": "whk_4Tq9Lm2Vx8Rb6Nc1Pz7K", "event": "post.published"},
    timeout=30,
)
print(resp.json())
```

```json
{ "success": true, "message": "Test post.published delivered (200)." }
```

```json
{ "success": false, "message": "No response within 5s" }
```

How test deliveries behave:

- They go out whatever the endpoint's `events`, `profileIds` and `accountIds` say, so you can test before subscribing.
- They are signed and carry the same headers as real deliveries.
- The body has `"test": true` and a new random `id` every time.
- They are tried once and never retried, and a failed test does not count toward switching the endpoint off.
- They show up in `GET /v1/webhooks/logs` with `test: true`.

Sample payloads are built from the event's schema, so ids and text are placeholders. Do not let a test event change real data: check `test` first in your handler.

```json
{
  "id": "0c3a8e4f-61b2-4d7a-b5e9-7f2c1d8a6b40",
  "event": "webhook.test",
  "timestamp": "2026-10-30T12:00:00.000Z",
  "test": true
}
```

## Receive webhooks on your laptop

Pozy needs a public URL. While you build, run a tunnel that forwards a public HTTPS address to your local server, then point a development endpoint at it:

```bash Terminal
# Start your receiver on port 3001, then in another terminal:
cloudflared tunnel --url http://localhost:3001
# or: ngrok http 3001
```

Create a separate endpoint for development rather than editing the production one, and give it its own secret:

```json
{
  "name": "Dev (Alex's laptop)",
  "url": "https://quiet-river-1234.trycloudflare.com/webhooks/pozy",
  "secret": "dev-only-secret-change-me",
  "events": ["post.published", "post.failed", "message.received"],
  "profileIds": ["670f9b1d4c2e8a7f3b6d5e10"]
}
```

The `profileIds` filter keeps real customer traffic away from your laptop: connect your test accounts to a dedicated test profile and only that profile's events reach the dev endpoint. Pause the endpoint with `isActive: false` when you stop for the day, so failures do not build up while the tunnel is closed.

## Replay real deliveries

Sample payloads are a starting point. For edge cases, replay something that really happened:

1. Find the delivery in `GET /v1/webhooks/logs`, filtered by `event` or `eventId`.
2. Call `POST /v1/webhooks/logs/redeliver` with the `eventId` and your dev endpoint's `webhookId`.

A redelivery keeps the original event id and body, so it also tests your deduplication: sending the same event twice should change nothing the second time. See [Retries and delivery logs](https://pozy.ai/docs/webhooks/retries#redeliver-an-event).

## Signed fixtures for automated tests

Save `requestPayload` from a log entry as a JSON file and sign it in your tests with the same secret your handler reads:

```ts Node
// webhook.test.ts (vitest or jest)
import crypto from "node:crypto";
import { readFileSync } from "node:fs";
import request from "supertest";
import { app } from "./server";

const secret = "test-secret";
process.env.POZY_WEBHOOK_SECRET = secret;

it("accepts a signed post.published event", async () => {
  const body = readFileSync("fixtures/post.published.json", "utf8");
  const signature = crypto.createHmac("sha256", secret).update(body).digest("hex");

  await request(app)
    .post("/webhooks/pozy")
    .set("Content-Type", "application/json")
    .set("X-Pozy-Event", "post.published")
    .set("X-Pozy-Event-Id", JSON.parse(body).id)
    .set("X-Pozy-Signature", signature)
    .send(body)
    .expect(200);
});

it("rejects a bad signature", async () => {
  await request(app).post("/webhooks/pozy").set("Content-Type", "application/json")
    .set("X-Pozy-Signature", "0".repeat(64)).send("{}").expect(401);
});
```
```python Python
# test_webhook.py (pytest)
import hashlib
import hmac
import json
import os

os.environ["POZY_WEBHOOK_SECRET"] = "test-secret"
from app import app  # noqa: E402

def sign(body: bytes) -> str:
    return hmac.new(b"test-secret", body, hashlib.sha256).hexdigest()

def test_accepts_signed_event():
    body = open("fixtures/post.published.json", "rb").read()
    resp = app.test_client().post(
        "/webhooks/pozy",
        data=body,
        content_type="application/json",
        headers={"X-Pozy-Signature": sign(body), "X-Pozy-Event-Id": json.loads(body)["id"]},
    )
    assert resp.status_code == 200

def test_rejects_bad_signature():
    resp = app.test_client().post("/webhooks/pozy", data=b"{}", content_type="application/json",
                                  headers={"X-Pozy-Signature": "0" * 64})
    assert resp.status_code == 401
```

Send the exact bytes you signed. If your test client re-serialises the JSON, the signature will not match, which is the same mistake to avoid in production.

## Checklist before going live

| Check | How |
| --- | --- |
| Signature verified on the raw body | Bad-signature test returns `401` |
| Handler answers in well under 5 seconds | `responseTime` in the logs |
| Duplicates are ignored | Redeliver the same event twice |
| Test events never touch real data | Handler checks `test` |
| Production endpoint has its own secret | Different from dev and staging |
| Monitoring on `failureCount` | `GET /v1/webhooks/settings` |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`POST /v1/webhooks/test`](https://pozy.ai/docs/api/testWebhook) | Send test 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/settings`](https://pozy.ai/docs/api/createWebhookSettings) | Create webhook |
