Skip to main content

Members

These routes administer funded accounts and Connect flows. Use your partner token here. Use the resulting member token on the Trading API and Management API for that user's trading data and automation.

The base URL is https://app.crosstrade.io/v1/api/partner. Request bodies are JSON (UTF-8, at most 64 KiB); responses contain success, the resource or error, and meta.requestId. Do not send a body on reads or authorization-code pickup requests.

RouteScopePurpose
POST /v1/api/partner/userspartner:provisionCreate a member or return the current state of an existing external_id. Requires Idempotency-Key.
GET /v1/api/partner/userspartner:readKeyset-paginated list, newest first. Optional limit (default 50, max 200) and cursor.
GET /v1/api/partner/users/{external_id}partner:readStatus read with an ETag (the membership version). Never returns a code.
POST /v1/api/partner/users/{external_id}/connect-linkpartner:provisionA new Connect link, purpose: authorize (fresh consent) or relink (broker re-link). Requires Idempotency-Key. Earlier unfinished flows of the same purpose are invalidated, including unexchanged codes.
POST /v1/api/partner/users/{external_id}/authorizations/{flow_id}/codepartner:provisionPickup delivery: the authorization code while it is unexpired and unexchanged.
POST /v1/api/partner/users/{external_id}/revokepartner:provisionEnd the seat and every credential your app holds. Idempotent. Allowed during billing suspension.
POST /v1/api/partner/users/{external_id}/reopenpartner:provisionStart a new seat for a revoked member. Requires If-Match with the current version and an Idempotency-Key.
GET /v1/api/partner/mepartner:readYour public policy, environments, seat cap, limits and service status.
GET /v1/api/partner/brokerspartner:readBroker link status for each member's shared Tradovate logins. Optional limit and cursor, like the member list.
GET /v1/api/partner/users/{external_id}/brokerspartner:readThe same for one member.

Select a route for its complete endpoint definition and code examples. GET Events and GET Usage complete the Partner endpoint reference.

Creating a member​

POST /v1/api/partner/users
Authorization: Bearer <PARTNER_ACCESS_TOKEN>
Content-Type: application/json
Idempotency-Key: create-th_8831-1

{
"external_id": "th_8831",
"email": "member@example.com",
"name": "Jamie Rivera",
"environments": [
"demo"
],
"authorization": {
"scope": "tv:trade manage:read",
"redirect_uri": "https://partner.example/oauth/callback",
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"code_challenge_method": "S256",
"state": "xyz",
"delivery": "pickup"
}
}
  • New email: 201 with the member (status: provisioned, ownership_verified: false, seat start, version: 1) and a connect object with flow_id, purpose, the one-time url and its expires. Billing for the seat starts now.
  • Existing CrossTrade email: 409 email_in_use, nothing written. A member never signs in to CrossTrade, so an address that already has its own CrossTrade account cannot be enrolled; ask your customer for a different address.
  • Same external_id again: the current state (200) and connect: null, unless the same Idempotency-Key replays the original response (Idempotent-Replayed: true).
  • Same external_id, different email or name: 409 external_id_conflict.

Repeating create after a revoke returns the revoked member; use reopen to start a new seat.

Request fields​

FieldRequiredRules
external_idYesYour permanent member id: 1–64 letters, digits, periods, underscores or hyphens
emailYesA new CrossTrade email address; trimmed and lowercased
nameNoDisplay name, 1–120 characters when supplied
environmentsNoNonempty, unique environment names (demo, live) within your partner profile; defaults to that profile. This records the enrollment request; runtime access also checks partner policy and consent
metadataNoJSON object, at most 1,024 bytes when serialized; returned with the member
authorizationYesThe OAuth request described below
Authorization fieldRequiredRules
scopeNoSpace-separated member scopes; defaults to your client's member-scope ceiling
redirect_uriYesExact registered HTTPS callback, including for pickup delivery
code_challengeYesBase64url-encoded SHA-256 digest of your PKCE verifier, without padding (43 characters)
code_challenge_methodNoOnly S256; the default when omitted
stateFor redirectNonempty for redirect delivery, at most 256 characters; generate and retain a random value per authorization
deliveryNoredirect or pickup. Omitted, the code delivery set on your API client under API Keys applies (redirect until you change it)
resourcesNoArray of exact absolute Trading and/or Management resource URLs to narrow the requested audiences

