Authentication
The 4rho API supports two authentication methods. All trading endpoints require authentication.
HMAC API Key (Recommended for Market Makers)
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 Tier —
standard(10/s),market_maker(100/s), orpremium(50/s)
Required Headers
Every authenticated request must include these headers:
| Header | Required for | Description |
|---|---|---|
X-4RHO-API-KEY | All requests | Your API key (e.g., 4rho_abc123...) |
X-4RHO-SIGNATURE | All requests | HMAC-SHA256 signature of the request |
X-4RHO-TIMESTAMP | All requests | Unix timestamp (seconds) |
X-4RHO-PASSPHRASE | All requests | Your passphrase |
X-4RHO-NONCE | Mutations (POST/PUT/DELETE) | Unique replay-protection token (any unique string per request — UUID recommended) |
Mutations require a nonce. Sending a
POST,PUT, orDELETEwithoutX-4RHO-NONCEreturns400 NONCE_REQUIRED. Each nonce is single-use per API key — reusing one returns400 REPLAYED_NONCE.GET/HEADrequests 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 inX-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 stringSHA256(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:
| Scope | Description |
|---|---|
read:account | Read user orders, trades, positions, balances |
write:account | Fiat on/off-ramp, claim winnings, redeem referrals |
manage:account | Link wallets, MFA, logout, revoke sessions |
trade:orders | Place and cancel individual orders |
trade:bulk | Batch operations (place/cancel many) |
manage:strategies | Create, update, delete trading strategies |
stream:market | Subscribe to market WebSocket feeds |
stream:user | Subscribe to user WebSocket feeds |
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.