Skip to content
Skip to main content

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 (Paper for 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

EndpointReturnsWhen 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}/pnlDay 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.

ConcernDetails
Auth header namesCase-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 valuesCompared character-for-character. Invalid values come back as 404 Not Found (see Error Handling).
Account ID casingCase-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 slashesSupported. /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 parametersNot used by the Accounts endpoints. Each call returns the current snapshot; there is no point-in-time variant.
Accept headerResponses are always application/json; charset=utf-8 on success.
CompressionSuccessful 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.
CachingResponses set Cache-Control: max-age=0, no-cache, no-store. Do not cache them in shared layers - the values are live.
Methods supportedGET only. POST against List or Detail returns 405 Method Not Allowed; POST against the P&L endpoint returns 404 Not Found. Stick to GET.
HEADReturns 405 Method Not Allowed on all three endpoints. For lightweight reachability checks, use OPTIONS or GET /v1/api/accounts - the response is small.
OPTIONSReturns 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/3The server advertises HTTP/3 via alt-svc. Clients that support it (curl 7.66+, modern browsers) will upgrade automatically. Plain HTTPS works fine.
Diagnostic headersSuccessful responses may include optional diagnostic headers. They are informational only and safe to ignore in client code.
Connectivity check

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

FieldTypeDescription
accountstringAccount 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.
accountStatusstringAccount 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".
accountTypestring"Paper" for paper accounts; "Live" for live trading accounts (other classifications like "Margin" or "Cash" are possible - treat anything other than "Paper" as live).
availableCashnumberSettled cash available to trade, in account currency.
buyingPowernumberMaximum dollar value of new positions you can open. (Detail endpoint exposes this as bp.)
equitynumberAccount equity (cash + position market value). For a quote-driven, live value, read accountValue from the P&L endpoint.
leveragenumberMaximum allowed leverage multiplier for the account.
maintenanceDeficitnumberShortfall against maintenance margin. 0 when the account is in good standing.
marginDeficitnumberShortfall against initial margin. 0 when the account is in good standing.
marginRationumberMargin coverage percentage (100 = fully margined, lower = approaching call).
optContractsTradednumberOption contracts traded today.
optLevelnumberOption approval level (0-4). Mirrors optionTradingLevel.
optionCashTotalBalancenumberCash balance attributable to options.
optionTradingLevelnumberOption approval level (0-4). Same value as optLevel; both names ship in the response.
overnightBpnumberBuying power available for overnight (non-day-trading) positions.
realizednumberRealized P&L today.
sharesTradednumberEquity shares traded today.
sodEquitynumberStart-of-day equity (yesterday's close + overnight settlements).
totalCommissionsnumberCommissions paid today.
totalLocateCostsnumberLocate fees paid today (always 0 on paper - locates are live-only).
usedLeveragenumberCurrent 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
}
]
}
Paper vs live

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 accounts have two IDs

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 bp and marginDeficit before placing an order; these update on fills and cash events.
  • Option permissioning - branch on optionTradingLevel before 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 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):

FieldTypeDescription
accountstringAccount identifier echoing the path parameter.
accountStatusstringAccount status. "Active" for trading-enabled accounts. Any other value indicates a non-trading state - gate all order placement on accountStatus === "Active".
accountTypestring"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.
availableCashnumberSettled cash available to trade.
bpnumberBuying power - maximum dollar value of new positions you can open. Same as buyingPower on the List response.
equitynumberAccount equity (cash + position market value). For a quote-driven, live value, read accountValue from the P&L endpoint.
leveragenumberMaximum allowed leverage multiplier.
maintenanceDeficitnumberShortfall against maintenance margin. 0 when the account is in good standing.
marginDeficitnumberShortfall against initial margin. 0 when the account is in good standing.
marginRationumberMargin coverage percentage (100 = fully margined, lower = approaching margin call).
optContractsTradednumberOption contracts traded today.
optLevelnumberOption approval level (0-4). Mirrors optionTradingLevel.
optionCashTotalBalancenumberCash balance attributable to options.
optionTradingLevelnumberOption approval level (0-4). Same value as optLevel; both names ship in the response.
overnightBpnumberBuying power for overnight (non-day-trading) positions.
realizednumberRealized P&L today.
sharesTradednumberEquity shares traded today.
sodEquitynumberStart-of-day equity.
totalCommissionsnumberCommissions paid today.
totalLocateCostsnumberLocate fees paid today (always 0 on paper - locates are live-only).
usedLeveragenumberCurrent 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
}
Buying power field name

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 availableCash set by your account's leverage; for paper and cash accounts it tracks availableCash directly.
  • overnightBp is the equivalent for positions held overnight - it's usually smaller than bp on live margin accounts because intraday leverage doesn't apply overnight.

