Anatomy of a Broker REST API
Understand authentication, core endpoints, idempotency keys, pagination, and rate-limit handling when integrating with a generic broker REST API.
Engineering content only — about software integration, not investment advice or trading signals. Connecting to live execution is your responsibility and carries real risk.
Most brokers that support programmatic access expose an HTTP REST API. The shapes differ, but the building blocks are remarkably consistent: a way to authenticate, a handful of resource endpoints, and a set of reliability mechanics (idempotency, pagination, rate limits). This article maps that anatomy using generic, illustrative endpoints so the patterns transfer to whichever broker you integrate with.
Authentication
Two schemes dominate. API keys are long-lived credentials you send on every request, usually as a header. OAuth 2.0 issues short-lived access tokens that you refresh, which limits the blast radius if a token leaks.
The non-negotiable rule: secrets stay server-side. Never embed a broker key in a browser bundle, mobile app, or public repo. Put a backend between your client and the broker, and inject credentials there.
curl https://api.broker.example/v1/account \
-H "Authorization: Bearer $BROKER_API_KEY"Store keys in a secrets manager or environment variables, scope them to the minimum permissions you need, and rotate them on a schedule.
Core endpoints
A broker API typically exposes these resources:
GET /v1/account— balances, buying power, account status.GET /v1/instruments— tradable symbols and their metadata (tick size, lot size, trading hours).GET /v1/positions— current holdings, quantities, and average prices.POST /v1/orders— submit an order.GET /v1/orders— list/track orders.DELETE /v1/orders/{id}— cancel.
A submission usually looks like this. The fields are mechanical instructions to the broker — nothing here decides what to trade; your application logic does that, and that decision is yours.
curl -X POST https://api.broker.example/v1/orders \
-H "Authorization: Bearer $BROKER_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f9a1c20-7b2e-4d11-9f0a-2c1d8e44a912" \
-d '{
"symbol": "ABC",
"side": "buy",
"quantity": 10,
"type": "limit",
"limit_price": 100.50,
"time_in_force": "day"
}'Idempotency keys
Networks fail mid-request. If you retry a POST /orders without protection, you risk placing a duplicate order. An idempotency key — a unique client-generated UUID — lets the broker recognize a retry and return the original result instead of creating a second order.
Generate the key once per logical order, persist it alongside your record, and reuse the same key on every retry of that specific request. Use a fresh key only for a genuinely new order.
Pagination
List endpoints cap results per page. Two common styles:
- Offset/limit:
?limit=100&offset=200. Simple, but can skip or repeat rows if data changes mid-scan. - Cursor-based: the response includes a
next_cursoryou pass back. More robust for live data.
import requests
def fetch_all_orders(base_url, headers):
orders, cursor = [], None
while True:
params = {"limit": 100}
if cursor:
params["cursor"] = cursor
resp = requests.get(f"{base_url}/v1/orders", headers=headers, params=params, timeout=10)
resp.raise_for_status()
body = resp.json()
orders.extend(body["data"])
cursor = body.get("next_cursor")
if not cursor:
return ordersRate limits and backoff
Brokers throttle requests, typically advertised via response headers:
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 3
X-RateLimit-Reset: 1718553600
When you exceed the limit you get HTTP 429, often with a Retry-After header. Respect it. For transient failures (429, 502, 503, timeouts), use exponential backoff with jitter — wait base * 2^attempt plus a random offset — so retries don't synchronize into a thundering herd.
import time, random, requests
def request_with_retry(method, url, *, max_attempts=5, **kwargs):
for attempt in range(max_attempts):
resp = requests.request(method, url, timeout=10, **kwargs)
if resp.status_code not in (429, 502, 503):
resp.raise_for_status()
return resp
retry_after = resp.headers.get("Retry-After")
delay = float(retry_after) if retry_after else (2 ** attempt) + random.random()
time.sleep(delay)
raise RuntimeError("exhausted retries")Error handling is the real work
Treat every call as fallible. Distinguish client errors (4xx — bad input, auth, validation) from server/transient errors (5xx, timeouts). Retry only the latter. Log the broker's error body, validate inputs before sending, and never assume a 200 means the order is filled — confirm state by reading back from GET /orders/{id}. Build for partial failure: an order can be accepted, rejected, or stuck in a pending state.
Where to go next
Once polling REST endpoints feels solid, the next bottleneck is freshness — REST is request/response, so you can't watch the market in real time without hammering the API. Continue with Streaming Market Data over WebSockets to learn how to receive live updates over a persistent connection, and how to reconcile that stream against the authoritative REST state you've already mastered.