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

# Sign a request

> Build the canonical request string, sign it with ECDSA secp256k1, attach the four headers, and send.

Every authenticated request to the BlooBank API carries the same four headers and a signature computed over a deterministic **canonical request string**. This page is the normative reference — get it right once and every endpoint works.

For working code in your language, see [Code examples](/get-started/authentication/code-examples). For diagnostics, see [Troubleshooting](/get-started/authentication/troubleshooting).

## The four required headers

| Header                | Format                            | Notes                                                                                                   |
| --------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `X-Access-Key`        | String                            | Your Access Key, exactly as issued. Case-sensitive.                                                     |
| `X-Access-Timestamp`  | Decimal integer                   | **Unix epoch in milliseconds (UTC).** 13 digits in 2026. Must be within **±10 seconds** of server time. |
| `X-Access-Request-Id` | String (UUID v4 recommended)      | Unique per request. Must not be reused within the last **1 hour** for the same Access Key.              |
| `X-Access-Signature`  | Base64 (RFC 4648, no line breaks) | ECDSA `secp256k1` signature over the canonical request.                                                 |

<Warning>
  **The private key never leaves your environment.** It is used to sign the canonical request locally. The only thing that travels over the wire is the resulting Base64 signature.
</Warning>

## Build the canonical request string

The signature is computed over a deterministic string both sides build identically:

```
{accessKey}:{requestId}:{timestamp}:{METHOD}:{pathname}:{bodySha256Hex}
```

| Field             | Value                                                    | Rules                                                                                                                                 |
| ----------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `{accessKey}`     | The exact value you send in `X-Access-Key`               | Verbatim.                                                                                                                             |
| `{requestId}`     | The exact value you send in `X-Access-Request-Id`        | Verbatim.                                                                                                                             |
| `{timestamp}`     | The exact value you send in `X-Access-Timestamp`         | Decimal integer, milliseconds.                                                                                                        |
| `{METHOD}`        | HTTP method                                              | **Uppercase**: `GET`, `POST`, `PUT`.                                                                                                  |
| `{pathname}`      | Request path only                                        | **No query string.** No trailing slash. URL-decoded. Must begin with `/`.                                                             |
| `{bodySha256Hex}` | SHA-256 of the **raw** request body in **lowercase hex** | For requests without a body, use the SHA-256 of the empty string: `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`. |

Fields are joined with the ASCII colon `:` (0x3A). No whitespace, no trailing newline, encoded as UTF-8.

## The three most common mistakes

<AccordionGroup>
  <Accordion title="Including the query string in pathname" icon="triangle-exclamation">
    If your URL is `/wallets/main/paymentOrders?status=SUCCESS`, the `{pathname}` field is `/wallets/main/paymentOrders` — the `?status=SUCCESS` part is stripped. Sign the **path only**.
  </Accordion>

  <Accordion title="Hashing a re-serialized body instead of raw bytes" icon="file-code">
    Hash the **exact bytes** that will travel on the wire — not a re-serialized version. If you parse JSON into an object and re-stringify it before hashing, whitespace, key order, and escape sequences may differ from what your HTTP client eventually sends. Result: `SIGNATURE_INVALID`.
  </Accordion>

  <Accordion title="Reading the clock twice" icon="clock">
    Read the current time **once**, store it in a variable, and use the same value in both the `X-Access-Timestamp` header and the `{timestamp}` field of the canonical string. Reading the clock twice — even microseconds apart — gives you a different value in each place.
  </Accordion>
</AccordionGroup>

## Sign and encode

<Steps>
  <Step title="SHA-256 of the canonical string">
    Compute the SHA-256 digest of the UTF-8 bytes of the canonical request. This yields a 32-byte digest.
  </Step>

  <Step title="ECDSA sign">
    Sign the digest with your private key on `secp256k1`.
  </Step>

  <Step title="Low-S normalization (mandatory)">
    The signature must be in **low-S** form (i.e., `s ≤ n/2`). Most modern libraries do this by default; some (notably WebCrypto) do not. Enable the `lowS` / `canonical` / `normalize_s` flag. See [Troubleshooting](/get-started/authentication/troubleshooting).
  </Step>

  <Step title="Base64 encode">
    Standard Base64 (RFC 4648), **not** URL-safe. No line breaks. Both DER and P1363 (`r || s`) signature encodings are accepted — whichever your library emits will work.
  </Step>

  <Step title="Attach as X-Access-Signature">
    Send the Base64 result in the `X-Access-Signature` header alongside the other three.
  </Step>
</Steps>

## Pseudocode

