Skip to main content
SIGNATURE_INVALID is the single most common error during first integration. The 11-step checklist below covers 99% of cases — work through it in order. The first four steps catch the vast majority of issues.

SIGNATURE_INVALID — 11-step checklist

1

HTTP method case

Is the method in the canonical string uppercase (GET, POST, PUT)? Lowercase fails. The header on the HTTP request line is already uppercase; the canonical string must match.
2

Raw body bytes

Are you hashing the exact bytes you are sending over the wire, or are you hashing a re-serialized version?The fix: log the body as a byte array right before it enters the HTTP client, hash those same bytes. If your code does JSON.stringify(JSON.parse(body)) between the two, whitespace and key order may diverge — the server hashes what arrives, not what you intended.
3

Timestamp unit

Is X-Access-Timestamp in milliseconds (13 digits for 2026)? Not seconds (10 digits), not microseconds (16 digits), not ISO 8601.
4

Timestamp consistency

Is the timestamp in the canonical string byte-for-byte identical to the one in the X-Access-Timestamp header? Read the clock once, store in a variable, reuse it. Two clock reads (even microseconds apart) can produce different values.
5

Low-S normalization

If everything above is correct and you are still getting SIGNATURE_INVALID, your signature is likely high-S.ECDSA signatures must use the low-S form (s ≤ n/2). Most libraries do this by default. Some — notably WebCrypto and some Go libraries — do not.Fix one of two ways:
  • Enable the lowS / canonical / normalize_s flag in your library.
  • Normalize manually: if s > n/2, replace s with n - s (where n is the curve order).
For secp256k1, n is:
See your library’s documentation for the canonical/lowS option name.
6

Pathname canonicalization

Quick check:
  • Did you sign the path without the /txengine/v1 prefix? The canonical string uses the full pathname as sent on the wire — /txengine/v1/wallets/main, not /wallets/main. The API Reference omits the prefix because it lives in the server base URL.
  • Did you include the query string by mistake? Strip everything from ? onwards.
  • Did you accidentally keep a trailing slash? /txengine/v1/wallets/main/ vs /txengine/v1/wallets/main are different paths.
  • Is the pathname URL-decoded? The canonical string uses the decoded pathname, while the HTTP request line keeps the encoded form. Encoded characters in the canonical string fail.
  • Does it start with /?
7

Empty body hash

For requests with no body, did you use the SHA-256 of the empty string?
Not the literal string "" (which would hash to a different value), not null, not a placeholder.
8

Base64 encoding

Standard Base64 (RFC 4648 — characters A–Z a–z 0–9 + /, padded with =). Not URL-safe Base64 (characters - _). No line breaks.
9

Field separator

The canonical request uses colon : between all six fields. Not |, not \n, not space, not comma.
10

UTF-8 encoding

Hash the canonical string as UTF-8 bytes. For ASCII-only values (the normal case) this is a no-op. If you use non-ASCII characters in your Access Key or Request ID (you should not, but it is allowed), confirm UTF-8 encoding.
11

Key pair match

Final check — confirm:
  • The private key you are signing with corresponds to the public key Bloobank has registered for this X-Access-Key.
  • Both sides are using the same curve (secp256k1 is the default; check your onboarding agreement if uncertain).
A common root cause: you rotated the local key but never re-registered the public half. Or: staging and production keys got swapped in the environment variables.

Escalating to support

If you have walked through all eleven steps and still see SIGNATURE_INVALID, open a support ticket with three values:
  1. decisionId — from error.details[0].metadata.decisionId, format acd_…. One is minted per access decision; it lets Bloobank trace the exact evaluation server-side.
  2. X-Access-Request-Id — the ID you sent on the failing request.
  3. Approximate UTC time of the failed call.
With those three, Bloobank can pull the full audit trail in seconds.

Other authentication errors

TIMESTAMP_INVALID (HTTP 400)

X-Access-Timestamp is not a decimal integer in milliseconds. Fix: Send the literal milliseconds as a string. No fractional part. No + prefix. No ISO 8601 format.

TIMESTAMP_SKEW_EXCEEDED (HTTP 401)

The header timestamp is more than ±10 seconds from server time. Fix:
  1. Sync your host clock via NTP (chronyd, ntpd, or your platform’s equivalent).
  2. On VMs that pause and resume, the clock can jump. Configure clock-drift correction on resume.
  3. Read the timestamp immediately before sending, not at the start of a long-running function.
To verify: chronyc tracking (Linux) or sntp pool.ntp.org (macOS) should show drift under one second.

REPLAY_DETECTED (HTTP 401)

The (X-Access-Key, X-Access-Request-Id) tuple was already used within the last hour. Fix: Generate a brand-new X-Access-Request-Id (UUID v4) for every attempt, including retries. Never persist and reuse a request id.
Concurrent signing is safe. You can sign multiple requests in parallel with the same private key — each request just needs its own fresh timestamp and X-Access-Request-Id. Sharing the private key across concurrent signings is not a replay risk.

MISSING_HEADER (HTTP 400)

One of the four required X-Access-* headers is absent. Fix: Verify every request carries all four. Common omissions:
  • Forgetting X-Access-Request-Id on GET requests (it is required on every request, not just writes).
  • Header middleware stripping case-sensitive header names.

CREDENTIAL_DISABLED / CREDENTIAL_REVOKED / CREDENTIAL_EXPIRED (HTTP 401)

Your Access Key is no longer usable. Fix: Contact your Bloobank account team — these statuses mean the credential was administratively disabled or revoked.

RBAC_DENY (HTTP 403)

Authentication succeeded, but the credential is not authorized for this action on this resource. Fix: Request the appropriate role binding from your Bloobank account team. Include the decisionId from error.details[0].metadata.decisionId — it identifies the exact RBAC evaluation that denied.

Diagnostic logging

For every failed request, log: With these five values, Bloobank support can locate the exact decision server-side in seconds.

Next

Sign a request

The signing protocol, end to end.

Errors

The full error model — branching, retries, catalog.