# Check that a webhook really came from Flow

> SkedX Flow signs every outbound webhook: the X-Webhook-Signature header is the hex HMAC-SHA256 of the raw request body, keyed with the webhook’s signing secret (shown once when the webhook is created). To verify, compute the HMAC over the exact bytes received — before parsing the JSON — and compare in constant time (Node crypto.timingSafeEqual, Python hmac.compare_digest, PHP hash_equals). After “Rotate secret”, Flow also sends X-Webhook-Signature-Previous, signed with the old secret, for 24 hours — accept a match on either. X-Flow-Delivery is a stable id per delivery: retries reuse it, so skip ids already handled. X-Flow-Event names the event. Answer with any 2xx within 2 seconds.

Product: SkedX Flow · Topic: APIs & Developers · Draft — steps not yet re-verified in the live product

Anyone who finds your webhook address could send it fake messages. 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.

## Step 1: Keep the signing secret safe

In **Settings -> Developers -> Outbound Webhooks** — **Flow -> another app** — each webhook you create with **+ New webhook** gets a **signing secret**, shown once. Your webhooks are listed below; 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).
Open it directly: https://flow.skode.ai/settings/?tab=webhooks

## Step 2: Read 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.

## Step 3: Compute 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:
```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
# 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 // 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);
```

## Step 4: See it pass — and fail

Here is the Python check running on a signed sample. The right signature passes. A wrong signature fails — and so does a body that changed by a single word on the way, which is the point: a tampered message never looks genuine. Answer anything that fails with `401` and do nothing else.

## Step 5: Rotate 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.

## Step 6: Ignore 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.

## 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](https://skode.ai/help/flow/developers/outbound-webhooks/).

## Useful links
- Outbound Webhooks in Flow — Settings → Developers → Outbound Webhooks: https://flow.skode.ai/settings/?tab=webhooks
- Flow API reference — every header and event, with examples: https://flow.skode.ai/docs/

Videos and screenshots © 2026 SkedX, with sample data only. Share and link to them freely; copying the SkedX product or its design is not allowed. Terms: https://skode.ai/terms/

## Related guides
- Get notified with outbound webhooks: https://skode.ai/help/flow/developers/outbound-webhooks/
- Secure an inbound hook: https://skode.ai/help/flow/developers/secure-an-inbound-hook/
- Build your Flow integration with an AI assistant: https://skode.ai/help/flow/developers/build-your-flow-integration-with-ai/

Source: https://skode.ai/help/flow/developers/check-a-webhook-came-from-flow/