Unknown fields are rejected. See Partner Authentication for scope and resource rules and the code guide for verifier generation.

Status model​

Track membership, authorization and broker connection health separately:

  • Membership: provisioned, connected, revoked.
  • Authorization (latest authorization flow): not_started, pending, code_ready, exchanged, denied, expired, revoked.
  • Broker (separate broker-status endpoint, per consented identity): unlinked, ready, reauth_required, unknown. Hosted Connect can also show sync_pending while initially syncing an identity.

connected means the member approved an authorization; it does not prove a token or broker session is still usable. Trade only with a valid member token and current consent. Older enrollment records can also expose pending_link or expired; new create requests do not enroll existing CrossTrade accounts.

Revoke reasons​

POST /v1/api/partner/users/{external_id}/revoke requires a reason from a fixed list; free text is refused with 400 validation_failed.

ReasonUse it when
subscription_canceledThe member ended their subscription with you
payment_failedTheir payment to you failed and you closed the seat
member_requestThe member asked you to disconnect
partner_terminatedYou ended the relationship
abuseAbuse or a policy violation
otherAnything else

The reason is recorded and included in the member.revoked event; it is not a field on the public member object. The seat closes immediately, but any billable portion of that UTC day still counts as a seat-day. The person receives a release email from CrossTrade explaining how to keep using the account on their own.

Accounts you create are managed accounts

While a seat you created is open, CrossTrade sends that person no email beyond security notices, and the account cannot sign in to CrossTrade directly: the Connect link is their only access. Support for your customers is yours. When the seat ends the account becomes an ordinary account that the person controls (Free, unless they bought a plan of their own); CrossTrade tells them how to set a password. If you reopen the seat later, any CrossTrade login they created in between stops working.

Concurrency and replay​

  • Idempotency-Key is required on create, connect-link and reopen: 1–200 printable ASCII characters without spaces. The same key and request replay the recorded result for 24 hours; changing the request is 409 idempotency_conflict, and an expired key is 409 idempotency_key_expired. A replay returns connect: null after the original link's 10-minute lifetime. Request a new link with a new key when needed; replay does not extend its expiry.
  • GET Member returns the current ETag, for example ETag: "v3"; reopen requires that value in If-Match and answers 412 precondition_failed when the member changed, 428 when the header is missing.
  • GET /v1/api/partner/users pages with cursor; order is creation time descending, then external_id descending. Omit cursor on the first request.

Your service state gates the control plane: an active partner may use all routes. A past-due partner within grace may read, revoke and request connect links, but cannot create, reopen or pick up authorization codes. A suspended partner may read and revoke only; a disabled partner may only revoke, subject to the credential still being valid. A provisioning partner may read only.

Responses​

Every success contains success, the resource at the top level, and meta.requestId. Every response carries X-Request-Id, Cache-Control: no-store and Pragma: no-cache. Create, individual member reads, revoke and reopen return the membership ETag and meta.etag when a version exists; list and connect-link responses do not. Timestamps are UTC, YYYY-MM-DDTHH:MM:SSZ. Failures use the shape on the Errors page.

The member object​

Returned by create, individual member reads, connect-link, revoke and reopen under member, or in members on the list. Code pickup and broker-status routes have separate shapes below.

FieldTypeMeaning
external_idstringYour id for the member
uidstring or nullThe CrossTrade account id; null while pending_link
emailstringThe address you enrolled, normalized (trimmed, lowercased)
namestring or nullThe display name you gave
statusstringpending_link, provisioned, connected, revoked or expired (see the status model above)
ownership_verifiedbooleanThe member redeemed the private Connect link; this is possession-of-link proof, not separate email-code verification
versioninteger or nullThe membership version; the same number as the ETag. null while pending_link
environmentsarray of stringSorted environment names recorded at enrollment
metadataobjectThe object you stored at creation, or an empty object
seatobject or nullstarted and ended timestamps; ended is null while the seat is open. null while pending_link
createdstringWhen the enrollment was created
authorizationobjectThe latest authorize flow: status, flow_id, purpose and generation. With no flow, only status: not_started and flow_id: null are returned. A relink does not replace this authorization summary
grantobject or nullversion and active of the member's consent to your app; null when no grant is included. active: false means the grant was revoked, for example by a disconnect or seat revocation
events_high_waterintegerIncluded inside member on an individual status read; the event sequence captured before reading that member. The list returns its corresponding high-water mark at the top level

