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.
| Route | Scope | Purpose |
|---|---|---|
POST /v1/api/partner/users | partner:provision | Create a member or return the current state of an existing external_id. Requires Idempotency-Key. |
GET /v1/api/partner/users | partner:read | Keyset-paginated list, newest first. Optional limit (default 50, max 200) and cursor. |
GET /v1/api/partner/users/{external_id} | partner:read | Status read with an ETag (the membership version). Never returns a code. |
POST /v1/api/partner/users/{external_id}/connect-link | partner:provision | A 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}/code | partner:provision | Pickup delivery: the authorization code while it is unexpired and unexchanged. |
POST /v1/api/partner/users/{external_id}/revoke | partner:provision | End the seat and every credential your app holds. Idempotent. Allowed during billing suspension. |
POST /v1/api/partner/users/{external_id}/reopen | partner:provision | Start a new seat for a revoked member. Requires If-Match with the current version and an Idempotency-Key. |
GET /v1/api/partner/me | partner:read | Your public policy, environments, seat cap, limits and service status. |
GET /v1/api/partner/brokers | partner:read | Broker link status for each member's shared Tradovate logins. Optional limit and cursor, like the member list. |
GET /v1/api/partner/users/{external_id}/brokers | partner:read | The 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:
201with the member (status: provisioned,ownership_verified: false, seat start,version: 1) and aconnectobject withflow_id,purpose, the one-timeurland itsexpires. 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_idagain: the current state (200) andconnect: null, unless the sameIdempotency-Keyreplays 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
| Field | Required | Rules |
|---|---|---|
external_id | Yes | Your permanent member id: 1–64 letters, digits, periods, underscores or hyphens |
email | Yes | A new CrossTrade email address; trimmed and lowercased |
name | No | Display name, 1–120 characters when supplied |
environments | No | Nonempty, 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 |
metadata | No | JSON object, at most 1,024 bytes when serialized; returned with the member |
authorization | Yes | The OAuth request described below |
| Authorization field | Required | Rules |
|---|---|---|
scope | No | Space-separated member scopes; defaults to your client's member-scope ceiling |
redirect_uri | Yes | Exact registered HTTPS callback, including for pickup delivery |
code_challenge | Yes | Base64url-encoded SHA-256 digest of your PKCE verifier, without padding (43 characters) |
code_challenge_method | No | Only S256; the default when omitted |
state | For redirect | Nonempty for redirect delivery, at most 256 characters; generate and retain a random value per authorization |
delivery | No | redirect or pickup. Omitted, the code delivery set on your API client under API Keys applies (redirect until you change it) |
resources | No | Array 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 showsync_pendingwhile 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.
| Reason | Use it when |
|---|---|
subscription_canceled | The member ended their subscription with you |
payment_failed | Their payment to you failed and you closed the seat |
member_request | The member asked you to disconnect |
partner_terminated | You ended the relationship |
abuse | Abuse or a policy violation |
other | Anything 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.
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-Keyis 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 is409 idempotency_conflict, and an expired key is409 idempotency_key_expired. A replay returnsconnect: nullafter 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";reopenrequires that value inIf-Matchand answers412 precondition_failedwhen the member changed,428when the header is missing. GET /v1/api/partner/userspages withcursor; order is creation time descending, thenexternal_iddescending. Omitcursoron 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.
| Field | Type | Meaning |
|---|---|---|
external_id | string | Your id for the member |
uid | string or null | The CrossTrade account id; null while pending_link |
email | string | The address you enrolled, normalized (trimmed, lowercased) |
name | string or null | The display name you gave |
status | string | pending_link, provisioned, connected, revoked or expired (see the status model above) |
ownership_verified | boolean | The member redeemed the private Connect link; this is possession-of-link proof, not separate email-code verification |
version | integer or null | The membership version; the same number as the ETag. null while pending_link |
environments | array of string | Sorted environment names recorded at enrollment |
metadata | object | The object you stored at creation, or an empty object |
seat | object or null | started and ended timestamps; ended is null while the seat is open. null while pending_link |
created | string | When the enrollment was created |
authorization | object | The 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 |
grant | object or null | version 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_water | integer | Included 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[].status | Meaning | What to do |
|---|---|---|
ready | The login is linked and its session is current | Nothing |
reauth_required | CrossTrade recorded a confirmed broker-session failure; reason can be expired or evicted. Broker access needs reauthorization | Send the managed member a relink Connect link |
unknown | A session exists but is past its expiry and no failure has been confirmed: renewal is pending | Read again shortly |
unlinked | The member removed that login | Ask 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.