Webhooks
A webhook is a URL that starts an automation when another service sends it a request. Add a Webhook trigger to an automation to get one.
The Webhook URL
Every webhook URL has this form:
https://api.work.studio/api/v1/workflow/webhook/ingress/<endpoint>
The <endpoint> part is generated for you and can't be changed. Send the body as JSON with Content-Type: application/json.
Signing Requests
Every new webhook is signed. Each request must carry a timestamp and an HMAC signature made with the webhook's signing secret. A request without them gets 401 {"ERROR":"Unauthorized"}.
The Signing Secret
The signing secret is shown once, when you create the webhook trigger, with a Copy button.
Important
Copy the signing secret when it's shown. It's stored encrypted and you won't see it again. If you lose it, open the trigger in Flow and select Rotate Secret next to Signing secret.
- A trigger's secret can be fetched once, by the trigger's creator or an admin, within 1 hour of creating it. Flow does this for you when you create the trigger.
- Created the trigger through the API or an assistant? Open it in Flow and select Rotate Secret to get a secret.
- After a rotation, the previous secret keeps working for 24 hours, so you have time to update the sending service.
How to Sign
- Take the current time in Unix seconds, for example
1761206400. This is the timestamp. - Build the string
<timestamp>.<raw body>: the timestamp, a dot, then the exact bytes of the body you send. - Compute the HMAC-SHA256 of that string, keyed with the signing secret, as lowercase hex.
- Send two headers:
X-Webhook-Timestamp: 1761206400
X-Webhook-Signature: sha256=3f8a…
BODY='{"orderId": "A-1001", "total": 129.00}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | sed 's/^.* //')
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-H "X-Webhook-Timestamp: $TS" \
-H "X-Webhook-Signature: sha256=$SIG" \
-d "$BODY"
import hashlib, hmac, json, os, time, requests
body = json.dumps({"orderId": "A-1001", "total": 129.00}).encode()
timestamp = str(int(time.time()))
sig = hmac.new(
os.environ["WEBHOOK_SECRET"].encode(),
timestamp.encode() + b"." + body,
hashlib.sha256,
).hexdigest()
r = requests.post(
os.environ["WEBHOOK_URL"],
data=body,
headers={
"Content-Type": "application/json",
"X-Webhook-Timestamp": timestamp,
"X-Webhook-Signature": f"sha256={sig}",
},
timeout=30,
)
print(r.status_code, r.text)
import crypto from 'node:crypto';
const body = JSON.stringify({ orderId: 'A-1001', total: 129.0 });
const timestamp = Math.floor(Date.now() / 1000).toString();
const sig = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(`${timestamp}.${body}`)
.digest('hex');
const res = await fetch(process.env.WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Timestamp': timestamp,
'X-Webhook-Signature': `sha256=${sig}`,
},
body,
});
console.log(res.status, await res.text());
Signing Rules
- Sign the exact bytes you send. Re-serializing the JSON after signing changes the bytes, and the signature fails.
- The timestamp must be within 5 minutes (300 seconds) of the server's clock, either way. An older one is refused, so a captured request can't be sent again later.
- The signature can be hex or base64. The
sha256=prefix is optional.
Other Senders
GitHub, Stripe, Slack and Shopify sign requests their own way. In the trigger's Signature verification settings, choose the service as the Sender:
Authentication
Authentication is a credential the sender sends in addition to the signature:
A missing or wrong credential gets 401. If you set Authentication, send its header together with the signature headers.
Webhooks that had "Require Authentication" on
A webhook that had the old Require Authentication switch on is now a work.studio API key webhook: senders must send a valid X-API-Key. An Authorization header on its own is refused with 401.
Turning Signing Off
You can turn Signature verification off for a sender that can't sign. Then anyone who has the URL can start the automation, unless you also set Authentication or an IP allowlist.
What the Sender Gets Back
The response doesn't include the automation's output.