---
title: "Webhook authentication - static token, HMAC and provider presets"
description: "Secure your webhook with a static token or HMAC SHA-256, including presets for Stripe, GitHub, Shopify, Slack, Typeform and Calendly."
canonical: "https://docs.workflow-webhooks.app/authentication"
---

# Authentication

Anyone who knows a webhook URL can send it a request, so authentication is what stops a
stranger firing your Shopify Flow workflows. Set it per webhook, under the webhook's
**Security** section.

## The three methods

| Method | How the caller proves itself | Use it when |
| --- | --- | --- |
| **None** | Nothing | Testing only - never in production |
| **Static token** | A fixed secret in a request header | Almost every integration (n8n, Make, Zapier, your own code) |
| **HMAC SHA-256** | A signature computed from the request and a shared secret | The sender signs its webhooks (Stripe, GitHub, Slack, …) |

> [!WARNING]
> **None** means the URL is the only secret, and URLs leak - into logs, browser history,
> screenshots and support tickets. Use it to get a first request flowing, then switch.

## Static token

Press the generate button on the webhook to get a strong random token, or paste your own.
The caller sends it in a header:

```bash
curl -X POST https://your-app-url/webhook/ab12cd34 \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: your-token" \
  -d '{"orderId":"1001"}'
```

### Changing the header name

Some systems can only send a header they already use. Set **Auth header name** in Advanced
Settings and we read the token from that header instead of `X-Api-Key`:

```bash
  -H "X-Custom-Auth: your-token"
```

Header names are case-insensitive. Reserved names are rejected - `Host`, `Authorization`,
`Cookie`, `X-Forwarded-*`, `X-Webhook-*`, `CF-*` and similar. Those are set or rewritten by
proxies and CDNs, so a token read from one would be attacker-controllable.

## HMAC SHA-256

The sender computes a signature over the request using a shared secret; we recompute it and
compare. A leaked request cannot be replayed with altered content, because the body no
longer matches the signature.

### Provider presets

If your sender is one of these, pick the preset and we verify using that provider's exact
scheme - header name, encoding and signing base string:

| Preset | Signature header |
| --- | --- |
| Stripe | `Stripe-Signature` |
| GitHub | `X-Hub-Signature-256` |
| Shopify | `X-Shopify-Hmac-Sha256` |
| Slack | `X-Slack-Signature` |
| Typeform | `Typeform-Signature` |
| Calendly | `Calendly-Webhook-Signature` |

Paste the signing secret from the provider's dashboard and you are done.

### Generic HMAC

With no preset, we use our own scheme: the caller sends `X-Signature`, the HMAC-SHA256 of
the `X-Webhook-*` header values using the signing secret.

```bash
curl -X POST https://your-app-url/webhook/ab12cd34 \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Data: value1value2" \
  -H "X-Signature: <hmac-sha256 of the X-Webhook-* values>" \
  -d '{"data":"payload"}'
```

## What a rejection looks like

Failed authentication returns `401` with a JSON body naming the reason - see
[History and troubleshooting](https://docs.workflow-webhooks.app/history-and-troubleshooting.md) for the full code list. Rejected calls still appear
in the Live Request Inspector while you are editing the webhook, so you can see exactly why
one bounced.

## Keeping the secret safe

- The token is **never returned** by our REST API or MCP server at any access level - they
  report only whether one is set. See [Developer API and MCP](https://docs.workflow-webhooks.app/developer-api-and-mcp.md).
- It is encrypted at rest.
- Rotating it takes effect immediately, so update your sender first.
- Stored invocation history masks the auth header, so a screenshot of History does not leak
  your token.