Equity and sodEquity

  • equity represents 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, read accountValue from the P&L endpoint.
  • sodEquity is the start-of-day equity, captured at the open. The day's P&L is then pnl.accountValue - sodEquity, which is exactly what the API already returns as pnl.dayPnl on the P&L endpoint.

Margin health

  • marginDeficit and maintenanceDeficit should both be 0 for a healthy account. A non-zero value signals you've crossed the initial / maintenance margin line.
  • marginRatio is a percentage view of margin coverage. 100 means fully margined; lower numbers indicate you're closer to a margin call.
  • usedLeverage / leverage shows how much of your allowed leverage you're actually using right now.

Option permissioning

  • optionTradingLevel (and its alias optLevel) report the account's option approval level on the standard 0-4 scale. 0 means 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.

note

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, and equityRatio to catch concentration before it becomes a margin event.
  • Daily activity - read dayPnl, dayRealized, dayUnrealized, and sharesTraded for today's session totals.
  • Per-symbol contribution - sort pnl[] by unrealizedPnL or pctPnLMove to surface winners and losers.

Request Example

CURL 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
FieldTypeDescription
accountValuenumberCurrent account equity (cash + position market value). Refreshes every quote tick.
allowedLeveragenumberMaximum leverage multiplier permitted on the account.
availableCashnumberSettled cash available to trade.
dayPnlnumberTotal P&L today (dayRealized + dayUnrealized).
dayRealizednumberP&L locked in today by closed positions and fills.
dayUnrealizednumberP&L floating on positions opened or held today.
equityRationumberEquity coverage ratio. 1 for unlevered (paper and cash) accounts; lower values appear on live margin accounts as margin is deployed.
exposurenumberTotal notional value of open equity positions. Open options are reported separately via optionCashUsed; add the two for an all-asset total.
optionCashUsednumberCash tied up in open option positions. Matches the sum of exposure across all open option rows in pnl[]. 0 when no options are open.
pnlarrayPer-lot breakdown - one row per lot, including lots that were fully closed earlier today. See below.
sharesTradednumberEquity shares traded today.
totalUnrealizednumberLifetime unrealized P&L across all open positions (not just today's contribution).
usedLeveragenumberLeverage actually deployed right now (e.g., 0.03 = 3% of allowed leverage in use).
Per-position pnl[] row
FieldTypeDescription
positionIdstringStable identifier for the lot. Use this to join against the Open Positions endpoint.
symbolstringPlain ticker for equities ("AAPL"); OCC-formatted symbol for options ("QQQ260514C00703000" = QQQ, expiry 2026-05-14, call, strike $703.00).
exposurenumberNotional value of the lot. 0 when the lot is closed (use this as the "is this lot still open?" flag).
dayRealizedPnlnumberRealized P&L from any partial or full closes of this lot today.
dayUnrealizedPnLnumberToday's change in floating P&L on the lot (today's price move only). 0 once the lot is closed.
dayPctPnLMovenumberPercentage change in the lot's value today.
realizedPnlnumberLifetime realized P&L on the lot, accumulated from every partial / full close since it was opened.
unrealizedPnLnumberLifetime floating P&L on the lot (cost basis vs. current quote). 0 once the lot is closed.
pctPnLMovenumberLifetime percentage P&L move on the lot.
Mirror the field names exactly

