Request Signing
HMAC-SHA256 signature algorithm with reference implementations.
All requests are signed with HMAC-SHA256 using your API secret as the key. The signature proves both authenticity and integrity — any change to method, path, body, timestamp, or nonce invalidates it.
Algorithm
Build the payload string
Concatenate fields with a literal . separator:
timestamp.method.path.nonce[.body]| Field | Source |
|---|---|
timestamp | The value of your X-Timestamp header |
method | HTTP method in uppercase (GET, POST, PUT, PATCH, DELETE) |
path | Request path including /open-api/v1 prefix, e.g. /open-api/v1/cards |
nonce | The value of your X-Nonce header |
body | The exact JSON body bytes — only for POST / PUT / PATCH |
Compute HMAC-SHA256
signature = base64(hmac_sha256(payload, api_secret))Send the signature
Set the result as the X-Signature header.
The exact JSON string used for signing must be the exact body sent on the wire. Many HTTP libraries (axios, requests, OkHttp's JSON helpers) re-serialize objects and silently re-order keys or change whitespace. Serialize once into a string, sign that string, and send that same string.
Reference implementations
const crypto = require('crypto');
const axios = require('axios');
function generateSignature(method, path, timestamp, nonce, body, apiSecret) {
let payload = `${timestamp}.${method.toUpperCase()}.${path}.${nonce}`;
if (body && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
payload += `.${bodyStr}`;
}
return crypto.createHmac('sha256', apiSecret).update(payload).digest('base64');
}
async function makeApiRequest(method, path, body, apiKey, apiSecret, baseUrl) {
const timestamp = Date.now().toString();
const nonce = crypto.randomBytes(16).toString('hex');
// Serialise ONCE — use the same string for signing and the request body.
let bodyStr = null;
if (body && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
bodyStr = JSON.stringify(body);
}
const signature = generateSignature(method, path, timestamp, nonce, bodyStr, apiSecret);
return axios({
method,
url: `${baseUrl}${path}`,
headers: {
'X-API-Key': apiKey,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
'Content-Type': 'application/json',
},
// Send as a string so axios does not re-serialise.
data: bodyStr,
});
}import base64
import hashlib
import hmac
import json
import time
import uuid
import requests
def generate_signature(method, path, timestamp, nonce, body, api_secret):
payload = f"{timestamp}.{method.upper()}.{path}.{nonce}"
if body and method.upper() in {"POST", "PUT", "PATCH"}:
body_str = body if isinstance(body, str) else json.dumps(body)
payload += f".{body_str}"
digest = hmac.new(
api_secret.encode("utf-8"),
payload.encode("utf-8"),
hashlib.sha256,
).digest()
return base64.b64encode(digest).decode("utf-8")
def make_api_request(method, path, body, api_key, api_secret, base_url):
timestamp = str(int(time.time() * 1000))
nonce = uuid.uuid4().hex
body_str = None
if body and method.upper() in {"POST", "PUT", "PATCH"}:
body_str = json.dumps(body) # Serialise ONCE.
signature = generate_signature(method, path, timestamp, nonce, body_str, api_secret)
headers = {
"X-API-Key": api_key,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
"Content-Type": "application/json",
}
# Use data=, NOT json=, to avoid re-serialisation.
return requests.request(method, f"{base_url}{path}", headers=headers, data=body_str)import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class BuveiClient {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final OkHttpClient CLIENT = new OkHttpClient();
public static String sign(String method, String path, String ts, String nonce,
String body, String secret) throws Exception {
StringBuilder payload = new StringBuilder()
.append(ts).append('.')
.append(method.toUpperCase()).append('.')
.append(path).append('.')
.append(nonce);
if (body != null && Arrays.asList("POST", "PUT", "PATCH").contains(method.toUpperCase())) {
payload.append('.').append(body);
}
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal(payload.toString().getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
}
public static Response call(String method, String path, Object body,
String apiKey, String apiSecret, String baseUrl) throws Exception {
String timestamp = String.valueOf(System.currentTimeMillis());
String nonce = UUID.randomUUID().toString().replace("-", "");
// Serialise ONCE.
String bodyStr = null;
if (body != null && Arrays.asList("POST", "PUT", "PATCH").contains(method.toUpperCase())) {
bodyStr = MAPPER.writeValueAsString(body);
}
String signature = sign(method, path, timestamp, nonce, bodyStr, apiSecret);
Request.Builder b = new Request.Builder()
.url(baseUrl + path)
.header("X-API-Key", apiKey)
.header("X-Timestamp", timestamp)
.header("X-Nonce", nonce)
.header("X-Signature", signature)
.header("Content-Type", "application/json");
if ("GET".equalsIgnoreCase(method)) b.get();
if ("DELETE".equalsIgnoreCase(method)) b.delete();
if (Arrays.asList("POST", "PUT", "PATCH").contains(method.toUpperCase())) {
b.method(method.toUpperCase(),
RequestBody.create(bodyStr, MediaType.parse("application/json")));
}
return CLIENT.newCall(b.build()).execute();
}
}