• Use Cases
  • Pricing
  • Security
  • Docs
Sign InStart free

The outbound integration layer for SaaS products: emit once, then let Meshes handle routing, retries, fan-out, and delivery history.

© Copyright 2026 Meshes, Inc. All Rights Reserved.

About
  • About
  • Security
  • Blog
  • Contact
  • FAQ
Product
  • Pricing
  • Demo
  • Integrations
  • Guides
  • Changelog
  • Status
Compare
  • All comparisons
  • Build vs buy
  • vs Zapier
  • vs Make
  • vs n8n
  • vs Paragon
  • vs Merge
Use Cases
  • All use cases
  • Payment failed
  • User signup fan-out
  • Churn prevention
  • Trial expired events
  • Lesson completion flows
  • Page completion triggers
  • Page visit Intercom flows
Developers
  • Documentation
  • Agents
  • Tools
  • API Reference
  • MCP Server
  • llms.txt
Legal
  • Terms of Service
  • Privacy Policy
  • Acceptable Use Policy
  • Cookie Policy
Developer tool

HMAC Signature Tester and Verifier

Generate and verify HMAC webhook signatures for Stripe, GitHub, Shopify, Slack, Meshes, and any custom provider. HMAC-SHA256 and HMAC-SHA512. Hex or base64. Everything runs in your browser.

All computation happens in your browser.
Secrets never leave this page. No payload, secret, signature, or result is sent to any server — verify with your browser's network tab.

Signs `{timestamp}.{body}` with HMAC-SHA256 in hex. Header is `Stripe-Signature: t={ts},v1={sig}`.

Included in the signed payload as `{t}.{body}` and sent as `t=` in the header.

Use the exact bytes the receiver will see. Parsing and re-serializing JSON often reorders keys and breaks signatures.

Fill in the payload and secret, then compute a signature. Output appears here.

An HMAC is a keyed-hash message authentication code: a hash computed over a message using a shared secret, so the receiver can confirm both integrity and origin. This tool computes and verifies those signatures in your browser using the Web Crypto API, with provider presets for the transformations Stripe, GitHub, Shopify, Slack, and Meshes apply before signing. No payload, secret, or signature is ever sent to a server.

Concept

How HMAC webhook signatures work

Two parties — the sender and the receiver — agree on a shared secret out of band, typically by creating a webhook endpoint in a dashboard and copying the generated signing secret into the receiver's configuration. Neither side ever transmits the secret over the wire after that point.

When the sender is ready to deliver a message, it computesHMAC(secret, signed_payload)and attaches the resulting digest to the request as a header value. The signed payload is usually the raw request body, but some providers wrap it first — Stripe signs {timestamp}.{body} and Slack signs v0:{timestamp}:{body} so a captured request cannot be replayed later.

On the receiving side, your server reads the raw bytes of the request body, applies the same pre-signing transformation, computes its own HMAC with its copy of the secret, and compares the two digests using a constant-time compare. If the digests match, the request is authentic and has not been altered. If they differ — by even one byte — the request is rejected.

Because HMAC is symmetric, it proves the message came from someone who holds the secret. It does not prove which party signed it, which is why HMAC secrets should be treated with the same care as API keys: rotate them on a schedule, store them in a secret manager, and never log them.

SenderHMAC(secret, body)→ signatureReceiverHMAC(secret, body)compare (timing-safe)POST /webhookbody + X-Signature headerShared secret (out of band)
Server-side verification

How to verify a webhook signature in Node.js, Python, or Go

All three examples read the raw request body, recompute the HMAC-SHA256 digest, and compare it to the received signature in constant time. A non-timing-safe compare such as == or === leaks information about the expected signature through timing side channels — always use the platform's constant-time helper.

import crypto from 'node:crypto';

// Sign the raw bytes of the request body — never the parsed JSON.
export function verifyWebhookSignature(params: {
  body: string;
  secret: string;
  receivedHex: string;
}): boolean {
  const expected = crypto
    .createHmac('sha256', params.secret)
    .update(params.body, 'utf8')
    .digest('hex');

  const expectedBuf = Buffer.from(expected, 'utf8');
  const receivedBuf = Buffer.from(params.receivedHex, 'utf8');

  if (expectedBuf.length !== receivedBuf.length) {
    return false;
  }

  // Constant-time compare. Never use `===` or `==` on signatures.
  return crypto.timingSafeEqual(expectedBuf, receivedBuf);
}
import hmac
import hashlib


def verify_webhook_signature(body: bytes, secret: str, received_hex: str) -> bool:
    expected = hmac.new(
        secret.encode("utf-8"),
        msg=body,
        digestmod=hashlib.sha256,
    ).hexdigest()

    # hmac.compare_digest is constant-time. Never use `==` on signatures.
    return hmac.compare_digest(expected, received_hex)
package webhook

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
)

