Skip to content
Skip to main content

OAuth 2.0 Partner Integration

Start here - Connect Trade

Connect Trade is TradeZero's chosen broker-connectivity partner. Third-party platforms that need to connect TradeZero customer accounts integrate through Connect Trade - unless TradeZero has specified a direct partner program for your product.

Portal API keys are for your own account only. Do not ask customers to paste TradeZero API secrets into your product.

Chosen partner: Connect Trade

Connect Trade is a unified broker-connectivity API. You integrate once against their normalized REST and WebSocket APIs. They maintain broker-approved connections - including TradeZero - with OAuth click-through for end users, server-side rate-limit handling, and compliance-approved connectivity. Your users authorize their TradeZero account through Connect Trade; you never hold their TradeZero password or Portal API secret.

For platforms that also support other brokerages, one Connect Trade integration covers TradeZero plus their broader broker network. Against their TradeZero capability matrix, the listed surfaces include accounts, orders, positions, equities trading, options (including multi-leg), and paper trading. Confirm the exact fields you need with Connect Trade before you design around them.

How to start with Connect Trade

  1. Go to connecttrade.com, review their API docs and FAQ.
  2. Sign up or contact Connect Trade sales.
  3. Build against their OpenAPI surface and use their TradeZero connection type for mutual users.

TradeZero does not issue self-service multi-user broker credentials from the Portal. Start with Connect Trade unless a TradeZero contact has already directed you to a direct OAuth program.

Direct TradeZero OAuth (this guide)

Use the rest of this guide only when TradeZero has provisioned your platform with partner client_id / client_secret credentials and a partner login URL. That is a coordinated onboarding path for selected programs - not the default for new third-party apps.

Under direct OAuth, users sign in on a TradeZero-hosted authorization page; your backend receives scoped, expiring access tokens. You never handle their password, multi-factor authentication, or a long-lived API secret.

If you are integrating your own account, use Authentication for REST and WebSocket keys, or TradeZero MCP for assistant-driven workflows - not this partner flow.

Partner credentials required

Direct OAuth uses a client_id and client_secret issued by TradeZero during partner onboarding. There is no self-service registration, and you cannot generate these credentials in the Portal. Use Connect Trade unless TradeZero has specified a direct program.

Plan for access-token lifetime

Access tokens expire. Read expires_in on every token response, implement refresh, and confirm with your TradeZero contact during onboarding that refresh is enabled for your program. Schedule renewal from expires_in - do not wait for a failed API call.

  • Third-party platforms should use Connect Trade. Direct partner credentials (client_id / client_secret) come from TradeZero onboarding only when TradeZero specifies a direct program - not from the Portal API-key flow.

  • If you are on a direct OAuth program, implement refresh and schedule renewal from each token's expires_in. Confirm refresh enablement with your TradeZero contact before you rely on silent renewals in production.

Who this is for

PathWhen to use it
Connect Trade (chosen partner)Third-party platforms connecting TradeZero customer accounts - journals, analytics, multi-broker apps - unless TradeZero specifies a direct program
Direct TradeZero OAuth (this guide)TradeZero has provisioned your product with partner credentials and a partner login URL
API keysTools for your own account only (script, spreadsheet, personal dashboard)
TradeZero MCPYour own account through an AI assistant (OAuth sign-in - not partner OAuth)

Third-party products - charting and research platforms, trade journals, options analytics, portfolio tools - begin with Connect Trade.

Why platforms cannot use API keys

Do not ask your users to paste a TradeZero API secret into your product. A platform that collects them ends up holding many live, non-expiring, fully privileged trading credentials - each customer's own key, not scoped to your app, hard to revoke for one integration without breaking everything else that uses that key, and able to do anything the customer could do.

Customer-account connectivity uses OAuth-style login instead: either through Connect Trade or through a TradeZero-hosted direct partner login when TradeZero provisions your program. Disconnect and revocation are coordinated through that path rather than by collecting Portal secrets.

Actors and terminology

