Skip to main content

Documentation Index

Fetch the complete documentation index at: https://developers.bloobank.com/llms.txt

Use this file to discover all available pages before exploring further.

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.
✓ 1736553600123     milliseconds (correct)
✗ 1736553600        seconds (TIMESTAMP_INVALID)
✗ 1736553600123456  microseconds (TIMESTAMP_INVALID or SKEW)
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:
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
See your library’s documentation for the canonical/lowS option name.
6

Pathname canonicalization

Quick check:
  • Did you include the query string by mistake? Strip everything from ? onwards.
  • Did you accidentally keep a trailing slash? /wallets/main/ vs /wallets/main are different paths.
  • Is the pathname URL-decoded? 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?
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
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.
✓ MEYCIQD0v...+/...=
✗ MEYCIQD0v...-_...=   (URL-safe variant — fails)
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.
If you have walked through all eleven and still see SIGNATURE_INVALID, capture the decisionId from error.details[0].metadata.decisionId and open a support ticket.

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.
// ✓ Node.js
headers['X-Access-Timestamp'] = String(Date.now());

// ✗ Common mistake
headers['X-Access-Timestamp'] = new Date().toISOString();

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.
// ✓ Right
async function callOnce(payload) {
  const requestId = randomUUID();  // fresh each time
  const sig = sign(...);
  return await http.post(url, payload, { 'X-Access-Request-Id': requestId, ... });
}

// ✗ Wrong — same id on retry
const requestId = randomUUID();  // hoisted out of the retry loop
for (let attempt = 0; attempt < 3; attempt++) {
  try { return await http.post(url, payload, { 'X-Access-Request-Id': requestId, ... }); }
  catch (e) { /* will fail on second iteration */ }
}

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:
FieldFrom
X-Access-Request-Id you sentYour request headers
error.statusResponse body
error.details[].reasonResponse body
decisionId (when present)error.details[0].metadata.decisionId
Approximate UTC time of the callYour clock
HTTP method and full pathRequest line
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.