Signing API Requests with HMAC-SHA256

How to sign API requests using HMAC-SHA256. AWS Signature Version 4, canonical request construction, Authorization header, and replay attack prevention.

ZERO UPLOAD · ALL LOCAL
  1. Type or paste any string into the input box — all four hashes update instantly as you type.
  2. Use the HEX / BASE64 toggle above the results to switch output format at any time.
  3. Click Copy next to any hash to copy it to the clipboard in the current format.
  4. Switch to HMAC, enter your secret key, and pick an algorithm to generate a keyed digest for API signatures or webhook verification.
  5. SHA-256 is the recommended algorithm for new integrations. MD5 and SHA-1 are shown for legacy compatibility only.

What this page covers

  • HTTP method part of the canonical string per the worked example
  • URL path part of the canonical string
  • Timestamp part of the canonical string
  • Request body hash included when the body carries the meaningful payload
  • Specific headers Content-Type, Host, and custom auth headers when they affect request semantics
MD5 LEGACY
SHA-1 LEGACY
SHA-256 RECOMMENDED
SHA-512 SECURE
SHA-256

Signing API Requests with HMAC-SHA256

In machine-to-machine APIs, request signing authenticates the client without sending credentials in plaintext. Instead of transmitting an API secret, the client signs a representation of the request - method, path, headers, timestamp - with HMAC-SHA256 using the secret as the key.1 The server recomputes the signature using its stored copy of the secret. Because only the holder of the secret can produce a valid signature, the server can authenticate the request without the client ever revealing the secret.

AWS Signature Version 4, the most widely implemented API signing scheme, uses HMAC-SHA256 at multiple levels: a derived signing key (HMAC applied four times to the secret, date, region, service, and constant string) signs a canonical request hash.23 Many APIs implement a simpler single-level HMAC over the request body or a canonical string. The pattern is consistent: choose what to sign, agree on a canonical format, and verify with HMAC-SHA256.

How request signing works

The client constructs a canonical representation of the request - typically a string joining the HTTP method, URL path, sorted query parameters, selected headers, and a hash of the request body.4 HMAC-SHA256 over this string with the API secret produces the signature. The client includes the signature in an Authorization or X-Signature header. The server reconstructs the same canonical string from the incoming request, computes HMAC-SHA256 with its stored secret, and compares. Consequently, any modification to the signed elements - URL, body, headers - invalidates the signature immediately.

Agreeing on the canonical form first

Write down the exact ordering, escaping, timestamp format, and body-hash rule before either side implements signing. The HMAC math is simple; the hard part is making sure both client and server sign the same byte sequence. A shared canonicalization test with known request examples prevents weeks of intermittent production failures. Without this agreement, the most common bug is a subtle mismatch where the client includes a header the server ignores, or the server decodes a URL segment the client left encoded, and both sides sign different bytes without realizing it.

You prevent weeks of intermittent failures by writing a shared canonicalization test with known request examples that both client and server must pass. The HMAC math is simple, but the hard part is guaranteeing that both sides sign the identical byte sequence every time. CapyToolkit computes HMAC-SHA256 locally so you can confirm the output format before agreeing the canonical string with another team.

Including a timestamp to prevent replay attacks

Signing the request content alone is insufficient if an attacker can capture a valid signed request and re-send it later. Building on this risk: most signing schemes include a timestamp in the signed payload and require the server to reject requests where the timestamp is more than a few minutes old. AWS Signature Version 4 uses an X-Amz-Date header that must be within five minutes of server time. The combination of signature (proves authenticity) and timestamp (bounds freshness) together prevent both forgery and replay.

Keeping clocks close enough to verify signatures

Server time must be reliable enough for the acceptance window you choose. If your servers drift by more than a few minutes, valid requests start failing and teams are tempted to widen the window. Configure time synchronization and monitor clock drift before relying on short replay windows for high-volume APIs. Network Time Protocol with a reliable stratum-1 source keeps most server fleets within a few milliseconds of UTC, which gives you a comfortable margin even with a tight five-minute acceptance window.

Designing a simple signing scheme

