# Webhook request format

> How to call an Agent Program's webhook: the URL, content types, the X-Willo-Signature scheme, idempotency and responses.

Generated from Willo's code at each release.

Source: https://www.willo.ai/docs/reference/webhook

Each Agent Program with a webhook trigger has its own URL. A POST to it starts one run. See [Webhook triggers](https://www.willo.ai/docs/agent-programs/webhook-triggers) for setup, signing examples and no-code recipes.

## Request

```http
POST https://api.willo.ai/webhooks/programs/<public_id>/<secret>
Content-Type: application/json
Idempotency-Key: 3f1c2a9e-…

{"lead": "alice@example.com"}
```

| Part | Rule |
| --- | --- |
| URL | Shown once on the Agents tab. The last path segment is the secret; Rotate issues a new URL and the old one stops at once. |
| Content-Type | `application/json`, `application/x-www-form-urlencoded`, `text/plain` |
| Body | Up to 64 KB. An empty POST is a plain "go". |
| `idempotency-key` | Optional. Repeats with the same key run once. Without it, the same body on the same UTC day runs once. Keys over 200 characters are hashed. |
| `x-willo-signature` | Optional, or required when the owner turns signing on. See below. |

## Signature

| Part | Value |
| --- | --- |
| Header | `x-willo-signature: t=<unix seconds>,v1=<hex HMAC-SHA256>` |
| Signed message | `<t>.<raw body>` |
| Key | the URL's last path segment |
| Algorithm | HMAC-SHA256, lowercase hex |
| Window | 5 minutes either side of Willo's clock |

Signing limits replays of a captured call. It does not protect a leaked URL; rotating does.

## What Willo keeps

The parsed body becomes the run's input, with values shaped like secrets removed. Of the request headers only these are kept: `content-type`, `idempotency-key`, `user-agent`, `x-willo-signature`.

## Responses

| Status | Code | Meaning |
| --- | --- | --- |
| 200 | `duplicate` | A repeat of a call already accepted (same Idempotency-Key, or the same body on the same UTC day). No second run. |
| 202 | `queued` | The call was accepted and one run is queued. |
| 400 | `invalid_body` | The body could not be parsed as the Content-Type it declares. |
| 401 | `invalid_secret` | The URL's secret is wrong. |
| 401 | `signature_expired` | The signature's timestamp is outside the allowed window. |
| 401 | `signature_invalid` | The signature does not match the body. |
| 401 | `signature_malformed` | The X-Willo-Signature header is not in the t=<unix>,v1=<hex> format. |
| 401 | `signature_required` | The trigger requires a signature and the call carries no X-Willo-Signature header. |
| 404 | `unknown_trigger` | No active trigger has this URL. It was rotated, revoked, archived, or never existed. |
| 409 | `program_not_active` | The program is paused, still a draft, or no longer uses a webhook trigger. |
| 413 | `payload_too_large` | The body is over the size limit. |
| 415 | `unsupported_media_type` | The Content-Type is not JSON, form-encoded or plain text. |
| 429 | `daily_cap` | The program has reached its runs-a-day limit. Calls run again tomorrow. Sent with `Retry-After`. |
| 429 | `queue_full` | The program has the most events it may hold waiting. Retry after Retry-After. Sent with `Retry-After`. |
| 429 | `rate_limited` | Too many calls in the last minute. Wait for Retry-After. Sent with `Retry-After`. |
| 503 | `unavailable` | Webhook triggers are temporarily unavailable. Retry later, honouring Retry-After when it is sent. |
