> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# CDP tracking plan: declare event schemas and triage violations

> Declare which events your apps send and the shape their properties take, enforce the contract at ingest with soft or strict mode, and read the violations feed to fix drifting producers before they corrupt your audiences.

# CDP Tracking Plan

A tracking plan is the contract between the apps that send you events and the CDP that stores them. You declare which event names exist and what their `properties` should look like, and every inbound payload is checked against that contract at ingest time. This guide covers declaring events, choosing between soft and strict enforcement, reading the violations feed, and keeping the plan under version control. The segments and scores documented in [CDP audiences](/guides/cdp-segments) are only as trustworthy as the events they filter over — a tracking plan is how you keep that input honest.

The same surface backs the dashboard at **Integrations → CDP → Tracking Plan**. Everything below also works from the API, so your CI can drive it.

## 1. What a tracking plan is

A tracking plan row declares one event:

* `event_name` — the `event` string your SDK sends (case-sensitive). One row per name; re-posting the same name updates it.
* `properties_schema` — a subset of JSON Schema: `properties` maps each property name to a rule (`type` and optionally an `enum` value set), and `required` lists properties that must be present. Types supported: `string`, `number`, `integer`, `boolean`, `object`, `array`.
* `enforcement` — `soft` (log violations, accept the event) or `strict` (reject violating events with HTTP 422).

At ingest time every `track`, `page`, `screen`, and `batch` leg whose event name matches a plan row is validated. Mismatches are recorded in the violations feed whether or not the event was rejected, so soft mode and strict mode share one audit trail.

Validation rules, in the order they fire:

* A property listed in the plan's `required` array that arrives missing or null → `missing_required`.
* A property whose value's type differs from the rule's `type` → `type_mismatch` (integer values satisfy a `number` rule).
* A property whose value falls outside the rule's `enum` set → `enum_mismatch`.
* A property present in the payload without a rule in the schema → `extra_property` (only flagged once the schema declares at least one property).
* An event name with no plan row at all → `unknown_event`, recorded as soft/accepted so SDK drift shows up without costing you events.

## 2. Prerequisites

* **Access.** An API key for your workspace, or a dashboard session with the owner, admin, or developer role. Analyst and marketer roles cannot edit the plan — a strict-mode flip changes whether customer events are accepted, so the write side is restricted.
* **An events list.** The event names you want to govern. Start with the two or three that drive critical segments or flows, not the whole catalog.
* **A naming convention for `event_name`.** It must match the `event` string in your SDK exactly, including case.

## 3. Declare an event

`POST /api/v1/cdp/tracking-plan` upserts a plan row, so the same call creates and later updates a row:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/cdp/tracking-plan \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "event_name": "Order Completed",
    "description": "Fires when checkout completes",
    "properties_schema": {
      "properties": {
        "order_total": { "type": "number" },
        "currency": { "type": "string", "enum": ["USD", "EUR", "GBP"] },
        "payment_method": { "type": "string" }
      },
      "required": ["order_total", "currency"]
    },
    "enforcement": "soft"
  }'
```

Response (fields marked `*` only included when you sent them — `description` defaults to `null`):

```json theme={null}
{
  "data": {
    "id": "ctp_evt_...",
    "event_name": "Order Completed",
    "description": "Fires when checkout completes",
    "properties_schema": { ... },
    "required_properties": ["currency", "order_total"],
    "enforcement": "soft",
    "created_by": "user_...",
    "created_at": "2026-08-31T09:59:00.000Z",
    "updated_at": "2026-08-31T09:59:00.000Z"
  }
}
```

Notes:

* Required properties can come from `properties_schema.required`, from a top-level `required_properties` array, or both — the server merges, deduplicates, and stores them sorted.
* New events almost always start in `soft` mode. Watch the violations feed for a few days, then flip to `strict` once the producers are clean.
* Schema fields beyond `properties`, `required`, and the governance `classification` block are stored but ignored by the validator, so you can paste a fuller JSON Schema and get forward-compat storage.

## 4. List, update, and delete

List the catalog (keyset-paginated — walk `pagination.cursor`):

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/cdp/tracking-plan?limit=100" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json theme={null}
{
  "data": {
    "data": [ { "id": "ctp_evt_...", "event_name": "Order Completed", "enforcement": "soft", ... } ],
    "counts": { "total": 3, "strict": 1 },
    "pagination": { "cursor": null, "has_more": false }
  }
}
```

Update a row by re-posting its `event_name` — the request above with `"enforcement": "strict"` flips enforcement in place. Delete stops validation for that event name (historical violations are kept):

```bash theme={null}
curl -X DELETE https://api.orbit.devotel.io/api/v1/cdp/tracking-plan/ctp_evt_abc123 \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json theme={null}
{ "data": { "deleted": true } }
```

