Authentication

The 4rho API supports two authentication methods. All trading endpoints require authentication.

API keys are issued by 4rho administrators. Each key has:

  • API Key — Public identifier (prefixed with 4rho_)
  • Secret — Used for HMAC signing (shown once at creation)
  • Passphrase — Additional authentication factor (shown once at creation)
  • Scopes — Granular permissions (e.g., trade:orders, stream:market)
  • Rate Limit Tierstandard (10/s), market_maker (100/s), or premium (50/s)

Required Headers

Every authenticated request must include these headers:

HeaderRequired forDescription
X-4RHO-API-KEYAll requestsYour API key (e.g., 4rho_abc123...)
X-4RHO-SIGNATUREAll requestsHMAC-SHA256 signature of the request
X-4RHO-TIMESTAMPAll requestsUnix timestamp (seconds)
X-4RHO-PASSPHRASEAll requestsYour passphrase
X-4RHO-NONCEMutations (POST/PUT/DELETE)Unique replay-protection token (any unique string per request — UUID recommended)

Mutations require a nonce. Sending a POST, PUT, or DELETE without X-4RHO-NONCE returns 400 NONCE_REQUIRED. Each nonce is single-use per API key — reusing one returns 400 REPLAYED_NONCE. GET/HEAD requests do not take a nonce; sending one is harmless but unnecessary.

Signing Algorithm

The signature is computed in two steps:

Step 1: Derive the HMAC key

hmac_key = SHA256(raw_secret)

Step 2: Compute the signature

For GET/HEAD (no nonce):

message   = timestamp + "\n" + method + "\n" + path + "\n" + SHA256(body)
signature = HMAC-SHA256(hmac_key, message)

For mutations (POST/PUT/DELETE — nonce inserted between timestamp and method):

message   = timestamp + "\n" + nonce + "\n" + method + "\n" + path + "\n" + SHA256(body)
signature = HMAC-SHA256(hmac_key, message)

Where:

  • timestamp — Unix epoch seconds as a string (must be within 30 seconds of server time)
  • nonce — The exact value sent in X-4RHO-NONCE (only present in the message for mutations)
  • method — HTTP method in uppercase (GET, POST, DELETE, PUT)
  • path — Request path only, no query string (e.g., /v1/orders)
  • body — Request body as string (empty string "" for GET/DELETE requests)
  • SHA256(body) — Hex-encoded SHA-256 hash of the body string
  • SHA256(raw_secret) — Hex-encoded SHA-256 hash of your raw secret

The \n separators are literal newline characters (not the two characters \ and n).

Example: Python

import hashlib
import hmac
import time
import requests

API_KEY = "4rho_your_key_here"
SECRET = "your_raw_secret"
PASSPHRASE = "your_passphrase"
BASE_URL = "https://api.4rho.com"

def sign_request(
    secret: str,
    method: str,
    path: str,
    body: str = "",
    nonce: str | None = None,
) -> dict:
    """
    Build the auth headers for a request. `nonce` is required for
    mutations (POST/PUT/DELETE) — pass a fresh unique string per call
    (a UUID4 is the safest default). For GET/HEAD pass `None` (the
    default).
    """
    timestamp = str(int(time.time()))
    body_hash = hashlib.sha256(body.encode()).hexdigest()

    if nonce:
        # Mutations: timestamp \n nonce \n method \n path \n body_hash
        message = f"{timestamp}\n{nonce}\n{method}\n{path}\n{body_hash}"
    else:
        # GET/HEAD: timestamp \n method \n path \n body_hash
        message = f"{timestamp}\n{method}\n{path}\n{body_hash}"

    # HMAC key is SHA256 of the raw secret
    hmac_key = hashlib.sha256(secret.encode()).hexdigest()
    signature = hmac.new(
        hmac_key.encode(),
        message.encode(),
        hashlib.sha256
    ).hexdigest()

    headers = {
        "X-4RHO-API-KEY": API_KEY,
        "X-4RHO-SIGNATURE": signature,
        "X-4RHO-TIMESTAMP": timestamp,
        "X-4RHO-PASSPHRASE": PASSPHRASE,
    }
    if nonce:
        headers["X-4RHO-NONCE"] = nonce
    return headers

# Example: GET request (no nonce, no body)
path = "/v1/user/positions"
headers = sign_request(SECRET, "GET", path)
resp = requests.get(f"{BASE_URL}{path}", headers=headers)
print(resp.json())

# Example: POST request (mutation — nonce required)
import json
import uuid

path = "/v1/orders"
body = json.dumps({"market_id": "...", "side": "BUY", "maker_amount": "1000000"})
nonce = uuid.uuid4().hex
headers = sign_request(SECRET, "POST", path, body, nonce=nonce)
headers["Content-Type"] = "application/json"
resp = requests.post(f"{BASE_URL}{path}", headers=headers, data=body)
print(resp.json())