TermMeaning
Resource ownerThe TradeZero customer connecting their account
Client / partnerYour platform
Authorization serverTradeZero-hosted partner login (path or subdomain - use the URL you were issued)
Resource serverThe TradeZero REST API
Client IDPublic identifier for your app (one for paper, one for live)
Client secretConfidential credential - keep it on your server only
Authorization codeOne-time value returned on redirect; exchange it promptly on your server
Access tokenBearer credential for API calls (JWT)
Refresh tokenObtains a new access token without another login
StateOpaque value you generate per request; returned unchanged (CSRF protection)

About the name "JWT exchange"

The token path is /v1/api/oauth2/jwtexchange. Despite the name, this is not RFC 7523 JWT bearer assertion. It is a standard RFC 6749 §4.1 authorization code exchange. The access and refresh tokens happen to be JWTs - do not use a JWT-assertion library against this endpoint.

Becoming a partner

Third-party platforms onboard with Connect Trade rather than requesting a direct TradeZero OAuth program. Direct credentials are issued only when TradeZero specifies a direct integration for your product.

When TradeZero does provision a direct partner integration, onboarding completes with:

ItemPurpose
Partner login URLThe authorize URL TradeZero issues for your program (path or subdomain form)
Demo client IDFor end users connecting a paper account
Live client IDFor end users connecting a live account
Client secretShared secret used with both client IDs
Registered redirect URIYour callback URL - must match on authorize and token exchange
Token endpoint URLConfirm the exact host and path for your program - see Step 4
Account entitlementAccess granted to each account that connects through your app - see Account entitlement
Egress IP registrationYour backend's production addresses, when your contact asks for them - see Preparing your servers

Your TradeZero contact provides the exact client_id strings, secret delivery method, redirect URI registration, and which accounts are entitled to connect through your platform.

Demo vs live client IDs

Use the client ID that matches the account the user is connecting:

Client IDAccounts that can complete login
DemoPaper accounts (portal IDs use a TZP prefix; do not treat the prefix as an API contract)
LiveLive brokerage accounts

If a user signs in with a paper account against the live client ID, the login page returns an error such as:

You must use a TradeZero Live account. Check your credentials and try again.

A live account against the demo client ID is rejected the same way. Route users to the authorize URL with the matching client ID - the same paper-vs-live split that API keys use on the Trading API.

Client ID values are partner-specific. Use only the IDs issued for your integration.

Account entitlement

Each end-user account must be granted access to your platform before it can complete login. This is a per-account permission applied on the TradeZero side - it is not something the customer can enable themselves, and it is not implied by having a funded or active account.

An account that has not been entitled reaches the login page normally and then fails authentication with Invalid Login Combination, even though the same credentials work elsewhere. Treat that as an entitlement question rather than a password problem.

During onboarding and pilot, send your TradeZero contact the account IDs you will use so they can be entitled ahead of time. Confirm with them how entitlement is applied to customer accounts as your integration scales.

A live account can also need platform selection enabled on the customer's TradeZero profile. If a user authorizes successfully but GET /v1/api/accounts returns an empty list, that indicates platform selection (or another provisioning issue on the live profile) - not the same failure mode as missing entitlement at login.

Preparing your servers

Token exchange and refresh run from your backend. During onboarding, TradeZero registers your production egress addresses when required so those calls are recognized. Confirm with your contact whether registration is required for your program, send every host that will call the token endpoint, and tell them when the set changes.

What to provideWhy
Production egress IP addressesWhen required for your program, so token calls from your servers are expected
A stable, identifying User-AgentLets TradeZero attribute and troubleshoot your traffic. Use a product identifier such as YourPlatform/1.0, and keep it consistent across deployments

Two practical notes:

  • Pace your requests. Spread scheduled refreshes rather than firing them all at once from a new IP range, and back off on retries instead of looping.
  • Keep the token request path clean. Prefer a direct HTTP client for token exchange and refresh. Intermediate wrappers (for example APM or tracing agents) can change headers or body encoding; if only token calls fail while the rest of your traffic is healthy, retry once without that wrapper.

Onboarding checklist

