Buvei

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]
FieldSource
timestampThe value of your X-Timestamp header
methodHTTP method in uppercase (GET, POST, PUT, PATCH, DELETE)
pathRequest path including /open-api/v1 prefix, e.g. /open-api/v1/cards
nonceThe value of your X-Nonce header
bodyThe 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();
    }
}

Common signing pitfalls