The realized fields are realizedPnl and dayRealizedPnl; the unrealized fields are unrealizedPnL and dayUnrealizedPnL. Mirror the keys exactly when you define your type model.

A symbol can appear more than once

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:

RowexposureDay vs lifetime fieldsWhat it tells you
AAPL294.50day == lifetimeOpen equity lot opened today. Day P&L is the whole P&L.
AMZN (...5781)0day == lifetimeEquity lot opened and closed today. Final realized P&L: 1.15.
AMZN (...2267)0day ≠ lifetimeEquity lot carried overnight and closed today. Lifetime realizedPnl (6.52) includes pre-today P&L; dayRealizedPnl (-3.59) is today's slice only.
GME0day ≠ lifetimeClosed today after being held overnight. Today contributed -147.62; the lot lost -573.48 over its full life.
NVDA23240.70day ≠ lifetimeStill open and carried overnight. The big unrealizedPnL (2041.20) was mostly earned on prior days; today only added 199.5.
QQQ260514C007030001192day == lifetimeOpen option lot opened today (QQQ, expiry 2026-05-14, call, strike $703.00). exposure matches optionCashUsed for the row.
TSLA260513C004375000day == lifetimeOption lot opened and closed today for a 10 realized profit.

Aggregates tie out from the rows:

  • dayUnrealized (248.58) = sum of every row's dayUnrealizedPnL (only AAPL, NVDA, and the open QQQ call contribute - everything else is 0 because it's closed).
  • dayRealized (-151.59) = sum of every row's dayRealizedPnl (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's unrealizedPnL, which is non-zero only on open lots (AAPL + NVDA + QQQ call).
  • optionCashUsed (1192) = sum of exposure across 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

  • realizedPnl accumulates as you close some or all of a lot. Full closes do not remove the row from pnl[] - the row stays for the rest of the trading day with exposure: 0 and unrealizedPnL: 0, and realizedPnl holds the final close-out P&L for accounting purposes.
  • unrealizedPnL is the floating P&L on whatever's still open. It reprices on every quote tick and is 0 for any row whose exposure is 0.
  • Detecting open vs closed at a glance: exposure > 0 means the lot is still open; exposure === 0 means 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:

SettingValueWhy
Base poll interval5000 msFast 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 msExponential backoff cap. Triggered when every polled endpoint fails together (likely a connectivity issue, not the API).
Request timeout15000 msCancels in-flight requests so they don't stack up during a network blip. Implement with AbortController on fetch.
Polling while backgroundedPausedWhen 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 /pnl instead, 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 open pnl[] row's unrealizedPnL / dayUnrealizedPnL move on every call. dayRealized, dayRealizedPnl, and sharesTraded update 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:

StatusBodyWhen you see it
404Not 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.
404404 page not found (plain text)URL was malformed (e.g., empty account ID segment). Fix the path on your end.
405405 method not allowedUsed 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 Found with body Not found\n — a security-conscious design that prevents account ID enumeration. Handle 404 uniformly rather than branching on 401/403.
  • Error bodies are text/plain, not JSON. Read res.text() and branch on the Content-Type header 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

warning

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.

  1. accountStatus === "Active" - the account is trading-enabled.
  2. bp >= orderValue - sufficient buying power for the trade.
  3. marginDeficit === 0 and maintenanceDeficit === 0 - no margin shortfall.
  4. optionTradingLevel >= N for option orders, where N is the level required by your strategy (e.g. 2 for long verticals, 3 for 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

  1. List accounts once at startup - GET /v1/api/accounts returns every account your key pair owns, with its accountType ("Paper" vs. other). Cache the IDs.
  2. Pull the per-account snapshot on selection - GET /v1/api/account/{accountId} for the cash, buying power, equity, and option settings.
  3. Subscribe to live P&L - either poll GET /v1/api/accounts/{accountId}/pnl on 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

Recipes


tip

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.