Before your first production login, confirm each of these with your TradeZero contact:

  • Issued partner login URL is live (path or subdomain - use exactly what you were given)
  • Issued token endpoint URL confirmed for your program
  • Demo and live client IDs issued, and you know which is which
  • Client secret delivered securely and stored server-side only
  • Redirect URI registered, matching byte for byte what you will send
  • Test accounts entitled for your platform - paper and live, as applicable
  • Production egress IPs registered, if your contact requires them
  • Stable User-Agent agreed for your server traffic
  • Authorization code exchange verified end to end
  • Refresh path implemented and verified with your TradeZero contact - see Step 7
  • Confirmed which API endpoints your tokens may call

Authorization flow

The diagram is the target integration shape. Verify refresh end to end with your TradeZero contact before you rely on silent renewals in production.

sequenceDiagram
participant User as EndUser
participant Partner as PartnerBackend
participant Auth as PartnerLogin
participant Token as TokenEndpoint
participant API as TradeZeroREST

User->>Partner: Clicks Connect
Partner->>User: Open authorize URL with state
User->>Auth: Username password MFA
Auth->>User: Redirect code and state
User->>Partner: Browser callback
Partner->>Token: POST authorization_code plus secret
Token->>Partner: access_token and refresh_token
Partner->>API: Authorization Bearer access_token
Partner->>Token: POST refresh_token when available

Step 1 - Authorization request

Open a browser tab to your partner login page with these query parameters:

https://partner.tradezero.com/{partner}
?redirect_uri=<url_encoded_redirect_uri>
&client_id=<client_id>
&response_type=code
&state=<unique_opaque_value>

Example (placeholders only):

https://partner.tradezero.com/yourpartner?redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&client_id=YOUR_CLIENT_ID&response_type=code&state=a7f3c9e1
ParameterRequiredNotes
redirect_uriYesURL-encode this value. It must match the URI registered for your client ID
client_idYesDemo client ID for paper; live client ID for live
response_typeYesAlways code
stateYesOpaque per-request value (random nonce or equivalent). Returned unchanged for CSRF checks
scopeNoReserved; granular scopes are not enforced
Use the login URL you were issued

New integrations are provisioned as a path under partner.tradezero.com - https://partner.tradezero.com/{partner}. Some earlier integrations were provisioned on a dedicated {partner}.tradezero.com subdomain and continue to work.

Use whichever URL TradeZero issued you, exactly as issued. Do not construct it by pattern-matching another partner's URL, and confirm with your contact before switching forms.

Step 2 - User authentication

TradeZero presents a hosted login page (username, password, and MFA). Credentials stay on TradeZero; your application never receives them.

Your application is not involved in this step and receives nothing until the redirect in step 3.

Step 3 - Redirect back to you

On success, TradeZero redirects the browser to your redirect_uri:

<redirect_uri>?code=<authorization_code>&state=<same_state>
ParameterNotes
codeAuthorization code - exchange it promptly on your server; treat a code as spent once you have POSTed it
stateMust match step 1. Reject the callback if it does not

Step 4 - Exchange the code for tokens

Send a back-channel POST from your server. Never put the client secret in a browser, mobile app, or other client-side code.

POST https://webapi.tradezero.com/v1/api/oauth2/jwtexchange
Accept: application/json
Confirm the token URL before you go live

The URL above is the published OpenAPI path on webapi.tradezero.com. Your TradeZero partner contact issues the authoritative token host and path for your program - use that value. Confirm it before you go live.

FieldValue
grant_typeauthorization_code
codeAuthorization code from step 3
client_idYour client ID (same environment as step 1)
client_secretYour client secret
redirect_uriMust decode to exactly the value used in step 1

Content-Type

Send a form body, not JSON. Start with application/x-www-form-urlencoded - that is what RFC 6749 §4.1.3 specifies and what most OAuth client libraries emit. The API Reference and samples also accept multipart/form-data with the same field names.

If one encoding fails with a JSON rejection from the token service, retry the same fields with the other before changing credentials. Prefer a normal Content-Length body; if chunked transfer encoding fails, retry with an explicit length.

Step 5 - Token response

200 OK, application/json:

