Menu
API keys and signing
Create a key in
API-key settings. The key
starts with bb/public/; the secret starts with bb/apisecret/. Both are
credentials. Do not put them in public source code or URLs.
Headers
Read requests normally require only x-bb-api-key. Signed requests require all
three headers:
| Header | Value |
|---|---|
x-bb-api-key | Complete API key |
x-bb-api-nonce | Unsigned decimal integer greater than the last accepted nonce for the key |
x-bb-api-hmac | Base64-encoded HMAC-SHA256 signature |
Enable trading permission for creating and cancelling orders. Enable bitcoin withdrawal permission for initiating withdrawals.
Signature calculation
digest = SHA256(UTF8(decimalNonce) || rawRequestBodyBytes)
message = UTF8(uppercaseMethod) || UTF8(uriPath) || digest
signature = Base64(HMAC_SHA256(Base64Decode(completeSecret), message))Decode the complete secret, including its prefix. The signed path is the decoded URL path and excludes the query string. For a bodyless request, use zero body bytes. Serialize a JSON body once and send precisely those signed bytes.
Nonce validation and retries
Coordinate signed requests per key. A timestamp alone does not prevent collisions between concurrent requests or out-of-order arrival. The server can consume a nonce before an operation fails; the next attempt needs a larger nonce.
After a timeout, inspect orders or withdrawals before retrying. A nonce is not an idempotency key, and repeating a request can create another transaction.
Node.js signing helper
Save the following as bb-request.mjs. Set BB_API_KEY, BB_API_SECRET, and a
coordinated BB_NONCE in your environment. It sends requests to production only
when explicitly executed. Replace example destination and path identifiers
before use.
import { createHash, createHmac } from 'node:crypto';
import { readFile } from 'node:fs/promises';
export function signature(method, path, nonce, body, secret) {
const digest = createHash('sha256').update(nonce).update(body).digest();
return createHmac('sha256', Buffer.from(secret, 'base64'))
.update(method.toUpperCase())
.update(path)
.update(digest)
.digest('base64');
}
// Importing the helper never sends a request.
if (process.argv[1] && import.meta.url === new URL(process.argv[1], 'file:').href) {
const [method, path, bodyFile] = process.argv.slice(2);
const { BB_API_KEY: key, BB_API_SECRET: secret, BB_NONCE: nonce } = process.env;
if (!key || !secret || !nonce || !/^\d+$/.test(nonce) || !method || !path?.startsWith('/v1/')) {
throw new Error('Set BB_API_KEY, BB_API_SECRET, BB_NONCE; pass METHOD /v1/path [body.json].');
}
const body = bodyFile ? await readFile(bodyFile) : Buffer.alloc(0);
const url = new URL(path, 'https://api.bb.no');
const headers = {
'x-bb-api-key': key,
'x-bb-api-nonce': nonce,
'x-bb-api-hmac': signature(method, decodeURIComponent(url.pathname), nonce, body, secret),
...(bodyFile ? { 'Content-Type': 'application/json' } : {}),
};
const response = await fetch(url, {
method,
headers,
body: bodyFile ? body : undefined,
redirect: 'error',
});
const result = await response.text();
if (!response.ok) throw new Error(`HTTP ${response.status}: ${result}`);
console.log(result);
}
For a bodyless cancellation:
node bb-request.mjs DELETE '/v1/orders/REPLACE_ORDER_ID'For an order, save the request body to body.json, then run:
node bb-request.mjs POST '/v1/orders' body.jsonSigning fixture
For method POST, path /v1/orders, nonce 1733314678, synthetic secret
bb/apisecret/ZZavHDgVRyGowg8blKfPDDRlN3+6h0/vOUA, and these exact body bytes:
{ "type": "ORDER_TYPE_MARKET", "direction": "DIRECTION_BUY", "amount": 100 }The expected signature is e6pQ5w9AqVhwHRWXuwS7ZwzRd0kH2GYpHSmtP0cTlSU=. This
is a test fixture, not a usable account credential.
Go signing helper
This function constructs a signed request without sending it. Pass the body bytes you intend to send, the complete secret, and a coordinated nonce. Execute the returned request with an HTTP client that has a timeout and rejects redirects.
package signing
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"strings"
)
func SignedRequest(method, path, key, secret, nonce string, body []byte) (*http.Request, error) {
if !strings.HasPrefix(path, "/v1/") {
return nil, fmt.Errorf("path must start with /v1/")
}
secretBytes, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return nil, fmt.Errorf("decode API secret: %w", err)
}
req, err := http.NewRequest(strings.ToUpper(method), "https://api.bb.no"+path, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
digest := sha256.Sum256(append([]byte(nonce), body...))
message := append([]byte(req.Method+req.URL.Path), digest[:]...)
mac := hmac.New(sha256.New, secretBytes)
if _, err := mac.Write(message); err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
req.Header.Set("x-bb-api-key", key)
req.Header.Set("x-bb-api-nonce", nonce)
req.Header.Set("x-bb-api-hmac", base64.StdEncoding.EncodeToString(mac.Sum(nil)))
if len(body) > 0 {
req.Header.Set("Content-Type", "application/json")
}
return req, nil
}