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.

  • 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

  1. Take the current time in Unix seconds, for example 1761206400. This is the timestamp.
  2. Build the string <timestamp>.<raw body>: the timestamp, a dot, then the exact bytes of the body you send.
  3. Compute the HMAC-SHA256 of that string, keyed with the signing secret, as lowercase hex.
  4. 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:

Sender Headers it sends Secret
Generic HMAC-SHA256 (default) X-Webhook-Signature, X-Webhook-Timestamp work.studio's signing secret
GitHub X-Hub-Signature-256 work.studio's signing secret, pasted into GitHub
Stripe Stripe-Signature Stripe's whsec_… secret, pasted into work.studio
Slack X-Slack-Signature, X-Slack-Request-Timestamp Your Slack app's Signing Secret, pasted into work.studio
Shopify X-Shopify-Hmac-Sha256 Shopify's secret, pasted into work.studio

Authentication

Authentication is a credential the sender sends in addition to the signature:

Authentication The sender sends
None (default) Nothing extra. The signature and the IP allowlist are the only checks
work.studio API key X-API-Key: svx_tk_…, a key from the same organization and environment that may run automations
Shared secret header A token in a header you name (default X-Webhook-Token)
Bearer token Authorization: Bearer <token>
Basic auth Authorization: Basic … with the user name and password you set
Connection The credential held by a connection

A missing or wrong credential gets 401. If you set Authentication, send its header together with the signature headers.

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

Case Status
The run started (default: Wait until the run has started) 200, with executionId and workflowRunId
Acknowledge immediately, or the run hadn't started within the wait timeout 202, with executionId
Missing or wrong signature, timestamp or credential 401 {"ERROR":"Unauthorized"}
Too many requests in a minute 429
Webhook not active 503

The response doesn't include the automation's output.