Example only - expires_in differs per integration, read it from your own response
{
"token_type": "Bearer",
"access_token": "<jwt>",
"expires_in": 28800,
"refresh_token": "<jwt>"
}
FieldMeaning
token_typeBearer (compare case-insensitively)
access_tokenJWT for the Authorization header on REST calls
expires_inAccess token lifetime in seconds - read it from the response, do not copy the example
refresh_tokenJWT used to obtain a new access token
refresh_token_expires_inMay appear on some responses; do not require it
Do not hard-code token lifetimes

Access token lifetime is configured per partner integration, so the value differs between programs and can change. Always schedule renewal from the expires_in value in each response rather than from a constant in your code. For refresh error handling, see Step 7 - do not infer expiry from a non-JSON response.

Step 6 - Call the API

Authorization: Bearer <access_token>

Trading API base URL: https://webapi.tradezero.com

Your TradeZero partner contact confirms which endpoints and account actions your tokens may use (for example research and portfolio reads versus order placement). Do not assume the same surface as Portal API keys until that is confirmed for your program.

A token is scoped to the user, not to a single account. A customer may hold more than one account, so after connecting call:

GET https://webapi.tradezero.com/v1/api/accounts
Authorization: Bearer <access_token>

Let the user choose an account rather than assuming the first result. If the list comes back empty after a successful authorize, see Account entitlement - look at the platform-selection note there; that is provisioning, not an empty portfolio.

Confirm with your TradeZero contact how rate limits apply to your partner tokens. The published table is expressed per API key; your program may share that model or have different budgets. Limits matter most for long-lived sessions that renew tokens on a schedule.

Step 7 - Refresh the token

Use the same endpoint with a different grant. No user interaction is required.

FieldValue
grant_typerefresh_token
refresh_tokenRefresh token from the previous response
client_idYour client ID
client_secretYour client secret

When refresh succeeds, expect a token JSON body in the same general shape as step 5 (token_type, access_token, expires_in, refresh_token). Refresh before the access token expires. When a refresh returns an authentication error from the token service (JSON), send the user through the authorize flow again.

Confirm with your TradeZero contact during onboarding that grant_type=refresh_token is enabled for your program, and whether each successful refresh returns a new refresh token (and whether the previous one is invalidated), so you store tokens correctly.

After refresh is verified for your program, schedule silent renewals from expires_in. If a refresh response is not JSON from the token service, check the issued token URL and form fields, then contact TradeZero with the full response rather than rotating credentials on your own.

Parameter and response reference

Authorization endpoint

Published form (path under partner.tradezero.com):

GET https://partner.tradezero.com/{partner}

Use the login URL TradeZero issued for your program if it differs (including a partner subdomain).

ParameterRequiredDescription
client_idYesDemo or live partner client ID
redirect_uriYesURL-encoded callback; must match registration
response_typeYesAlways code
stateYesOpaque per-request value, returned unchanged
scopeNoReserved

Token endpoint

Published form:

POST https://webapi.tradezero.com/v1/api/oauth2/jwtexchange
Accept: application/json

Confirm the host and path for your program during onboarding.

Fieldauthorization_coderefresh_token
grant_typeRequiredRequired
codeRequired-
redirect_uriRequired-
refresh_token-Required
client_idRequiredRequired
client_secretRequiredRequired

Response fields

FieldType
token_typestring
access_tokenstring
expires_ininteger
refresh_tokenstring
refresh_token_expires_ininteger, when returned

Error responses

Successful exchange returns 200. Errors are not returned in the RFC 6749 §5.2 error / error_description shape, so an OAuth library that expects that format will not surface a useful message. Parse status and body yourself.

When the token service rejects the exchange, it returns 400 with a JSON body in this shape:

{
"statusCode": "BadRequest",
"message": "User was unable to be authorized. Contact TradeZero Support",
"detail": null
}

Build around these distinctions:

  • JSON 400 from the token service. The message is a generic rejection - it can cover several request problems (for example a bad or reused code, a redirect_uri mismatch, or credential issues). Do not branch on the text. Re-check fields against Step 4 or Step 7, then escalate with the full body if it still fails.
  • 403 / Access Denied (HTML body). Network-edge response for the calling address or client fingerprint - not a bad authorization code. Confirm egress registration and User-Agent with your contact (Preparing your servers).
  • 404, empty body, or any non-JSON body. Confirm the issued token URL, that you are POSTing a form body, and (for first exchange) that your egress addresses are registered if your contact required them. The same checks apply to both authorization_code and refresh_token. Then contact TradeZero with headers and the body verbatim.
  • Brief 403 on REST after a fresh token. Retry with short backoff, then escalate with the account ID and timestamp if it continues.

