Skip to main content

Partner Authentication

Partner authentication uses OAuth client credentials for your service and authorization code with PKCE for each member. Both use the same confidential OAuth client. Token requests are form-encoded POSTs to https://app.crosstrade.io/v1/oauth/token; responses are JSON with Cache-Control: no-store.

The resulting member token is what opens the existing Trading and Management APIs for that user. Your partner token handles onboarding and seat administration. Keep the two token types separate in your application.

How the authentication flow works​

Two credentials serve different purposes:

CredentialHow you get itWhat it opensLifetime
Partner token xt_at_...client_credentials with your client id and secret/v1/api/partner/* (members, events, usage, your policy)1 hour, request a new one
Member token xt_at_... plus refresh token xt_rt_...authorization_code with PKCE, after the member allows your app on hosted Connect/v1/api/tv/* and /v1/api/manage/* for that member, within what they approved1 hour; refresh for up to 90 days
Eight-step Partner API flow. Your backend gets a partner token and creates a funded member. The member follows the private Connect link, accepts terms, links Tradovate and allows your app. Your backend picks up the authorization code and exchanges it with the PKCE verifier for that member's access and refresh tokens. Store the pair by external_id and use the member access token directly on the Trading and Management APIs. Keep using the partner token for lifecycle events and seat revocation.

Blue: your service's partner credentials. Gold: the member's hosted consent. Green: that member's API access. Numbers match the code guide walkthrough. Open full-size diagram.

Three rules apply to every integration:

  • The client authenticates with HTTP Basic on every token request. A secret in the form body is refused with invalid_client.
  • Member tokens exist only because the member pressed Allow on a page CrossTrade hosts. Your app never sees a CrossTrade password, and it cannot mint a member token from the partner token.
  • Every token is bound to an audience. A partner token on /v1/api/tv and a member token on /v1/api/partner both answer 401.

Partner tokens (your service)​

Your confidential client authenticates with HTTP Basic (client_secret_basic) and requests the client_credentials grant:

POST /v1/oauth/token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=partner:read%20partner:provision&resource=https://app.crosstrade.io/v1/api/partner
{
"access_token": "xt_at_...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "partner:read partner:provision"
}

Partner tokens live one hour, carry only the partner audience and are used as Authorization: Bearer xt_at_... on /v1/api/partner/*. Request another client-credentials token when needed; this grant does not issue a refresh token. Client secrets rotate with a planned overlap (both secrets valid for up to 24 hours) or, in an emergency, immediately. Emergency rotation invalidates tokens issued under the old client version and stops your app's copiers until explicitly re-enabled under current authority. Protective monitors continue enforcing their limits.

Member tokens (your app, acting for a member)​

Member tokens come only from the hosted Connect flow. You never see the member's password. The flow is authorization_code with PKCE (S256 only):

  1. Create the member (or request a connect-link) with your PKCE code_challenge, the scope you want, your registered redirect_uri, a state and the delivery mode.
  2. The member completes the hosted page. With delivery: redirect the code arrives at your redirect_uri with state and iss; with delivery: pickup you fetch it with POST /v1/api/partner/users/{external_id}/authorizations/{flow_id}/code using your partner token.
  3. Exchange the code within 300 seconds:
The redirect URI is a page of your app

Register it under Partner Platform, API Keys: an HTTPS page on your own domain, never a CrossTrade address. With delivery: redirect the member's browser lands there carrying the code; with delivery: pickup nobody visits it, but the code is still bound to it and the exchange below must send the same value. Hosted Connect has the full comparison.

POST /v1/oauth/token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=xt_ac_...&code_verifier=...&redirect_uri=https://partner.example/oauth/callback
{
"access_token": "xt_at_...",
"refresh_token": "xt_rt_...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "tv:read tv:trade manage:read"
}

Scopes​

Use a member access token from the exchange above directly on either approved API:

GET /v1/api/tv/accounts/DEMO12345678/positions
Authorization: Bearer <MEMBER_ACCESS_TOKEN>
GET /v1/api/manage/tradovate/monitors
Authorization: Bearer <MEMBER_ACCESS_TOKEN>

The member is selected by the token, and the account in the first URL must belong to an identity that member shared. See the code guide for runnable examples.

ScopeAudienceGrants
partner:read/v1/api/partnerRead members, usage, events, your policy
partner:provision/v1/api/partnerCreate, link, revoke and reopen members (includes read)
tv:read/v1/api/tvRead the member's consented Tradovate identities
tv:trade/v1/api/tvPlace, change and cancel orders there (includes tv:read)
manage:read/v1/api/manageRead the member's Tradovate monitors, copiers and ATM templates
manage:write/v1/api/manageCreate and change the automation your app owns (includes manage:read)

A member token cannot exceed your client's registered member scopes, the authorization request or the member's approval. Read the returned scope rather than assuming every requested permission was granted.

Omit resource at code exchange to keep the audiences covered by the approved scopes. To narrow access, send an exact, absolute resource URL: https://app.crosstrade.io/v1/api/tv or https://app.crosstrade.io/v1/api/manage. Scopes for excluded audiences are dropped, and refreshing cannot restore that access. The authorization request can also narrow audiences through its resources array.

For partner tokens, scope=partner:read gives read-only access; an unregistered scope is invalid_scope. The only valid partner resource is https://app.crosstrade.io/v1/api/partner. Starting a monitor or copier needs manage:write and tv:trade on a member token carrying both Management and Trading audiences.

Refresh rotation​

POST /v1/oauth/token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=xt_rt_...
{
"access_token": "xt_at_...",
"refresh_token": "xt_rt_...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "tv:read tv:trade manage:read"
}

Every successful rotation returns a new pair, retires the presented refresh token and deactivates its associated access token. Serialize refreshes per member and atomically replace the stored pair before using it.

An identical refresh request retried less than 30 seconds after rotation returns the same token pair only while its successor refresh token is still active. If that successor was already used, the retry returns invalid_grant; it does not follow the chain to the newest pair. Reusing a retired refresh token at or after 30 seconds revokes the whole family. On invalid_grant, stop retrying that token and start a new authorization if you have no current pair. Families expire at most 90 days after issuance; rotation does not extend that deadline. Use expires_in for the access-token lifetime, which can be shorter near that deadline.

Revocation​

POST /v1/oauth/revoke uses HTTP Basic client authentication and a form-encoded token parameter. An access token revokes that access token only. A refresh token revokes its entire family, including access tokens in that family. Unknown or already-revoked tokens also return 200 with an empty object:

{}

Token revocation does not end a funded seat. Use the member revoke endpoint to end funding and revoke the member's grant and credentials. Managed members request this through your service because they cannot sign in to My Account while their seat is open. A user with ordinary dashboard access can also disconnect a connected app there.

Grant revocation stops your app's copiers; protective monitors keep enforcing their limits. Refresh-family revocation emits authorization.family_revoked; a dashboard disconnect emits authorization.revoked. Revoking a seat emits member.revoked and also ends the grant. Revoking a single access token does not emit those grant or family events.

Failures​

The token endpoint answers failures in the RFC 6749 shape; a 401 adds WWW-Authenticate: Basic realm="crosstrade":

{
"error": "invalid_client",
"error_description": "client authentication method mismatch"
}
StatuserrorMeaning
401invalid_clientUnknown client, wrong secret, or the secret was sent in the form body instead of the Basic header
400invalid_grantThe code is expired, used, or bound to another client or redirect_uri; the PKCE verifier does not match; the refresh token is retired or its family is revoked
400invalid_scopeA scope the client is not registered for
400invalid_targetA resource the client may not address
400unauthorized_clientYour client is not registered for that grant type
400unsupported_grant_typeAnything but client_credentials, authorization_code and refresh_token
400invalid_requestA missing or malformed parameter; error_description names it
503temporarily_unavailableToken or policy storage is temporarily unavailable
500server_errorThe token request failed unexpectedly

Bearer failures on the APIs carry an RFC 6750 WWW-Authenticate challenge with the resource metadata URL. Common codes:

StatusCodeMeaning
401invalid_token / unauthorizedMissing, expired, revoked or wrong-audience token
403insufficient_scopeThe token lacks the scope this operation needs
403partner_suspended, partner_disabled, partner_not_activeYour service state does not allow this operation
503auth_unavailable, policy_unavailableCrossTrade could not verify the token; retry shortly