Skip to main content
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/rules
Returns the identity-resolution rules that map the inputs you provide (anonymous_id, email, phone, user_id) onto a single profile. Request

Get a profile’s resolved traits by email

GET /api/v1/cdp/profiles/by-email/{email}/traits
Request

Error envelope

422

Compute a conversion-lift report for an audience activation

POST /api/v1/cdp/audience/activations/{id}/lift
Record per-arm conversions for one activation run and compute its incrementality report. Configure a holdout percent on the destination (Audience → Activation in the dashboard) and Orbit carves a randomized control group that is never uploaded; the endpoint then compares the exposed (treatment) arm against the unexposed (control) arm. Body: treatment_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 seven POST /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:
  1. Take the exact JSON you will send; the HMAC covers those raw bytes.
  2. Sign <timestamp>.<nonce>.<raw body> with your workspace ingest secret (mint one via POST /api/v1/cdp/secrets); prefix the hex digest with v1=.
  3. Send the result as x-orbit-cdp-signature, x-orbit-cdp-timestamp, x-orbit-cdp-nonce on one of the seven operations.
The full wire contract and runnable signing loops in Node, Python, and Go live in the CDP ingest signing guide. The worked sample for 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 returns HTTP 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.
Author violations you want enforced in your workspace’s tracking plan; until then violations log without rejects. When a strict-mode rejection lands, the TRACKING_PLAN_VIOLATION runbook decodes the fired rule and the fix paths.

Get one account’s score detail

GET /api/v1/cdp/accounts/score — substitute your company name into the account query parameter
The response carries the account-wide rollup plus a member breakdown — each contact’s individual churn, intent, propensity, LTV, and lifecycle stage — so a CSM can see WHICH contacts drive the account’s risk.

Track 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.
The 200 ack is the same shape for every ingest path (track, identify, group, screen, page, alias, batch). 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}/identify
Supply at least one of userId 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}/alias
Batch accepts a batch 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}/events
Both read surfaces resolve the same identity to the same contact_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 narrow
The endpoint returns the raw event log and accepts after/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 same seg_ / 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.
A server-side script or job identifies one profile at a time through the same ingest gateway the SDK flush loop uses — supply at least one of 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
The ack is the standard ingest envelope — 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.
Membership lives under Contacts (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
Pagination metadata comes back under 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.
For a one-shot CSV handoff (BI tooling, an agency export) read the member rows straight off the segment; for a recurring profile or membership export into a warehouse destination, use 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.
This is the archive/re-hydration walk of 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.
Filing the request starts the cancellable cooling-off window (per-tenant, 7 days by default) and returns the 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
A 409 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.
Each destination outcome is written to the tenant’s audit chain for Art. 17 proof; a 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-models
The server-owned catalog lists the four built-in models — churn_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
Response

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 returns status: "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).
For the full train → evaluate → score loop, CLV tiering, drift checks, and uplift persuadability, see the CDP predictive models guide.

Check a predictive model for input drift

GET /api/v1/cdp/predictive-models/{model}/drift
Compare the live scoring population’s feature + prediction distribution against the model’s training distribution. A PSI above your threshold (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
Response

Materialise a predictive-model segment

POST /api/v1/cdp/predictive-models/{model}/activate
Turns the ranked top-N score list into a first-class static segment — the same one-click activation the dashboard’s Audience → Predictive models page performs. Returns 201 with the created segment and its member count, and 422 with the fit report when the model is not fitted yet. Request
Response
Because activation WRITES a segment, this endpoint requires the contacts:write scope (catalog/train/score/drift only need contacts:read).