Code samples

Placeholders only - never commit real secrets. Samples use the published token URL; substitute the URL TradeZero confirmed for your program if it differs.

Authorization code exchange (cURL)

authorization_code - x-www-form-urlencoded
curl -sS -X POST 'https://webapi.tradezero.com/v1/api/oauth2/jwtexchange' \
-H 'Accept: application/json' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=YOUR_AUTHORIZATION_CODE' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'client_secret=YOUR_CLIENT_SECRET' \
--data-urlencode 'redirect_uri=https://app.example.com/callback'
authorization_code - multipart/form-data
curl -sS -X POST 'https://webapi.tradezero.com/v1/api/oauth2/jwtexchange' \
-H 'Accept: application/json' \
-F 'grant_type=authorization_code' \
-F 'code=YOUR_AUTHORIZATION_CODE' \
-F 'client_id=YOUR_CLIENT_ID' \
-F 'client_secret=YOUR_CLIENT_SECRET' \
-F 'redirect_uri=https://app.example.com/callback'

Refresh (cURL)

refresh_token - x-www-form-urlencoded
curl -sS -X POST 'https://webapi.tradezero.com/v1/api/oauth2/jwtexchange' \
-H 'Accept: application/json' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=YOUR_REFRESH_TOKEN' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'client_secret=YOUR_CLIENT_SECRET'
refresh_token - multipart/form-data
curl -sS -X POST 'https://webapi.tradezero.com/v1/api/oauth2/jwtexchange' \
-H 'Accept: application/json' \
-F 'grant_type=refresh_token' \
-F 'refresh_token=YOUR_REFRESH_TOKEN' \
-F 'client_id=YOUR_CLIENT_ID' \
-F 'client_secret=YOUR_CLIENT_SECRET'

Node.js (server-side)

URLSearchParams sends application/x-www-form-urlencoded with a correct Content-Length. To send multipart instead, build a FormData and pass it as body without setting Content-Type - the field names and values are unchanged.

tradezeroOAuth.mjs
// Prefer the token URL issued for your program if it differs from the published host.
const TOKEN_URL = 'https://webapi.tradezero.com/v1/api/oauth2/jwtexchange';

async function postToken(fields) {
const res = await fetch(TOKEN_URL, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(fields),
});

const raw = await res.text();

// Errors are not RFC 6749 shaped, and a non-JSON body did not come from the
// token service - keep the two cases distinguishable for support.
let parsed = null;
try {
parsed = JSON.parse(raw);
} catch {
/* left null on purpose */
}

if (!res.ok || !parsed) {
const err = new Error(`TradeZero token request failed: ${res.status}`);
err.status = res.status;
err.body = raw;
err.fromTokenService = parsed !== null;
throw err;
}
return parsed;
}

export function exchangeAuthorizationCode({code, clientId, clientSecret, redirectUri}) {
return postToken({
grant_type: 'authorization_code',
code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri,
});
}

export function refreshAccessToken({refreshToken, clientId, clientSecret}) {
return postToken({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: clientId,
client_secret: clientSecret,
});
}

Schedule the next renewal from the expires_in value returned alongside the token, not from a constant. After refresh is verified for your program, that renewal is a refresh-token call; otherwise guide the user through authorize again within the access-token window.

Token lifecycle

  • Persist expires_in alongside each access token and schedule renewal from that value - do not wait for a failed API call, and do not assume a fixed lifetime.
  • Store access and refresh tokens encrypted at rest, scoped to the individual end user.
  • Never log tokens or the client secret.
  • When a refresh returns an authentication error (JSON from the token service), send the user through the authorize flow again. For a 404, empty body, or any non-JSON response, confirm the issued token URL and form body, then see Troubleshooting and contact TradeZero rather than treating it as simple expiry.
  • Implement the refresh path as part of onboarding and verify it with your TradeZero contact before you schedule silent renewals in production.

