DEVELOPER CALCULATOR

HMAC generator

Compute an HMAC signature from a message and a secret key, in hex and base64, and compare it against the signature you were sent. Everything runs in your browser — nothing is uploaded, stored or logged.

Updated September 2026
Your key and your message never leave this page. Everything is computed in your browser — nothing is uploaded, stored or logged, and a refresh clears it.
Hexlower case
Base64standard alphabet

What this tool does

Put the message in the box, the secret key underneath, choose the hash, and the signature appears in hex and base64 as you type. Paste a signature you were sent into the last field and the page tells you whether it matches what your key and message produce.

That last part is the reason this page exists. Almost every time someone needs an HMAC by hand, they are debugging a webhook that will not verify, and the question is never "what is HMAC" but "which of the four things I could have got wrong did I get wrong". Being able to change the key encoding or the algorithm and watch the verdict flip from red to green answers it in seconds.

The key can be given as text, hex or base64, because providers hand them out in all three and the same key decoded the wrong way is a completely different key. The byte counts next to the buttons are there to catch that: if a 64-character key shows as 64 bytes when the provider says it is a 32-byte key, you are hashing the hex string rather than the bytes it represents.

What HMAC is, and why not just hash the two together

HMAC is a way of proving two things at once: that a message has not been altered, and that it came from someone holding the shared secret. It takes a hash function and a key, and produces a fixed-length signature. Anyone with the key can recompute it; anyone without it cannot produce a valid one for a message they invented.

The obvious homemade version — hash the key and the message concatenated — is broken, and it is worth knowing why, because the mistake still appears in production code. Hash functions in the MD5, SHA-1 and SHA-2 families are built by absorbing the message block by block and keeping a running state, and the digest is that state. So given hash(secret + message) and the length of the secret, an attacker can carry on from where the function stopped and compute hash(secret + message + extra) for content of their choosing — without knowing the secret at all. That is a length-extension attack, and it turns a signature into a forgery machine.

HMAC exists to prevent exactly that. It hashes twice with two derived keys — an inner and an outer pad — so the digest an attacker sees is never the internal state of a hash that has consumed the secret. This is why the answer to "should I roll my own MAC" is no, and why every language has an HMAC function in its standard library.

Choosing the algorithm

HMAC-SHA-256 is the default and the right answer unless something specifies otherwise. It is what Stripe, GitHub, Slack, Shopify and most of the rest use for webhook signatures, and it is what you should reach for in your own APIs.

HMAC-SHA-512 gives a longer signature and is often faster on 64-bit hardware; there is no security reason to prefer it, and no reason to avoid it either. HMAC-SHA-384 exists mostly because certain standards name it.

HMAC-SHA-1 is a special case worth understanding. SHA-1 is broken for collisions, which rules it out for signatures and certificates — but HMAC does not depend on collision resistance in the same way, so HMAC-SHA-1 is not considered practically broken and remains in wide use, notably in AWS Signature Version 2 and older OAuth. Existing systems using it are not on fire; new systems should still use SHA-256.

You will notice MD5 is not offered. HMAC-MD5 has the same theoretical standing as HMAC-SHA-1, but browsers deliberately provide no MD5, and there is no good reason to build anything new on it.

The key is bytes, not a string

This is where most mismatches come from. HMAC signs bytes with bytes; a key written as text has to be turned into bytes, and there is more than one way to do that.

A webhook secret such as whsec_abc123 is normally used as literal UTF-8 text — the bytes of the characters, exactly as printed. But a key handed to you as 64 hex characters is almost certainly a 32-byte key written down in hex, and hashing the 64 characters as text gives a different, wrong signature. The same applies to base64: a key like c2VjcmV0… may be intended as the bytes it decodes to, not as the string. The provider documentation is the only authority on which, and the byte count on this page is the quickest way to check you have read it correctly.

Length is much less interesting than encoding. HMAC accepts keys of any length: shorter than the hash block size and they are padded, longer and they are hashed down first. A key of at least the digest length — 32 bytes for SHA-256 — is the sensible floor, and there is no benefit past the block size. What matters far more is that the key is random and secret: a memorable passphrase used as an HMAC key is a passphrase an attacker can guess offline, at speed, with no rate limit.

Verifying a webhook signature

The pattern is the same at every provider. They compute an HMAC of the request body using the secret you both hold, and send it in a header — X-Hub-Signature-256, Stripe-Signature, X-Signature. Your job is to compute the same thing and compare.

Three details decide whether it works. First, sign the raw body, not a re-serialised version of it: parsing JSON and dumping it again changes whitespace and key order, and the signature is over bytes. Most frameworks make the raw body available but stop doing so once a body parser has run, which is the single most common cause of a webhook that will not verify.

