Sherman Web Design Docs: switch app
Open the studio

Webhooks with flows

A webhook is one service telling another that something happened, with a POST to a URL. Flows handle both directions: they can send a webhook when your records change, and they can receive webhooks from services like Stripe, Planning Center, Slack or GitHub.

Send a webhook when records change

Give a POST flow an on_record_change trigger and an http_request step. The flow then runs by itself after records in the collections (or form entries) you name are created, changed or deleted, and POSTs the change to the other service. How triggers work, which values the flow sees and their limits are on Run a flow when records change.

{ "name": "notify-crm", "method": "POST",
  "on_record_change": { "collections": ["people"], "events": ["create", "update"] },
  "steps": [
    { "type": "http_request", "url": "https://hooks.example-crm.com/incoming",
      "headers": { "Authorization": "Bearer {{secret.CRM_TOKEN}}" },
      "body": { "event": "{{trigger.event}}", "id": "{{trigger.record_id}}", "person": "{{trigger.record}}" } }
  ] }
  • Where it can go. The URL must be https and its host must be on your site's list of allowed domains.
  • Secrets. Never type a key into a step. Save it as a site secret and write {{secret.NAME}} in the request's headers or body. Each secret lists the hosts it may be sent to (for example hooks.example-crm.com); a flow that would send it anywhere else is refused, and so is a redirect to another host.
  • Retries. If the other service fails or answers with an error, the triggered run fails and is tried again after 30 seconds, 2 minutes, 10 minutes and 1 hour (5 tries in all). list_flow_triggers shows each run and its error. Because the whole run is tried again, put the webhook step last, or make the receiver tolerate the same event twice.
  • Just a notification? Add "optional": true: a failure then never stops the flow (later steps can check {{steps.<id>.ok}}), and if the secret hasn't been set yet the request is skipped. Optional requests are not retried. In a flow a visitor calls (a form, say), "background": true together with optional sends the request after the visitor gets their answer, so a slow service never makes them wait.

Receive webhooks

Every POST flow has its own address, https://<your site>/api/flows/<flow name>. Paste it into the other service's webhook settings. In the steps, {{input.*}} is the parsed body, {{request.raw_body}} the body exactly as sent (up to 256 KB) and {{header.<name>}} any header, written in lowercase. Receiving needs no allowed-domain entry. Senders don't send cookies, so keep webhook flows free of cookies and sessions.

Anyone can POST to that address, so make verify_hmac the first step. It checks the sender's signature with the signing secret you saved, and stops a forged or tampered call with 401 {"error":"invalid signature"} before anything else runs. Save the signing secret with set_secret, then paste its real value in the studio under the site's Secrets.

verify_hmac fields

FieldWhat it does
keyExactly "{{secret.NAME}}".
key_encoding, key_prefixHow the secret's text becomes the key: utf8 (default), base64 or hex, after removing key_prefix (e.g. "whsec_") from the start.
signatureWhere the signature is, usually a header: "{{header.x-signature}}".
prefixText removed from the start of each signature, e.g. "sha256=".
encodingThe signature's format: hex (default, any case) or base64.
signature_separator, signature_key, signature_kv_separatorFor a header holding a list: split on the separator (up to 10 entries), keep only entries with this key, key and value split by = (default) or another separator. Any matching entry passes, so senders rotating secrets keep working.
timestamp or timestamp_keyWhere the sender's timestamp is: a template such as a header, or an entry of the signature list (e.g. t). Use one, not both. It is then {{hmac.timestamp}} in message.
tolerance_seconds1–86400. Calls whose timestamp is further than this from now are refused, so a captured call can't be replayed later. 300 is typical.
messageWhat was signed. Default "{{request.raw_body}}".
algorithmsha256 (default), sha1 or sha512.
on_failInstead of the 401: {"status": 400-499} or {"redirect": ...}.

Examples

A simple sha256= header (GitHub's X-Hub-Signature-256):

{ "type": "verify_hmac", "key": "{{secret.GITHUB_WEBHOOK_SECRET}}",
  "signature": "{{header.x-hub-signature-256}}", "prefix": "sha256=" }

Planning Center sends the plain hex signature, with no prefix, in X-PCO-Webhooks-Authenticity, keyed by the subscription's authenticity secret:

{ "type": "verify_hmac", "key": "{{secret.PCO_WEBHOOK_SECRET}}",
  "signature": "{{header.x-pco-webhooks-authenticity}}" }

Stripe (Stripe-Signature: t=…,v1=…; the whsec_… secret is used as text, as is):

{ "type": "verify_hmac", "key": "{{secret.STRIPE_WEBHOOK_SECRET}}",
  "signature": "{{header.stripe-signature}}", "signature_separator": ",", "signature_key": "v1",
  "timestamp_key": "t", "message": "{{hmac.timestamp}}.{{request.raw_body}}", "tolerance_seconds": 300 }

Standard Webhooks / Svix (webhook-signature: v1,<base64>; the whsec_ secret is base64):

{ "type": "verify_hmac", "key": "{{secret.WEBHOOK_SECRET}}", "key_prefix": "whsec_", "key_encoding": "base64",
  "signature": "{{header.webhook-signature}}", "signature_separator": " ", "signature_key": "v1",
  "signature_kv_separator": ",", "encoding": "base64", "timestamp": "{{header.webhook-timestamp}}",
  "message": "{{header.webhook-id}}.{{hmac.timestamp}}.{{request.raw_body}}", "tolerance_seconds": 300 }

Slack (X-Slack-Signature: v0=…):

{ "type": "verify_hmac", "key": "{{secret.SLACK_SIGNING_SECRET}}",
  "signature": "{{header.x-slack-signature}}", "prefix": "v0=",
  "timestamp": "{{header.x-slack-request-timestamp}}",
  "message": "v0:{{hmac.timestamp}}:{{request.raw_body}}", "tolerance_seconds": 300 }

After verify_hmac, the rest is an ordinary flow: parse_json, store_record, send_email and so on, ending with respond {"ok": true}. Most senders retry until they get a 200, so answer 200 once an event is handled, and never before the signature check. For another provider, look up in its docs which secret, header, list format, timestamp and signed text it uses, and test with a signed sample first.

No separate webhooks screen

There's no webhooks settings page or delivery log: flows are how webhooks are done. For outgoing ones, list_flow_triggers shows recent runs, tries and errors; for incoming ones, the sending service's own dashboard shows what it sent and what your flow answered.

Pricing

Each flow run, incoming call or triggered run (each try), is 3.01 credits per 1,000, like any flow call. The http_request itself costs nothing extra.

Updated October 31, 2026