Account Information
The Account Information APIs cover three read endpoints that together drive every account-aware tool you'll build: list the accounts your key pair owns, retrieve a snapshot of any one account's balances and option settings, and pull the live P&L view (including a per-position breakdown).
Overview
The Account Information APIs let you:
- List user accounts - discover every account your API credentials own, each row including its type (
Paperfor paper accounts), status, balances, and option trading level. - Retrieve account details - read the full snapshot for one account: cash, buying power, equity, margin deficits, leverage, and option settings.
- Retrieve account values and P&L - pull the live profit/loss view with day and total realized / unrealized figures plus a per-position
pnl[]breakdown. Values reflect the latest quote on every call.
For push-style P&L updates without polling, the P&L Stream over WebSocket delivers the same aggregates and per-position rows in real time.
At a glance
| Endpoint | Returns | When to call |
|---|---|---|
GET /v1/api/accounts | {accounts: [...]} - all accounts your key pair owns, each row a full snapshot. | Once at startup. Cache the IDs. |
GET /v1/api/account/{accountId} | Single account snapshot: cash, buying power, equity, leverage, option settings. | On account selection, before pre-trade validation, and whenever balances need refresh. |
GET /v1/api/accounts/{accountId}/pnl | Day and total P&L aggregates plus a per-position pnl[] breakdown. | On a dashboard tick (~5s) or replace with the P&L Stream. |
All three are pure reads, GET only, JSON-only on success, and return 404 Not Found with a plain-text body for any auth or ownership failure.
HTTP semantics
These details apply uniformly to all three endpoints and are worth knowing before you write your first call.
| Concern | Details |
|---|---|
| Auth header names | Case-insensitive per RFC 7230. TZ-API-KEY-ID, tz-api-key-id, and mixed-case all work. Pick a convention and stick to it. |
| Auth header values | Compared character-for-character. Invalid values come back as 404 Not Found (see Error Handling). |
| Account ID casing | Case-insensitive in the path. GET /v1/api/account/tzp12345678 returns the same account as GET /v1/api/account/TZP12345678. The response body always echoes the canonical uppercase form in the account field. |
| Trailing slashes | Supported. /v1/api/accounts/, /v1/api/account/{id}/, and /v1/api/accounts/{id}/pnl/ all return the same data as their no-trailing-slash equivalents. |
| Query parameters | Not used by the Accounts endpoints. Each call returns the current snapshot; there is no point-in-time variant. |
Accept header | Responses are always application/json; charset=utf-8 on success. |
| Compression | Successful responses are Content-Encoding: gzip. Most HTTP clients (including fetch) decompress automatically; if you are rolling your own client, advertise Accept-Encoding: gzip to avoid a manual decompression step. |
| Caching | Responses set Cache-Control: max-age=0, no-cache, no-store. Do not cache them in shared layers - the values are live. |
| Methods supported | GET only. POST against List or Detail returns 405 Method Not Allowed; POST against the P&L endpoint returns 404 Not Found. Stick to GET. |
HEAD | Returns 405 Method Not Allowed on all three endpoints. For lightweight reachability checks, use OPTIONS or GET /v1/api/accounts - the response is small. |
OPTIONS | Returns 200 OK with Allow: GET on the List endpoint and an empty body. No CORS headers are emitted, so these endpoints are not callable from a browser cross-origin without a proxy. |
| TLS / HTTP/3 | The server advertises HTTP/3 via alt-svc. Clients that support it (curl 7.66+, modern browsers) will upgrade automatically. Plain HTTPS works fine. |
| Diagnostic headers | Successful responses may include optional diagnostic headers. They are informational only and safe to ignore in client code. |
GET /v1/api/accounts is a lightweight credential check: a small JSON payload that confirms your API key pair is valid (200) or not (404).
List User Accounts
GET /v1/api/accounts
Retrieve a list of all trading accounts associated with your API credentials. This is typically the first endpoint you'll call to identify which accounts you have access to.
Use Cases
- Startup discovery - on first launch, learn which account IDs your key pair can trade so every subsequent call has a valid path parameter.
- Account picker - render a selection UI for users whose key pair owns more than one account.
- Auth-health check - a lightweight call to confirm keys are valid (
200) or invalid (404). - Bulk dashboard - the rows are full snapshots, so a single call gives you balances, option settings, and traded volume for every account at once.
Request Example
curl 'https://webapi.tradezero.com/v1/api/accounts' \
-H 'Accept: application/json' \
-H 'TZ-API-KEY-ID: {YOUR_CLIENT_ID}' \
-H 'TZ-API-SECRET-KEY: {YOUR_CLIENT_SECRET}'
Response Structure
The endpoint returns an object with a single accounts array. Each row carries the full per-account snapshot - the same fields you'd get back from Retrieve Account Details. The only difference between the two shapes is that List rows expose buying power as buyingPower, while Detail uses the shorter bp - same value, two names.
| Field | Type | Description |
|---|---|---|
account | string | Account identifier. Paper accounts start with TZP; live accounts use other prefixes. TradeZero America accounts return their 2TZ account number here, not the portal login - see the note below. |
accountStatus | string | Account status. "Active" for trading-enabled accounts. Any other value indicates the account is in a non-trading state - always gate order placement on accountStatus === "Active". |
accountType | string | "Paper" for paper accounts; "Live" for live trading accounts (other classifications like "Margin" or "Cash" are possible - treat anything other than "Paper" as live). |
availableCash | number | Settled cash available to trade, in account currency. |
buyingPower | number | Maximum dollar value of new positions you can open. (Detail endpoint exposes this as bp.) |
equity | number | Account equity (cash + position market value). For a quote-driven, live value, read accountValue from the P&L endpoint. |
leverage | number | Maximum allowed leverage multiplier for the account. |
maintenanceDeficit | number | Shortfall against maintenance margin. 0 when the account is in good standing. |
marginDeficit | number | Shortfall against initial margin. 0 when the account is in good standing. |
marginRatio | number | Margin coverage percentage (100 = fully margined, lower = approaching call). |
optContractsTraded | number | Option contracts traded today. |
optLevel | number | Option approval level (0-4). Mirrors optionTradingLevel. |
optionCashTotalBalance | number | Cash balance attributable to options. |
optionTradingLevel | number | Option approval level (0-4). Same value as optLevel; both names ship in the response. |
overnightBp | number | Buying power available for overnight (non-day-trading) positions. |
realized | number | Realized P&L today. |
sharesTraded | number | Equity shares traded today. |
sodEquity | number | Start-of-day equity (yesterday's close + overnight settlements). |
totalCommissions | number | Commissions paid today. |
totalLocateCosts | number | Locate fees paid today (always 0 on paper - locates are live-only). |
usedLeverage | number | Current leverage actually deployed (e.g., 0.03 = 3% of allowed leverage used). |
Response Example
{
"accounts": [
{
"account": "TZP12345678",
"accountStatus": "Active",
"accountType": "Paper",
"availableCash": 964081.43,
"buyingPower": 964081.43,
"equity": 993445.07,
"leverage": 1,
"maintenanceDeficit": 0,
"marginDeficit": 0,
"marginRatio": 100,
"optContractsTraded": 0,
"optLevel": 3,
"optionCashTotalBalance": 0,
"optionTradingLevel": 3,
"overnightBp": 964081.43,
"realized": 0,
"sharesTraded": 0,
"sodEquity": 993445.07,
"totalCommissions": 0,
"totalLocateCosts": 0,
"usedLeverage": 0
}
]
}
Drive paper-vs-live behavior off the accountType field. Paper rows return "Paper"; live rows return anything else. Account ID prefixes are a portal-routing convention, not an API contract - see Account Types.
TradeZero America (TZA) accounts show two identifiers in the portal: a login (for example ZKA47093) and an account number that begins with 2TZ (shown as "Account", for example 2TZ35844). Only the 2TZ account number works for API requests - the login is not accepted as an {accountId}. You don't need to detect this in code: GET /v1/api/accounts returns the correct API-usable identifier in its account field, so use that value for every {accountId} path parameter and request body. Sending the login instead fails account resolution (404 Not Found, or "...Account for User was not found, or User doesn't have entitlements." on locate, order-placement, and cancel calls). Most other TradeZero accounts have a single ID and aren't affected.
Retrieve Account Details
GET /v1/api/account/{accountId}
Retrieve the full snapshot for a single account: cash and buying power, equity, margin deficits, leverage, today's traded volume / commissions, and option trading settings.
Use Cases
- Account dashboard - display a complete balance and option-settings overview.
- Pre-trade validation - check
bpandmarginDeficitbefore placing an order; these update on fills and cash events. - Option permissioning - branch on
optionTradingLevelbefore sending option orders. - Structural account data - read the slow-moving reference values:
accountStatus,leverage,sodEquity, account currency, option approval level. The "today" counters on this endpoint (realized,sharesTraded,totalCommissions,optContractsTraded,optionCashTotalBalance) reflect session-open values rather than live intraday activity - use the P&L endpoint for current totals.
Request Example
curl 'https://webapi.tradezero.com/v1/api/account/TZP12345678' \
-H 'Accept: application/json' \
-H 'TZ-API-KEY-ID: {YOUR_CLIENT_ID}' \
-H 'TZ-API-SECRET-KEY: {YOUR_CLIENT_SECRET}'
Replace TZP12345678 with an account ID returned by the List User Accounts endpoint.
Response Structure
The endpoint returns the account object directly (not wrapped in an array):
| Field | Type | Description |
|---|---|---|
account | string | Account identifier echoing the path parameter. |
accountStatus | string | Account status. "Active" for trading-enabled accounts. Any other value indicates a non-trading state - gate all order placement on accountStatus === "Active". |
accountType | string | "Paper" for paper accounts; "Live" for live trading accounts. Other values (e.g. "Margin", "Cash") are possible for specially-classified live accounts - always treat anything other than "Paper" as live. |
availableCash | number | Settled cash available to trade. |
bp | number | Buying power - maximum dollar value of new positions you can open. Same as buyingPower on the List response. |
equity | number | Account equity (cash + position market value). For a quote-driven, live value, read accountValue from the P&L endpoint. |
leverage | number | Maximum allowed leverage multiplier. |
maintenanceDeficit | number | Shortfall against maintenance margin. 0 when the account is in good standing. |
marginDeficit | number | Shortfall against initial margin. 0 when the account is in good standing. |
marginRatio | number | Margin coverage percentage (100 = fully margined, lower = approaching margin call). |
optContractsTraded | number | Option contracts traded today. |
optLevel | number | Option approval level (0-4). Mirrors optionTradingLevel. |
optionCashTotalBalance | number | Cash balance attributable to options. |
optionTradingLevel | number | Option approval level (0-4). Same value as optLevel; both names ship in the response. |
overnightBp | number | Buying power for overnight (non-day-trading) positions. |
realized | number | Realized P&L today. |
sharesTraded | number | Equity shares traded today. |
sodEquity | number | Start-of-day equity. |
totalCommissions | number | Commissions paid today. |
totalLocateCosts | number | Locate fees paid today (always 0 on paper - locates are live-only). |
usedLeverage | number | Current leverage actually deployed (e.g., 0.03 = 3% of allowed leverage used). |
Response Example
{
"account": "TZP12345678",
"accountStatus": "Active",
"accountType": "Paper",
"availableCash": 964081.43,
"bp": 964081.43,
"equity": 993445.07,
"leverage": 1,
"maintenanceDeficit": 0,
"marginDeficit": 0,
"marginRatio": 100,
"optContractsTraded": 0,
"optLevel": 3,
"optionCashTotalBalance": 0,
"optionTradingLevel": 3,
"overnightBp": 964081.43,
"realized": 0,
"sharesTraded": 0,
"sodEquity": 993445.07,
"totalCommissions": 0,
"totalLocateCosts": 0,
"usedLeverage": 0
}
The List response exposes buying power as buyingPower; the Detail response exposes it as bp. Both carry the same number. The Normalizing the response snippet below collapses both shapes into a single buyingPower field for shared client code.
Normalizing the response
If your client reads both List and Detail responses, normalize the two shapes at the HTTP boundary before any downstream code touches the object. The pattern below collapses them into a single buyingPower field that the rest of your app can rely on:
type RawAccount = Record<string, unknown>;
interface Account {
account: string;
accountStatus: string;
accountType: string;
availableCash: number;
buyingPower: number; // unified - reads `buyingPower` or falls back to `bp`
equity: number;
leverage: number;
marginDeficit: number;
maintenanceDeficit: number;
marginRatio: number;
optionTradingLevel: number;
overnightBp: number;
realized: number;
sodEquity: number;
totalCommissions: number;
totalLocateCosts: number;
usedLeverage: number;
}
function normalizeAccount(raw: RawAccount): Account {
return {
...(raw as unknown as Account),
buyingPower: (raw.buyingPower ?? raw.bp ?? 0) as number,
};
}
If the API ever unifies the field name on its own, this normalizer keeps working without changes.
Key Metrics Explained
Buying power (bp / buyingPower)
- The maximum dollar value of new positions you can open right now.
- For live margin accounts this is a multiple of
availableCashset by your account'sleverage; for paper and cash accounts it tracksavailableCashdirectly. overnightBpis the equivalent for positions held overnight - it's usually smaller thanbpon live margin accounts because intraday leverage doesn't apply overnight.
Equity and sodEquity
equityrepresents account equity (cash + position market value). On the Detail/List response it does not reprice on every quote tick and stays stable across short polling intervals. For a value that does reprice on every call, readaccountValuefrom the P&L endpoint.sodEquityis the start-of-day equity, captured at the open. The day's P&L is thenpnl.accountValue - sodEquity, which is exactly what the API already returns aspnl.dayPnlon the P&L endpoint.
Margin health
marginDeficitandmaintenanceDeficitshould both be0for a healthy account. A non-zero value signals you've crossed the initial / maintenance margin line.marginRatiois a percentage view of margin coverage.100means fully margined; lower numbers indicate you're closer to a margin call.usedLeverage/leverageshows how much of your allowed leverage you're actually using right now.
Option permissioning
optionTradingLevel(and its aliasoptLevel) report the account's option approval level on the standard 0-4 scale.0means options aren't enabled; higher levels unlock multi-leg and uncovered strategies. Branch on this value before sending option orders to confirm the account is permissioned for the strategy you're about to send.
Retrieve Account Values and Profit/Loss
GET /v1/api/accounts/{accountId}/pnl
Pull the live P&L view for an account: aggregate day and total realized / unrealized figures, current exposure and leverage, and a per-lot breakdown that covers both open positions and lots closed earlier today. Values reflect the latest available quote on every call, so this is the endpoint to poll (or replace with the P&L Stream) for a live dashboard.
This endpoint is also the primary read for the Open Positions page. The same response envelope covers both the account totals documented here and the per-position rows documented there.
Use Cases
- Live P&L dashboard - the per-position
pnl[]rows let you render a fill-by-fill performance table without a separate positions call. - Risk monitoring - watch
usedLeverage,exposure, andequityRatioto catch concentration before it becomes a margin event. - Daily activity - read
dayPnl,dayRealized,dayUnrealized, andsharesTradedfor today's session totals. - Per-symbol contribution - sort
pnl[]byunrealizedPnLorpctPnLMoveto surface winners and losers.
Request Example
curl 'https://webapi.tradezero.com/v1/api/accounts/TZP12345678/pnl' \
-H 'Accept: application/json' \
-H 'TZ-API-KEY-ID: {YOUR_CLIENT_ID}' \
-H 'TZ-API-SECRET-KEY: {YOUR_CLIENT_SECRET}'
Response Structure
The response is an object with account-level aggregates plus a pnl[] array of per-position breakdowns.
Account-level fields
| Field | Type | Description |
|---|---|---|
accountValue | number | Current account equity (cash + position market value). Refreshes every quote tick. |
allowedLeverage | number | Maximum leverage multiplier permitted on the account. |
availableCash | number | Settled cash available to trade. |
dayPnl | number | Total P&L today (dayRealized + dayUnrealized). |
dayRealized | number | P&L locked in today by closed positions and fills. |
dayUnrealized | number | P&L floating on positions opened or held today. |
equityRatio | number | Equity coverage ratio. 1 for unlevered (paper and cash) accounts; lower values appear on live margin accounts as margin is deployed. |
exposure | number | Total notional value of open equity positions. Open options are reported separately via optionCashUsed; add the two for an all-asset total. |
optionCashUsed | number | Cash tied up in open option positions. Matches the sum of exposure across all open option rows in pnl[]. 0 when no options are open. |
pnl | array | Per-lot breakdown - one row per lot, including lots that were fully closed earlier today. See below. |
sharesTraded | number | Equity shares traded today. |
totalUnrealized | number | Lifetime unrealized P&L across all open positions (not just today's contribution). |
usedLeverage | number | Leverage actually deployed right now (e.g., 0.03 = 3% of allowed leverage in use). |
Per-position pnl[] row
| Field | Type | Description |
|---|---|---|
positionId | string | Stable identifier for the lot. Use this to join against the Open Positions endpoint. |
symbol | string | Plain ticker for equities ("AAPL"); OCC-formatted symbol for options ("QQQ260514C00703000" = QQQ, expiry 2026-05-14, call, strike $703.00). |
exposure | number | Notional value of the lot. 0 when the lot is closed (use this as the "is this lot still open?" flag). |
dayRealizedPnl | number | Realized P&L from any partial or full closes of this lot today. |
dayUnrealizedPnL | number | Today's change in floating P&L on the lot (today's price move only). 0 once the lot is closed. |
dayPctPnLMove | number | Percentage change in the lot's value today. |
realizedPnl | number | Lifetime realized P&L on the lot, accumulated from every partial / full close since it was opened. |
unrealizedPnL | number | Lifetime floating P&L on the lot (cost basis vs. current quote). 0 once the lot is closed. |
pctPnLMove | number | Lifetime percentage P&L move on the lot. |
The realized fields are realizedPnl and dayRealizedPnl; the unrealized fields are unrealizedPnL and dayUnrealizedPnL. Mirror the keys exactly when you define your type model.
The pnl[] array carries one row per lot, not per symbol. The common case where this matters is when you have both an overnight position and a same-day add in the same ticker - they're tracked separately with the same symbol but different positionIds, each with its own day and lifetime P&L.
The day-vs-lifetime gap tells you which is which. If dayUnrealizedPnL === unrealizedPnL (and dayRealizedPnl === realizedPnl), the lot was opened today, so today's P&L is the whole P&L. If they differ, the lot was carried over from a previous session - the lifetime fields include P&L accumulated before today.
Always join with the Open Positions endpoint by positionId, not by symbol, when you need to attach a P&L row to a specific lot. If you only want a per-symbol aggregate, sum the matching pnl[] rows by symbol yourself.
Response Example
{
"accountValue": 993542.06,
"allowedLeverage": 1,
"availableCash": 968951.40,
"dayPnl": 96.99,
"dayRealized": -151.59,
"dayUnrealized": 248.58,
"equityRatio": 1,
"exposure": 23535.20,
"optionCashUsed": 1192,
"pnl": [
{
"positionId": "2260512190020399758",
"symbol": "AAPL",
"exposure": 294.50,
"dayRealizedPnl": 0,
"dayUnrealizedPnL": 0.08,
"dayPctPnLMove": 0.03,
"realizedPnl": 0,
"unrealizedPnL": 0.08,
"pctPnLMove": 0.03
},
{
"positionId": "2260512190030795781",
"symbol": "AMZN",
"exposure": 0,
"dayRealizedPnl": 1.15,
"dayUnrealizedPnL": 0,
"dayPctPnLMove": -0.40,
"realizedPnl": 1.15,
"unrealizedPnL": 0,
"pctPnLMove": -0.40
},
{
"positionId": "2260425072826832267",
"symbol": "AMZN",
"exposure": 0,
"dayRealizedPnl": -3.59,
"dayUnrealizedPnL": 0,
"dayPctPnLMove": 1.37,
"realizedPnl": 6.52,
"unrealizedPnL": 0,
"pctPnLMove": -2.48
},
{
"positionId": "2260428072825462235",
"symbol": "BRLS",
"exposure": 0,
"dayRealizedPnl": -0.03,
"dayUnrealizedPnL": 0,
"dayPctPnLMove": 0,
"realizedPnl": 0.14,
"unrealizedPnL": 0,
"pctPnLMove": -14.91
},
{
"positionId": "2260512190026150778",
"symbol": "CRM",
"exposure": 0,
"dayRealizedPnl": -0.68,
"dayUnrealizedPnL": 0,
"dayPctPnLMove": 0.47,
"realizedPnl": -0.68,
"unrealizedPnL": 0,
"pctPnLMove": 0.47
},
{
"positionId": "2260421072916751411",
"symbol": "GME",
"exposure": 0,
"dayRealizedPnl": -147.62,
"dayUnrealizedPnL": 0,
"dayPctPnLMove": 2.63,
"realizedPnl": -573.48,
"unrealizedPnL": 0,
"pctPnLMove": 9.51
},
{
"positionId": "2260421072916766412",
"symbol": "NVDA",
"exposure": 23240.70,
"dayRealizedPnl": 0,
"dayUnrealizedPnL": 199.5,
"dayPctPnLMove": 0.87,
"realizedPnl": 0,
"unrealizedPnL": 2041.20,
"pctPnLMove": 9.63
},
{
"positionId": "2260512193112437606",
"symbol": "QQQ260514C00703000",
"exposure": 1192,
"dayRealizedPnl": 0,
"dayUnrealizedPnL": 49,
"dayPctPnLMove": 4.29,
"realizedPnl": 0,
"unrealizedPnL": 49,
"pctPnLMove": 4.29
},
{
"positionId": "2260512193136876622",
"symbol": "TSLA260513C00437500",
"exposure": 0,
"dayRealizedPnl": 10,
"dayUnrealizedPnL": 0,
"dayPctPnLMove": 2.35,
"realizedPnl": 10,
"unrealizedPnL": 0,
"pctPnLMove": 2.35
},
{
"positionId": "2260423072827941260",
"symbol": "TSLA",
"exposure": 0,
"dayRealizedPnl": -10.82,
"dayUnrealizedPnL": 0,
"dayPctPnLMove": 2.50,
"realizedPnl": 44.85,
"unrealizedPnL": 0,
"pctPnLMove": -11.44
}
],
"sharesTraded": 256,
"totalUnrealized": 2090.28,
"usedLeverage": 0.02
}
The response above is a useful tour of every state a pnl[] row can be in. The highlights:
| Row | exposure | Day vs lifetime fields | What it tells you |
|---|---|---|---|
AAPL | 294.50 | day == lifetime | Open equity lot opened today. Day P&L is the whole P&L. |
AMZN (...5781) | 0 | day == lifetime | Equity lot opened and closed today. Final realized P&L: 1.15. |
AMZN (...2267) | 0 | day ≠ lifetime | Equity lot carried overnight and closed today. Lifetime realizedPnl (6.52) includes pre-today P&L; dayRealizedPnl (-3.59) is today's slice only. |
GME | 0 | day ≠ lifetime | Closed today after being held overnight. Today contributed -147.62; the lot lost -573.48 over its full life. |
NVDA | 23240.70 | day ≠ lifetime | Still open and carried overnight. The big unrealizedPnL (2041.20) was mostly earned on prior days; today only added 199.5. |
QQQ260514C00703000 | 1192 | day == lifetime | Open option lot opened today (QQQ, expiry 2026-05-14, call, strike $703.00). exposure matches optionCashUsed for the row. |
TSLA260513C00437500 | 0 | day == lifetime | Option lot opened and closed today for a 10 realized profit. |
Aggregates tie out from the rows:
dayUnrealized(248.58) = sum of every row'sdayUnrealizedPnL(only AAPL, NVDA, and the open QQQ call contribute - everything else is0because it's closed).dayRealized(-151.59) = sum of every row'sdayRealizedPnl(the AMZNs, BRLS, CRM, GME, TSLA equity, and TSLA call - GME is the dominant contributor).dayPnl(96.99) =dayRealized + dayUnrealized.totalUnrealized(2090.28) = sum of every row'sunrealizedPnL, which is non-zero only on open lots (AAPL + NVDA + QQQ call).optionCashUsed(1192) = sum ofexposureacross open option rows (just the one QQQ call in this snapshot).
Understanding P&L Values
Day vs. lifetime
- The
day*fields (dayPnl,dayRealized,dayUnrealized,dayUnrealizedPnL,dayRealizedPnl) reset at the start of each trading day and only track today's contribution. - The non-
day*fields (realizedPnl,unrealizedPnL,pctPnLMove,totalUnrealized) carry lifetime numbers for the position since it was first opened. - For a position you opened today, the
day*and lifetime numbers will match. For a position carried overnight, the lifetime numbers continue to compound.
Realized vs. unrealized
realizedPnlaccumulates as you close some or all of a lot. Full closes do not remove the row frompnl[]- the row stays for the rest of the trading day withexposure: 0andunrealizedPnL: 0, andrealizedPnlholds the final close-out P&L for accounting purposes.unrealizedPnLis the floating P&L on whatever's still open. It reprices on every quote tick and is0for any row whoseexposureis0.- Detecting open vs closed at a glance:
exposure > 0means the lot is still open;exposure === 0means it was closed out today and is being retained for the realized-P&L total.
Polling vs. streaming
- Each call hits a fresh quote, so this endpoint is appropriate for dashboards that need a snapshot. For a continuous feed without polling pressure, subscribe to the P&L Stream, which delivers the same aggregates and per-position rows incrementally over WebSocket.
Best Practices
Polling cadence
Recommended values for an integration that polls all three Accounts endpoints in parallel:
| Setting | Value | Why |
|---|---|---|
| Base poll interval | 5000 ms | Fast enough for a live balances and P&L UI; slow enough to keep request volume well below any reasonable per-second limit. |
| Max poll interval (backoff) | 60000 ms | Exponential backoff cap. Triggered when every polled endpoint fails together (likely a connectivity issue, not the API). |
| Request timeout | 15000 ms | Cancels in-flight requests so they don't stack up during a network blip. Implement with AbortController on fetch. |
| Polling while backgrounded | Paused | When the app is not in the foreground, stop polling to save battery and API quota. Resume on foreground, reset the backoff. |
Some endpoint-specific tuning on top of that:
GET /v1/api/accounts- cache aggressively. The set of accounts your key pair owns rarely changes; refresh on sign-in or user action rather than on a timer. There is no benefit to polling List on an interval.GET /v1/api/account/{accountId}- Detail is the canonical snapshot of an account's static profile and balances. Intraday it refreshes the cash and buying-power fields (availableCash,bp/buyingPower,overnightBp) after fills and cash events. For values that move on every quote tick or fill - day P&L, today's realized totals, today's traded counts - read/pnlinstead, which is the live source for that data.GET /v1/api/accounts/{accountId}/pnl- this is the live one.accountValue,dayPnl,dayUnrealized,exposure, and every openpnl[]row'sunrealizedPnL/dayUnrealizedPnLmove on every call.dayRealized,dayRealizedPnl, andsharesTradedupdate on each fill or close. For a continuous view, prefer the P&L Stream over polling - it delivers the same aggregates and per-position rows incrementally and removes the round-trip latency. Only fall back to polling if you can't open a persistent WebSocket connection.
Error Handling
The Accounts endpoints return a focused set of HTTP status codes. The cases you need to handle in code:
| Status | Body | When you see it |
|---|---|---|
404 | Not found\n (plain text) | Missing or invalid credentials; account ID not owned by your key pair; account ID malformed; POST against /pnl. All surface with the same status and body, so handle them with a single code path. |
404 | 404 page not found (plain text) | URL was malformed (e.g., empty account ID segment). Fix the path on your end. |
405 | 405 method not allowed | Used POST / HEAD against /v1/api/accounts or /v1/api/account/{id}. Stick to GET. |
405 | (empty) | HEAD against /v1/api/accounts/{id}/pnl. The PnL route returns 405 specifically for HEAD with no body. |
Notes:
- No 401/403 is returned. Auth failures and account mismatches return
404 Not Foundwith bodyNot found\n— a security-conscious design that prevents account ID enumeration. Handle404uniformly rather than branching on 401/403. - Error bodies are
text/plain, not JSON. Readres.text()and branch on theContent-Typeheader before parsing. - Idempotent and safe. All three Accounts endpoints are pure reads. Retrying a failed call has no side effects, so plain exponential backoff on transient network errors is sufficient.
- A 404 on a known-good account ID is usually a credentials problem. Auth failures share the 404 status with genuine not-found responses, so when nothing else in your client has changed, check the key pair first - especially after a rotation.
Pre-trade safety checklist
Always verify bp (or buyingPower on the list response) and accountStatus === "Active" before placing orders. Never rely on a cached snapshot older than your poll interval - balances and buying power move on every fill.
Before sending an order, read the Detail snapshot for the account and verify all four of the following. The full reference implementation is in Pre-Trade Validation below; for a complete workflow that also fans out /positions, /routes, and optionTradingLevel checks in parallel, see the Pre-Trade Validation recipe.
accountStatus === "Active"- the account is trading-enabled.bp >= orderValue- sufficient buying power for the trade.marginDeficit === 0andmaintenanceDeficit === 0- no margin shortfall.optionTradingLevel >= Nfor option orders, whereNis the level required by your strategy (e.g.2for long verticals,3for short premium). Validating this client-side surfaces a clear precondition error to the trader before the order is sent.
Security Considerations
- Never expose API keys in client-side code - all Accounts calls should originate from a server you control.
- Cache account data server-side to minimize round trips.
- Log every Accounts call (status, account ID, latency) for audit and to detect the 404 pattern that signals a credentials problem.
Common Workflows
The three endpoints are designed to work together. The patterns below show how to combine them into the dashboard and validation flows that most integrations require.
Building an Account Dashboard
- List accounts once at startup -
GET /v1/api/accountsreturns every account your key pair owns, with itsaccountType("Paper"vs. other). Cache the IDs. - Pull the per-account snapshot on selection -
GET /v1/api/account/{accountId}for the cash, buying power, equity, and option settings. - Subscribe to live P&L - either poll
GET /v1/api/accounts/{accountId}/pnlon a short cadence, or move to the P&L Stream so per-position updates push to you.
For a reference implementation of this pattern (parallel reads, exponential backoff on transient errors, normalized account+P&L merge), see the Live Account Dashboard recipe.
Merging account and P&L into one dashboard view
A few fields (availableCash, usedLeverage, sharesTraded) appear on both the Detail and the PnL responses. The PnL endpoint is the one that reprices on every call, so it's the authoritative source for any value that moves on quote ticks or fills. The recommended merge pattern is: prefer PnL values where they exist, fall back to Account values where it's safe, and refuse to fall back across the day-only metrics that PnL alone tracks accurately.
Bear in mind that the Account-side equivalents of the day-only metrics (account.realized, account.sharesTraded, account.optionCashTotalBalance) reflect session-boundary state rather than tick-level activity. The fallbacks below are kept as a safety net only; the live values for dayRealized, sharesTraded, and optionCashUsed come from PnL.
// Shape of the per-position rows in the PnL response.
interface PnlEntry {
positionId: string;
symbol: string;
exposure: number;
realizedPnl: number;
unrealizedPnL: number;
dayRealizedPnl: number;
dayUnrealizedPnL: number;
pctPnLMove: number;
dayPctPnLMove: number;
}
// Shape of the PnL response. `Account` was defined in the
// "Normalizing the response" snippet earlier on this page.
interface Pnl {
accountValue: number;
allowedLeverage: number;
availableCash: number;
dayPnl: number;
dayRealized: number;
dayUnrealized: number;
equityRatio: number;
exposure: number;
optionCashUsed: number;
pnl: PnlEntry[];
sharesTraded: number;
totalUnrealized: number;
usedLeverage: number;
}
interface AccountMetrics {
accountValue: number;
buyingPower: number;
availableCash: number;
overnightBp: number;
sodEquity: number;
dayPnl: number;
dayRealized: number;
dayUnrealized: number;
totalUnrealized: number;
exposure: number;
usedLeverage: number;
allowedLeverage: number;
equityRatio: number;
sharesTraded: number;
optionCashUsed: number;
}
function computeAccountMetrics(account: Account | null, pnl: Pnl | null): AccountMetrics {
return {
accountValue: pnl?.accountValue ?? account?.equity ?? 0,
buyingPower: account?.buyingPower ?? 0,
availableCash: pnl?.availableCash ?? account?.availableCash ?? 0,
overnightBp: account?.overnightBp ?? 0,
sodEquity: account?.sodEquity ?? 0,
dayPnl: pnl?.dayPnl ?? 0,
dayRealized: pnl?.dayRealized ?? account?.realized ?? 0,
dayUnrealized: pnl?.dayUnrealized ?? 0,
totalUnrealized: pnl?.totalUnrealized ?? 0,
exposure: pnl?.exposure ?? 0,
usedLeverage: pnl?.usedLeverage ?? account?.usedLeverage ?? 0,
allowedLeverage: pnl?.allowedLeverage ?? account?.leverage ?? 0,
equityRatio: pnl?.equityRatio ?? 0,
sharesTraded: pnl?.sharesTraded ?? account?.sharesTraded ?? 0,
optionCashUsed: pnl?.optionCashUsed ?? account?.optionCashTotalBalance ?? 0,
};
}
Notice that dayPnl, dayUnrealized, totalUnrealized, exposure, and equityRatio do not fall back to anything when PnL is unavailable. These come exclusively from the PnL endpoint, so a missing PnL response should render as 0 (or an explicit loading state), not a stale Account-derived value. accountValue is the one deliberate exception in the snippet - it falls back to account.equity so the headline equity figure stays populated during a brief PnL gap. Pick whichever trade-off matches your client's UX.
Computing day P&L percentage
The PnL endpoint returns dayPnl in dollars but does not surface a day-percentage figure at the account level. Compute it from accountValue and dayPnl, using yesterday's equity (live equity minus today's P&L) as the denominator:
function computeDayPnlPct(accountValue: number, dayPnl: number): number {
const previousEquity = accountValue - dayPnl;
// Guard against near-zero denominators at session open before any equity is captured.
if (Math.abs(previousEquity) < 0.01) return 0;
return (dayPnl / previousEquity) * 100;
}
Both inputs come from the P&L endpoint - accountValue and dayPnl are returned together on every call, so the denominator is always consistent with the numerator.
Pre-Trade Validation
async function validateOrderPlacement(accountId, orderValue) {
const account = await getAccountDetails(accountId); // GET /v1/api/account/{accountId}
if (account.bp < orderValue) {
return { valid: false, reason: 'Insufficient buying power' };
}
if (account.accountStatus !== 'Active') {
return { valid: false, reason: `Account not active: ${account.accountStatus}` };
}
if (account.marginDeficit > 0 || account.maintenanceDeficit > 0) {
return { valid: false, reason: 'Account in margin deficit' };
}
return { valid: true };
}
Monitoring Account Health
async function checkAccountHealth(accountId) {
const account = await getAccountDetails(accountId); // GET /v1/api/account/{accountId}
const pnl = await getAccountPnL(accountId); // GET /v1/api/accounts/{accountId}/pnl
const alerts = [];
// Watch how much of the allowed leverage is actually deployed.
// Always use pnl.usedLeverage / pnl.allowedLeverage - the Account-side
// equivalents stay at 0 on the Detail snapshot and are not live values.
if (pnl.allowedLeverage > 0 && pnl.usedLeverage / pnl.allowedLeverage > 0.8) {
alerts.push('High leverage utilization - risk of margin call on adverse moves');
}
// Day-only losses, scaled by live equity. Use `dayPnl` rather than `totalUnrealized`
// if you want to alert on intraday drawdown rather than lifetime drawdown.
if (pnl.accountValue > 0 && pnl.dayPnl / pnl.accountValue < -0.05) {
alerts.push('Day P&L worse than -5% of account value');
}
// Surface the single worst-performing **open** position. Filter out closed-today
// rows (exposure === 0) so the alert points at a position the user can still act on.
const openRows = pnl.pnl.filter((row) => row.exposure > 0);
const worst = openRows.length
? openRows.reduce((acc, row) =>
row.dayUnrealizedPnL < acc.dayUnrealizedPnL ? row : acc,
)
: null;
if (worst && worst.dayUnrealizedPnL < 0) {
alerts.push(`Largest day-loss on open position: ${worst.symbol} (${worst.dayUnrealizedPnL.toFixed(2)})`);
}
// Margin shortfall - rare on paper, but the Detail snapshot does carry it.
if (account.marginDeficit > 0 || account.maintenanceDeficit > 0) {
alerts.push('Account in margin deficit');
}
return alerts;
}
Additional Resources
- Account Types - paper vs. live accounts, what each can do over API, and how to read
accountTypecorrectly. - Authentication - how API keys work and which environments (live vs paper) they can reach.
- P&L Stream (WebSocket) - push-style equivalent of
GET /accounts/{id}/pnl. - Equity Trading - the natural next step once you've confirmed buying power.
- API Reference - Accounts - the OpenAPI-generated reference for all three endpoints.
Recipes
- Authenticate & Discover Account - first call from your API keys: confirm credentials, identify paper vs live, and enumerate routes.
- List All Your Accounts - list with
GET /accounts, call per-account detail in parallel when you need thebpfield, then partition onaccountType. - Live Account Dashboard - the canonical dashboard pattern: parallel
/account/{id},/positions, and/pnlreads merged into a single periodic snapshot with backoff. - P&L Stream Dashboard - replace the
/pnlpoll with a WebSocket subscription, including snapshot + incremental merge. - Pre-Trade Validation - the full reference implementation of the pre-trade safety checklist above.
Cache the value of the account field after listing accounts - it is the path parameter for every other account-scoped endpoint on the API (orders, positions, P&L). Note the response field is account, not accountId.