Webhook triggers
Start an Agent Program from Zapier, Make, n8n, a form tool or your own code with its webhook URL, optional signing, and the responses to expect.
On this page
A webhook trigger gives a program its own URL. Each POST to it starts one run, with the request's body as the run's input. Use it when another app should start the program: Zapier, Make, n8n, a form tool such as Tally, Cal.com, or code you write.
Get the URL
- Ask Willo's chat for a program that runs "when my form posts" or "from Zapier". Willo sets the trigger to webhook.
- Open Agents in the left rail, then the program, then its Trigger card.
- Click Reveal URL and copy it. It is shown once.
Willo's chat never shows the URL, and can't look it up: the chat thread is visible to everyone on the business. If you lose the URL, Rotate URL issues a new one and the old one stops working at once.
The URL has this shape. Its last segment is the secret:
https://api.willo.ai/webhooks/programs/<public id>/<secret>
Send a call
curl -X POST "$WILLO_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"lead": "alice@example.com", "source": "website form"}'- Body: JSON, form-encoded or plain text, up to 64 KB. An empty POST is a plain "go".
Idempotency-Key: optional. Calls with the same key run once. Without a key, the same body sent twice on the same UTC day runs once. Vary one of them to run again.- Response:
202with{"status": "queued"}when a run is queued. Runs start within a few minutes, one at a time per program.
The run's trace on the Agents tab shows the call under Trigger, and the runs list labels it Webhook.
No-code recipes
Leave Require signature off for these: they can't compute a signature.
- Zapier: add a Webhooks by Zapier action, event POST, paste the URL, payload type json, and add the fields the program should see.
- Make: add an HTTP → Make a request module, method POST, the URL, body type Raw, content type JSON, and the JSON body.
- n8n: add an HTTP Request node, method POST, the URL, turn on Send Body, content type JSON, and the fields.
- Tally: in the form's Integrations, add a Webhook and paste the URL. Each submission arrives as JSON with the form's fields.
Signing calls from your own code
For code you write, switch on Require signature on the program's Trigger card. Each call must then carry:
X-Willo-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>
The HMAC key is the URL's last segment. The signed message is the timestamp, a dot, and the exact raw body: <t>.<body>. Willo accepts a timestamp up to five minutes either side of its clock. Sign the same bytes you send.
import crypto from "node:crypto";
const url = process.env.WILLO_WEBHOOK_URL;
const secret = new URL(url).pathname.split("/").pop();
const body = JSON.stringify({ lead: "alice@example.com", source: "website form" });
const t = Math.floor(Date.now() / 1000);
const v1 = crypto.createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
"X-Willo-Signature": `t=${t},v1=${v1}`,
},
body,
});
console.log(res.status, await res.json());import hashlib, hmac, json, os, time, uuid
import urllib.request
url = os.environ["WILLO_WEBHOOK_URL"]
secret = url.rstrip("/").rsplit("/", 1)[-1]
body = json.dumps({"lead": "alice@example.com", "source": "website form"}).encode()
t = int(time.time())
v1 = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
req = urllib.request.Request(url, data=body, method="POST", headers={
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
"X-Willo-Signature": f"t={t},v1={v1}",
})
with urllib.request.urlopen(req) as resp:
print(resp.status, resp.read().decode())Signing stops a captured call being replayed later. It does not protect a leaked URL; rotating does.
Responses
| Status | Meaning |
|---|---|
202 | Queued. One run will start. |
200 | A duplicate of a call already accepted. No second run. |
401 | Wrong secret, or a missing, malformed, expired or invalid signature. |
404 | Unknown or rotated URL. |
409 | The program is paused or still a draft. |
413 | Body over 64 KB. |
415 | Not JSON, form-encoded or plain text. |
429 | Over 60 calls a minute, 20 calls waiting, or the daily limit. Honour Retry-After. |
Every code, with the exact error names, is on Webhook request format.
What asks first
A webhook call is outside content. In every mode, a step that reaches other people, such as sending a message or deploying, asks you first. Research, content work and messages to you run unattended.
Start from Gmail, Slack or Stripe
To run a program when something happens in Gmail, Slack or Stripe, point a no-code tool that watches that app at this webhook. Programs don't use connectors, so the webhook is how the event reaches the program.
Related: Agent Program limits, Email triggers.
Questions
Can a program start when a Stripe payment or a Slack message happens?
Yes, through its webhook. Point a no-code tool that watches Stripe or Slack, or your own system, at the program's webhook URL, and each event starts a run.
What if the webhook URL leaks?
Rotate it on the Agents tab. The old URL stops working at once. Signing limits replayed calls but does not protect a leaked URL.