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

# Webhooks

> Deliver signed project events to an HTTPS endpoint.

Webhooks send selected Polpo project events to your backend as signed HTTP `POST` requests.

## Create a webhook

<Tabs>
  <Tab title="Dashboard">
    Open the project, then go to **Project Settings > Webhooks** and add an HTTPS endpoint.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    curl -X POST "$POLPO_URL/v1/webhooks" \
      -H "Authorization: Bearer $POLPO_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://example.com/polpo-webhook",
        "events": ["task:*", "mission:completed"]
      }'
    ```
  </Tab>
</Tabs>

The `201` response has this shape:

```json theme={null}
{
  "ok": true,
  "data": {
    "id": "9d4b3c54-5788-4adc-9dbe-f5ef9af2ad9c",
    "url": "https://example.com/polpo-webhook",
    "events": ["task:*", "mission:completed"],
    "created_at": "2026-07-13T10:05:00.000Z",
    "secret": "64-hex-character-signing-secret"
  }
}
```

<Warning>
  The signing secret is returned only when the webhook is created or its secret is rotated. Store it before discarding the response.
</Warning>

If `events` is omitted, it defaults to `["*"]`.

## Event filters

Filters support exact event names, category wildcards such as `task:*`, and the global `*` wildcard. Combine multiple filters in the `events` array.

The event catalog is generated from the runtime and can change as event types are added. Read the current public catalog instead of maintaining a hard-coded list:

```bash theme={null}
curl https://api.polpo.sh/v1/events/catalog
```

## Payload

Every delivery body contains the event name, event-specific data, and an ISO timestamp:

```json theme={null}
{
  "event": "task:transition",
  "data": {
    "taskId": "task_123",
    "from": "in_progress",
    "to": "done"
  },
  "timestamp": "2026-07-13T10:05:00.000Z"
}
```

The delivery ID is not part of the JSON body. It is sent in a header.

## Headers and signature

Each request includes:

| Header                | Value                            |
| --------------------- | -------------------------------- |
| `X-Polpo-Signature`   | `t=<unix-seconds>,v1=<hex-hmac>` |
| `X-Polpo-Event`       | Event name                       |
| `X-Polpo-Delivery-Id` | Delivery UUID                    |
| `User-Agent`          | `Polpo-Webhook/1.0`              |

The signature is HMAC-SHA256 over `<timestamp>.<raw-request-body>`. Verify the raw bytes before parsing JSON and compare signatures in constant time:

```typescript theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyPolpoWebhook(
  rawBody: string,
  signatureHeader: string,
  secret: string,
): boolean {
  const fields = Object.fromEntries(
    signatureHeader.split(",").map((part) => part.split("=", 2)),
  );
  const timestamp = fields.t;
  const received = fields.v1;
  if (!timestamp || !received) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const expectedBytes = Buffer.from(expected, "hex");
  const receivedBytes = Buffer.from(received, "hex");
  return (
    expectedBytes.length === receivedBytes.length &&
    timingSafeEqual(expectedBytes, receivedBytes)
  );
}
```

Also reject timestamps outside a short tolerance window in your application to limit replay attacks.

## Delivery and retries

A `2xx` response marks an attempt successful. Each request has a 10-second timeout.

Polpo retries network failures, timeouts, HTTP `408`, HTTP `429`, and `5xx` responses. Backoff is exponential with jitter and is capped at one hour. A delivery is attempted at most five times. Other `4xx` responses are treated as permanent failures.

Every attempt is persisted with status, status code, truncated response body, error, duration, attempt number, and next retry time.

## Delivery operations

List recent attempts, newest first:

```bash theme={null}
curl "$POLPO_URL/v1/webhooks/{webhook-id}/deliveries?limit=50" \
  -H "Authorization: Bearer $POLPO_API_KEY"
```

Use the returned `next_cursor` as the `before` query parameter for the next page.

Queue a new delivery from a past payload:

```bash theme={null}
curl -X POST \
  "$POLPO_URL/v1/webhooks/{webhook-id}/deliveries/{delivery-id}/redeliver" \
  -H "Authorization: Bearer $POLPO_API_KEY"
```

Rotate the signing secret:

```bash theme={null}
curl -X POST "$POLPO_URL/v1/webhooks/{webhook-id}/rotate-secret" \
  -H "Authorization: Bearer $POLPO_API_KEY"
```

Rotation returns the new secret once. New deliveries, including manual redeliveries, use the current secret.