Second, sign exactly what they signed. Several providers do not sign the body alone but a constructed string — Stripe signs timestamp.payload, others prefix a version. If the documentation shows a template, reproduce it character for character, including the separator.

Third, compare in constant time. hash_equals() in PHP, crypto.timingSafeEqual in Node, hmac.compare_digest in Python. A normal string comparison returns as soon as two characters differ, and that timing difference is enough, over many attempts, to let an attacker discover a valid signature byte by byte. The comparison on this page is a plain one, which is fine because it happens on your own screen with nobody timing it — in your server code it is not.

And check the timestamp if there is one. A signature stays valid forever, so a replayed request with an old but genuine signature will verify unless you reject anything older than a few minutes.

Why the signature does not match

In rough order of how often each turns out to be the culprit: the body was parsed and re-serialised before signing; the key was decoded with the wrong encoding; the message included or omitted a trailing newline; the output was compared as hex against a base64 signature or the other way round; the provider signs a constructed string rather than the body; the header carries a prefix such as sha256= that was not stripped; or the wrong key was used, because the test and live secrets look alike.

This page is built to isolate those. Paste the body, paste the key, paste the signature and change one thing at a time. Switch the key encoding between text and hex and watch the hex output change completely — if one of them matches the signature you were sent, that was your bug. Compare the byte counts against what the provider says the sizes should be. And if a hex comparison fails, glance at the base64 row before assuming the key is wrong.

The prefix case is handled for you: a signature pasted as sha256=abc… is compared without the prefix, since that is how GitHub and several others send it.

Your key never leaves your browser

Everything on this page is computed locally, using your browser own crypto.subtle implementation. The key and the message are never uploaded, never written to a cookie or local storage, and never logged. Refreshing the page clears both.

For this tool that is not a nicety, it is the only thing that makes it usable at all. A webhook secret is a live credential: anyone holding it can forge requests your system will accept as genuine. Pasting one into a server-side HMAC tool means handing a working credential to a stranger and hoping — and no promise on the page can be checked from the outside. Here, you can check it: open your network tab and type. Beyond the analytics beacon this site sends on every page load — a URL and a page title, the same as every other page here — nothing is sent. Disconnect from the network entirely and the signature still computes.

Even so, treat a key you have pasted anywhere with the caution the situation deserves. If a secret has been through a browser on a shared machine, or you are not certain which tool you used last time, rotating it costs a minute and settles the question.

A worked example

Type The quick brown fox jumps over the lazy dog as the message, key as the key, leave the encoding on text and the algorithm on SHA-256. The hex row reads f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8, which is the published test vector for that pair — the same value every correct implementation produces, and a quick way to confirm that this page agrees with your own code.

Now switch the algorithm to SHA-1 and the signature changes completely rather than shortening from the same beginning; switch the key encoding to hex and you get an error, because key is not valid hex. Paste the SHA-256 value into the last field, set the algorithm back, and the verdict turns green. Change one character of the message and it turns red — which is exactly the behaviour a webhook receiver relies on.

No. The key and the message are used by your browser own crypto implementation and stay in the page. There is no upload, no cookie, no local storage and no logging, which is also why a refresh clears both fields.

HMAC-SHA-256 unless the system you are talking to specifies another. SHA-512 is equally fine and often faster on 64-bit hardware. HMAC-SHA-1 is still safe in practice despite SHA-1 being broken for collisions, but it belongs to existing systems rather than new ones.

Usually the message or the key differ by something invisible. A trailing newline, a re-serialised JSON body, a key decoded as text when it was hex, or a signature compared in the wrong output format. Check the byte counts shown here against what your code is actually passing in.

No. It proves integrity and authenticity, it does not hide anything — the message is not concealed by signing it. If the content also needs to be secret, encrypt it, and prefer an authenticated mode such as AES-GCM that provides both in one operation.

At least as long as the digest — 32 bytes for SHA-256 — and randomly generated rather than chosen. Anything beyond the hash block size (64 bytes for SHA-256) adds nothing, because longer keys are hashed down first. Randomness matters far more than length.

Not in server code. A normal comparison stops at the first differing byte, and that timing difference can be measured and exploited over many requests. Use hash_equals(), crypto.timingSafeEqual() or hmac.compare_digest(). The comparison on this page is a plain one because it runs on your own machine with no attacker able to time it.

Browsers deliberately do not implement MD5 in their cryptography interface. HMAC-MD5 is not considered practically broken in the way MD5 signatures are, but there is no reason to start anything new with it, and plenty of reason not to.

Was this calculator helpful?

Tap a star to rate it. Your feedback helps us improve the tools people rely on most.