Skip to main content

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):
Each accepted event lands in the tenant schema’s 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_at drives the dashboard event index; the client’s timestamp field is stored but never trusted for ordering.
  • Retries are safe. message_id dedups 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 null or 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.
Point event-triggered journeys and 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 ships Order 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), or off (passthrough). No schema row exists for an event type → the event passes through, which keeps the surface back-compatible before you declare anything.
Enforcement runs on the ingest hot path, per (tenant, event type). Every violation is recorded durably to 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_id rides on cdp_events.context, so no schema migration and no per-consumer re-derivation — funnels, paths, and session traits read the same stamped id.
The lookup is fail-open: any error mints an extra session boundary rather than delaying or dropping the customer event. An out-of-order or backdated client timestamp cannot start spurious sessions — only a genuine gap in either direction does.

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 it spike, drop, normal, or insufficient_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 an after cursor for incremental polling and a before cursor 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