Skip to main content
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. For diagnostics, see Troubleshooting.

The four required headers

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.

Build the canonical request string

The signature is computed over a deterministic string both sides build identically:
The six fields are joined with the ASCII colon : (0x3A). No whitespace, no trailing newline, encoded as UTF-8.
No body, no exceptions. For requests without a body, {bodySha256Hex} is the SHA-256 of the empty string — the hash of zero bytes:
Not the hash of the literal string "" (which would produce a different value), not null, not a placeholder.
Sign the full pathname, prefix included. The API Reference lists endpoints without the /txengine/v1 prefix because the prefix is part of the server base URL (https://txengine.bloobank.com/txengine/v1). The signature, however, must always be computed over the full pathname as sent on the wire — /txengine/v1/wallets/..., never /wallets/.... Signing the short path is a common cause of SIGNATURE_INVALID.

The three most common mistakes

If your URL is /txengine/v1/wallets/main/paymentOrders?status=SUCCESS, the {pathname} field is /txengine/v1/wallets/main/paymentOrders — the ?status=SUCCESS part is stripped. Sign the path only, query removed.
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.
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.

Sign and encode

1

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

ECDSA sign

Sign the digest with your private key on secp256k1.
3

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

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

Attach as X-Access-Signature

Send the Base64 result in the X-Access-Signature header alongside the other three.

Signature encoding

The server accepts both common ECDSA signature encodings:
  • DER (ASN.1) — variable length, typical of OpenSSL and most Java and Python libraries.
  • P1363 (r || s, 64 raw bytes) — typical of WebCrypto and some Go libraries.
You do not need to convert from one to the other. Whichever encoding your library emits works, as long as it is Base64-encoded with standard Base64 (RFC 4648) — not URL-safe, no line breaks. Low-S normalization remains mandatory for either encoding: if s > n/2, replace s with n − s (where n is the curve order).

Signing curve

All examples on this page use secp256k1, the most common choice. The signing curve is agreed during onboarding — if your credential was assigned a different curve, substitute it wherever secp256k1 appears; the rest of the protocol is identical.

Pseudocode

Worked example — GET with query, no body

Target request:
Intermediate values: Canonical request:
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:
Intermediate values: 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:
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 the exception record metadata.id (format exc_…) on failure — they let support trace your request server-side. See Errors.

Next

Code examples

Working clients in Node.js, Python, Go, Java, PHP, and .NET.

Troubleshooting

Diagnose SIGNATURE_INVALID in 11 steps.

Errors

Branch on error.status, retry safely.