splitforms.com
Free tool · runs in your browser

HMAC-SHA256 Generator & Webhook Signature Verifier

Calculate an HMAC from a secret and a message, or check the signature a webhook sent you. Presets match how GitHub, Stripe, Shopify, Slack and splitforms sign their requests. Nothing you type is sent to a server.

Verify a signature

Paste the signature header you received. A sha256= or v0= prefix is stripped, and the v1= value is read from a full Stripe-Signature header. The check uses the secret, body, timestamp, algorithm and output encoding on the left.

The bytes are compared in constant time. Your secret and body never leave this page: the HMAC is calculated by your browser's Web Crypto API and nothing is sent to a server.

What is HMAC?

HMAC (hash-based message authentication code) mixes a shared secret into a hash of the message. Only someone with the secret can produce the right value, and changing a single byte of the message changes the whole result. Webhook senders put the HMAC of the request body in a header; the receiver calculates it again over the raw body with the same secret and compares the two.

Try GitHub's documented test vector: secret It's a Secret to Everybody and body Hello, World! with the GitHub preset give sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17.

HMAC verification code

Verify on the server with the raw request body and a timing-safe compare. Both snippets check a sha256=<hex> header like GitHub's and splitforms'.

Node.js: createHmac and timingSafeEqual

verify-signature.mjs

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the exact request bytes (a Buffer), not re-serialized JSON.
// header: e.g. req.headers["x-hub-signature-256"] = "sha256=<hex>"
export function verifySignature(rawBody, header, secret) {
  const received = Buffer.from(String(header).replace(/^sha256=/, ""), "hex");
  const expected = createHmac("sha256", secret).update(rawBody).digest();
  return received.length === expected.length && timingSafeEqual(received, expected);
}

Python: hmac.compare_digest

verify_signature.py

import hashlib
import hmac

# raw_body: the exact request bytes, not re-serialized JSON.
# header: e.g. request.headers["X-Hub-Signature-256"] = "sha256=<hex>"
def verify_signature(raw_body: bytes, header: str, secret: str) -> bool:
    received = header.removeprefix("sha256=")
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(received, expected)

For Stripe, Shopify and Slack code, see how to verify webhook signatures with HMAC.

HMAC and webhook signature FAQ

Why verify against the raw body and not re-serialized JSON?

The sender signs the exact bytes it sent. Parsing the JSON and calling JSON.stringify again can change whitespace, key order, number formatting or Unicode escaping, and any one changed byte gives a different HMAC. Read the raw request body first, verify it, then parse it.

Hex or base64: which output should I use?

Use whatever the provider's header carries. GitHub, Stripe, Slack and splitforms send hex; Shopify sends base64. Both encode the same digest bytes, so a SHA-256 HMAC is 64 hex characters or 44 base64 characters.

Why use a timing-safe compare?

A normal string compare stops at the first different character, so response times can leak how much of a guessed signature was right. Compare the bytes in constant time with crypto.timingSafeEqual in Node or hmac.compare_digest in Python. This page compares the bytes in constant time too.

How do timestamps protect against replay attacks?

An HMAC proves who sent a request, not when. Stripe and Slack sign a timestamp together with the body, so you can reject requests older than a few minutes (both suggest 5) and an attacker can't change the timestamp without breaking the signature. Where no timestamp is signed, deduplicate on an event or submission ID.

Is my secret sent anywhere?

No. The HMAC is calculated in your browser with the Web Crypto API (crypto.subtle). The secret, body and signature stay on this page and nothing is stored.

Related

More free form tools

Form backend cost calculator

Enter monthly submissions and compare the cheapest listed plan across eight form backends.

Email deliverability checker

Enter a domain to check its SPF, DMARC and MX records and get a plain-English fix for each.

SPF record checker

Look up a domain's SPF record, see each mechanism explained and count DNS lookups against the 10-lookup limit.

DMARC record generator

Pick a policy and report address and copy a ready _dmarc TXT record.

DMARC checker

Look up a domain's DMARC record and see the policy and every tag explained.

DKIM record checker

Enter a domain and selector to check its DKIM key, with presets for common senders.

SPF record generator

Tick your email providers and copy one SPF TXT record with a lookup count.

Contact form spam checker

Paste form HTML to audit its honeypot, endpoint, CAPTCHA markers and spam resistance.

Honeypot field generator

Generate a hidden anti-spam input with your chosen name and hiding technique.

Form action URL tester

Check a form action URL for reachability, CORS headers, POST acceptance and backend.

Email obfuscator

Encode your email so spam scrapers can't read it but people can still click it.

Mailto link generator

Build a mailto: URL with recipients, CC, BCC, subject and a pre-filled body.

Alphanumeric regex tester

Test letters and numbers only, spaces, handles and Unicode; copy validation patterns.

Email regex tester

Test the WHATWG email regex and two other patterns against edge cases and your own list.

Phone number regex tester

Test numbers against E.164, US/Canada and digits-only patterns and copy code snippets.

ZIP code regex tester

Test US ZIP, ZIP+4, Canadian and UK postal codes and copy the regex or HTML pattern.

Number regex tester

Test digits, decimals, money and thousands separators and copy validation patterns.

Date regex tester

Test dd/mm/yyyy, mm/dd/yyyy and ISO dates with leap-year checks and copy JS or HTML patterns.

Name regex tester

Test name patterns for O'Brien, accents and Unicode letters; copy JS or HTML pattern snippets.

Username regex tester

Test username presets for GitHub, Instagram and X; check reserved names and copy JS or HTML patterns.

IP address regex tester

Test IPv4, IPv6, CIDR and private-range patterns and copy JS or HTML pattern snippets.

URL regex tester

Test website URLs against http(s) regex patterns and new URL(), and copy JS, HTML or PHP code.

URL encoder and decoder

Encode text, build an x-www-form-urlencoded body or parse a query string.

Password regex generator

Pick strength rules, test passwords live and copy the regex or HTML pattern.

UTM builder

Build campaign URLs with UTM parameters, tag a CSV in bulk and check tagged links.

HTML to JSX converter

Paste HTML and get JSX or a working React/Next.js form component.

Image to Base64 converter

Convert images to Base64 data URLs in your browser and copy HTML and CSS snippets.

Base64 to image converter

Paste a Base64 data URI to preview and download the decoded image.

Base64 decode & encode

Decode Base64 to readable UTF-8 text or encode text to Base64, locally in your browser.

URL to Base64 converter

Fetch a direct image URL in your browser and get its Base64 data URL.

HTML form generator

Build a form visually and copy HTML, React JSX or Tailwind code.

AI form generator

Describe your form in plain English and copy working HTML.

Time regex tester

Test 24-hour, AM/PM and ISO time patterns and copy JavaScript, Python or HTML snippets.

Credit card regex tester

Test Visa, Mastercard 2-series, Amex and more card patterns with brand detection and a Luhn check.

Hex color regex tester

Test #RGB, #RRGGBB and alpha hex colors plus rgb() and hsl() values with live color swatches.

UUID regex tester

Test UUID v1-v8, nil/max and GUID formats with version and variant checks; copy JS or HTML patterns.

See all free tools