The CDP event model
Every behavioral fact Orbit learns about a customer — a page view, a purchase, a link click — enters the platform as a CDP event. This page defines that event: what shape it has, where it is stored, how it binds to a contact, how a tracking plan governs what is accepted, how events group into sessions, and what consumes them downstream. Segments, computed traits, attribution, and funnels all read the same event stream; this page is the concept those guides assume.Section 1 — The event stream: one append-only table per tenant
Inbound events arrive on the Segment-compatible ingest surface —POST /api/v1/cdp/track, /identify, /page, /group, /alias,
/screen, and /batch — recognized by the URL’s ingest_id and an
HMAC signature against your tenant’s ingest secret. A track call
carries an event name, a properties payload, an optional context
envelope, an optional client timestamp, and identity hints
(userId or anonymousId — the Segment spec requires one of the two):
cdp_events table — one
append-only table per tenant, alongside usage_events, per the tenant
catalog in Tenant isolation. Rows are never
updated or deleted by the platform (only erased on a GDPR erasure cascade
from the owning contact). The stored row keeps the ingest truth
faithfully:
Two practical consequences of this shape:
- The server time is authoritative.
received_atdrives the dashboard event index; the client’stimestampfield is stored but never trusted for ordering. - Retries are safe.
message_iddedups SDK retries, so an at-least-once client does not double-count.
Platform-emitted events: whatsapp_flow_submitted
Not every event on the stream arrives from your SDK through the HTTP
ingest surface. Orbit itself mints events when a customer action outside
the browser deserves to be on the profile — each one enters through the
same ingest chokepoint as SDK traffic, so tracking-plan, sessionization,
and downstream consumers cannot tell the difference. The first of these is
the WhatsApp Flow completion:
Landing guarantees — deliberately identical to the custom-field
write-back
that runs beside it:
- Additive write. The event appends to the stream the moment the customer completes the Flow; nothing already on the profile is removed or downgraded.
- Blank answers are ignored. A
nullor empty answer never wipes a value an earlier submission set — re-running a Flow with an optional field skipped is safe. - Unmatched field ids are skipped. A Flow field with no matching custom-field definition still rides on the event’s properties (segments and journeys can target it), but it is never invented as a new contact field.
performed_event segment operators at
whatsapp_flow_submitted; the per-submission landed_in_cdp marker on
the submissions list
tells you whether a given row has landed.
Section 2 — Event identity: how an event binds to a contact
An event is born with the identity hints the SDK sent (user_id,
anonymous_id, or both) — it does not yet point at a person. A downstream
identity-resolution pass matches those hints to a contact row and stamps
contact_id, and from then on the event reads as that person. Merges follow
the event: when duplicate contacts collapse, the event’s ownership follows
the golden record.
The contract, hierarchy, and review flow for that matching are covered in
Identity resolution — this page only fixes
the distinction it matters on: identity hints ride on the event; binding
happens after ingest, not at ingest.
Section 3 — Event schemas and tracking plans
Left ungoverned, an event stream drifts: one SDK shipsOrder Completed,
another order_completed, a third adds a totle typo property, and every
downstream consumer quietly fragments. Orbit exposes two cooperating
declare-and-enforce surfaces to stop that:
- Tracking plan (
/api/v1/cdp/tracking-plan/*) — the tenant-level catalog of allowed events. Each entry declares an event name, its description, the properties it may carry, the required-property list, and an enforcement mode. - Event schemas (
/api/v1/cdp/schemas/*) — a per-event-type JSON Schema with its own enforcement mode:strict(rejects a violating event),warn(accepts and records the violation), oroff(passthrough). No schema row exists for an event type → the event passes through, which keeps the surface back-compatible before you declare anything.
cdp_tracking_plan_violations — and a
spike in violation volume fires an operator alert, so an SDK regression
that starts shipping malformed events surfaces in minutes rather than at
the next manual audit.
Declare a schema when the cost of property drift is real: when segments
filter on the properties, when destination syncs map fields, or when a
data dictionary needs to hold. Until then, passthrough costs nothing.
Section 4 — Sessionization
A stream of individual “what happened” rows is not directly the analytic grain most reports want; those reports read at the session grain — “the visit that produced this purchase.” Orbit sessions the stream at ingest, server-side, rather than leaving session reconstruction to every consumer:- The subject of a session is
COALESCE(contact_id, anonymous_id, user_id)— the same journey key funnel and path analytics use. - Consecutive events for one subject belong to one session as long as the gap between them stays under the industry-standard 30-minute timeout. Exceed it (or there is no prior event for the subject) and a fresh session id is minted.
- The resolved
session_idrides oncdp_events.context, so no schema migration and no per-consumer re-derivation — funnels, paths, and session traits read the same stamped id.
Section 5 — Event governance
Three tenant-owned controls sit on top of the stream:- Event-property governance (
/api/v1/cdp/event-property-governance/*) — a data dictionary for the other half of the catalog (contact traits are classified separately). Each event property can carry a sensitivity level, data categories (financial, health/PHI, government id, …), an accountable owner, the allowed uses (analytics, activation, ML training, …), and a masking strategy for exports. A property with no entry resolves to the safe public/ungated default. - Event-volume anomaly detection (
GET /api/v1/cdp/event-volume-anomaly) — compares the most recent complete hourly ingest bucket against a learned baseline of the preceding hours and classifies itspike,drop,normal, orinsufficient_data. A broken SDK deploy or an expired ingest key shows first as a volume drop — before any tracking-plan violation appears. - The event debugger (
GET /api/v1/cdp/events/debug) — a live tail of the incoming stream with per-field filters (type, event name, source, identity, message id, spillover status) and a free-text search across the payload. Newest-events use anaftercursor for incremental polling and abeforecursor for back-pagination. The debugger streams the full event payload — which routinely carries checkout PII — so it is scope-gated the same way as the contact surface.
Section 6 — Where events land
Events are only the raw material; the consumers sit downstream:
For the full parameter-level surface of every endpoint named here, see
the CDP API reference; this page names
the model and stops there.
Cross-references
- Tenant isolation —
cdp_events/usage_eventsper tenant schema. - Identity resolution — how hints become a contact binding.
- CDP segments — building audiences over this stream.
- WhatsApp Flow submissions —
the
whatsapp_flow_submittedevent’s source surface. - CDP API reference — endpoint-level detail.