POST /v1/api/partner/users​

201 Created, a new email, the seat opens now. Headers: ETag: "v1".

{
"success": true,
"member": {
"external_id": "th_8831",
"uid": "k3Qz9pLa",
"email": "member@example.com",
"name": "Jamie Rivera",
"status": "provisioned",
"ownership_verified": false,
"version": 1,
"environments": [
"demo"
],
"metadata": {},
"seat": {
"started": "2026-10-15T14:02:11Z",
"ended": null
},
"created": "2026-10-15T14:02:11Z",
"authorization": {
"status": "pending",
"flow_id": "pcf_a1b2c3d4e5f6",
"purpose": "authorize",
"generation": 1
},
"grant": null
},
"connect": {
"flow_id": "pcf_a1b2c3d4e5f6",
"purpose": "authorize",
"url": "https://app.crosstrade.io/v1/connect/xt_tk_...",
"expires": "2026-10-15T14:12:11Z"
},
"meta": {
"requestId": "preq_7d8f159d49d107bb",
"etag": "\"v1\""
}
}

200 OK, the external_id already exists: its current state, and connect is null. A replay with the original Idempotency-Key retains the recorded status and member snapshot and adds Idempotent-Replayed: true; its connect becomes null after the link expires.

{
"success": true,
"member": {
"external_id": "th_8831",
"uid": "k3Qz9pLa",
"email": "member@example.com",
"name": "Jamie Rivera",
"status": "connected",
"ownership_verified": true,
"version": 1,
"environments": [
"demo"
],
"metadata": {},
"seat": {
"started": "2026-10-15T14:02:11Z",
"ended": null
},
"created": "2026-10-15T14:02:11Z",
"authorization": {
"status": "exchanged",
"flow_id": "pcf_a1b2c3d4e5f6",
"purpose": "authorize",
"generation": 1
},
"grant": {
"version": 1,
"active": true
}
},
"connect": null,
"meta": {
"requestId": "preq_3c1d9e8f7a6b5c40",
"etag": "\"v1\""
}
}

GET /v1/api/partner/users​

members holds member objects ordered by creation time descending, then external_id descending. next_cursor is an opaque string, null on the last page; pass it back as cursor. To recover an expired event cursor, capture events_high_water from the first list page, reconcile all pages, then resume events from that captured value.

{
"success": true,
"members": [
{
"external_id": "th_8831",
"uid": "k3Qz9pLa",
"email": "member@example.com",
"name": "Jamie Rivera",
"status": "connected",
"ownership_verified": true,
"version": 1,
"environments": [
"demo"
],
"metadata": {},
"seat": {
"started": "2026-10-15T14:02:11Z",
"ended": null
},
"created": "2026-10-15T14:02:11Z",
"authorization": {
"status": "exchanged",
"flow_id": "pcf_a1b2c3d4e5f6",
"purpose": "authorize",
"generation": 1
},
"grant": {
"version": 1,
"active": true
}
},
{
"external_id": "th_8832",
"uid": "m8Qz1pLb",
"email": "second@example.com",
"name": "Sam Lee",
"status": "revoked",
"ownership_verified": true,
"version": 2,
"environments": [
"demo"
],
"metadata": {
"plan": "pro"
},
"seat": {
"started": "2026-10-15T13:20:40Z",
"ended": "2026-10-16T09:02:13Z"
},
"created": "2026-10-15T13:20:40Z",
"authorization": {
"status": "revoked",
"flow_id": "pcf_f6e5d4c3b2a1",
"purpose": "authorize",
"generation": 1
},
"grant": {
"version": 2,
"active": false
}
}
],
"next_cursor": null,
"as_of": "2026-10-16T10:00:00Z",
"events_high_water": 42,
"meta": {
"requestId": "preq_9a8b7c6d5e4f3a21"
}
}

GET /v1/api/partner/users/{external_id}​

Headers: ETag: "v1". Never contains a code or a Connect URL.

