Reference
Webhook request format
How to call an Agent Program's webhook: the URL, content types, the X-Willo-Signature scheme, idempotency and responses.
On this page
Each Agent Program with a webhook trigger has its own URL. A POST to it starts one run. See Webhook triggers for setup, signing examples and no-code recipes.
Request
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. |