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
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.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.Timestamp unit
Is
X-Access-Timestamp in milliseconds (13 digits for 2026)? Not seconds (10 digits), not microseconds (16 digits), not ISO 8601.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.Low-S normalization
If everything above is correct and you are still getting See your library’s documentation for the canonical/lowS option name.
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_sflag in your library. - Normalize manually: if
s > n/2, replaceswithn - s(wherenis the curve order).
secp256k1, n is: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/mainare different paths. - Is the pathname URL-decoded? Encoded characters in the canonical string fail.
- Does it start with
/?
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.Base64 encoding
Standard Base64 (RFC 4648 — characters
A–Z a–z 0–9 + /, padded with =). Not URL-safe Base64 (characters - _). No line breaks.Field separator
The canonical request uses colon
: between all six fields. Not |, not \n, not space, not comma.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.
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 (
secp256k1is the default; check your onboarding agreement if uncertain).
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.
TIMESTAMP_SKEW_EXCEEDED (HTTP 401)
The header timestamp is more than ±10 seconds from server time.
Fix:
- Sync your host clock via NTP (
chronyd,ntpd, or your platform’s equivalent). - On VMs that pause and resume, the clock can jump. Configure clock-drift correction on resume.
- Read the timestamp immediately before sending, not at the start of a long-running function.
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.
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-Idon 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:| Field | From |
|---|---|
X-Access-Request-Id you sent | Your request headers |
error.status | Response body |
error.details[].reason | Response body |
decisionId (when present) | error.details[0].metadata.decisionId |
| Approximate UTC time of the call | Your clock |
| HTTP method and full path | Request line |
Next
Sign a request
The signing protocol, end to end.
Errors
The full error model — branching, retries, catalog.