{
"success": true,
"member": {
"external_id": "th_8831",
"uid": "k3Qz9pLa",
"email": "member@example.com",
"name": "Jamie Rivera",
"status": "connected",
"ownership_verified": true,
"version": 1,
"environments": [
"demo"
],
"metadata": {},
"seat": {
"started": "2026-10-15T14:02:11Z",
"ended": null
},
"created": "2026-10-15T14:02:11Z",
"authorization": {
"status": "exchanged",
"flow_id": "pcf_a1b2c3d4e5f6",
"purpose": "authorize",
"generation": 1
},
"grant": {
"version": 1,
"active": true
},
"events_high_water": 42
},
"meta": {
"requestId": "preq_5e4d3c2b1a0f9e87",
"etag": "\"v1\""
}
}

POST /v1/api/partner/users/{external_id}/connect-link​

201 Created with the member and a new connect object; generation counts up per purpose. Requires Idempotency-Key. Earlier unfinished flows of the same purpose are closed. With purpose: relink, omit authorization; the flow repairs an identity in the standing grant and does not issue new OAuth tokens.

{
"success": true,
"member": {
"external_id": "th_8831",
"uid": "k3Qz9pLa",
"email": "member@example.com",
"name": "Jamie Rivera",
"status": "connected",
"ownership_verified": true,
"version": 1,
"environments": [
"demo"
],
"metadata": {},
"seat": {
"started": "2026-10-15T14:02:11Z",
"ended": null
},
"created": "2026-10-15T14:02:11Z",
"authorization": {
"status": "pending",
"flow_id": "pcf_1122334455aa",
"purpose": "authorize",
"generation": 2
},
"grant": {
"version": 1,
"active": true
}
},
"connect": {
"flow_id": "pcf_1122334455aa",
"purpose": "authorize",
"url": "https://app.crosstrade.io/v1/connect/xt_tk_...",
"expires": "2026-10-16T09:10:00Z"
},
"meta": {
"requestId": "preq_2f3e4d5c6b7a8900"
}
}

POST /v1/api/partner/users/{external_id}/authorizations/{flow_id}/code​

202 Accepted while the member has not allowed your app:

{
"success": true,
"authorization": {
"status": "pending",
"flow_id": "pcf_a1b2c3d4e5f6"
},
"meta": {
"requestId": "preq_1a2b3c4d5e6f7a80"
}
}

200 OK with the code. Pickup is repeatable until exchange or expiry; exchanging the code is single-use. expires is the code's deadline (300 seconds from issue), and the flow must also remain open:

{
"success": true,
"authorization": {
"status": "code_ready",
"flow_id": "pcf_a1b2c3d4e5f6",
"code": "xt_ac_...",
"expires": "2026-10-15T14:13:52Z"
},
"meta": {
"requestId": "preq_8f7e6d5c4b3a2910"
}
}

200 OK after the code was exchanged, without credentials:

{
"success": true,
"authorization": {
"status": "exchanged",
"flow_id": "pcf_a1b2c3d4e5f6"
},
"meta": {
"requestId": "preq_0a9b8c7d6e5f4a31"
}
}

A flow that expired, was denied, was revoked or was replaced by a newer link answers 410 flow_closed; a flow that is not yours, not this member's or not pickup delivery answers 404 not_found.

POST /v1/api/partner/users/{external_id}/revoke​

200 OK. already_revoked is false on the call that ended the seat and true on repeats. The first revoke increments the membership version (here, from 1 to 2); repeats keep that version. Use the returned ETag for a later reopen. An exchanged authorization flow remains exchanged in the summary even after revocation; the grant becomes inactive.

{
"success": true,
"member": {
"external_id": "th_8831",
"uid": "k3Qz9pLa",
"email": "member@example.com",
"name": "Jamie Rivera",
"status": "revoked",
"ownership_verified": true,
"version": 2,
"environments": [
"demo"
],
"metadata": {},
"seat": {
"started": "2026-10-15T14:02:11Z",
"ended": "2026-11-02T09:30:00Z"
},
"created": "2026-10-15T14:02:11Z",
"authorization": {
"status": "exchanged",
"flow_id": "pcf_a1b2c3d4e5f6",
"purpose": "authorize",
"generation": 1
},
"grant": {
"version": 2,
"active": false
}
},
"already_revoked": false,
"meta": {
"requestId": "preq_6b5a4938271605f4",
"etag": "\"v2\""
}
}

POST /v1/api/partner/users/{external_id}/reopen​