```text theme={"dark"}
accessKey  = "..."                                  // from onboarding
privateKey = load_secp256k1_private_key()           // from your secret store

timestamp  = current_unix_time_millis_utc()         // single read
requestId  = uuid_v4()                              // fresh per attempt

rawBody       = exact_bytes_of_request_body         // empty bytes if no body
bodySha256Hex = lowercase_hex(sha256(rawBody))

canonical = accessKey + ":" +
            requestId + ":" +
            timestamp + ":" +
            upper(http_method) + ":" +
            pathname_without_query + ":" +
            bodySha256Hex

digest       = sha256(utf8_bytes(canonical))
signatureRaw = ecdsa_secp256k1_sign(privateKey, digest, lowS=true)
signatureB64 = base64(signatureRaw)

send_http_request(
  method   = http_method,
  url      = base_url + pathname + optional_query_string,
  headers  = {
    "X-Access-Key":        accessKey,
    "X-Access-Timestamp":  timestamp,
    "X-Access-Request-Id": requestId,
    "X-Access-Signature":  signatureB64,
    "Content-Type":        "application/json"
  },
  body     = rawBody
)
```

## Worked example — `GET` with query, no body

**Target request:**

```http theme={"dark"}
GET /wallets/production-main?filter=status%3DACTIVE HTTP/1.1
Host: txengine.bloobank.com
```

**Intermediate values:**

| Field                 | Value                                                              |
| --------------------- | ------------------------------------------------------------------ |
| `accessKey`           | `7KPfTs9N2xYbHmRdZ4vG6qAeL1wUcJ3MtB8oXnV5hWkS`                     |
| `requestId`           | `550e8400-e29b-41d4-a716-446655440000`                             |
| `timestamp`           | `1736553600123`                                                    |
| `method`              | `GET`                                                              |
| `pathname` (no query) | `/wallets/production-main`                                         |
| Raw body              | *(empty)*                                                          |
| `bodySha256Hex`       | `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` |

**Canonical request:**

```
7KPfTs9N2xYbHmRdZ4vG6qAeL1wUcJ3MtB8oXnV5hWkS:550e8400-e29b-41d4-a716-446655440000:1736553600123:GET:/wallets/production-main:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```

Sign that string with your private key and Base64 the result. The `?filter=status%3DACTIVE` query stays on the URL line but is **not** included in the canonical request.

## Worked example — `POST` with JSON body

**Target request:**

```http theme={"dark"}
POST /wallets/production-main/paymentOrders HTTP/1.1
Host: txengine.bloobank.com
Content-Type: application/json

{"direction":"IN","network":"br.gov.bcb.pix","idempotencyKey":"invoice-2026-0184","amount":25000,"currency":"BRL","instrument":{"type":"PIX_CASH_IN_EMV_DYNAMIC","expiresIn":86400}}
```

**Intermediate values:**

| Field           | Value                                                   |
| --------------- | ------------------------------------------------------- |
| `accessKey`     | `7KPfTs9N2xYbHmRdZ4vG6qAeL1wUcJ3MtB8oXnV5hWkS`          |
| `requestId`     | `7c9e6679-7425-40de-944b-e07fc1f90ae7`                  |
| `timestamp`     | `1736553605456`                                         |
| `method`        | `POST`                                                  |
| `pathname`      | `/wallets/production-main/paymentOrders`                |
| `bodySha256Hex` | (SHA-256 of the exact bytes shown above, lowercase hex) |

The canonical request follows the same format. If your serializer changes a single byte of the JSON between hashing and sending, you will get `SIGNATURE_INVALID` — hash the bytes you are about to send, not a re-parsed copy.

## Send the request

The final HTTP request:

```http theme={"dark"}
POST /wallets/production-main/paymentOrders HTTP/1.1
Host: txengine.bloobank.com
Content-Type: application/json
X-Access-Key: 7KPfTs9N2xYbHmRdZ4vG6qAeL1wUcJ3MtB8oXnV5hWkS
X-Access-Timestamp: 1736553605456
X-Access-Request-Id: 7c9e6679-7425-40de-944b-e07fc1f90ae7
X-Access-Signature: <BASE64_SIGNATURE>

{"direction":"IN","network":"br.gov.bcb.pix",...}
```

A successful response is `201 Created` with the created payment order in the body.

## Security checklist

* **Never hardcode the private key.** Load it from a secret manager.
* **Never transmit the private key.** The signature alone goes over the wire.
* **Synchronize your clock.** With a ±10-second window, NTP is mandatory.
* **Generate a fresh `X-Access-Request-Id` per attempt.** Including retries.
* **Use HTTPS only.** TLS encrypts the body; the signature only proves authenticity.
* **Rotate credentials periodically.** Every 6–12 months at minimum.
* **Log `decisionId` and `excRecordId` on failure** — they let support trace your request server-side. See [Errors](/get-started/errors/overview).

## Next

<CardGroup cols={3}>
  <Card title="Code examples" icon="code" href="/get-started/authentication/code-examples">
    Working clients in Node.js, Python, Go, Java, PHP, and .NET.
  </Card>

  <Card title="Troubleshooting" icon="wrench" href="/get-started/authentication/troubleshooting">
    Diagnose SIGNATURE\_INVALID in 11 steps.
  </Card>

  <Card title="Errors" icon="bug" href="/get-started/errors/overview">
    Branch on error.status, retry safely.
  </Card>
</CardGroup>