// VerifyWebhookSignature recomputes an HMAC-SHA256 over body with secret and
// compares it to receivedHex in constant time.
func VerifyWebhookSignature(body []byte, secret string, receivedHex string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(body)
	expected := hex.EncodeToString(mac.Sum(nil))

	// hmac.Equal is constant-time. Never use == on signatures.
	return hmac.Equal([]byte(expected), []byte(receivedHex))
}
Debugging

Common HMAC pitfalls

  • Signing the parsed body instead of the raw body. Most web frameworks parse JSON before your handler runs. Re-serializing the parsed object often reorders keys, changes whitespace, or drops unicode escapes. Sign the exact bytes the sender signed. In Express, read from the raw body buffer; in FastAPI, read from await request.body(); in Go, do the HMAC before json.Unmarshal.
  • Trailing newlines and CRLF. Some HTTP clients append a newline to request bodies. Some proxies convert line endings from \n to \r\n. Either will change the digest. If you cannot find the difference, hash the body with SHA-256 on both sides and compare before you worry about HMAC.
  • Encoding mismatches. GitHub and Stripe emit hex. Shopify emits base64. If you hand a base64 signature to a hex comparator you will always get a mismatch — even when the underlying digest is identical. The Verify tab on this page surfaces this specifically because it is so common.
  • Non-timing-safe comparisons. A byte-by-byte compare that returns on the first mismatch leaks the expected signature through timing differences. Use crypto.timingSafeEqual, hmac.compare_digest, or hmac.Equal.
  • Missing replay protection. HMAC proves the body did not change, not that the request is fresh. If your signing scheme does not already include a timestamp, add one and reject any request whose timestamp is older than a small window (Stripe uses five minutes).
  • Algorithm downgrade. Hard-code the algorithm your receiver expects. Do not let the sender dictate it through an alg header or similar field — that lets an attacker pick a weaker algorithm if one is ever introduced.
FAQ

Frequently asked questions

What is HMAC-SHA256?

HMAC-SHA256 is a keyed-hash message authentication code built on the SHA-256 hash function and a shared secret. Given the same secret and message, two parties can independently compute the same 32-byte digest. The receiver recomputes the digest from the request body and its copy of the secret, then compares it to the signature in the request header. If the digests match, the request was produced by someone who knows the secret and the body has not been altered in transit.

Is HMAC the same as a digital signature?

No. HMAC is symmetric — sender and receiver share the same secret, and either side can produce a valid signature. A digital signature like RSA or Ed25519 is asymmetric: the private key signs and the public key verifies, so only the holder of the private key could have signed. HMAC is faster and simpler, which is why most webhook providers use it, but it cannot prove which side signed a given request.

How long should my webhook signing secret be?

At least 32 bytes of cryptographically random data. NIST SP 800-107 recommends a key length equal to the hash output size, so 32 bytes for SHA-256 and 64 bytes for SHA-512. Generate it from a CSPRNG, store it alongside your other secrets, and rotate it when an engineer with access leaves or when you suspect it has been exposed.

Can HMAC prevent replay attacks?

Not by itself. An HMAC proves the body has not changed, but nothing stops an attacker from capturing a signed request and resending it later. Providers address this by putting a timestamp inside the signed payload — Stripe signs `{timestamp}.{body}`, Slack signs `v0:{timestamp}:{body}` — and receivers reject signatures whose timestamp is more than a few minutes old. If you are designing your own signing scheme, include a timestamp and a short tolerance window.

Why do Stripe and GitHub sign payloads differently?

Stripe includes the timestamp in the signed payload to give you built-in replay protection. GitHub signs only the raw body and relies on TLS plus a short delivery window. Shopify signs the raw body too but emits the digest in base64 instead of hex. The math is identical — the difference is the pre-signing transformation and the header format. That is why a generic HMAC calculator gives the right digest but the wrong answer: it does not know which bytes the provider actually signed.

Why does my signature not match even though the secret is right?

Almost always a byte-level payload mismatch. Common causes: your framework parsed the JSON body and re-serialized it with different key ordering or whitespace; a proxy added or stripped a trailing newline; the body was decoded as UTF-16 instead of UTF-8; or the preset adds a timestamp prefix you are not including. Sign against the raw bytes you received on the wire, not a parsed object.

Should I use `==` or `timingSafeEqual` to compare signatures?

Always use a constant-time compare such as `crypto.timingSafeEqual` in Node.js, `hmac.compare_digest` in Python, or `hmac.Equal` in Go. A normal `==` compare can leak information about the expected signature through timing differences on each byte, which lets an attacker recover the signature one character at a time. The cost of a constant-time compare is trivial; the risk of skipping it is not.

Does this tool send my secret anywhere?

No. Every HMAC computation runs in your browser through the Web Crypto API. There is no server action, no API call, no analytics event tied to the payload or secret, and nothing written to localStorage. You can verify this by opening your browser devtools and watching the network tab while you generate a signature — nothing leaves the page.

About Meshes

Meshes can sign outbound webhooks with HMAC-SHA256 when you enable it on the destination, and handles key rotation, per-workspace credentials, and delivery retries so your customers' endpoints can verify every request without you owning the plumbing.

Docs