> ## 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.

# Troubleshooting

> Diagnose SIGNATURE_INVALID, TIMESTAMP_SKEW_EXCEEDED, REPLAY_DETECTED, and other authentication failures.

`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

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Timestamp unit">
    Is `X-Access-Timestamp` in **milliseconds** (13 digits for 2026)? Not seconds (10 digits), not microseconds (16 digits), not ISO 8601.

    ```text theme={"dark"}
    ✓ 1736553600123     milliseconds (correct)
    ✗ 1736553600        seconds (TIMESTAMP_INVALID)
    ✗ 1736553600123456  microseconds (TIMESTAMP_INVALID or SKEW)
    ```
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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 `/`?
  </Step>

  <Step title="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.
  </Step>

  <Step title="Base64 encoding">
    Standard Base64 (RFC 4648 — characters `A–Z a–z 0–9 + /`, padded with `=`). **Not** URL-safe Base64 (characters `- _`). No line breaks.

    ```text theme={"dark"}
    ✓ MEYCIQD0v...+/...=
    ✗ MEYCIQD0v...-_...=   (URL-safe variant — fails)
    ```
  </Step>

  <Step title="Field separator">
    The canonical request uses **colon `:`** between all six fields. Not `|`, not `\n`, not space, not comma.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

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.

```ts theme={"dark"}
// ✓ 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.

```ts theme={"dark"}
// ✓ 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:

| 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                           |

With these five values, BlooBank support can locate the exact decision server-side in seconds.

## Next

<CardGroup cols={2}>
  <Card title="Sign a request" icon="signature" href="/get-started/authentication/sign-request">
    The signing protocol, end to end.
  </Card>

  <Card title="Errors" icon="bug" href="/get-started/errors/overview">
    The full error model — branching, retries, catalog.
  </Card>
</CardGroup>
