Webhooks

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:

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" }'
Your endpoint answered 2xx
{ "success": true, "message": "Test post.published delivered (200)." }
It did not
{ "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.

webhook.test
{
  "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:

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:

POST /v1/webhooks/settings
{
  "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.

#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);
});

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