---
title: "Webhook Authentication: HMAC, Tokens, mTLS and IP Allow-listing Compared | WebhookRelay"
meta:
  "og:description": "A practical guide to webhook authentication: how HMAC signatures, shared-secret tokens, basic auth, mTLS, IP allow-listing and JWT work, their trade-offs, and when to use each to verify incoming webhooks."
  "og:title": "Webhook Authentication: HMAC, Tokens, mTLS and IP Allow-listing Compared"
  description: "A practical guide to webhook authentication: how HMAC signatures, shared-secret tokens, basic auth, mTLS, IP allow-listing and JWT work, their trade-offs, and when to use each to verify incoming webhooks."
---

![Stripes](https://webhookrelay.com/blog/webhook-authentication/images/stripes.svg)

# **Webhook Authentication: HMAC, Tokens, mTLS and IP Allow-listing Compared**

A practical guide to webhook authentication: how HMAC signatures, shared-secret tokens, basic auth, mTLS, IP allow-listing and JWT work, their trade-offs, and when to use each to verify incoming webhooks.

A webhook endpoint is a public URL that accepts POST requests and does something consequential — creates orders, deploys code, moves money. If anyone on the internet can forge a request your handler trusts, that's a problem. **Webhook authentication** is how you prove an incoming request really came from the provider you expect, and that nobody altered it on the way.

There isn't one right method; there are several, with different guarantees and costs. This guide walks through each, compares them, and tells you when to reach for which.

## [The strategies, from most to least common](#the-strategies-from-most-to-least-common)

### [1. HMAC signatures (the default for a reason)](#_1-hmac-signatures-the-default-for-a-reason)

The dominant approach. The provider and you share a secret. For every webhook, the provider computes an **HMAC** (a keyed hash, usually SHA-256) over the raw request body and puts the result in a header — `Stripe-Signature`, `X-Hub-Signature-256` (GitHub), `X-Shopify-Hmac-Sha256`, and so on. You recompute the HMAC with the same secret and compare.

```
import hmac, hashlib

def verify(raw_body: bytes, header_sig: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header_sig)  # constant-time!
```

Why it wins:

- **Proves authenticity _and_ integrity** — only someone with the secret could produce the signature, and any change to the body breaks it.
- **The secret never travels** on the wire; only the derived signature does.
- Works over plain HTTPS with no client certs or infrastructure.

Two rules that trip people up: **verify against the raw bytes**, not a re-serialized JSON object (re-encoding changes bytes and breaks the match), and **use a constant-time comparison** to avoid timing attacks. Many providers also include a timestamp you should check to reject replays. We cover the mechanics in depth in [how to verify a webhook signature](https://webhookrelay.com/blog/webhook-authentication/blog/verify-webhook-signature/), and you can test your logic with the free [HMAC generator & verifier](https://webhookrelay.com/blog/webhook-authentication/hmac-verification/).

### [2. Shared-secret tokens](#_2-shared-secret-tokens)

The simplest scheme: the provider sends a secret value in a header (e.g. `Authorization: Bearer <token>` or a custom `X-Webhook-Token`), and you compare it to a known value. Easy to set up, but weaker than HMAC because the secret travels with every request — anyone who captures one request (a logged header, a misconfigured proxy) has your secret forever. It also says nothing about payload integrity. Fine for low-stakes internal webhooks; reach for HMAC when the events matter.

### [3. Basic auth](#_3-basic-auth)

Username and password in the `Authorization` header. Same trade-offs as a shared token — the credential is sent on every request — but it's universally supported and trivial to configure on both ends. Only acceptable over HTTPS. Handy when a provider can't do HMAC but can do basic auth, or for quickly locking down an internal endpoint.

### [4. mTLS (mutual TLS)](#_4-mtls-mutual-tls)

Both sides present X.509 certificates during the TLS handshake; your server only completes the connection if the client's cert is signed by a CA you trust. This authenticates the _connection_ itself, before any HTTP is exchanged — strong, but operationally heavy: you manage certificates, rotation and a CA. Reserved for high-security, often regulated, B2B integrations where both parties control the infrastructure.

### [5. IP allow-listing](#_5-ip-allow-listing)

Only accept requests from the provider's published IP ranges. Cheap defense in depth, but **not sufficient alone**: it doesn't prove payload integrity, provider IP ranges change (breaking you silently), and shared cloud IPs can be reused by others. Best paired with HMAC.

There's a flip side worth knowing: when _you_ are the one calling someone else's allow-listed endpoint, your requests need a **predictable source IP**. A forwarder with a [static outgoing IP](https://webhookrelay.com/blog/webhook-authentication/features/static-outgoing-ip/) gives you one stable address to hand to a partner's firewall, instead of a rotating pool you can't allow-list.

### [6. JWT](#_6-jwt)

The provider signs a **JSON Web Token** (typically with HMAC or an RSA/ECDSA key) and sends it in the `Authorization` header. You verify the signature and the standard claims (`exp` for expiry, `iss` for issuer, `aud` for audience). JWTs carry their own expiry and metadata, which makes them nice for short-lived authorization and for verifying with a _public_ key (so your side never holds a shared secret). The cost is more moving parts than a plain HMAC body signature.

## [Quick comparison](#quick-comparison)

| Method | Proves payload integrity? | Secret on the wire? | Setup cost | Good for |
| --- | --- | --- | --- | --- |
| **HMAC signature** | Yes | No | Low | The default — almost everything |
| **Shared token** | No | Yes | Very low | Low-stakes / internal |
| **Basic auth** | No | Yes | Very low | Legacy / quick lockdown |
| **mTLS** | Connection-level | No | High | Regulated B2B |
| **IP allow-list** | No | n/a | Low | Defense in depth only |
| **JWT** | Yes (token) | Depends | Medium | Short-lived / public-key verify |

## [When to use which](#when-to-use-which)

- **Building a new endpoint?** Start with **HMAC signature verification**. It's the best ratio of security to effort and what most providers already speak.
- **Provider can't sign bodies?** Use **basic auth or a shared token over HTTPS**, and add **IP allow-listing** on top.
- **Money, health, or compliance on the line?** Consider **mTLS** for connection-level guarantees, still with HMAC at the application layer.
- **Need expiring, delegated access or public-key verification?** Reach for **JWT**.
- **Always:** layer it. Signature _plus_ IP allow-list _plus_ a timestamp/replay check is meaningfully stronger than any single control.

## [How Webhook Relay fits in](#how-webhook-relay-fits-in)

Webhook Relay sits in front of your services and can enforce authentication on its **endpoints** — supporting **HMAC**, **JWT** and **basic auth** so unauthenticated traffic is rejected before it ever reaches your code (see the [webhook auth docs](https://webhookrelay.com/blog/webhook-authentication/docs/webhooks/auth/hmac/) for the options). On the outbound side, the [static outgoing IP](https://webhookrelay.com/blog/webhook-authentication/features/static-outgoing-ip/) gives you a stable address to put on a partner's allow-list, and [transformations](https://webhookrelay.com/blog/webhook-authentication/features/transform-webhooks/) let you add or rewrite auth headers in flight when a destination expects a different scheme than the source provides.

For the broader picture — replay protection, raw-body verification, HTTPS, and least-privilege handlers — see our [webhook security best practices](https://webhookrelay.com/blog/webhook-authentication/blog/webhook-security/).

## [The short version](#the-short-version)

- **HMAC signatures** are the default: they prove authenticity _and_ integrity and never expose the secret. Verify the **raw body** with a **constant-time** compare.
- **Tokens and basic auth** are simple but send the secret every time — fine for low-stakes endpoints over HTTPS.
- **mTLS** and **JWT** cover stronger or more dynamic needs at higher operational cost.
- **IP allow-listing** is great defense in depth but never a standalone control.
- **Layer multiple methods**, and add a timestamp/replay check.

Test your signature logic now with the [HMAC verifier](https://webhookrelay.com/blog/webhook-authentication/hmac-verification/), read [how to verify a webhook signature](https://webhookrelay.com/blog/webhook-authentication/blog/verify-webhook-signature/), or [create a free account](https://my.webhookrelay.com/register) to put authenticated endpoints in front of your services.