## 5. Soft vs strict enforcement

The mode decides what happens when a violation fires on a matched plan row:

* **Soft** — the event is accepted and stored as usual, and the violation is recorded for you to review. Producers drift without losing data.
* **Strict** — the event is rejected with HTTP 422 and the violation is recorded with `was_rejected: true`. Nothing untyped enters `cdp_events`, so segments and models stay pure.

Two operational rules:

1. Ship new events in `soft`. The feed tells you whether your SDK matches what you declared before you arm the gate.
2. Flip to `strict` only for events whose producers you control end-to-end. A customer-side SDK on a mobile version you cannot force-update will 422 real purchases if the plan and SDK disagree.

Unmatched event names (no plan row) are always accepted — strictness only applies to events you have declared. If a strongly-governed workspace wants to reject undeclared events wholesale, that's a policy to raise with Support rather than a per-event setting.

## 6. Reading the violations feed

`GET /api/v1/cdp/tracking-plan/violations` is newest-first and filterable by `event_name` and `enforcement`, with a cursor for paging:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/cdp/tracking-plan/violations?event_name=Order%20Completed&enforcement=strict&limit=25" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json theme={null}
{
  "data": {
    "rows": [
      {
        "id": "ctp_vio_...",
        "plan_event_id": "ctp_evt_...",
        "event_name": "Order Completed",
        "event_id": "evt_...",
        "violation_type": "type_mismatch",
        "violation_path": "properties.order_total",
        "expected": "number",
        "actual": "string",
        "enforcement": "strict",
        "was_rejected": true,
        "created_at": "2026-08-31T10:04:11.000Z"
      }
    ],
    "next_cursor": "eyJ..."
  }
}
```

Each row gives you the failing path, the expected shape, and what actually arrived — enough to pinpoint the producer. `was_rejected` tells you whether the event was dropped (strict) or stored anyway (soft). In the dashboard, the **Violations** tab on the Tracking Plan page shows the same stream with event-name and enforcement filters.

## 7. Plan-as-code in CI/CD

Treat the plan like any other infrastructure — a JSON file in your repo, applied on deploy:

```bash theme={null}
#!/usr/bin/env bash
# scripts/sync-tracking-plan.sh — apply plans/tracking-plan.json
set -euo pipefail
for file in plans/*.event.json; do
  curl -sS -X POST https://api.orbit.devotel.io/api/v1/cdp/tracking-plan \
    -H "X-API-Key: $ORBIT_API_KEY" \
    -H "Content-Type: application/json" \
    --data "@$file" > /dev/null
done
```

```json theme={null}
// plans/order-completed.event.json
{
  "event_name": "Order Completed",
  "description": "Fires when checkout completes",
  "properties_schema": { "properties": { ... }, "required": ["order_total"] },
  "enforcement": "soft"
}
```

Because `POST` is an upsert, running the script twice is a no-op when nothing changed. A common pipeline: authors edit the JSON in a pull request, CI posts it on merge, and the violations feed confirms the schema and producers agree before anyone flips `enforcement` to `strict`. Keep `curl` exit codes checked so a 422 on the plan itself fails the deploy.

## 8. Troubleshooting

**Events rejected with 422 in strict mode, unexpectedly.** A producer is sending a payload that diverges from the plan. Open the violations feed filtered to that event — the `violation_path` and `actual` fields name the offending property. Either fix the SDK payload, or widen the schema (add the property rule, extend the `enum`) if the new shape is legitimate. If you cannot fix the producer immediately, flip the event back to `soft` to stop the data loss while you triage.

**`missing_required` across every event of a type.** Usually a producer version that stopped sending the property (a refactor renamed it) or sent it as null. Compare the `actual` payloads; the fix is on the producer side.

**`extra_property` noise.** Your SDK sends something the plan does not declare. Add a rule for it — optional properties only need a `type` entry under `properties`.

**Schema evolution.** Adding new optional properties to a payload is always safe. Removing a property, narrowing a type, or changing a value set are breaking changes: coordinate the plan edit with the producer rollout, and under `soft` enforcement watch the feed while both versions are live.

**`unknown_event` entries.** An event arrived that no row governs. Either declare a row for it, or fix the caller's event-name casing/spelling — the match is case-sensitive.

## See also

* [CDP audiences](/guides/cdp-segments) — the segments and scores your plan rows protect
* [CDP predictive models](/guides/cdp-predictive-models) — models trained on the events the plan validates
* [CDP API reference](/api-reference/endpoints/cdp) — tracking events, event schemas, and data catalog endpoints
* [CDP event model](/concepts/cdp-event-model) — how `track`, `page`, and `screen` payloads are shaped
