Events and webhooks
CrossTrade records the member, authorization, broker and partner transitions listed below as durable events with a per-partner, gap-free sequence. Polling the feed is the recovery path; webhooks deliver the same lifecycle events asynchronously.
The feed
GET /v1/api/partner/events uses a partner token with partner:read. See GET Events for headers, query parameters and Python/JavaScript/cURL examples. Omit cursor for the first request; otherwise supply the integer next_cursor from the previous page. limit defaults to 50, with a maximum of 200. Events are retained for at least 30 days; unresolved webhook deliveries can keep older events in the log.
- A cursor below the floor answers
410 cursor_expired: captureevents_high_waterfrom the first page of a fresh member listing, reconcile all member pages, then resume the feed from that captured sequence. Using a later page's high-water mark can skip changes made during the listing. - A cursor above
high_wateris400 validation_failed. - Deduplicate by
id, tolerate reordering, and re-read the resource when in doubt.
A page of the feed (common fields shown; optional attribution fields omitted):
{
"success": true,
"events": [
{
"id": "pev_3f9a1c2b4d5e6f70",
"sequence": 41,
"version": 1,
"type": "member.created",
"created": "2026-10-15T14:02:11Z",
"partner_id": "p_8f1c2d3e",
"external_id": "th_8831",
"data": {
"status": "provisioned",
"version": 1
}
},
{
"id": "pev_4a0b2d3c5e6f7a81",
"sequence": 42,
"version": 1,
"type": "member.revoked",
"created": "2026-11-02T09:30:00Z",
"partner_id": "p_8f1c2d3e",
"external_id": "th_8831",
"data": {
"status": "revoked",
"version": 2,
"reason": "subscription_canceled"
}
}
],
"next_cursor": 42,
"floor": 0,
"high_water": 42,
"has_more": false,
"as_of": "2026-11-02T09:31:00Z",
"meta": {
"requestId": "preq_7a6b5c4d3e2f1a09"
}
}
| Field | Meaning |
|---|---|
events[].id | Stable event id (pev_...); the same id on every webhook retry. Deduplicate by it |
events[].sequence | Gap-free per partner; the feed is ordered by it |
events[].version | The event schema version, 1 |
events[].type | One of the types below |
events[].created | When the change happened |
events[].external_id | Member identifier when supplied; null for partner-level events and some authorization notices |
events[].uid, client_id, grant_version | Optional attribution fields on feed records. Webhook bodies omit these fields; correlate by event id and sequence |
events[].data | Type-specific details, for example the member's new status and version, the reason of a revoke, or the scope and grant version of a granted authorization |
next_cursor | The sequence to pass back as cursor; unchanged when the page is empty |
floor | Highest sequence already pruned, initially 0. The feed returns events after the cursor; a cursor below this boundary is 410 cursor_expired |
high_water | The newest sequence at the time of the read |
has_more | Another page follows |
as_of | When the page was read |
Event types
There is no per-type subscription: filter on type in your application. Events enter the feed independently of webhook delivery. Webhooks require a configured destination and permission at send time. external_id usually identifies the member, but can be null on authorization notices as well as partner events; feed records may also provide uid and client_id.
| Type | When | data |
|---|---|---|
member.created | A brand-new account was created for a member and its seat opened | status: provisioned, version: 1 |
member.reopened | A revoked member got a new seat through reopen | status: provisioned, version (the new one) |
member.revoked | The seat ended: your revoke call, your service leaving the platform, or the member deleting their account | status: revoked, version, reason (your reason, or account_deleted) |
authorization.granted | The member pressed Allow; a code was issued | scope (what they allowed), version (the grant version) |
authorization.denied | The member ended the flow without allowing: declined the terms (reason: declined) or chose "I'll do this later" (reason: later). Send a new link to resume | reason |
authorization.revoked | The user disconnected the app through their ordinary account, or deleted the account. Seat revocation is reported by member.revoked and also revokes the grant | reason: member_revoked or account_deleted |
authorization.family_revoked | A refresh-token family was revoked, usually because a retired refresh token was presented again | family_id, reason (refresh_reuse or the stated reason) |
broker.ready | A Tradovate login is linked and usable: at link time on the hosted page, and again when a login that had a confirmed failure renews or relinks successfully | environment, status: ready, plus user_id on a recovery |
broker.reauth_required | CrossTrade recorded a confirmed broker-session failure. Send a relink Connect URL so the member can authorize that login again | environment, user_id, status: reauth_required, reason (expired or evicted) |
broker.unlinked | The member removed a Tradovate login | environment, user_id (the Tradovate user id) |
partner.activated | Your service was activated | status, previous, state_version, reason |
partner.past_due | A payment is past due; grace period running | same |
partner.suspended | Grace expired or a commercial hold was placed | same |
partner.reactivated | Service resumed after past due, suspended or disabled | same |
partner.disabled | Your service was disabled | same |
member.expired | A legacy pending enrollment expired; current create requests do not start pending enrollments | status: expired |
Delivery rechecks the member and, for grant-bound events, the grant. Final notices (member.revoked, member.expired, authorization.revoked, authorization.family_revoked, authorization.denied, partner.suspended, partner.disabled) bypass those member/grant checks. An administrative partner disable pauses webhook delivery altogether, including final notices; do not rely on a final webhook as guaranteed delivery.
One more type, webhook.test, is sent only while you set up or rotate the webhook in the dashboard: the dashboard issues your signing secret, delivers one signed test event to the URL, and saves the webhook once your endpoint answers with a 2xx status. A test event has sequence 0, is signed like every other delivery, is never retried and never appears in the events feed.
Fills, positions and order rejections are not events; read them through the Trading API.
Webhooks
Configure your HTTPS webhook URL and signing secret under Partner Platform, API Keys. Each attempt is a POST with the event as the body, its id and type in X-CrossTrade-Event-Id and X-CrossTrade-Event-Type, and one signature header carrying the timestamp and one v1 signature per currently valid secret:
X-CrossTrade-Signature: t=1791503999,v1=hex(hmac_sha256(secret, "1791503999." + raw_body))
A delivery's headers and body are shown below, with illustrative signature values and pretty-printed JSON for readability. Actual delivery uses compact JSON with sorted keys. Verify the signature against the raw received bytes before parsing or reformatting them.
POST /crosstrade/webhook HTTP/1.1
Host: partner.example
Content-Type: application/json
User-Agent: CrossTrade-Partner-Webhooks/1
X-CrossTrade-Event-Id: pev_4a0b2d3c5e6f7a81
X-CrossTrade-Event-Type: member.revoked
X-CrossTrade-Signature: t=1793000000,v1=5b2d7c0f3e9a8b1c4d6e2f7a9b0c1d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c
{
"created": "2026-11-02T09:30:00Z",
"data": {
"reason": "subscription_canceled",
"status": "revoked",
"version": 2
},
"external_id": "th_8831",
"id": "pev_4a0b2d3c5e6f7a81",
"partner_id": "p_8f1c2d3e",
"sequence": 42,
"type": "member.revoked",
"version": 1
}
The dashboard's test delivery has the same headers and this body:
{
"created": "2026-10-15T13:39:08Z",
"data": {
"message": "Test delivery from your Partner Platform dashboard. Answer with any 2xx status."
},
"external_id": null,
"id": "pev_test_df825526fed3c8db",
"partner_id": "p_8f1c2d3e",
"sequence": 0,
"type": "webhook.test",
"version": 1
}
Split the header on commas, read t, and accept the delivery when any v1 value matches your HMAC over the exact bytes (during a secret rotation two v1 values can be present). Reject malformed signatures and timestamps more than five minutes from your current time. Deduplicate by event id: retries carry the same event with a fresh timestamp and signature. Answer 2xx within a few seconds.
CrossTrade schedules delivery after the event commits, with a periodic sweep for recovery. Webhooks can arrive more than once or out of order, especially after retries; use the ordered feed when sequence matters. Persist the event durably before returning 2xx, then process it asynchronously and deduplicate by id.
Network failures, 408, 429 and 5xx are retried with backoff, up to 12 attempts. A numeric Retry-After is considered alongside the backoff, with its contribution capped at one hour. Other non-2xx responses are treated as failed deliveries. Failed events remain available for recovery and support can replay them with the same id. Planned signing-secret rotation can keep the previous secret valid for up to 24 hours.