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:
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" }'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);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()){ "success": true, "message": "Test post.published delivered (200)." }{ "success": false, "message": "No response within 5s" }How test deliveries behave:
- They go out whatever the endpoint's
events,profileIdsandaccountIdssay, so you can test before subscribing. - They are signed and carry the same headers as real deliveries.
- The body has
"test": trueand a new randomidevery 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/logswithtest: 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.
{
"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:
# Start your receiver on port 3001, then in another terminal:
cloudflared tunnel --url http://localhost:3001
# or: ngrok http 3001Create a separate endpoint for development rather than editing the production one, and give it its own secret:
{
"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:
- Find the delivery in
GET /v1/webhooks/logs, filtered byeventoreventId. - Call
POST /v1/webhooks/logs/redeliverwith theeventIdand your dev endpoint'swebhookId.
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.
#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:
// 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);
});# 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 == 401Send 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 |
Send test webhook |
GET /v1/webhooks/logs |
List webhook delivery logs |
POST /v1/webhooks/logs/redeliver |
Redeliver a webhook event |
POST /v1/webhooks/settings |
Create webhook |