Skip to main content

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: capture events_high_water from 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_water is 400 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"
}
}
FieldMeaning
events[].idStable event id (pev_...); the same id on every webhook retry. Deduplicate by it
events[].sequenceGap-free per partner; the feed is ordered by it
events[].versionThe event schema version, 1
events[].typeOne of the types below
events[].createdWhen the change happened
events[].external_idMember identifier when supplied; null for partner-level events and some authorization notices
events[].uid, client_id, grant_versionOptional attribution fields on feed records. Webhook bodies omit these fields; correlate by event id and sequence
events[].dataType-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_cursorThe sequence to pass back as cursor; unchanged when the page is empty
floorHighest sequence already pruned, initially 0. The feed returns events after the cursor; a cursor below this boundary is 410 cursor_expired
high_waterThe newest sequence at the time of the read
has_moreAnother page follows
as_ofWhen 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.

TypeWhendata
member.createdA brand-new account was created for a member and its seat openedstatus: provisioned, version: 1
member.reopenedA revoked member got a new seat through reopenstatus: provisioned, version (the new one)
member.revokedThe seat ended: your revoke call, your service leaving the platform, or the member deleting their accountstatus: revoked, version, reason (your reason, or account_deleted)
authorization.grantedThe member pressed Allow; a code was issuedscope (what they allowed), version (the grant version)
authorization.deniedThe 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 resumereason
authorization.revokedThe user disconnected the app through their ordinary account, or deleted the account. Seat revocation is reported by member.revoked and also revokes the grantreason: member_revoked or account_deleted
authorization.family_revokedA refresh-token family was revoked, usually because a retired refresh token was presented againfamily_id, reason (refresh_reuse or the stated reason)
broker.readyA 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 successfullyenvironment, status: ready, plus user_id on a recovery
broker.reauth_requiredCrossTrade recorded a confirmed broker-session failure. Send a relink Connect URL so the member can authorize that login againenvironment, user_id, status: reauth_required, reason (expired or evicted)
broker.unlinkedThe member removed a Tradovate loginenvironment, user_id (the Tradovate user id)
partner.activatedYour service was activatedstatus, previous, state_version, reason
partner.past_dueA payment is past due; grace period runningsame
partner.suspendedGrace expired or a commercial hold was placedsame
partner.reactivatedService resumed after past due, suspended or disabledsame
partner.disabledYour service was disabledsame
member.expiredA legacy pending enrollment expired; current create requests do not start pending enrollmentsstatus: 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.