Guide

Verify Webhook Signatures (HMAC Guide)

A webhook endpoint sits on the public internet, which means anyone who learns the URL can POST forged events to it. The defense is a signature: the sender computes an HMAC of the exact bytes it sent, keyed by a shared secret, and your handler recomputes it and compares before trusting a single field. This guide shows how that works, how to verify a Nylas webhook, how the major providers differ, and how to stop replay attacks.

Written by Caleb Geene Director, Site Reliability Engineering

VerifiedCLI 3.1.16 · Gmail, Outlook · last tested June 8, 2026

Command references used in this guide: nylas webhook verify, nylas webhook server, and nylas webhook rotate-secret.

Why must you verify webhook signatures?

You must verify because a webhook endpoint is an unauthenticated, publicly reachable URL — once an attacker knows or guesses it, they can POST anything to it. Without a signature check, your handler can't tell a real message.created event from a forged one, so it might create records, send replies, or trigger workflows on attacker-controlled data. The OWASP input-validation guidance treats all external input as untrusted, and a webhook body is external input.

A signature closes that gap. The sender holds a secret shared only with you, computes a keyed hash of the exact payload, and sends it in a header. Because an attacker doesn't have the secret, they can't produce a matching hash, so a body that fails the check is rejected before any logic runs. This is the single most important control on a webhook receiver.

How does HMAC signature verification work?

HMAC (Hash-based Message Authentication Code) combines your secret key with the message through a hash function — SHA-256 in nearly every modern webhook scheme. The sender computes HMAC-SHA256(secret, raw_body) and puts the hex digest in a header; your handler recomputes the same value over the bytes it received and compares. A match proves both that the secret-holder sent it and that not one byte changed in transit.

Two details are easy to get wrong and both are security-critical. First, hash the raw request body, not a re-serialized object — JSON key reordering or whitespace changes break the digest even though the data is identical. Second, compare with a constant-time function (hmac.compare_digest, crypto.timingSafeEqual), never ==, so timing differences can't leak the expected signature byte by byte.

import hmac, hashlib

def verify(raw_body: bytes, signature: str, secret: str) -> bool:
    # Hash the EXACT bytes received — never a re-encoded JSON object.
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    # Constant-time compare — never use ==, which leaks timing.
    return hmac.compare_digest(expected, signature)

# In the handler: reject BEFORE parsing or acting on the payload.
# if not verify(request.body, request.headers["x-nylas-signature"], SECRET):
#     return Response(status=401)

How do you verify a Nylas webhook?

Nylas signs every webhook with an x-nylas-signature header — an HMAC-SHA256 of the raw body keyed by your webhook secret. The nylas webhook verify command runs that exact check for you: pass the raw payload, the secret, and the signature from the header, and it confirms or rejects the message. That lets you test a handler against a captured fixture without deploying anything.

For live development, nylas webhook server --tunnel --secret receives real events over a cloudflared tunnel and verifies each signature before printing it, so you see exactly what your production handler should accept. The documentation explicitly warns to pass the unmodified raw body — reformatting the JSON before verifying will break the digest, the most common false rejection.

# Verify a captured payload offline (exact raw bytes required)
nylas webhook verify \
  --payload-file event.json \
  --secret "$NYLAS_WEBHOOK_SECRET" \
  --signature "$SIG_FROM_HEADER"

# Or receive and verify live events over a tunnel
nylas webhook server --tunnel --secret "$NYLAS_WEBHOOK_SECRET"

How do webhook signatures compare across providers?

The scheme differs by provider, but the principle is identical: an HMAC (or signature) over the raw body in a named header. The table below maps four common schemes so you can port a verification routine. Stripe and GitHub document their verification in detail, and Stripe also embeds a timestamp in the header to defend against replay, which the next section covers.

ProviderHeaderScheme
Nylasx-nylas-signatureHMAC-SHA256 hex
StripeStripe-SignatureHMAC-SHA256 with t= timestamp
GitHubX-Hub-Signature-256HMAC-SHA256 hex, sha256= prefix
Twilio SendGridX-Twilio-Email-Event-Webhook-SignatureECDSA public-key signature

How do you stop replay attacks and rotate secrets?

A valid signature doesn't stop replay: an attacker who captures a real signed request can resend it. Defend by including a timestamp in the signed data — Stripe puts t= in the header — and rejecting any request whose timestamp is more than a few minutes old. A 5-minute tolerance window is typical; anything older is dropped even if the signature checks out.

Secrets also leak, so rotate them. nylas webhook rotate-secret issues a new signing secret for a webhook; deploy the new value to your verifier, confirm deliveries still pass, then retire the old one. Treat the webhook secret like any credential — never log it, never commit it, and rotate immediately if it appears in a stack trace or a shared environment. See the webhook events reference for the events you'll be verifying.

Next steps