Skip to content

Authentication

Every request is authenticated with a signed API key (HMAC-SHA256). Your tenant administrator creates the key in the admin dashboard and gives it to your backend, which uses it to sign every request.


Overview

Every API request must include three headers computed from your API Key ID and Secret:

Header Description
X-API-Key Your API Key ID (public identifier)
X-Timestamp Current Unix timestamp in seconds
X-Signature HMAC-SHA256 signature of the canonical request string

Getting Your Credentials

Your tenant administrator creates the key in the admin dashboard (Settings → API Keys → Create API Key) and shares the Key ID and Secret with you. The secret is shown only once at creation.

The response contains:

Field Description
keyId Public identifier -- use as X-API-Key header
secret HMAC signing key -- never expose this publicly

Store the secret securely

The secret is displayed only once at creation time. Store it in a secrets manager, environment variable, or secure vault. It cannot be retrieved later.


Signing a Request

Step 1: Build the Canonical String

The canonical string has four components separated by newlines:

{timestamp}\n{METHOD}\n{path}\n{bodyHash}
Component Description Example
timestamp Unix epoch seconds (same as X-Timestamp) 1708600000
METHOD HTTP method in uppercase POST
path Request path with leading slash, no host /vaults
bodyHash SHA-256 hex digest of the request body (empty string hash if no body) e3b0c44298fc1c14...

Step 2: Compute the HMAC Signature

Sign the canonical string with your secret using HMAC-SHA256, then encode the result as a lowercase hex string.

Step 3: Send the Request

Include all three headers:

X-API-Key: {keyId}
X-Timestamp: {timestamp}
X-Signature: {hexSignature}

Timestamp window

The server rejects requests with timestamps older than 30 seconds. Ensure your server clock is synchronized (NTP).


Code Examples

Python

import hashlib
import hmac
import time
import requests

API_KEY_ID = "your-key-id"
API_SECRET = "your-secret"
BASE_URL = "https://api.example.com"

def sign_request(method: str, path: str, body: str = "") -> dict:
    timestamp = str(int(time.time()))
    body_hash = hashlib.sha256(body.encode()).hexdigest()
    canonical = f"{timestamp}\n{method}\n{path}\n{body_hash}"
    signature = hmac.new(
        API_SECRET.encode(), canonical.encode(), hashlib.sha256
    ).hexdigest()
    return {
        "X-API-Key": API_KEY_ID,
        "X-Timestamp": timestamp,
        "X-Signature": signature,
    }

# GET request
path = "/vaults"
headers = sign_request("GET", path)
response = requests.get(f"{BASE_URL.rstrip('/')}{path}", headers=headers)

# POST request
path = "/vaults"
body = '{"externalId":"cust_123","name":"Alice"}'
headers = sign_request("POST", path, body)
headers["Content-Type"] = "application/json"
response = requests.post(f"{BASE_URL.rstrip('/')}{path}", headers=headers, data=body)

Node.js

const crypto = require("crypto");

const API_KEY_ID = "your-key-id";
const API_SECRET = "your-secret";

function signRequest(method, path, body = "") {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
  const canonical = `${timestamp}\n${method}\n${path}\n${bodyHash}`;
  const signature = crypto
    .createHmac("sha256", API_SECRET)
    .update(canonical)
    .digest("hex");
  return {
    "X-API-Key": API_KEY_ID,
    "X-Timestamp": timestamp,
    "X-Signature": signature,
  };
}

// GET example
const headers = signRequest("GET", "/vaults");
fetch("https://api.example.com/vaults", { headers });

// POST example
const body = JSON.stringify({ externalId: "cust_123", name: "Alice" });
const postHeaders = signRequest("POST", "/vaults", body);
postHeaders["Content-Type"] = "application/json";
fetch("https://api.example.com/vaults", {
  method: "POST",
  headers: postHeaders,
  body,
});

C

using System.Security.Cryptography;
using System.Text;

var apiKeyId = "your-key-id";
var apiSecret = "your-secret";

string SignRequest(string method, string path, string body = "")
{
    var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
    var bodyHash = Convert.ToHexStringLower(
        SHA256.HashData(Encoding.UTF8.GetBytes(body)));
    var canonical = $"{timestamp}\n{method}\n{path}\n{bodyHash}";
    var signature = Convert.ToHexStringLower(
        HMACSHA256.HashData(
            Encoding.UTF8.GetBytes(apiSecret),
            Encoding.UTF8.GetBytes(canonical)));
    return signature; // Set X-API-Key, X-Timestamp, X-Signature headers
}

cURL (Bash)

API_KEY_ID="your-key-id"
API_SECRET="your-secret"
TIMESTAMP=$(date +%s)
METHOD="GET"
PATH_URL="/vaults"
BODY=""

BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')
CANONICAL="${TIMESTAMP}\n${METHOD}\n${PATH_URL}\n${BODY_HASH}"
SIGNATURE=$(echo -ne "$CANONICAL" | openssl dgst -sha256 -hmac "$API_SECRET" -hex | awk '{print $NF}')

curl "https://api.example.com${PATH_URL}" \
  -H "X-API-Key: $API_KEY_ID" \
  -H "X-Timestamp: $TIMESTAMP" \
  -H "X-Signature: $SIGNATURE"

IP Allowlist

API keys can optionally be restricted to specific IP addresses. If an allowlist is configured, requests from non-allowed IPs receive 401 Unauthorized.

Configure IP allowlists from the dashboard under Settings > API Keys > Edit.


Error Responses

Status Meaning
401 Unauthorized Missing, invalid, or expired API key. Signature mismatch. IP not allowed. Timestamp drift > 30s. Replay — the same (keyId, timestamp, signature) tuple was already accepted within the drift window; re-sign with a fresh timestamp.
403 Forbidden Valid credentials but insufficient scope for the requested operation.
429 Too Many Requests Rate limit exceeded (120 req/min/key by default). Honour Retry-After.

Beyond authentication

Three runtime behaviors are part of the partner contract — read them before you ship:

  • Idempotency-Key is required on POST /transactions/withdraw and POST /transactions/transfer. Generate a UUID per logical operation; replays return the original response.
  • Replay protection. Each accepted signature is single-use within the 30-second drift window — your retry MUST regenerate the signature.
  • Rate limit. 120 authenticated requests / minute / key, sliding window. 429 includes Retry-After.

Full reference: Reliability & Limits.


Next Steps