> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polymorfa.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Create webhook endpoints, verify signatures, test receivers, rotate secrets, and retry deliveries.

Webhooks send events, such as incoming messages, to your HTTPS endpoint. Manage
endpoints with `webhooks` on a team or project client, and check each request
with the `webhooks` utilities.

| Task | Permission |
| - | - |
| List and read endpoints | `webhooks:read` |
| Create, update, delete, test, rotate secrets | `webhooks:manage` |
| Read deliveries and attempts | `webhook-deliveries:read` |
| Retry a delivery | `webhook-deliveries:retry` |

Project endpoints receive the project's Number events, such as messages and
calls. Team endpoints receive team events, such as customer and campaign
lifecycle events. See [Webhooks](/api/webhooks) for every event type.

## Create an endpoint

```typescript theme={null}
const created = await project.webhooks.create({
  url: "https://example.com/polymorfa/webhooks",
  eventTypes: ["message.received", "session.status"],
});

console.log(created.data.webhook.id);
const secret = created.data.secret; // Shown once. Store it now.
```

Store the signing secret in your secret manager. The API returns it only in this
response. `secretAvailable` is `false` when the response cannot include it.

Change an endpoint with `update(webhookId, { ... })`. Set `enabled: false` to
pause deliveries, and call `delete(webhookId)` to remove it.

## Verify a webhook

Every delivery carries an `x-webhook-signature` header. Verify it against the
exact request bytes before you read the event:

```typescript theme={null}
import { PolymorfaValidationError, WebhookSignatureError, webhooks } from "@polymorfa/sdk";

export default {
  async fetch(request: Request, env: { WEBHOOK_SECRET: string }): Promise<Response> {
    if (request.method !== "POST") return new Response(null, { status: 405 });

    const body = await request.arrayBuffer();
    try {
      const event = await webhooks.verify({
        body,
        signature: request.headers.get("x-webhook-signature") ?? "",
        secret: env.WEBHOOK_SECRET,
      });
      console.log(event.id, event.event);
      return Response.json({ received: event.id });
    } catch (error) {
      if (error instanceof WebhookSignatureError || error instanceof PolymorfaValidationError) {
        return new Response("Invalid webhook", { status: 400 });
      }
      throw error;
    }
  },
};
```

This handler runs as a Cloudflare Worker. The same `webhooks.verify` call works
in any handler that can read the raw body, including Node.js, Deno, and Bun.
Read the body as bytes. Parsing the JSON and serializing it again changes the
bytes and breaks the signature.

`webhooks.verify` rejects a bad signature before it parses the body. Use
`webhooks.verifySignature` to get a `true` or `false` result without parsing.

Delivery retries reuse the event `id`. Store processed IDs and skip repeats.

## Handle each event type

`isEvent` narrows an event to its payload type:

```typescript theme={null}
import { isEvent, type WebhookEvent } from "@polymorfa/sdk";

export function route(event: WebhookEvent) {
  if (isEvent(event, "message.received")) {
    console.log("message", event.session, event.payload);
  } else if (isEvent(event, "message.failed")) {
    console.log("failed", event.payload.error, event.payload.code);
  } else if (isEvent(event, "campaign.stopped")) {
    console.log("campaign stopped", event.payload.campaignId);
  } else {
    console.log("other event", event.event);
  }
}
```

Event types the installed SDK does not know still verify. Their payload is not
typed.

## Send a test delivery

```typescript theme={null}
const test = await project.webhooks.test(
  "<webhook-id>",
  { eventType: "message.received" },
  { idempotencyKey: crypto.randomUUID() },
);

console.log(test.data.eventId, test.data.deliveryId);
```

On a team client, `test` accepts only `eventType`. On a project client, it also
accepts a base64-encoded native event `body` together with the `sessionId` of a
Number in that project. See [Send a test delivery](/api/webhooks#send-a-test-delivery).

## Test your receiver without the API

`webhooks.createFixture` signs an event body locally, with no API call. Use it
in unit tests:

```typescript theme={null}
import { webhooks } from "@polymorfa/sdk";

const secret = "test-signing-secret";
const fixture = await webhooks.createFixture({
  event: {
    id: "evt_test_1",
    session: "number_test",
    timestamp: "2026-09-27T00:00:00Z",
    event: "session.status",
    payload: { status: "connected" },
  },
  secret,
});

// Send fixture.body with fixture.headers to your handler under test.
const event = await webhooks.verify({
  body: fixture.body,
  signature: fixture.headers["x-webhook-signature"],
  secret,
});
console.log(event.event); // "session.status"
```

Pass `fixture.body` to your handler as is. Use a test secret, never a
production one.

Fixtures cover the native format only. Endpoints that use the Meta format are
signed with the `X-Hub-Signature-256` header. See
[Delivery formats](/api/webhooks#delivery-formats).

## Rotate a signing secret

```typescript theme={null}
const rotated = await project.webhooks.rotateSecret("<webhook-id>", {
  overlapSeconds: 3600,
});

const newSecret = rotated.data.secret;
console.log(rotated.data.secretMetadata.previousValidUntil);
```

The response returns the new secret once. The previous secret stays valid until
`previousValidUntil`. Deploy the new secret to your receiver before then.

## Inspect and retry deliveries

```typescript theme={null}
for await (const delivery of await project.webhookDeliveries.list({ status: "failed" })) {
  console.log(delivery.id, delivery.eventId, delivery.lastOutcome?.statusCode);

  if (delivery.capabilities.retryable) {
    await project.webhookDeliveries.retry(delivery.id);
  }
}
```

`listAttempts(deliveryId)` and `retrieveAttempt(deliveryId, attemptId)` show each
attempt. A failed attempt includes a redacted excerpt of your endpoint's
response. To send an older event again, see
[Replay an event](/sdks/typescript/events#replay-an-event).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.