200 OK: the version goes up by one (2 to 3 here), a new seat starts, and a new Connect flow is issued because the member must allow your app again. Headers: ETag with the new version. A member who acquired their own paid CrossTrade plan after release may be ineligible for reopening (409 existing_account_ineligible).

{
"success": true,
"member": {
"external_id": "th_8831",
"uid": "k3Qz9pLa",
"email": "member@example.com",
"name": "Jamie Rivera",
"status": "provisioned",
"ownership_verified": true,
"version": 3,
"environments": [
"demo"
],
"metadata": {},
"seat": {
"started": "2026-11-20T08:00:00Z",
"ended": null
},
"created": "2026-10-15T14:02:11Z",
"authorization": {
"status": "pending",
"flow_id": "pcf_99887766ccdd",
"purpose": "authorize",
"generation": 3
},
"grant": null
},
"connect": {
"flow_id": "pcf_99887766ccdd",
"purpose": "authorize",
"url": "https://app.crosstrade.io/v1/connect/xt_tk_...",
"expires": "2026-11-20T08:10:00Z"
},
"meta": {
"requestId": "preq_c0ffee0123456789",
"etag": "\"v3\""
}
}

GET /v1/api/partner/brokers​

The stored connection state of every login your members shared with you, using your partner token and without a live broker request. brokers lists only identities in an active grant. This is a connection-health summary, not a guarantee that a trade will pass scope, environment, account or policy checks. Unlike this filtered summary, the Trading API's aggregate /accounts route requires consent covering all linked identities.

brokers[].statusMeaningWhat to do
readyThe login is linked and its session is currentNothing
reauth_requiredCrossTrade recorded a confirmed broker-session failure; reason can be expired or evicted. Broker access needs reauthorizationSend the managed member a relink Connect link
unknownA session exists but is past its expiry and no failure has been confirmed: renewal is pendingRead again shortly
unlinkedThe member removed that loginAsk them to link it again (relink) if your service needs it

all_ready is true only when the member has at least one shared login and every one is ready. Connection changes also produce broker.reauth_required, broker.ready and broker.unlinked events. Use the feed or webhooks for updates and this endpoint to reconcile current state.

{
"success": true,
"members": [
{
"external_id": "th_8831",
"uid": "k3Qz9pLa",
"status": "connected",
"grant": {
"version": 1,
"active": true
},
"brokers": [
{
"environment": "demo",
"user_id": "4815162",
"name": "jamie_r",
"status": "reauth_required",
"reason": "expired",
"expires": "2026-10-15T20:00:00Z",
"linked": "2026-10-15T14:08:40Z"
},
{
"environment": "demo",
"user_id": "4815163",
"name": "jamie_r2",
"status": "ready",
"reason": null,
"expires": "2026-10-16T09:12:00Z",
"linked": "2026-10-15T14:09:02Z"
}
],
"all_ready": false
},
{
"external_id": "th_8832",
"uid": "m8Qz1pLb",
"status": "revoked",
"grant": {
"version": 2,
"active": false
},
"brokers": [],
"all_ready": false
}
],
"next_cursor": null,
"as_of": "2026-10-16T01:00:00Z",
"meta": {
"requestId": "preq_7e6d5c4b3a291807"
}
}

GET /v1/api/partner/users/{external_id}/brokers answers one such record under member, or 404 member_not_found.

GET /v1/api/partner/me​

Your service as the control plane sees it. status is the policy status (provisioning before activation, then active, past_due, suspended or disabled); policy says what that status allows right now; limits are your per-partner budgets.

{
"success": true,
"partner": {
"id": "p_8f1c2d3e",
"slug": "acme_signals",
"name": "Acme Signals, LLC",
"status": "active",
"environments": [
"demo"
],
"seat_cap": 500,
"open_seats": 12,
"limits": {
"requests_per_minute": 600,
"reduction_requests_per_minute": 120,
"max_inflight": 4,
"max_identities_per_member": 2,
"max_accounts_per_member": 10,
"max_automation_per_member": 5
},
"stripe_mode": "live",
"live_enabled": false,
"billing": {
"status": "active",
"verified": true,
"grace_expires_at": null
},
"policy": {
"allows_new_seats": true,
"allows_entries": true,
"allows_reads": true
}
},
"meta": {
"requestId": "preq_4d3c2b1a09f8e7d6"
}
}

The limits above are illustrative. Use your actual /me response; budgets are shared across your partner's tokens and requests can have different costs.