Skip to main content
SkedX
Help · Flow · APIs & Developers

Check that a webhook really came from Flow

Anyone who finds your webhook address could send it fake messages.

5 min readFor adminsIn review

Flow signs every delivery with a secret only you and Flow know, so your system can check each one is genuine and unchanged before acting on it. It takes a few lines of code, and the same lines work in any language.

In review: written from the product, not yet walked through live.

1Keep the signing secret safe

In Settings → Developers → Outbound Webhooks — Flow → another app (1) — each webhook you create with + New webhook (2) gets a signing secret, shown once. Your webhooks are listed below (3); their addresses work like passwords, so they’re blurred here. Store the secret where your system keeps secrets — for example a setting called FLOW_WEBHOOK_SECRET — never in your code. If you lose it, rotate it (step 5).
The Outbound Webhooks screen: Flow → another app (1), + New webhook (2) and a webhook in the list (3), with every address blurred

Open Outbound Webhooks in SkedX Flow

2Read the headers on each delivery

Every delivery arrives as a POST with these headers:
  • X-Webhook-Signature — the signature to check.
  • X-Webhook-Signature-Previous — the same, made with your old secret, for 24 hours after you rotate.
  • X-Flow-Event — what happened, such as message.received.
  • X-Flow-Delivery — this delivery’s id. A retry of the same delivery keeps the same id.

3Compute the signature from the raw body

The signature is an HMAC-SHA256 of the exact bytes Flow sent, keyed with your secret, written as hex. Compute it from the raw body before your framework parses the JSON — parsing and re-writing it changes the bytes, and the check fails. Then compare the two in constant time, never with ==.
Node.js
// Node.js — rawBody is a Buffer of the request body, sig the X-Webhook-Signature header
const crypto = require('crypto');
const expected = crypto
  .createHmac('sha256', process.env.FLOW_WEBHOOK_SECRET)
  .update(rawBody)
  .digest('hex');
// timingSafeEqual throws when lengths differ — check first
const ok = sig.length === expected.length &&
  crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
Python
# Python — raw_body is bytes, sig the X-Webhook-Signature header
import hashlib, hmac, os
expected = hmac.new(os.environ["FLOW_WEBHOOK_SECRET"].encode(), raw_body,
                    hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, sig)
PHP
<?php // PHP
$rawBody  = file_get_contents('php://input');
$sig      = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $rawBody, getenv('FLOW_WEBHOOK_SECRET'));
$ok       = hash_equals($expected, $sig);

4See it pass — and fail

Here is the Python check running on a signed sample. The right signature passes (1). A wrong signature fails (2) — and so does a body that changed by a single word on the way (3), which is the point: a tampered message never looks genuine. Answer anything that fails with 401 and do nothing else.
The Python check run three times: the right signature gives ok: True (1), a wrong signature gives ok: False (2), and a changed body gives ok: False (3)

5Rotate the secret without missing anything

Open the webhook and click Rotate secret. Flow shows the new secret once, and for the next 24 hours sends both signatures: X-Webhook-Signature with the new secret and X-Webhook-Signature-Previous with the old one. Accept a match on either while you switch your system to the new secret; after 24 hours only the new one is sent.

6Ignore repeats, answer fast

If your system doesn’t answer with a 2xx within 2 seconds, Flow sends the same delivery again — after 30 seconds, 5 minutes, 30 minutes and 2 hours. Save each X-Flow-Delivery id you’ve handled and skip ones you’ve seen, so a retry never runs twice. Reply 200 first, then do slow work in the background.

© 2026 SkedX · Sample data only · Share and link freely · Copying the product or its design is not allowed · Terms

Common questions

My check always fails. What’s wrong?

Almost always the body: your framework parsed the JSON and you signed the re-written text. Sign the raw bytes as received. Also check the secret has no extra space or line break, and that you compare against the header’s exact value.

Is the signature sent as sha256=…?

No — X-Webhook-Signature is the bare hex digest. (Inbound hooks are the other way round: there, your app sends X-Flow-Signature: sha256=… to Flow.)

Can someone replay an old, genuine delivery at me?

The signature proves Flow sent those exact bytes, not when. Keep the X-Flow-Delivery ids you’ve handled and skip repeats — that stops both Flow’s own retries and a replayed copy from running twice.

What happens if my endpoint is down for a while?

Flow retries each delivery five times in all over about two and a half hours, then keeps it in the webhook’s delivery log, where Send again replays it. See Get notified with outbound webhooks.
Next in APIs & DevelopersConnect Flow to Zapier →

Useful links

Previous: Get notified with outbound webhooks · All APIs & Developers guides

Did this guide help?

View as markdown

Keep reading

READY WHEN YOU ARE

One record
Start there

Free plan, no card. Five tools replaced in an afternoon; your data stays yours.

pick a slot. we call you, on your clock.
pick a slot.
we call you,
on your clock.

No card. We only use your details to set you up or call you back.

A 20-minute call with a real person, on your clock.
SetupUnder 5 min
CardNot required
Replaces5 tools
Your dataExport anytime