REST Endpoints
The Trading API provides REST endpoints for order management, position tracking, trade history, account information, and market data. All endpoints return JSON responses.
Base URL
The Gateway exposes REST and WebSocket on the same host and port (default http://localhost:8080). Every REST endpoint is versioned under the /v1/ prefix — for example GET http://localhost:8080/v1/orders.
The only unversioned paths are the Kubernetes-style health probes served at the host root:
GET /health— overall healthGET /livez— liveness probeGET /readyz— readiness probe
The same Base URL applies whether the Gateway is running in backtest, paper, or live mode — mode selection is a server-side GATEWAY_PROVIDER / GATEWAY_MODE concern, not a client URL change.
Authentication
Include your API credentials in the request headers as configured during connection setup. See the Connection Guide for details.
Response Format
All successful responses return JSON. Error responses use a standard ApiError body:
{
"code": "ORDER_NOT_FOUND",
"message": "Order not found: abc123",
"details": { "resource": "order", "id": "abc123" }
}
code is the machine-readable discriminator (see the Error Codes and Retry Strategy section below), message is a human-readable description, and details carries per-code structured context (may be absent).
Common Status Codes
| Status | Description |
|---|---|
| 200 | Success |
| 201 | Created (for POST /orders) |
| 400 | Invalid request parameters |
| 401 | Missing or invalid API key |
| 404 | Resource not found |
| 409 | Conflict (e.g., order not modifiable, reset cooldown) |
| 422 | Order rejected by the broker |
| 429 | Rate limit exceeded |
| 500 | Internal error |
| 501 | Operation not supported by the configured provider |
| 502 | Upstream provider returned an error |
| 503 | Provider unavailable or Gateway shutting down |
Error Codes and Retry Strategy
Every error response carries a machine-readable code field. Client libraries should branch on code, not on message.
Canonical ErrorCode enumeration
code | HTTP status | Meaning | Retryable? |
|---|---|---|---|
INVALID_REQUEST | 400 | Malformed request body or missing field | No |
INVALID_VALUE | 400 | A specific field has a disallowed value (details.field, details.provided) | No |
VALIDATION_ERROR | 400 | Multi-field validation failure (details carries the validator report) | No |
UNAUTHORIZED | 401 | API key missing or invalid | No |
ORDER_NOT_FOUND | 404 | No order with the given id | No |
POSITION_NOT_FOUND | 404 | No position with the given id | No |
SYMBOL_NOT_FOUND | 404 | Symbol is not supported by the active provider | No |
OCO_GROUP_NOT_FOUND | 404 | No OCO group with the given id | No |
ORDER_NOT_MODIFIABLE | 409 | Order already filled, cancelled, or otherwise terminal | No |
RESET_COOLDOWN | 409 | Circuit-breaker reset called within the 60s cooldown | No for this instance — the cooldown is server-enforced; wait out the 60 seconds before the next reset attempt |
ORDER_REJECTED | 422 | Broker rejected the order — see details.reject_code | Usually no; only retry when the rejection is explicitly transient (see below) |
RATE_LIMITED | 429 | Gateway or provider rate limit hit | Yes — honour details.retry_after_seconds |
INTERNAL_ERROR | 500 | Unexpected server-side failure | No |
UNSUPPORTED_OPERATION | 501 | The active provider does not implement this feature | No |
PROVIDER_ERROR | 502 | Upstream broker returned an error | Yes — exponential backoff |
SHUTTING_DOWN | 503 | Gateway process is terminating, new orders are refused | No for this instance |
PROVIDER_UNAVAILABLE | 503 | Upstream broker is unreachable or the adapter circuit breaker is open | Yes — exponential backoff |
Rate Limiting (429)
The Gateway returns RATE_LIMITED when either its own ingress limits or the upstream provider's limits are hit. The details object carries retry hints:
{
"code": "RATE_LIMITED",
"message": "Rate limit exceeded",
"details": {
"retry_after_seconds": 30,
"reset_at": "2025-01-15T10:31:00Z"
}
}
retry_after_seconds— how long to wait before the next request (integer seconds, may be absent if the provider didn't supply a hint).reset_at— absolute UTC timestamp when the window resets (ISO-8601, may be absent).
Prefer retry_after_seconds when both are present; fall back to computing the delay from reset_at otherwise. Where neither is present, fall back to the exponential backoff schedule below. Add jitter to whichever value you pick to avoid thundering herds.
Note: the Gateway does not currently set an HTTP
Retry-Afterheader — this is a deliberate deviation from RFC 6585 / 7231. Clients that rely on library-level header-based retry logic should be configured to read the JSON body instead.
Retryable codes are RATE_LIMITED, PROVIDER_UNAVAILABLE, and PROVIDER_ERROR. ORDER_REJECTED is only retryable when the rejection is explicitly transient — case-insensitive self-trade or wash in the message field (or in a passthrough details.reject_code) are the two the Gateway classifies as retryable. Everything else is terminal — surface the error to the caller, do not retry blindly.
Retry Schedule
For retryable errors (including 429 with no explicit hint and 503 PROVIDER_UNAVAILABLE), use exponential backoff with full jitter:
| Setting | Recommended value |
|---|---|
| Initial delay | 500 ms |
| Factor | 2× per failed attempt |
| Maximum delay | 30 seconds |
| Jitter | full (delay = random_between(0, current_delay)) |
| Max attempts | 5 (or 60 seconds total elapsed, whichever is shorter) |
| Per-request timeout | 10 seconds |
After the max-attempts budget is exhausted, surface the error to the strategy — do not keep retrying indefinitely.
Idempotent retries for POST endpoints
POST /v1/orders is not idempotent by default. If the original request reached the Gateway and an upstream timeout or connection drop happened after the order was placed but before the response landed, a naive retry creates a duplicate order.
Always supply a stable client_order_id when retrying a POST /v1/orders call:
- Generate the
client_order_idbefore the first attempt (e.g. a UUID). - Reuse the same
client_order_idon every retry of the same logical order. - On an ambiguous failure, query
GET /v1/orders?client_order_id=<id>before retrying — if the order already exists, reconcile rather than re-submit.
DELETE and PATCH endpoints are naturally idempotent by target id; the same retry schedule applies to them without extra safeguards.
ORDER_REJECTED details
422 responses carry the broker's rejection reason in details.reject_code. The Gateway normalises a small set of canonical values; broker-specific strings (for example OANDA's INSUFFICIENT_MARGIN, MARKET_HALTED) pass through unchanged for codes that aren't in the canonical list.
Canonical reject_code values:
reject_code | Meaning |
|---|---|
INSUFFICIENT_FUNDS | Account lacks buying power / margin for this order |
MARKET_CLOSED | Venue is closed for the instrument |
INVALID_QUANTITY | Quantity is zero, negative, or below the venue's minimum lot size |
INVALID_SYMBOL | Symbol rejected by the broker (not available on this account / tier) |
{
"code": "ORDER_REJECTED",
"message": "Order rejected: insufficient funds for order",
"details": {
"reject_code": "INSUFFICIENT_FUNDS"
}
}
Orders
POST /v1/orders
Submit a new order.
Request Body (OrderRequest)
| Field | Type | Required | Description |
|---|---|---|---|
symbol | string | Yes | Trading symbol (e.g., "AAPL", "EUR/USD") |
side | string | Yes | BUY or SELL |
quantity | string | Yes | Order quantity (decimal string) |
order_type | string | Yes | MARKET, LIMIT, STOP, STOP_LIMIT, TRAILING_STOP |
limit_price | string | No | Required for LIMIT and STOP_LIMIT orders |
stop_price | string | No | Required for STOP and STOP_LIMIT orders |
time_in_force | string | No | GTC (default), DAY, IOC, FOK |
client_order_id | string | No | Client-provided ID for idempotency |
stop_loss | string | No | Stop loss price for bracket order |
take_profit | string | No | Take profit price for bracket order |
reduce_only | boolean | No | If true, can only reduce position |
post_only | boolean | No | If true, reject if would immediately fill |
oco_group_id | string | No | Link orders for one-cancels-other |
hidden | boolean | No | If true, order not visible in order book |
display_quantity | string | No | Visible quantity for iceberg orders |
leverage | string | No | Leverage multiplier |
margin_mode | MarginMode | No | CROSS or ISOLATED margin mode |
position_id | string | No | Target position ID (for hedging accounts) |
trailing_distance | string | No | Distance for TRAILING_STOP orders |
trailing_type | TrailingType | No | ABSOLUTE or PERCENT trailing type |
Example Request
{
"symbol": "AAPL",
"side": "BUY",
"quantity": "100",
"order_type": "LIMIT",
"limit_price": "150.00",
"time_in_force": "GTC"
}
Response (201 Created)
Returns an OrderHandle with the assigned order ID:
{
"id": "ord_abc123",
"status": "PENDING",
"client_order_id": null
}
Error Responses
| Status | code | When |
|---|---|---|
| 400 | INVALID_REQUEST / INVALID_VALUE / VALIDATION_ERROR | Missing field, bad value (e.g. negative quantity), multi-field validator failure |
| 401 | UNAUTHORIZED | API key missing or invalid |
| 422 | ORDER_REJECTED | Broker rejected — see details.reject_code (e.g. INSUFFICIENT_FUNDS, MARKET_CLOSED) |
| 429 | RATE_LIMITED | Ingress or upstream rate limit — honour details.retry_after_seconds |
| 501 | UNSUPPORTED_OPERATION | Active provider does not support this order type |
| 502 | PROVIDER_ERROR | Upstream broker returned an error — retry with exponential backoff |
| 503 | PROVIDER_UNAVAILABLE / SHUTTING_DOWN | Broker unreachable or Gateway is shutting down |
GET /v1/orders
List open orders.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
symbol | string | Filter by symbol |
status | string[] | Filter by status (multiple allowed) |
side | string | Filter by side (BUY or SELL) |
client_order_id | string | Filter by client order ID |
oco_group_id | string | Filter by OCO group ID |
since | datetime | Filter orders created after this time |
until | datetime | Filter orders created before this time |
limit | integer | Maximum number of orders to return |
Response (200 OK)
[
{
"id": "ord_abc123",
"symbol": "AAPL",
"side": "BUY",
"order_type": "LIMIT",
"quantity": "100",
"filled_quantity": "0",
"remaining_quantity": "100",
"limit_price": "150.00",
"status": "OPEN",
"time_in_force": "GTC",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
]
GET /v1/orders/{order_id}
Get a specific order by ID.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
order_id | string | The order ID |
Response (200 OK)
Returns the full Order object (same schema as list response items).
Error Responses
| Status | code | When |
|---|---|---|
| 401 | UNAUTHORIZED | API key missing or invalid |
| 404 | ORDER_NOT_FOUND | No order with the given order_id |
| 429 | RATE_LIMITED | Rate limit hit — retry after details.retry_after_seconds |
| 502 | PROVIDER_ERROR | Upstream broker returned an error |
| 503 | PROVIDER_UNAVAILABLE | Broker unreachable |
PATCH /v1/orders/{order_id}
Modify an existing order. Only non-null fields will be updated.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
order_id | string | The order ID |
Request Body (ModifyOrderRequest)
| Field | Type | Description |
|---|---|---|
quantity | string | New total quantity (must be >= filled_quantity) |
limit_price | string | New limit price |
stop_price | string | New stop price |
stop_loss | string | New stop loss price |
take_profit | string | New take profit price |
trailing_distance | string | New trailing distance |
Example Request
{
"limit_price": "151.00"
}
Response (200 OK) — ModifyOrderResult
| Field | Type | Required | Description |
|---|---|---|---|
order | Order | Yes | The modified (or replacement) order. |
previous_order_id | string | No | Previous order ID if the provider used cancel-replace (e.g. Alpaca). null when modified in-place. |
Example
{
"order": {
"id": "ord_abc123",
"symbol": "AAPL",
"side": "BUY",
"order_type": "LIMIT",
"quantity": "100",
"filled_quantity": "0",
"remaining_quantity": "100",
"limit_price": "151.00",
"status": "OPEN",
"time_in_force": "GTC",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:31:00Z"
},
"previous_order_id": null
}
Error Responses
| Status | code | When |
|---|---|---|
| 400 | INVALID_REQUEST / INVALID_VALUE | Missing or invalid field in the modify payload |
| 401 | UNAUTHORIZED | API key missing or invalid |
| 404 | ORDER_NOT_FOUND | No order with the given order_id |
| 409 | ORDER_NOT_MODIFIABLE | Order already filled, cancelled, or otherwise terminal |
| 429 | RATE_LIMITED | Rate limit hit — honour details.retry_after_seconds |
| 501 | UNSUPPORTED_OPERATION | Provider does not support order modification |
| 502 | PROVIDER_ERROR | Upstream broker returned an error |
| 503 | PROVIDER_UNAVAILABLE | Broker unreachable |
DELETE /v1/orders/{order_id}
Cancel a specific order.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
order_id | string | The order ID |
Response (200 OK)
{
"success": true,
"order": {
"id": "ord_abc123",
"status": "CANCELLED",
...
}
}
Error Responses
| Status | code | When |
|---|---|---|
| 401 | UNAUTHORIZED | API key missing or invalid |
| 404 | ORDER_NOT_FOUND | No order with the given order_id |
| 409 | ORDER_NOT_MODIFIABLE | Order already filled or in a terminal state |
| 429 | RATE_LIMITED | Rate limit hit |
| 502 | PROVIDER_ERROR | Upstream broker returned an error |
| 503 | PROVIDER_UNAVAILABLE | Broker unreachable |
DELETE /v1/orders
Cancel all open orders.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
symbol | string | Filter by symbol (optional, omit to cancel all) |
Response (200 OK)
{
"cancelled_count": 5,
"failed_count": 0,
"failed_order_ids": null
}
GET /v1/orders/history
Get historical orders (filled, cancelled, rejected, expired).
Query Parameters
Same as GET /v1/orders.
Response (200 OK)
Returns an array of Order objects.
Positions
GET /v1/positions
Get all open positions.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
symbol | string | Filter by symbol (optional) |
Response (200 OK)
[
{
"id": "pos_xyz789",
"symbol": "AAPL",
"side": "LONG",
"quantity": "100",
"average_entry_price": "150.25",
"current_price": "151.50",
"unrealized_pnl": "125.00",
"realized_pnl": "0",
"opened_at": "2025-01-15T10:30:01Z",
"updated_at": "2025-01-15T11:00:00Z"
}
]
GET /v1/positions/{position_id}
Get a specific position by ID.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
position_id | string | The position ID |
Response (200 OK)
Returns the full Position object.
Error Responses
404- Position not found503- Provider unavailable
DELETE /v1/positions/{position_id}
Close a specific position.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
position_id | string | The position ID |
Request Body (optional)
| Field | Type | Description |
|---|---|---|
quantity | string | Quantity to close (omit for full close) |
order_type | string | MARKET (default) or LIMIT |
limit_price | string | Required if order_type is LIMIT |
cancel_associated_orders | boolean | Cancel SL/TP orders (default: true) |
Response (200 OK)
Returns an OrderHandle for the close order.
Error Responses
404- Position not found503- Provider unavailable
DELETE /v1/positions
Close all open positions.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
symbol | string | Filter by symbol (optional, omit to close all) |
Response (200 OK)
Returns an array of OrderHandle objects for each close order created.
Trades
GET /v1/trades
Get trade/fill history.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
symbol | string | Filter by symbol |
order_id | string | Filter by order ID |
since | datetime | Return trades after this time (inclusive) |
until | datetime | Return trades before this time (exclusive) |
limit | integer | Maximum number of trades to return |
Response (200 OK)
[
{
"id": "trd_def456",
"order_id": "ord_abc123",
"symbol": "AAPL",
"side": "BUY",
"quantity": "100",
"price": "150.25",
"commission": "0.50",
"commission_currency": "USD",
"is_maker": false,
"timestamp": "2025-01-15T10:30:01Z"
}
]
Account
GET /v1/account
Get account information including balances and margin.
Response (200 OK)
{
"balance": "10000.00",
"equity": "10500.00",
"margin_used": "2000.00",
"margin_available": "8500.00",
"unrealized_pnl": "500.00",
"currency": "USD"
}
Error Responses
400- Invalid request503- Provider unavailable
Market Data
GET /v1/quotes/{symbol}
Get the current quote for a symbol.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
symbol | string | Symbol (e.g., "AAPL", "BTC/USD") |
Response (200 OK)
{
"symbol": "AAPL",
"provider": "alpaca",
"bid": "150.20",
"bid_size": "100",
"ask": "150.25",
"ask_size": "200",
"last": "150.22",
"timestamp": "2025-01-15T10:30:00.123Z"
}
Error Responses
404- Symbol not found503- Provider unavailable
GET /v1/bars/{symbol}
Get historical OHLCV bars for a symbol.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
symbol | string | Symbol (e.g., "S:AAPL", "C:BTCUSD") |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
timeframe | Timeframe | Yes | Bar interval: 1m, 2m, 5m, 10m, 15m, 30m, 1h, 2h, 4h, 12h, 1d, 1w |
start | datetime | No | Start of date range (inclusive) |
end | datetime | No | End of date range (inclusive) |
limit | integer | No | Maximum number of bars to return |
Response (200 OK)
[
{
"symbol": "AAPL",
"provider": "polygon",
"timeframe": "1h",
"timestamp": "2025-01-15T10:00:00Z",
"open": "150.00",
"high": "151.50",
"low": "149.75",
"close": "151.25",
"volume": "1234567"
}
]
Error Responses
400- Invalid request (bad timeframe or symbol)404- Symbol not found503- Provider unavailable
System
GET /v1/capabilities
Get provider capabilities describing supported features.
Response (200 OK)
{
"supported_asset_classes": ["STOCK", "CRYPTO"],
"supported_order_types": ["MARKET", "LIMIT", "STOP", "STOP_LIMIT"],
"position_mode": "NETTING",
"features": ["trailing_stop", "oco", "bracket_orders"],
"max_leverage": "20"
}
GET /v1/status
Get connection status to the trading provider.
Response (200 OK)
{
"connected": true,
"latency_ms": 15,
"last_heartbeat": "2025-01-15T10:30:00Z"
}
GET /v1/circuit-breakers
Inspect the state of the Gateway's two internal circuit breakers. Useful for diagnosing a Gateway that is refusing traffic because an upstream breaker has tripped.
The Gateway runs two breakers:
adapter— trips when the upstream broker adapter (Alpaca, Binance, etc.) produces repeated failures. Prevents hammering a provider that is already failing.exit_order— trips when stop-loss / take-profit placement repeatedly fails. Prevents opening unprotected positions when SL/TP submission is unreliable.
Response (200 OK) — CircuitBreakerStatusResponse
| Field | Type | Description |
|---|---|---|
adapter | CircuitBreakerSnapshot | Adapter circuit breaker (provider outage detection). |
exit_order | CircuitBreakerSnapshot | Exit-order circuit breaker (SL/TP placement failure detection). |
CircuitBreakerSnapshot schema
| Field | Type | Description |
|---|---|---|
state | string | "open" (tripped, requests blocked) or "closed" (normal). |
failure_count | integer | Recent failures within the breaker's time window. >= 0. |
Example
{
"adapter": { "state": "closed", "failure_count": 0 },
"exit_order": { "state": "open", "failure_count": 5 }
}
POST /v1/circuit-breakers/reset
Atomically reset both circuit breakers to the closed state. Intended for operator use after the upstream condition has been resolved.
Request Body — none. The endpoint resets both breakers in a single call; it does not accept a per-breaker selector.
Response (200 OK) — CircuitBreakerStatusResponse
Returns the post-reset snapshot, using the same schema as GET /v1/circuit-breakers. Both state fields will be "closed" and both failure_count fields will be 0 on a successful reset.
Response (409 Conflict) — ApiError with code: RESET_COOLDOWN
Returned when either breaker re-trips within 60 seconds of the last successful reset. The 60-second cooldown prevents reset loops while the upstream condition is still unstable. Clients should back off and retry once the underlying provider or SL/TP placement issue is resolved.
Health probes
The Gateway exposes three unversioned probes at the host root. These are intended for Kubernetes-style liveness and readiness checks and do not carry a /v1/ prefix.
GET /health
Overall health check. Returns 200 OK if the service is running.
Response (200 OK)
{
"status": "ok"
}
GET /livez
Liveness probe. Returns 200 OK whenever the Gateway process is responsive, independent of provider state. Use this for Kubernetes livenessProbe.
GET /readyz
Readiness probe. Returns 200 OK only when at least one provider is configured and ready to serve traffic. Use this for Kubernetes readinessProbe.
Response (200 OK)
{
"ready": true,
"providers": ["alpaca-paper"],
"provider_count": 1
}
Response (503 Service Unavailable)
Returned when no providers are configured:
{
"ready": false,
"providers": [],
"provider_count": 0,
"message": "No providers configured"
}
Related Documentation
- WebSocket Messages - Real-time event streaming
- Order Types - Full order schema reference
- Position Types - Full position schema reference
- Connection Guide - How to connect to the Trading API