Start a workflow
Webhooks
Give a workflow a URL. Anything that can send an HTTP request can start it, with its payload as the first step's input.
A webhook starts a run when something posts to its URL. Payment providers, form tools, your own backend, a script on a laptop: if it can send an HTTP request, it can start a workflow.
Setting one up
Add the webhook step
Put the Webhook step at the start of the workflow. It shows the URL to call and the workflow's signing secret.
Give the URL to the sender
Paste it wherever the other system asks for a webhook or callback URL.
Read the payload
The request body arrives as the webhook step's output: {{webhook.data.body}} is the whole payload exactly as it was sent, and {{webhook.data.body.orderId}} a field of it.
Signing requests
Each project has its own signing secret. When a webhook requires a signature, a request without a valid one is refused before a run starts, so nobody who only knows the URL can start your workflow.
The signature is an HMAC-SHA256 of the exact bytes of the body, keyed with the secret, sent as hex in the X-Webhook-Signature header with a sha256= prefix.
BODY='{"orderId":"A-1029","amount":49.95}'
SECRET='your-signing-secret'
# The digest is over exactly the bytes that get sent,
# so the body is built once and used twice.
SIG=$(printf %s "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed "s/^.* //")
curl -X POST 'https://your-webhook-url' \
-H 'Content-Type: application/json' \
-H "X-Webhook-Signature: sha256=$SIG" \
-d "$BODY"const crypto = require("node:crypto")
const url = "https://your-webhook-url"
const secret = "your-signing-secret"
// Serialise once. Signing an object and posting a re-serialised
// copy is how this goes wrong: the bytes differ and the 401
// looks like a bad secret.
const body = JSON.stringify({ orderId: "A-1029", amount: 49.95 })
const signature = crypto.createHmac("sha256", secret).update(body).digest("hex")
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Webhook-Signature": "sha256=" + signature,
},
body,
}).then(async (response) => console.log(response.status, await response.text()))import hashlib
import hmac
import json
import requests
url = "https://your-webhook-url"
secret = "your-signing-secret"
# data=, not json=: the signature must be over the exact bytes sent.
body = json.dumps({"orderId": "A-1029", "amount": 49.95}, separators=(",", ":"))
signature = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest()
response = requests.post(
url,
data=body,
headers={
"Content-Type": "application/json",
"X-Webhook-Signature": "sha256=" + signature,
},
)
print(response.status_code, response.text)Sign the body exactly as you send it. Most "invalid signature" errors come from signing one serialisation of the JSON and sending another, for example with different spacing.
Using an API key instead
A webhook can also be called with its API key in the Authorization header, as Bearer va_…. Use this when the sender can set a header but cannot compute a signature.
The response
Send Accept: application/json for a JSON response, or Accept: text/plain for a readable, terminal-style one, which is handy when calling from a shell.
Limits
Webhooks are rate limited per webhook, in front of the signature check, so a flood of bad requests cannot run up work or cost. Each call that gets through starts its own run; ten calls are ten runs, side by side.
Was this page helpful?