UUID Studio

API Signature Tester

HMAC signatures for webhooks & APIs.

  • 🔒 No data stored or uploaded
  • âš¡ 100% client-side
  • 🆓 Free, no account

Need more than one tool at a time? Open the full Workbench - or press Ctrl+K to jump to any tool.

Hash, HMAC, AES-GCM/CBC + RSA-OAEP, codecs, JWT decoding, UUID v4, and secure random - all client-side. JWTs use Base64URL (three segments), not a single MIME Base64 block - use Decode JWT below, not raw Base64 decode.

About API Signature Tester

Request signing proves that a request came from someone holding a shared secret and that nothing in it was altered on the way. Almost every scheme works the same way: build a canonical string from selected parts of the request, HMAC it with the secret, and send the result in a header for the receiver to recompute and compare.

The canonical string is where every implementation differs and where essentially every bug lives. Which parts are included - method, path, query string, selected headers, a body hash, a timestamp - and exactly how they are joined, ordered and normalised is defined by the provider, and getting any detail wrong produces a signature that is simply different with no clue as to why. AWS SigV4, Stripe, GitHub and Shopify all specify different canonical forms.

The most frequent single cause of failure is the request body. If a framework parses JSON and your signing code re-serialises it, key order, whitespace and unicode escaping all change, and the signature no longer matches. You must capture the raw bytes before any body-parsing middleware runs, which in Express means express.raw() on that route and in FastAPI means await request.body().

Most schemes also include a timestamp and reject requests outside a tolerance window - five minutes is typical - to prevent an intercepted request being replayed later. That means clock skew on either machine causes intermittent, hard-to-reproduce failures, and it is worth checking NTP before suspecting the signing logic.

Signing runs in your browser, so a real secret is not transmitted - though a test secret is still the better habit.

How to use the API Signature Tester

  1. Enter the request parts - method, path, query, body - exactly as they will be sent.
  2. Set the shared secret, decoding it from Base64 or hex first if that is how it was issued.
  3. Generate the signature and compare it against what the provider expects.
  4. If they differ, check the body first: it must be the raw bytes, not a re-serialised object.

API Signature Tester in code

The same operation this tool performs, in the languages you are most likely to need it.

Verifying a webhook - the raw body problem
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const app = express();

// express.raw() on THIS route only, before any JSON parser.
// If express.json() runs first, req.body is a parsed object and
// the original bytes are gone - the signature can never match.
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.get("X-Signature-256") ?? "";
  const expected = "sha256=" + createHmac("sha256", process.env.SECRET)
    .update(req.body)            // Buffer of the RAW bytes
    .digest("hex");

  const a = Buffer.from(sig);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return res.sendStatus(401);
  }

  const payload = JSON.parse(req.body.toString("utf8"));   // parse AFTER
  res.sendStatus(200);
});
Python (FastAPI) with replay protection
import hmac, hashlib, time
from fastapi import FastAPI, Request, HTTPException

TOLERANCE = 300   # seconds

@app.post("/webhook")
async def webhook(request: Request):
    raw = await request.body()            # raw bytes, before parsing
    sig = request.headers.get("x-signature", "")
    ts  = request.headers.get("x-timestamp", "")

    # Reject stale requests so an intercepted one cannot be replayed
    if not ts.isdigit() or abs(time.time() - int(ts)) > TOLERANCE:
        raise HTTPException(400, "stale timestamp")

    # The timestamp must be INSIDE the signed string, or an attacker
    # could simply change it.
    signed = ts.encode() + b"." + raw
    expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, sig):
        raise HTTPException(401, "bad signature")
Reproducing a signature on the command line
# Sign a file's exact bytes
openssl dgst -sha256 -hmac "$SECRET" -hex payload.json

# Sign a string without a trailing newline
printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex

# Some providers send Base64 rather than hex
printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64

# Save what actually arrived, so you can sign the real bytes
# rather than something you retyped:
#   tee /tmp/body.bin in your handler, then sign that file.

When you need this

  • Reproducing a provider's webhook signature to find out why yours differs.
  • Building the signature for an outbound request to an API that requires signing.
  • Confirming you have the right shared secret.
  • Testing that your verification code rejects a tampered payload.
  • Checking whether a failure is the canonical string, the encoding, or clock skew.

Common problems and what causes them

Signing a re-serialised body
This is the number one cause. A body parser turns JSON into an object, and re-serialising changes key order, whitespace and escaping. Capture the raw bytes before any parsing middleware and sign those.
Hex versus Base64 signature encoding
GitHub sends hex with a "sha256=" prefix; Stripe and others use different formats and Base64 appears frequently. Compare decoded bytes rather than strings, and strip any prefix.
The secret used as text when it is encoded
Many providers issue the shared secret as Base64 or hex. Using the printable string as the HMAC key produces a different key and a failed signature. Decode to bytes first.
Clock skew causing intermittent failures
Schemes with a timestamp tolerance - typically five minutes - reject requests outside it. Skew on either machine produces failures that come and go and look like a logic bug. Check NTP.
The timestamp not included in the signed string
If the timestamp is only a header and not part of what is signed, an attacker can change it and replay an old request indefinitely. It must be inside the canonical string.
Non-constant-time comparison
Comparing signatures with == leaks how many leading characters matched, which enables byte-at-a-time forgery over many attempts. Use timingSafeEqual, compare_digest or hmac.Equal.
Canonical string details differing
Header ordering, whether the query string is included, trailing slashes, URL-encoding of the path - every provider specifies these differently. Follow their documentation literally rather than assuming a common pattern.

FAQ

Why does my webhook signature never match?
Almost always because you are signing a re-serialised body rather than the raw bytes. Frameworks parse JSON and re-encoding changes key order and whitespace. Capture the body before any parser runs. After that, check hex versus Base64, and whether the secret needed decoding.
What is a canonical string?
The exact text that gets HMAC'd - typically the method, path, some headers, a body hash and a timestamp, joined in a specified order and format. Both sides must build it byte-identically, and the specification differs for every provider.
Why does signature verification need a timestamp?
To stop replay. Without one, an intercepted request stays valid forever. A timestamp inside the signed string plus a tolerance window - usually five minutes - limits the window, at the cost of making clock skew a real failure mode.
Why must I compare signatures in constant time?
An ordinary comparison returns at the first differing byte, so response time reveals how much of the signature was correct. Over many requests that allows an attacker to construct a valid signature byte by byte.
Should I sign the request or just use TLS?
TLS protects the channel; signing proves the origin and integrity of the message itself. That still matters when the request passes through proxies, load balancers or a queue, and it is what lets a receiver verify a payload it did not receive directly.

Related reading