POST Create Connect Link
Issue a new private hosted Connect link for an existing member. Use authorize for new or changed consent, or relink to repair a broker identity already covered by an active grant. Earlier unfinished flows of the same purpose are closed, including unexchanged authorization codes.
Endpoint
POST https://app.crosstrade.io/v1/api/partner/users/{external_id}/connect-link
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 |
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
external_id | string | Yes | Member id used at creation; 1–64 ASCII letters, digits, periods, underscores or hyphens |
Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
purpose | string | Yes | authorize or relink |
authorization | object | For authorize | Fresh OAuth request, including a new PKCE challenge. See authorization fields. Omit for relink |
No query parameters. The body is limited to 64 KiB; unknown fields are rejected. This route is available while active or past due within grace. A revoked member needs POST Reopen Member.
Code examples
For a new authorization, set PARTNER_TOKEN, XT_REDIRECT_URI, CODE_CHALLENGE, OAUTH_STATE and IDEMPOTENCY_KEY. Generate and retain a fresh PKCE pair with the exact body and key before sending. Replace th_8831 with your member id.
- Python
- JavaScript
- cURL
import os
import requests
body = {
"purpose": "authorize",
"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/th_8831/connect-link",
headers={
"Authorization": f"Bearer {os.environ['PARTNER_TOKEN']}",
"Idempotency-Key": os.environ["IDEMPOTENCY_KEY"],
},
json=body,
timeout=30,
)
response.raise_for_status()
connect = response.json()["connect"]
# Store flow_id with the verifier and deliver the URL privately.
# An idempotent replay can return connect=None after the original URL expires.
const response = await fetch(
"https://app.crosstrade.io/v1/api/partner/users/th_8831/connect-link",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PARTNER_TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": process.env.IDEMPOTENCY_KEY,
},
body: JSON.stringify({
purpose: "authorize",
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",
},
}),
},
);
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
const { connect } = await response.json();
// Store flow_id with the verifier and deliver the URL privately.
// An expired idempotent replay can return connect=null.
curl -sS -X POST "https://app.crosstrade.io/v1/api/partner/users/th_8831/connect-link" \
-H "Authorization: Bearer $PARTNER_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d "{
\"purpose\": \"authorize\",
\"authorization\": {
\"scope\": \"tv:read manage:read\",
\"redirect_uri\": \"$XT_REDIRECT_URI\",
\"code_challenge\": \"$CODE_CHALLENGE\",
\"code_challenge_method\": \"S256\",
\"state\": \"$OAUTH_STATE\",
\"delivery\": \"pickup\"
}
}"
For a broker repair, replace the body with this object and use a new key:
{
"purpose": "relink"
}
Relinking does not issue a new authorization code or token pair and cannot add identities outside the standing grant.
Response
201 Created. This example starts a second authorization flow for an already connected member. No membership ETag is returned.
{
"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"
}
}
The URL lasts 10 minutes and is redeemed by one browser. Result replay lasts 24 hours but returns connect: null after the original URL expires. Use a new request and key for a fresh URL. See Hosted Connect for the member's browser flow.
Errors
| Status | Error | Meaning |
|---|---|---|
| 400 | validation_failed | Invalid purpose, authorization, path or idempotency key |
| 401 | unauthorized | Invalid partner token |
| 403 | insufficient_scope, partner_not_enabled, partner_suspended, partner_disabled, partner_not_active | Scope or service policy blocks a new link |
| 404 | member_not_found | Unknown member |
| 409 | conflict | Member is revoked or expired |
| 409 | grant_required | Relink requires an existing active grant; request authorization instead |
| 409 | partner_tag_required | Set your business name first |
| 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 | Retain and retry the exact request and key |
See Partner errors for common availability errors and response shapes.