Access note for the BigQuery reverse-ETL config endpoints —
GET /api/v1/cdp/reverse-etl and PATCH /api/v1/cdp/reverse-etl accept the developer role in addition to owner / admin. The generated per-operation descriptions below may still say “Owner / admin only” because they aren’t annotated in the route schema yet; this overlay is the authoritative source until they are.Worked request and response samples
Copy a request body as written, substitute your own ids, and compare the response envelope. Errors follow Devotel Orbit’s{ error, meta } envelope, shown once below under Error envelope.
Resolve an identity
GET /api/v1/cdp/identity-resolution/rulesGet a profile’s resolved traits by email
GET /api/v1/cdp/profiles/by-email/{email}/traitsError envelope
422
Compute a conversion-lift report for an audience activation
POST /api/v1/cdp/audience/activations/{id}/lifttreatment_converted and control_converted (integers, joined from your analytics or warehouse), plus an optional confidence_level (default 0.95). Response carries each arm’s size and conversion rate, absolute and relative lift, the confidence interval, z-score, p-value, and an is_significant flag. A run with no holdout returns tracking_method: "no_holdout". Owner / admin / developer only.
Ingest operations use HMAC signing, not X-API-Key
The sevenPOST /cdp/v1/{ingest_id}/* operations below — track, identify, page, screen, group, alias, batch — still carry an X-API-Key header in the generated request examples. That header is wrong on this surface: the ingest gateway accepts only x-orbit-cdp-signature, x-orbit-cdp-timestamp, and x-orbit-cdp-nonce, and rejects Clerk sessions and API keys. Substitute your ingest id from the CDP source config for {ingest_id}, and compute the signature yourself — copy-pasting a signed curl never works, since the signature changes with every edit to the body:
- Take the exact JSON you will send; the HMAC covers those raw bytes.
- Sign
<timestamp>.<nonce>.<raw body>with your workspace ingest secret (mint one viaPOST /api/v1/cdp/secrets); prefix the hex digest withv1=. - Send the result as
x-orbit-cdp-signature,x-orbit-cdp-timestamp,x-orbit-cdp-nonceon one of the seven operations.
track further up this section shows the complete call — headers, body, ack — end to end.
Tracking-plan violations return 422 with a per-field diff
With strict enforcement, a non-conforming payload returnsHTTP 422 with a structured reason you (or your producer logs) can act on. The rejected messageId is never consumed, so a corrected retry is accepted cleanly.
Get one account’s score detail
GET /api/v1/cdp/accounts/score — substitute your company name into the
account query parameterTrack an event (Segment-compatible ingest)
POST /cdp/v1/{ingest_id}/track — the ingest endpoint lives outside
/api/v1; sign the raw body with your tenant ingest secret{ingest_id} comes from the CDP source config page; sign the raw request
body with the tenant ingest secret as the four x-orbit-cdp-* headers.
Drop the template userId or anonymousId in exactly as shown, fill the
event name you want to log, and the response ack ties the event back to
the contact it resolved to.
contact_id is null when the payload
didn’t resolve to a contact yet (anonymous id awaits an identify) and
deduped flips true when a previously accepted event with the same
message id is replayed.
Identify a user and bind traits
POST /cdp/v1/{ingest_id}/identifyuserId or anonymousId plus the traits you want
to attach. The same contact_id you got back from the track call is
returned after the identity is bound.
Ingest events in a batch with an alias bridge
POST /cdp/v1/{ingest_id}/batch and
POST /cdp/v1/{ingest_id}/aliasbatch array of events (track / identify / group /
page / screen); each element triggers its own resolve. alias ties an
anonymousId to a userId — send it before the user actually signed in
so the fan-out attached browsing history to the account.
Get a profile’s segments plus recent history
GET /api/v1/cdp/profiles/by-user-id/{user_id}/segments and
GET /api/v1/cdp/profiles/by-user-id/{user_id}/eventscontact_id;
from there, segments returns the membership set (event- or
realtime-sourced) and events returns the newest-N activity for the
visible timeline.
List the live event stream with filters
GET /api/v1/cdp/events/debug — the live event-tail surface; filter on
type, event_name, user_id, or a substring q to narrowafter/before
cursors to poll. next_cursor is what you pass back as after to live-
tail the stream from this point.
Worked sequences for the CDP surface past the first 15 operations
By the page contract, operations past the first 15 render in cURL and TypeScript only. The five sequences below fill the gap for the calls an integration reaches next — they cover those trimmed operations in the full six-language matrix (bash, typescript, python, go, ruby, php) per the SDK language policy. Copy a request, substitute your own ids, and compare the envelope; ids use the sameseg_ / cnt_ / evt_ prefixes as the tracking-plan and erasure-propagation guides.
1. Identify a profile from a server (signed ingest)
POST /cdp/v1/{ingest_id}/identify — HMAC-signed; no API key accepted on
this surface. Owner of any SDK client can call it.userId or anonymousId plus the traits to attach. The 200 ack echoes the contact_id the identity resolved to; the same call in a batch envelope sits above under Ingest events in a batch.
Request
contact_id is non-null as soon as the userId resolves against an existing contact.
2. Page through a segment’s members
GET /api/v1/contacts/segments/{id}/members?limit=…&cursor=… — reads
the last-materialised membership snapshot; requires contacts:read.contact_segments), not the CDP ingest path — this is the same read the dashboard’s segment member view makes. Keep the returned next_cursor and pass it back as cursor until the page comes back empty; call POST /api/v1/contacts/segments/{id}/refresh first if you need the very latest audience.
Request
meta.pagination.cursor — pass that value as ?cursor= on the next request. Each member is the contact row as of the last materialisation.
3. Export a segment to CSV
GET /api/v1/contacts/segments/{id}/export.csv — streams the
materialised membership as a CSV attachment; requires contacts:export.GET /api/v1/cdp/reverse-etl/profile-exports and GET /api/v1/cdp/reverse-etl/segment-exports to route it through a configured reverse-ETL destination instead.
Request
4. Page through the raw event stream oldest-first
GET /api/v1/cdp/events/export?since=…&until=…&cursor=… —
chronological bulk export; requires contacts:read.cdp_events: oldest-first, filtered by any of since / until / type / contact_id / user_id / anonymous_id, with an opaque keyset cursor on each response. Keep the cursor and loop until has_more flips false. For GDPR Art. 20 portability pass the subject’s contact_id; for a warehouse re-sync volume pass nothing and let it walk the full window.
Request
5. File a GDPR erasure and propagate it downstream
POST /api/v1/contacts/{id}/gdpr/erasure-request then
POST /api/v1/cdp/erasure/{erasureId}/propagate — owners/admins file;
propagation is a destructive compliance action, owner/admin only.erasure_* id you keep for the rest of the flow. After the scheduler completes the soft-erasure cascade (see the erasure propagation guide for the cascade order), call the propagate endpoint on that id to fan the deletion out to every wired Nango destination (HubSpot, Salesforce, Braze, Iterable, and the like) — each destination’s outcome is audit-logged and returned inline.
Request — step 1: file the request
ERASURE_COOLING_OFF_ACTIVE means a request is already pending or executing for this contact — cancel it via POST /api/v1/contacts/{id}/gdpr/erasure-request/{requestId}/cancel before filing again.
Request — step 2: propagate the completed erasure downstream
Substitute the erasure_* id returned in step 1 once the cascade reports status: completed.
skipped_no_connection row means the tenant hasn’t wired that destination. Propagate BEFORE the hard-delete cascade purges the contact row — after hard-delete the endpoint returns 409 ERASURE_CONTACT_NOT_FOUND, and identifiers can no longer be resolved for downstream destinations.
Discover the predictive-model catalog
GET /api/v1/cdp/predictive-modelschurn_propensity, conversion_intent, lifetime_value, engagement_fatigue — with each model’s kind, output unit, algorithm, and the six first-party features it fits over. Call it first so the model path param on train/score/activate/drift always carries a valid key.
Request
Worked chain: catalog → train → score
The typical integration first trains the model on the tenant’s resolved-outcome history, then scores the model’s activation population — so the catalog call above is followed by a train call; a successful train returnsstatus: "trained" (200), while a thin resolved-outcome sample returns the same report with status: "insufficient_data" (HTTP 422 — wait for more labelled outcomes, then retry).
Check a predictive model for input drift
GET /api/v1/cdp/predictive-models/{model}/driftthreshold (default 0.25) means the live distribution has shifted enough to question the ranking. Run drift before trusting a ranking from a fit you trained days ago.
Request
Materialise a predictive-model segment
POST /api/v1/cdp/predictive-models/{model}/activate201 with the created segment and its member count, and 422 with the fit report when the model is not fitted yet.
Request
contacts:write scope (catalog/train/score/drift only need contacts:read).