For an internal API, a practical signing scheme is: concatenate the HTTP method, request path, ISO-8601 timestamp, and SHA-256 hash of the request body into a canonical string, compute HMAC-SHA256 with the API secret, and include the hex signature plus timestamp in request headers. The server validates the signature and rejects requests older than sixty seconds. Yet even this simple scheme requires careful attention to the canonical format - any ambiguity about which headers or query parameters are included, or the encoding of special characters, creates implementation bugs that produce valid-format but failing signatures.

Limiting the blast radius of signing keys

Issue separate signing keys per client, environment, or integration rather than sharing one global secret. If one key is exposed, you can rotate it without disrupting every integration. Keep the key identifier in the request header so the server can choose the correct secret without leaking timing information about which clients exist. A per-client key also lets you revoke access for a single compromised integration without forcing every other client to update their stored secrets simultaneously, which is especially valuable in microservice architectures where dozens of services share the same API.

When signature verification fails, debug the canonical string first

When HMAC signature verification fails in production, log both the client's canonical string and the server's reconstructed canonical string before comparing digests. The HMAC computation is deterministic: identical canonical strings always produce identical digests for the same key, so hash a canonical string the way SigV4 does and you have a third value to check both sides against. Differences in the canonical string almost always trace to URL encoding (the client percent-encodes the path while the server does not), extra whitespace in header values, or clock skew that pushes the timestamp outside the server's acceptance window.

For AWS Signature Version 4 specifically, the canonical query string requires parameter names and values sorted alphabetically by name, with each value percent-encoded using uppercase hex digits (%2F rather than %2f).5 Lowercase percent-encoding causes a mismatch that is invisible without logging both canonical strings side by side. Log the full string including newlines when debugging; do not trim or abbreviate it.

When to use this

Use request signing when your API must authenticate machine-to-machine requests without exposing a secret in transit. Before wiring up server-side verification, check that your canonical string produces the exact signature you expect. You should implement or require signing for any API endpoint that processes payments, modifies user data, or initiates infrastructure changes.

Examples

Simple HMAC-SHA256 request signing

Before
Method: POST
Path: /api/orders
Timestamp: 2026-05-23T14:00:00Z
Body hash (SHA-256): b94d27b9...
After
Canonical string: "POST\n/api/orders\n2026-05-23T14:00:00Z\nb94d27b9..."
HMAC-SHA256 signature: abc123...
Authorization: HMAC-SHA256 key_id=mykey, sig=abc123, ts=2026-05-23T14:00:00Z

AWS Signature Version 4 - key derivation

Before
Secret: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"
Date: 20260523, Region: us-east-1, Service: s3
After
kDate    = HMAC-SHA256("AWS4" + secret,  "20260523")
kRegion  = HMAC-SHA256(kDate,           "us-east-1")
kService = HMAC-SHA256(kRegion,         "s3")
kSigning = HMAC-SHA256(kService,        "aws4_request")
Signature = HMAC-SHA256(kSigning,       canonicalRequest)

AWS derives a date/region/service-scoped signing key rather than using the raw secret, limiting the blast radius if a derived key is exposed.

Sources
  1. 1.

    H. Krawczyk, M. Bellare, and R. Canetti, "HMAC: Keyed-Hashing for Message Authentication," RFC 2104, IETF, February 1997. https://www.rfc-editor.org/rfc/rfc2104.html

  2. 2.

    "SHA-2," Wikipedia, accessed June 2026. https://en.wikipedia.org/wiki/SHA-2

  3. 3.

    AWS SDK for Go, "v4.go — deriveSigningKey," github.com/aws/aws-sdk-go, accessed June 2026. https://github.com/aws/aws-sdk-go/blob/main/aws/signer/v4/v4.go

  4. 4.

    Cloudflare, "Token Authentication for Cached Private Content and APIs," blog.cloudflare.com, accessed June 2026. https://blog.cloudflare.com/token-authentication-for-cached-private-content-and-apis

  5. 5.

    AWS SDK for Go, "Issue #2969 — CanonicalQueryString sorting before encoding," github.com/aws/aws-sdk-go-v2, accessed June 2026. https://github.com/aws/aws-sdk-go-v2/issues/2969

FAQ