API reference
This page is for developers who write their own integration with OptAlgo once and then keep using it: a trading script, a strategy engine that sends signals, a dashboard, a spreadsheet export. It covers the rules that do not change from one endpoint to the next, the machine-readable schema, and two small clients you can paste into a project.
- Every endpoint, with request and response examples, is in the OptAlgo LLM reference (section 7, Reference). It is written for AI agents and is just as precise for people; the plain-text copy is at docs.optalgo.com/optalgo-llm.md.
- The interactive reference, rendered from the OpenAPI schema, is the API explorer.
- Creating keys, scopes, autonomy and limits: API keys.
Base URL and versioning
https://api.optalgo.com/v1
The version is in the path. Within /v1, OptAlgo only adds: new endpoints, new optional request fields, new response fields, new values in places documented as open lists. Write your client so that it ignores response fields it does not know. A change that removes something or changes what a field means ships as /v2, and /v1 keeps working next to it for at least 90 days. During that time /v1 responses carry a Deprecation header and a Sunset header with the date /v1 stops; log them if you want to be warned early.
Request bodies are strict in the other direction: unknown fields are refused with 422, never silently ignored, so a typo cannot change what an order does.
Authentication
Send the key in a header on every request:
X-API-Key: oa_live_...
Never put the key in a URL, a browser page or a committed file. Keys need the Plus or Pro plan; an account has up to 5 active keys, each with scopes (read, bots, trade), an autonomy level and optional limits. A revoked key answers 401 at once.
Where your program reads the key
Use one convention on every machine, so the same code runs on a laptop and on a server:
- the environment variables
OPTALGO_API_KEYandOPTALGO_API_BASE, if set (servers, containers, CI); - otherwise the file
~/.config/optalgo/credentials.env, readable only by its owner (chmod 600):
OPTALGO_API_KEY=oa_live_...
OPTALGO_API_BASE=https://api.optalgo.com/v1
The OptAlgo device login writes exactly this file, so the key never has to be copied by hand: the program (or an AI agent) starts a login, you approve it in the OptAlgo app, and the key goes from the API into the file. The commands are in Connect an agent without ever handling the key. In a shell, load the file only inside the command that needs it:
set -a; . ~/.config/optalgo/credentials.env; set +a; curl -sS "$OPTALGO_API_BASE/me" -H "X-API-Key: $OPTALGO_API_KEY"
Requests and responses
- JSON in, JSON out (
Content-Type: application/json), exceptGET /v1/trades?format=csv(text/csv) and the agent login'sformat=env(text/plain). - Timestamps are epoch seconds (UTC) unless a field is ISO 8601 (
as_of,expires_at,resets_at, thecreated_atof a connect attempt). - Money is in the quote currency of the account (USDT on most).
- A signal or close answers
202 Acceptedwith atransaction_id: it is queued, not executed.GET /v1/signals/{transaction_id}returns the outcome; poll it until the status is final.
Errors
Every error is JSON with a stable machine code in error, a sentence for people in detail, and sometimes extra fields next to them:
{"error": "limit_exceeded", "detail": "Leverage 10 is above this key's max_leverage (5).",
"limit": "max_leverage", "value": 10, "allowed": 5}
Backtest and market endpoints (/v1/backtests…, /v1/markets) are written for agents. GET /v1/markets and POST /v1/backtests/validate answer {"data": …, "next": [{"action", "method", "path", "why"}]}; the older backtest endpoints keep their fields at the top level and add next beside them, so the payload is data when present, else the body without next. Their errors are {"error", "message", "detail", "fix", "docs", "next"[, "field"]} plus the code's own fields: fix says what to change, docs links the code's entry, next the call to make next.
Branch on error, never on detail or message (their wording can change). Request-body validation errors keep FastAPI's shape, {"detail": [{"loc": [...], "msg": "..."}]} with status 422. The main codes:
| HTTP | error | What your code should do |
|---|---|---|
| 401 | invalid_api_key | Stop. The key is missing, revoked, or its account was closed. |
| 403 | plan_required, forbidden | Stop and tell the user; retrying does not help. |
| 403 | scope_required | The body names required_scope and a fix_url: the user opens it and turns the scope on for this same key in the app, then retry the same request with the same Idempotency-Key. No new key needed. |
| 403 | limit_exceeded, key_paused | A limit the account owner set. Do not retry with the same values. |
| 404 | not_found | Wrong id, or not this user's. |
| 409 | conflict, no_open_position | The current state forbids it; read the state again. |
| 409 | idempotency_in_progress | The same Idempotency-Key is still running; retry in a moment. |
| 422 | invalid_request and others | Fix the request as detail says. |
| 429 | rate_limited | Wait Retry-After seconds, then retry. |
| 502 | listener_unavailable | The signal may still have been taken. Poll the status_url in the body before you send anything again. |
| 502, 503 | listener_error, limit_check_unavailable, unavailable | Nothing was done; retry later. |
| 500 | internal_error | Reported to OptAlgo automatically; retry later. |
The full table, with every extra field, is in section 6 of the LLM reference.
Rate limits
Per key: 120 requests per minute overall and 30 per minute for signals and closes. Every response carries:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | The limit of the tightest bucket that applied to this call (120 on reads, 30 on a signal or close) |
X-RateLimit-Remaining | What is left of it in the current minute |
Retry-After | On a 429 only: seconds to wait |
The agent login has its own limits per IP address: 10 logins per minute, 10 claims per minute and 5 wrong claim codes per 10 minutes (429 rate_limited with retry_after); polling the token faster than interval answers 429 slow_down with a Retry-After header.
Idempotency
POST /v1/bots, POST /v1/bots/{bot_id}/signal, POST /v1/bots/{bot_id}/close, POST /v1/bots/{bot_id}/start, POST /v1/bots/{bot_id}/stop and POST /v1/tickets accept an Idempotency-Key header: 1 to 128 characters of your choice, scoped to your API key.
- The first call runs; its
2xxanswer is kept for 24 hours. A repeat with the same key returns that answer withIdempotent-Replayed: trueand does nothing again. - A repeat while the first call is still running gets
409 idempotency_in_progress. - Errors are not stored, so a failed call can be retried with the same key.
- Use one fresh key (a UUID) per intended action, and reuse it only to retry that same action after a timeout or a dropped connection. That is what makes "send the signal again" safe.
Pagination and time windows
Lists are newest first and paged with a cursor, never with page numbers:
GET /v1/trades?limit=50answers{"trades": [...], "next_before": 1791300000.0}. Passbefore=<next_before>for the next page;next_beforeisnullon the last page.limitis 1 to 100.sinceanduntil(epoch seconds, onopened_at) select a window, and combine withbefore.GET /v1/trades?format=csvreturns up to 5,000 rows in one response, with the same filters; it is the easier way to export history.GET /v1/logs?limit=50&before=<epoch>pages the log feed the same way: pass theatof the oldest row you have.
OpenAPI schema
The API describes itself in OpenAPI 3:
- live: https://api.optalgo.com/v1/openapi.json (public, no key);
- a copy taken at every docs build: https://docs.optalgo.com/openapi.json;
- rendered and searchable: the API explorer.
The schema covers every /v1 route, grouped by tag (Account, Exchanges, Bots, Multi-symbol, Signals, Results, Tickets, Connect, Agent login), with the ApiKeyAuth security scheme (header X-API-Key) on every operation except the agent login routes (/v1/agent/*), which are how a program gets a key. The live schema is cached for 5 minutes. Generate a typed client from it if you prefer (for example with openapi-generator or openapi-typescript); the minimal clients below need no generator.
A minimal client in Python
About 80 lines with requests: the key from the environment or the credentials file, errors raised as exceptions with their code, Retry-After honoured, and an idempotency key on every write so a retry can never act twice.
"""Minimal OptAlgo API client. pip install requests"""
import os
import time
import uuid
from pathlib import Path
import requests
class OptAlgoError(Exception):
def __init__(self, status, code, detail, body):
super().__init__(f"HTTP {status} {code}: {detail}")
self.status, self.code, self.detail, self.body = status, code, detail, body
def load_credentials(path=Path.home() / ".config" / "optalgo" / "credentials.env"):
values = {}
if path.exists():
for line in path.read_text().splitlines():
name, sep, value = line.partition("=")
if sep and not name.startswith("#"):
values[name.strip()] = value.strip()
key = os.environ.get("OPTALGO_API_KEY") or values.get("OPTALGO_API_KEY")
base = os.environ.get("OPTALGO_API_BASE") or values.get("OPTALGO_API_BASE") or "https://api.optalgo.com/v1"
if not key:
raise RuntimeError("No OptAlgo API key: set OPTALGO_API_KEY or run the agent login")
return key, base.rstrip("/")
class OptAlgo:
def __init__(self, key=None, base=None, retries=3):
if key is None:
key, base = load_credentials()
self.base, self.retries = base or "https://api.optalgo.com/v1", retries
self.http = requests.Session()
self.http.headers.update({"X-API-Key": key, "User-Agent": "my-optalgo-client/1"})
def request(self, method, path, idempotency_key=None, **kwargs):
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else {}
for attempt in range(self.retries + 1):
last = attempt == self.retries
try:
r = self.http.request(method, self.base + path, headers=headers, timeout=60, **kwargs)
except (requests.ConnectionError, requests.Timeout):
if last or (method != "GET" and not idempotency_key):
raise # a write without a key is never resent
time.sleep(2 ** attempt)
continue
if r.status_code < 400:
return r.json() if "json" in r.headers.get("Content-Type", "") else r.text
body = r.json() if "json" in r.headers.get("Content-Type", "") else {}
code = body.get("error") or ("invalid_request" if r.status_code == 422 else f"http_{r.status_code}")
if not last and (code == "rate_limited" or code == "idempotency_in_progress"):
time.sleep(float(r.headers.get("Retry-After", 2)))
continue
raise OptAlgoError(r.status_code, code, body.get("detail"), body)
# Writes take an idempotency key; pass the same one to retry the same action.
def summary(self):
return self.request("GET", "/summary")
def bots(self):
return self.request("GET", "/bots")
def signal(self, bot_id, payload, idempotency_key=None):
return self.request("POST", f"/bots/{bot_id}/signal", idempotency_key or str(uuid.uuid4()), json=payload)
def close(self, bot_id, ticker=None, close_ratio=None, idempotency_key=None):
body = {k: v for k, v in {"ticker": ticker, "close_ratio": close_ratio}.items() if v is not None}
return self.request("POST", f"/bots/{bot_id}/close", idempotency_key or str(uuid.uuid4()), json=body)
def outcome(self, transaction_id):
return self.request("GET", f"/signals/{transaction_id}")
def trades(self, **params): # every page, newest first
while True:
page = self.request("GET", "/trades", params=params)
yield from page["trades"]
if not page["next_before"]:
return
params["before"] = page["next_before"]
Use it:
api = OptAlgo()
print(api.summary()["pnl"]["total"])
sent = api.signal(bot_id, {"position_side": "LONG", "ticker": "SOLUSDT.P", "leverage": 3})
while (o := api.outcome(sent["transaction_id"]))["status"] in ("pending", "received", "dispatched", "retrying"):
time.sleep(3)
print(o["status"], o.get("warnings"))
for t in api.trades(status="closed", mode="live"):
print(t["symbol"], t["pnl"])
A minimal client in JavaScript
The same shape with fetch (Node 18 or later). Run it on a server or your own machine, never in a browser page: a key in page source is a leaked key.
// Minimal OptAlgo API client. Node 18+, no dependencies.
import { randomUUID } from 'node:crypto';
import { existsSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
export class OptAlgoError extends Error {
constructor(status, code, detail, body) {
super(`HTTP ${status} ${code}: ${detail}`);
Object.assign(this, { status, code, detail, body });
}
}
export function loadCredentials(path = join(homedir(), '.config', 'optalgo', 'credentials.env')) {
const values = {};
if (existsSync(path)) {
for (const line of readFileSync(path, 'utf8').split('\n')) {
const i = line.indexOf('=');
if (i > 0 && !line.startsWith('#')) values[line.slice(0, i).trim()] = line.slice(i + 1).trim();
}
}
const key = process.env.OPTALGO_API_KEY || values.OPTALGO_API_KEY;
const base = process.env.OPTALGO_API_BASE || values.OPTALGO_API_BASE || 'https://api.optalgo.com/v1';
if (!key) throw new Error('No OptAlgo API key: set OPTALGO_API_KEY or run the agent login');
return { key, base: base.replace(/\/$/, '') };
}
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
export class OptAlgo {
constructor({ key, base, retries = 3 } = {}) {
if (!key) ({ key, base } = loadCredentials());
Object.assign(this, { key, base: base || 'https://api.optalgo.com/v1', retries });
}
async request(method, path, { body, params, idempotencyKey } = {}) {
const url = this.base + path + (params ? `?${new URLSearchParams(params)}` : '');
const headers = { 'X-API-Key': this.key, Accept: 'application/json' };
if (body !== undefined) headers['Content-Type'] = 'application/json';
if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
for (let attempt = 0; ; attempt++) {
const last = attempt >= this.retries;
let r;
try {
r = await fetch(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body),
signal: AbortSignal.timeout(60_000) });
} catch (err) { // network error or timeout
if (last || (method !== 'GET' && !idempotencyKey)) throw err; // a write without a key is never resent
await sleep(2 ** attempt * 1000);
continue;
}
const isJson = (r.headers.get('content-type') || '').includes('json');
const data = isJson ? await r.json() : await r.text();
if (r.ok) return data;
const code = (isJson && data.error) || (r.status === 422 ? 'invalid_request' : `http_${r.status}`);
if (!last && (code === 'rate_limited' || code === 'idempotency_in_progress')) {
await sleep(Number(r.headers.get('retry-after') || 2) * 1000);
continue;
}
throw new OptAlgoError(r.status, code, isJson ? data.detail : data, data);
}
}
summary() { return this.request('GET', '/summary'); }
bots() { return this.request('GET', '/bots'); }
signal(botId, payload, idempotencyKey = randomUUID()) {
return this.request('POST', `/bots/${botId}/signal`, { body: payload, idempotencyKey });
}
close(botId, { ticker, closeRatio } = {}, idempotencyKey = randomUUID()) {
const body = {};
if (ticker) body.ticker = ticker;
if (closeRatio !== undefined) body.close_ratio = closeRatio;
return this.request('POST', `/bots/${botId}/close`, { body, idempotencyKey });
}
outcome(transactionId) { return this.request('GET', `/signals/${transactionId}`); }
async *trades(params = {}) { // every page, newest first
for (;;) {
const page = await this.request('GET', '/trades', { params });
yield* page.trades;
if (!page.next_before) return;
params = { ...params, before: page.next_before };
}
}
}
Use it:
const api = new OptAlgo();
console.log((await api.summary()).pnl.total);
const sent = await api.signal(botId, { position_side: 'LONG', ticker: 'SOLUSDT.P', leverage: 3 });
for await (const t of api.trades({ status: 'closed', mode: 'live' })) console.log(t.symbol, t.pnl);
Rules worth building in from the start
- A
202is not a fill. PollGET /v1/signals/{transaction_id}until the status is final, and compareGET /v1/tradesbefore you report a position as open. - Never resend a signal because you did not see a result. Reuse the same
Idempotency-Keyif you must retry, and after a502 listener_unavailablepoll thestatus_urlfirst. - Pace your polling on
X-RateLimit-Remaining. Positions are cached 10 seconds on the server, the summary changes on trade events, equity only when a trade closes. - Read
GET /v1/meat start-up: the key's scopes, autonomy and limits tell you what will be refused before you send it.