POST Create Member
Create a managed CrossTrade account for a new email address, open its funded seat and issue a private Connect link. Billing starts when the seat opens, even if the member does not finish Connect. An email already attached to a CrossTrade account returns 409 email_in_use.
Endpoint
POST https://app.crosstrade.io/v1/api/partner/users
Headers
| Name | Required | Value |
|---|---|---|
| Authorization | Yes | Bearer <partner access token> with partner:provision |
| Content-Type | Yes | application/json (UTF-8) |
| Idempotency-Key | Yes | Unique per logical request; 1–200 printable ASCII characters without spaces |
No path or query parameters. The JSON body may be at most 64 KiB; unknown fields are rejected.
Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
external_id | string | Yes | Your permanent member id: 1–64 ASCII letters, digits, periods, underscores or hyphens |
email | string | Yes | New CrossTrade email address; trimmed and lowercased |
name | string | No | Display name, 1–120 characters when supplied |
environments | array of string | No | Nonempty, unique demo and/or live values within your partner profile. Defaults to that profile |
metadata | object | No | Your metadata, at most 1,024 serialized bytes |
authorization | object | Yes | Consent request using the fields below |
Authorization fields
| Name | Type | Required | Description |
|---|---|---|---|
scope | string | No | Space-separated member scopes; defaults to your client's member-scope ceiling |
redirect_uri | string | Yes | Exact registered HTTPS URI, including for pickup delivery |
code_challenge | string | Yes | SHA-256 PKCE challenge, base64url without padding (43 characters) |
code_challenge_method | string | No | S256 only; the default when omitted |
state | string | For redirect | Random correlation value; nonempty for redirect delivery, at most 256 characters |
delivery | string | No | redirect or pickup. Defaults to your API client's code-delivery setting (redirect until changed) |
resources | array of string | No | Exact absolute Trading and/or Management resource URLs to narrow audiences |
See Partner Authentication for scopes and audiences. A request for tv:read manage:read, as below, asks for read access to both member APIs.
Code examples
Set PARTNER_TOKEN, XT_MEMBER_EMAIL, XT_REDIRECT_URI, CODE_CHALLENGE, OAUTH_STATE and IDEMPOTENCY_KEY. Generate a fresh PKCE pair and retain its verifier, the request body and the key before sending. Retries must reuse those values.
- Python
- JavaScript
- cURL
import os
import requests
body = {
"external_id": "th_8831",
"email": os.environ["XT_MEMBER_EMAIL"],
"name": "Jamie Rivera",
"environments": ["demo"],
"authorization": {
"scope": "tv:read manage:read",
"redirect_uri": os.environ["XT_REDIRECT_URI"],
"code_challenge": os.environ["CODE_CHALLENGE"],
"code_challenge_method": "S256",
"state": os.environ["OAUTH_STATE"],
"delivery": "pickup",
},
}
response = requests.post(
"https://app.crosstrade.io/v1/api/partner/users",
headers={
"Authorization": f"Bearer {os.environ['PARTNER_TOKEN']}",
"Idempotency-Key": os.environ["IDEMPOTENCY_KEY"],
},
json=body,
timeout=30,
)
response.raise_for_status()
result = response.json()
connect = result["connect"] # May be None for an existing member or an expired replay.
print(response.status_code, result["member"]["status"])
# If connect is present, deliver connect["url"] privately to this member.
const body = {
external_id: "th_8831",
email: process.env.XT_MEMBER_EMAIL,
name: "Jamie Rivera",
environments: ["demo"],
authorization: {
scope: "tv:read manage:read",
redirect_uri: process.env.XT_REDIRECT_URI,
code_challenge: process.env.CODE_CHALLENGE,
code_challenge_method: "S256",
state: process.env.OAUTH_STATE,
delivery: "pickup",
},
};
const response = await fetch("https://app.crosstrade.io/v1/api/partner/users", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PARTNER_TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": process.env.IDEMPOTENCY_KEY,
},
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
const result = await response.json();
const connect = result.connect; // Can be null on duplicate create or expired replay.
console.log(response.status, result.member.status);
// If connect is present, deliver connect.url privately to this member.
curl -sS -X POST "https://app.crosstrade.io/v1/api/partner/users" \
-H "Authorization: Bearer $PARTNER_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d "{
\"external_id\": \"th_8831\",
\"email\": \"$XT_MEMBER_EMAIL\",
\"name\": \"Jamie Rivera\",
\"environments\": [
\"demo\"
],
\"authorization\": {
\"scope\": \"tv:read manage:read\",
\"redirect_uri\": \"$XT_REDIRECT_URI\",
\"code_challenge\": \"$CODE_CHALLENGE\",
\"code_challenge_method\": \"S256\",
\"state\": \"$OAUTH_STATE\",
\"delivery\": \"pickup\"
}
}"
Responses
201 Created
A new seat and Connect link, with ETag: "v1". The link expires in 10 minutes. Keep flow_id with the PKCE verifier for code pickup and exchange.
{
"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\""
}
}
Existing member and replay
The same external_id with the same email and name returns 200, the current member and connect: null. Use POST Create Connect Link for fresh consent, or POST Reopen Member if revoked.
Reusing the original idempotency key replays the recorded status and member snapshot with Idempotent-Replayed: true for 24 hours. Once the original link expires, replay returns connect: null; replay does not extend the link lifetime.
Errors
| Status | Error | Meaning |
|---|---|---|
| 400 | validation_failed | Invalid body, authorization fields or missing idempotency key |
| 401 | unauthorized | Invalid partner token |
| 403 | insufficient_scope, partner_not_enabled, partner_past_due, partner_suspended, partner_disabled, partner_not_active | Scope or service policy blocks new seats |
| 409 | email_in_use, email_ambiguous | Email already belongs to a CrossTrade account, or matches multiple accounts |
| 409 | external_id_conflict | That id has a different email or name |
| 409 | partner_tag_required, seat_cap_reached | Set your business name, or resolve your seat limit |
| 409 | idempotency_conflict, idempotency_in_progress, idempotency_key_expired | See replay rules |
| 413 / 415 | validation_failed | Body too large or unsupported content type/encoding |
| 429 | rate_limited, too_many_concurrent_requests | Honor Retry-After |
| 503 | outcome_unknown | The database result could not be confirmed; retain and retry the same request and key |
See Partner errors for common availability errors and response shapes.