ACCESS_KEY and PRIVATE_KEY, run. Every example below builds the canonical request string, signs it with ECDSA secp256k1, attaches the four headers, and sends a real request to the BlooBank API.
Replace placeholders with values from your secret store. Never commit the private key — use environment variables or a secret manager.
Complete signer client
The example below signs aPOST /wallets/{wallet}/paymentOrders request and returns the created payment order.
// npm install @noble/curves @noble/hashes
import { secp256k1 } from '@noble/curves/secp256k1';
import { sha256 } from '@noble/hashes/sha256';
import { randomUUID } from 'node:crypto';
class BlooBank {
constructor(accessKey, privateKeyHex) {
this.baseUrl = 'https://txengine.bloobank.com/txengine/v1';
this.accessKey = accessKey;
this.privateKey = privateKeyHex;
}
sign(method, pathname, rawBody) {
const timestamp = Date.now().toString();
const requestId = randomUUID();
const bodyHex = Buffer.from(sha256(rawBody)).toString('hex');
const canonical = `${this.accessKey}:${requestId}:${timestamp}:${method.toUpperCase()}:${pathname}:${bodyHex}`;
const digest = sha256(new TextEncoder().encode(canonical));
const sigBytes = secp256k1.sign(digest, this.privateKey, { lowS: true }).toCompactRawBytes();
const signatureB64 = Buffer.from(sigBytes).toString('base64');
return { timestamp, requestId, signatureB64 };
}
async request(method, pathname, body) {
const rawBody = body ? new TextEncoder().encode(JSON.stringify(body)) : new Uint8Array(0);
const { timestamp, requestId, signatureB64 } = this.sign(method, pathname, rawBody);
const res = await fetch(`${this.baseUrl}${pathname}`, {
method,
headers: {
'Content-Type': 'application/json',
'X-Access-Key': this.accessKey,
'X-Access-Timestamp': timestamp,
'X-Access-Request-Id': requestId,
'X-Access-Signature': signatureB64,
},
body: body ? rawBody : undefined,
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
return res.json();
}
createPaymentOrder(wallet, body) {
return this.request('POST', `/wallets/${wallet}/paymentOrders`, body);
}
}
// ---- usage ----
const client = new BlooBank(
process.env.BLOOBANK_ACCESS_KEY,
process.env.BLOOBANK_PRIVATE_KEY, // 64-char hex
);
const order = await client.createPaymentOrder('production-main', {
direction: 'IN',
network: 'br.gov.bcb.pix',
idempotencyKey: 'invoice-2026-0184',
amount: 25000, // BRL 250.00 in cents
currency: 'BRL',
instrument: { type: 'PIX_CASH_IN_EMV_DYNAMIC', expiresIn: 86400 },
});
console.log(order);
# pip install coincurve requests
import base64, hashlib, json, os, time, uuid, requests
from coincurve import PrivateKey
class BlooBank:
BASE_URL = 'https://txengine.bloobank.com/txengine/v1'
def __init__(self, access_key: str, private_key_hex: str):
self.access_key = access_key
self.priv = PrivateKey(bytes.fromhex(private_key_hex))
def sign(self, method: str, pathname: str, raw_body: bytes):
timestamp = str(int(time.time() * 1000))
request_id = str(uuid.uuid4())
body_hex = hashlib.sha256(raw_body).hexdigest()
canonical = f'{self.access_key}:{request_id}:{timestamp}:{method.upper()}:{pathname}:{body_hex}'
digest = hashlib.sha256(canonical.encode('utf-8')).digest()
signature = self.priv.sign(digest, hasher=None) # DER, accepted
return timestamp, request_id, base64.b64encode(signature).decode('ascii')
def request(self, method: str, pathname: str, body=None):
raw_body = json.dumps(body, separators=(',', ':')).encode() if body else b''
ts, rid, sig = self.sign(method, pathname, raw_body)
r = requests.request(
method, self.BASE_URL + pathname,
headers={
'Content-Type': 'application/json',
'X-Access-Key': self.access_key,
'X-Access-Timestamp': ts,
'X-Access-Request-Id': rid,
'X-Access-Signature': sig,
},
data=raw_body if body else None,
)
r.raise_for_status()
return r.json()
def create_payment_order(self, wallet, body):
return self.request('POST', f'/wallets/{wallet}/paymentOrders', body)
client = BlooBank(
os.environ['BLOOBANK_ACCESS_KEY'],
os.environ['BLOOBANK_PRIVATE_KEY'], # 64-char hex
)
print(client.create_payment_order('production-main', {
'direction': 'IN',
'network': 'br.gov.bcb.pix',
'idempotencyKey': 'invoice-2026-0184',
'amount': 25000,
'currency': 'BRL',
'instrument': {'type': 'PIX_CASH_IN_EMV_DYNAMIC', 'expiresIn': 86400},
}))
// go get github.com/btcsuite/btcd/btcec/v2
// go get github.com/btcsuite/btcd/btcec/v2/ecdsa
package main
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
"github.com/btcsuite/btcd/btcec/v2"
btcecdsa "github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/google/uuid"
)
const baseURL = "https://txengine.bloobank.com/txengine/v1"
type BlooBank struct {
AccessKey string
Priv *btcec.PrivateKey
}
func New(accessKey, privHex string) (*BlooBank, error) {
raw, err := hex.DecodeString(privHex)
if err != nil { return nil, err }
priv, _ := btcec.PrivKeyFromBytes(raw)
return &BlooBank{AccessKey: accessKey, Priv: priv}, nil
}
func (b *BlooBank) sign(method, pathname string, rawBody []byte) (ts, reqID, sig string) {
ts = strconv.FormatInt(time.Now().UnixMilli(), 10)
reqID = uuid.NewString()
bodyHex := fmt.Sprintf("%x", sha256.Sum256(rawBody))
canonical := fmt.Sprintf("%s:%s:%s:%s:%s:%s", b.AccessKey, reqID, ts, method, pathname, bodyHex)
digest := sha256.Sum256([]byte(canonical))
s := btcecdsa.Sign(b.Priv, digest[:])
sig = base64.StdEncoding.EncodeToString(s.Serialize())
return
}
func (b *BlooBank) request(method, pathname string, body any) (map[string]any, error) {
var rawBody []byte
if body != nil { rawBody, _ = json.Marshal(body) }
ts, reqID, sig := b.sign(method, pathname, rawBody)
req, _ := http.NewRequest(method, baseURL+pathname, bytes.NewReader(rawBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Access-Key", b.AccessKey)
req.Header.Set("X-Access-Timestamp", ts)
req.Header.Set("X-Access-Request-Id", reqID)
req.Header.Set("X-Access-Signature", sig)
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 { return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, out) }
var v map[string]any
_ = json.Unmarshal(out, &v)
return v, nil
}
func main() {
c, _ := New(os.Getenv("BLOOBANK_ACCESS_KEY"), os.Getenv("BLOOBANK_PRIVATE_KEY"))
order, err := c.request("POST", "/wallets/production-main/paymentOrders", map[string]any{
"direction": "IN",
"network": "br.gov.bcb.pix",
"idempotencyKey": "invoice-2026-0184",
"amount": 25000,
"currency": "BRL",
"instrument": map[string]any{"type": "PIX_CASH_IN_EMV_DYNAMIC", "expiresIn": 86400},
})
if err != nil { panic(err) }
fmt.Printf("%+v\n", order)
}
// Maven: org.bouncycastle:bcprov-jdk18on:1.80, com.google.code.gson:gson:2.13.1
import com.google.gson.Gson;
import org.bouncycastle.crypto.ec.CustomNamedCurves;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.crypto.signers.ECDSASigner;
import org.bouncycastle.util.encoders.Hex;
import java.math.BigInteger;
import java.net.URI;
import java.net.http.*;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.*;
public class BlooBankSigner {
private static final String BASE_URL = "https://txengine.bloobank.com/txengine/v1";
private static final String ACCESS_KEY = System.getenv("BLOOBANK_ACCESS_KEY");
private static final String PRIVATE_KEY = System.getenv("BLOOBANK_PRIVATE_KEY");
public static void main(String[] args) throws Exception {
var payload = Map.of(
"direction", "IN",
"network", "br.gov.bcb.pix",
"idempotencyKey", "invoice-2026-0184",
"amount", 25000,
"currency", "BRL",
"instrument", Map.of("type", "PIX_CASH_IN_EMV_DYNAMIC", "expiresIn", 86400)
);
var json = new Gson().toJson(payload);
var pathname = "/wallets/production-main/paymentOrders";
var timestamp = String.valueOf(Instant.now().toEpochMilli());
var requestId = UUID.randomUUID().toString();
var bodyHex = bytesToHex(sha256(json.getBytes()));
var canonical = ACCESS_KEY + ":" + requestId + ":" + timestamp + ":POST:" + pathname + ":" + bodyHex;
var digest = sha256(canonical.getBytes());
var curve = CustomNamedCurves.getByName("secp256k1");
var domain = new ECDomainParameters(curve.getCurve(), curve.getG(), curve.getN());
var priv = new ECPrivateKeyParameters(new BigInteger(1, Hex.decode(PRIVATE_KEY)), domain);
var signer = new ECDSASigner();
signer.init(true, priv);
BigInteger[] rs = signer.generateSignature(digest);
// Low-S normalization
BigInteger halfN = curve.getN().shiftRight(1);
BigInteger s = rs[1].compareTo(halfN) > 0 ? curve.getN().subtract(rs[1]) : rs[1];
byte[] sig = derEncode(rs[0], s);
var signatureB64 = Base64.getEncoder().encodeToString(sig);
var resp = HttpClient.newHttpClient().send(
HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + pathname))
.header("Content-Type", "application/json")
.header("X-Access-Key", ACCESS_KEY)
.header("X-Access-Timestamp", timestamp)
.header("X-Access-Request-Id", requestId)
.header("X-Access-Signature", signatureB64)
.POST(HttpRequest.BodyPublishers.ofString(json))
.build(),
HttpResponse.BodyHandlers.ofString()
);
System.out.println(resp.statusCode() + " " + resp.body());
}
static byte[] sha256(byte[] b) throws Exception { return MessageDigest.getInstance("SHA-256").digest(b); }
static String bytesToHex(byte[] b) { var sb = new StringBuilder(); for (var x : b) sb.append(String.format("%02x", x)); return sb.toString(); }
static byte[] derEncode(BigInteger r, BigInteger s) {
byte[] rb = r.toByteArray(), sb = s.toByteArray();
byte[] out = new byte[6 + rb.length + sb.length];
out[0] = 0x30; out[1] = (byte)(4 + rb.length + sb.length);
out[2] = 0x02; out[3] = (byte) rb.length; System.arraycopy(rb, 0, out, 4, rb.length);
out[4 + rb.length] = 0x02; out[5 + rb.length] = (byte) sb.length;
System.arraycopy(sb, 0, out, 6 + rb.length, sb.length);
return out;
}
}
<?php
// Uses OpenSSL.
class BlooBank {
const BASE_URL = 'https://txengine.bloobank.com/txengine/v1';
private string $accessKey;
private $privateKey; // OpenSSL resource
public function __construct(string $accessKey, string $privateKeyPem) {
$this->accessKey = $accessKey;
$this->privateKey = openssl_pkey_get_private($privateKeyPem);
if (!$this->privateKey) throw new Exception('Invalid private key');
}
public function createPaymentOrder(string $wallet, array $body): array {
return $this->request('POST', "/wallets/$wallet/paymentOrders", $body);
}
private function sign(string $method, string $pathname, string $rawBody): array {
$ts = (string) intval(microtime(true) * 1000);
$rid = bin2hex(random_bytes(16)); // UUID-like
$bodyHex = hash('sha256', $rawBody);
$canonical = "$this->accessKey:$rid:$ts:" . strtoupper($method) . ":$pathname:$bodyHex";
$digest = hash('sha256', $canonical, true);
openssl_sign($digest, $signature, $this->privateKey, OPENSSL_ALGO_SHA256);
return [$ts, $rid, base64_encode($signature)];
}
private function request(string $method, string $pathname, ?array $body): array {
$rawBody = $body ? json_encode($body, JSON_UNESCAPED_SLASHES) : '';
[$ts, $rid, $sig] = $this->sign($method, $pathname, $rawBody);
$ch = curl_init(self::BASE_URL . $pathname);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_POSTFIELDS => $rawBody ?: null,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"X-Access-Key: $this->accessKey",
"X-Access-Timestamp: $ts",
"X-Access-Request-Id: $rid",
"X-Access-Signature: $sig",
],
]);
$res = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 400) throw new Exception("HTTP $code: $res");
return json_decode($res, true);
}
}
$client = new BlooBank(getenv('BLOOBANK_ACCESS_KEY'), file_get_contents('/path/to/privateKey.pem'));
print_r($client->createPaymentOrder('production-main', [
'direction' => 'IN',
'network' => 'br.gov.bcb.pix',
'idempotencyKey' => 'invoice-2026-0184',
'amount' => 25000,
'currency' => 'BRL',
'instrument' => ['type' => 'PIX_CASH_IN_EMV_DYNAMIC', 'expiresIn' => 86400],
]));
// NuGet: BouncyCastle.Cryptography
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Org.BouncyCastle.Asn1.Sec;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Math;
public class BlooBank {
private const string BaseUrl = "https://txengine.bloobank.com/txengine/v1";
private readonly string _accessKey;
private readonly string _privateKeyHex;
public BlooBank(string accessKey, string privateKeyHex) {
_accessKey = accessKey;
_privateKeyHex = privateKeyHex;
}
public async Task<JsonElement> CreatePaymentOrderAsync(string wallet, object body) {
var json = JsonSerializer.Serialize(body);
var pathname = $"/wallets/{wallet}/paymentOrders";
var (ts, rid, sig) = Sign("POST", pathname, Encoding.UTF8.GetBytes(json));
using var http = new HttpClient();
var req = new HttpRequestMessage(HttpMethod.Post, BaseUrl + pathname) {
Content = new StringContent(json, Encoding.UTF8, "application/json"),
};
req.Headers.Add("X-Access-Key", _accessKey);
req.Headers.Add("X-Access-Timestamp", ts);
req.Headers.Add("X-Access-Request-Id", rid);
req.Headers.Add("X-Access-Signature", sig);
var res = await http.SendAsync(req);
res.EnsureSuccessStatusCode();
return JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
}
private (string ts, string rid, string sig) Sign(string method, string pathname, byte[] rawBody) {
var ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
var rid = Guid.NewGuid().ToString();
var bodyHex = Convert.ToHexString(SHA256.HashData(rawBody)).ToLowerInvariant();
var canonical = $"{_accessKey}:{rid}:{ts}:{method}:{pathname}:{bodyHex}";
var digest = SHA256.HashData(Encoding.UTF8.GetBytes(canonical));
var curve = SecNamedCurves.GetByName("secp256k1");
var domain = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H);
var priv = new ECPrivateKeyParameters(new BigInteger(1, Convert.FromHexString(_privateKeyHex)), domain);
var signer = new ECDsaSigner();
signer.Init(true, priv);
var rs = signer.GenerateSignature(digest);
// Low-S normalization
var halfN = curve.N.ShiftRight(1);
var s = rs[1].CompareTo(halfN) > 0 ? curve.N.Subtract(rs[1]) : rs[1];
// DER encode
var derR = rs[0].ToByteArray(); var derS = s.ToByteArray();
var der = new byte[6 + derR.Length + derS.Length];
der[0] = 0x30; der[1] = (byte)(4 + derR.Length + derS.Length);
der[2] = 0x02; der[3] = (byte) derR.Length; Array.Copy(derR, 0, der, 4, derR.Length);
der[4 + derR.Length] = 0x02; der[5 + derR.Length] = (byte) derS.Length;
Array.Copy(derS, 0, der, 6 + derR.Length, derS.Length);
return (ts, rid, Convert.ToBase64String(der));
}
}
Handling errors in code
Branch onerror.status, never on error.message. See Handling errors for the full pattern.
try {
const order = await client.createPaymentOrder('production-main', body);
// ...
} catch (err) {
const status = err.body?.error?.status;
switch (status) {
case 'SIGNATURE_INVALID':
console.error('Check canonical string and low-S normalization');
break;
case 'TIMESTAMP_SKEW_EXCEEDED':
console.error('Clock drift — sync via NTP');
break;
case 'REPLAY_DETECTED':
console.error('Generate a new X-Access-Request-Id and retry');
break;
case 'IDEMPOTENCY_KEY_IN_USE_WITH_DIFFERENT_PARAMS':
console.error('Idempotency key already used with different body');
break;
case 'RESOURCE_EXHAUSTED':
// exponential backoff and retry — see /get-started/errors/retry-strategy
break;
default:
throw err;
}
}
from requests.exceptions import HTTPError
try:
order = client.create_payment_order('production-main', body)
except HTTPError as e:
err = e.response.json().get('error', {})
status = err.get('status')
if status == 'SIGNATURE_INVALID':
print('Check canonical string and low-S normalization')
elif status == 'TIMESTAMP_SKEW_EXCEEDED':
print('Clock drift — sync via NTP')
elif status == 'REPLAY_DETECTED':
print('Generate a new X-Access-Request-Id and retry')
elif status == 'IDEMPOTENCY_KEY_IN_USE_WITH_DIFFERENT_PARAMS':
print('Idempotency key already used with different body')
else:
raise
What’s next
Troubleshooting
11-step SIGNATURE_INVALID checklist.
Idempotency
Make every retry safe.
API reference
Every endpoint with try-it playground.