Security requirements

RequirementDetail
Protect the client secretServer-side only - never in browsers, mobile binaries, or public repositories
Verify stateReject callbacks when state does not match
Exchange the code promptlyDo not hold codes; exchange on your server as soon as the callback arrives
Exchange server-sideThe token request carries your client secret
Match redirect_uri exactlySame value on authorize and token exchange
Store tokens securelyEncrypted, per user, never logged
HTTPS onlyRedirect URI and all API calls
Refresh proactivelyBefore the access token expires - once refresh is confirmed for your program

What TradeZero provides

  • Username, password, and MFA are entered only on a TradeZero-hosted page
  • The authorization code cannot be exchanged without your client secret
  • Tokens expire, limiting the window of any compromise

Troubleshooting

SymptomWhat to do
No client_id / cannot registerPartner credentials come from TradeZero onboarding - not the Portal. For your own account, use Authentication
"You must use a TradeZero Live account"The authorize URL used the live client_id with a paper login. Use the demo client ID for paper (TZP…) accounts
Live account rejected on loginYou are using the demo client_id. Switch to the live client ID
"Invalid client ID."The client_id in the authorize URL is not recognized for this partner path. Check it against the exact strings issued for your integration - do not infer them from other environments
"Invalid Login Combination" with credentials that work elsewhereThe account is not entitled for your platform. Send the account ID to your TradeZero contact
Token exchange fails (400 JSON)Generic token-service rejection - see Error responses. Re-check form fields before rotating credentials
Token exchange returns 404 / non-JSONConfirm the issued token URL, form body, active client IDs, and egress registration if required; escalate with full response headers
Requests rejected with Access Denied / 403 HTMLNetwork-edge response for the calling address or client fingerprint. Confirm egress IP registration (if required) and a stable User-Agent - see Preparing your servers
Authenticated but zero accountsAfter a successful authorize, platform selection / live-profile provisioning - see Account entitlement. Contact TradeZero with the account ID
Brief 403 after a fresh tokenRetry with short backoff, then contact TradeZero if it continues
Refresh rejected with an authentication error (JSON)Treat as expiry or supersession. Send the user through the authorize flow again
Refresh returns a 404, empty body, or non-JSON responseConfirm the issued token URL and form fields first. Do not assume simple expiry - contact TradeZero with the timestamp and your request details
Wrong login URLUse the exact partner login URL issued to you during onboarding - see Step 1

Getting support

Reach your TradeZero partner contact or the shared channel set up for your integration. Partner OAuth issues are not handled through general customer support, which cannot see your client configuration.

Include the following so TradeZero can trace the request:

IncludeWhy it helps
Timestamp in UTC, with the timezone statedLogs are searched by time
Your correlation or request ID, if your client generates oneThe fastest way to find a single request
The client_id used, and whether the account is paper or liveNarrows configuration immediately - never send the client secret
The account ID or login involvedEntitlement and provisioning questions are answered per account
The exact URL, method, and Content-Type you sentDistinguishes a request-shape problem from a credential one
The full response headers, not just the status codeIdentifies where a request stopped
The response body verbatim, including non-JSON bodiesA non-JSON body is itself the diagnostic signal
Whether it fails always or intermittently, and from which environmentsSeparates configuration problems from network-path ones

Redact the client secret, access tokens, refresh tokens, and authorization codes. Token length and prefix are fine to share.

If the failure appeared without a deployment on your side, say so - that helps TradeZero focus the investigation.

Partner model

TopicBehavior
Chosen broker-connectivity partnerConnect Trade - use this for third-party platforms unless TradeZero specifies a direct program
Direct registrationManual onboarding with TradeZero - not self-service
ScopesThe scope parameter is reserved; permissions are defined for your partner program, not per-request scopes
Client typeConfidential server-side clients (PKCE is not required for this model)
DiscoveryNo .well-known metadata document - use the endpoints issued for your program
Disconnect / revokeCoordinate account disconnect and partner-wide revocation with TradeZero
RefreshUse grant_type=refresh_token for long-lived sessions - confirm enablement for your program during onboarding; see Step 7