Example: TypeScript (Node.js)

import crypto from 'crypto';

const API_KEY = '4rho_your_key_here';
const SECRET = 'your_raw_secret';
const PASSPHRASE = 'your_passphrase';
const BASE_URL = 'https://api.4rho.com';

function signRequest(
  secret: string,
  method: string,
  path: string,
  body: string = '',
  nonce?: string,
): Record<string, string> {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const bodyHash = crypto.createHash('sha256').update(body).digest('hex');

  // Mutations: timestamp \n nonce \n method \n path \n body_hash
  // GET/HEAD : timestamp \n method \n path \n body_hash
  const message = nonce
    ? `${timestamp}\n${nonce}\n${method}\n${path}\n${bodyHash}`
    : `${timestamp}\n${method}\n${path}\n${bodyHash}`;

  const hmacKey = crypto.createHash('sha256').update(secret).digest('hex');
  const signature = crypto
    .createHmac('sha256', hmacKey)
    .update(message)
    .digest('hex');

  const headers: Record<string, string> = {
    'X-4RHO-API-KEY': API_KEY,
    'X-4RHO-SIGNATURE': signature,
    'X-4RHO-TIMESTAMP': timestamp,
    'X-4RHO-PASSPHRASE': PASSPHRASE,
  };
  if (nonce) headers['X-4RHO-NONCE'] = nonce;
  return headers;
}

// Example: GET request (no nonce)
const path = '/v1/user/positions';
const headers = signRequest(SECRET, 'GET', path);
const resp = await fetch(`${BASE_URL}${path}`, { headers });
console.log(await resp.json());

// Example: POST request (mutation — nonce required)
const orderPath = '/v1/orders';
const orderBody = JSON.stringify({ market_id: '...', side: 'BUY', maker_amount: '1000000' });
const nonce = crypto.randomUUID();
const orderHeaders = {
  ...signRequest(SECRET, 'POST', orderPath, orderBody, nonce),
  'Content-Type': 'application/json',
};
const orderResp = await fetch(`${BASE_URL}${orderPath}`, {
  method: 'POST',
  headers: orderHeaders,
  body: orderBody,
});
console.log(await orderResp.json());

Example: TypeScript (Browser / Web Crypto API)

const API_KEY = '4rho_your_key_here';
const SECRET = 'your_raw_secret';
const PASSPHRASE = 'your_passphrase';

async function sha256Hex(data: string): Promise<string> {
  const buffer = await crypto.subtle.digest(
    'SHA-256',
    new TextEncoder().encode(data)
  );
  return Array.from(new Uint8Array(buffer))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
}

async function signRequest(
  secret: string,
  method: string,
  path: string,
  body: string = ''
): Promise<Record<string, string>> {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const bodyHash = await sha256Hex(body);
  const message = `${timestamp}\n${method}\n${path}\n${bodyHash}`;

  const hmacKey = await sha256Hex(secret);
  const key = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(hmacKey),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );
  const sigBuffer = await crypto.subtle.sign(
    'HMAC',
    key,
    new TextEncoder().encode(message)
  );
  const signature = Array.from(new Uint8Array(sigBuffer))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');

  return {
    'X-4RHO-API-KEY': API_KEY,
    'X-4RHO-SIGNATURE': signature,
    'X-4RHO-TIMESTAMP': timestamp,
    'X-4RHO-PASSPHRASE': PASSPHRASE,
  };
}

Clock Synchronization

Use GET /v1/time to synchronize your clock with the server. Requests with timestamps more than 30 seconds from server time are rejected.

{ "time": 1709136000 }

JWT Bearer Token

For browser-based applications, authenticate via OAuth and include the JWT in the Authorization header:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

JWTs are obtained through the OAuth flow (/v1/auth/oauth/:provider or POST /v1/auth/privy) and refreshed via POST /v1/auth/refresh.

Permission Scopes

API keys are assigned granular scopes:

ScopeDescription
read:accountRead user orders, trades, positions, balances
write:accountFiat on/off-ramp, claim winnings, redeem referrals
manage:accountLink wallets, MFA, logout, revoke sessions
trade:ordersPlace and cancel individual orders
trade:bulkBatch operations (place/cancel many)
manage:strategiesCreate, update, delete trading strategies
stream:marketSubscribe to market WebSocket feeds
stream:userSubscribe to user WebSocket feeds
deploy:marketsOperator-issued: register externally-deployed markets and read deploy params

Requests to endpoints requiring a scope that your key lacks will receive a 403 Forbidden response with the code INSUFFICIENT_SCOPE.

IP Allowlist

API keys can optionally be restricted to specific IP addresses. Requests from unlisted IPs are rejected with 403 Forbidden.