Skip to content
FactorQX
beginnerbroker-apirestintegration

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.

3 min read

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.

Authenticated request with an API key
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.

Submitting an order
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_cursor you pass back. More robust for live data.
Following cursor pagination
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 orders

Rate 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.

Retry with exponential backoff and jitter
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.

Educational content. This article covers software development and research methods only. It is not investment advice, a trading signal, or a recommendation. See our disclaimer.
Anatomy of a Broker REST API · FactorQX