> ## 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.

# Troubleshooting: TRACKING_PLAN_VIOLATION on CDP ingest

> Recovery runbook for the 422 TRACKING_PLAN_VIOLATION rejection on signed CDP ingest — decode which plan rule fired, decide between fixing the producer or amending the plan, and know why no retry will pass a tenant-owned policy gate.

# Troubleshooting: TRACKING\_PLAN\_VIOLATION on CDP ingest

A signed-HMAC call to `/cdp/v1/{ingest_id}/{track|page|screen|group|batch}`
came back `422 TRACKING_PLAN_VIOLATION`. The signature was fine — your
workspace's tracking plan owns this gate, and the plan rejected the event
itself. This page decodes which plan rule fired, how to decide between
relaxing the plan and fixing the event, and what not to retry.

The same class of failure in soft mode is invisible here — soft-mode
violations log without rejects, so this page only has something to say once
your plan flips an event to `enforcement=strict`. For the plan mechanics,
see the [CDP tracking plan guide](/guides/cdp-tracking-plan); for the
signing contract that gets you past the 401 gate first, see
[CDP ingest signing](/guides/cdp-ingest-signing).

## 1. Confirm it is the plan, not the signature or the schema

Three rejection shapes live on the ingest surface, and each has a different
owner:

* `401 CDP_UNAUTHORIZED` — the HMAC check failed (signature, timestamp
  skew, nonce reuse). That is a transport problem; fix the signing loop,
  not the payload. See
  [CDP ingest signing](/guides/cdp-ingest-signing#5-expected-responses-and-failure-modes).
* `422 SCHEMA_VALIDATION_FAILED` — a workspace-authored event schema in
  strict mode rejected the payload's JSON shape. The `error.validation_errors`
  array carries `{ path, message, expected, received }` per failure.
* `422 TRACKING_PLAN_VIOLATION` — the tracking plan rejected the event.
  The `error.details.violations` array tells you which rule.

Read `error.code` before anything else. Only the third one is a plan
decision; the other two never reach the plan.

## 2. Read `details.violations` — the rule that fired

The envelope names the event and lists every rule it broke:

```json theme={null}
{
  "error": {
    "code": "TRACKING_PLAN_VIOLATION",
    "status": 422,
    "message": "Event rejected by tracking plan (enforcement=strict). See `details.violations` for the mismatch list.",
    "details": {
      "event_name": "signed_up",
      "violations": [
        {
          "type": "extra_property",
          "path": "properties.referrer",
          "expected": "absent",
          "actual": "organic"
        }
      ]
    }
  },
  "meta": { "request_id": "req_7xKqT3", "timestamp": "2026-08-26T12:00:41.000Z" }
}
```

Each violation gives `type`, `path` into the payload, the `expected` shape,
and the `actual` value that arrived. The same rows land in the violations
feed, so you can page back through a burst:
`GET /api/v1/cdp/tracking-plan/violations?event_name=<name>&enforcement=strict`.
The dashboard view is **Integrations → CDP → Tracking Plan → Violations**.

## 3. Which rule fired — the five violation types

The plan's strict mode fires these in order; the `type` field tells you
which one tripped:

* `missing_required` — a property in the plan's `required` list arrived
  missing or null. The producer stopped sending it (or renamed it).
* `type_mismatch` — the value's type differs from the plan's rule
  (`integer` satisfies a `number` rule).
* `enum_mismatch` — the value fell outside the plan's `enum` set.
* `extra_property` — a property arrived that the plan does not declare
  (only flagged once the schema declares at least one property).
* `unknown_event` — the event name has no plan row. This one is recorded
  soft and accepted, so it never causes a 422 — it shows up in the feed so
  SDK drift is visible without costing events.

A `missing_required` on every event of a type almost always means a
producer version stopped sending the property. An `extra_property` means
the producer sends something the plan does not yet declare.

## 4. The switch — your plan runs enforcement=strict

The tracking plan is a tenant-owned policy gate: you declared the event
contract, and you chose `strict` on that event's row. The platform enforces
your choice — it cannot override it, and retrying the same bytes changes
nothing. Decide among three fixes:

1. **Fix the producer to match the plan.** The payload drifted; correct
   the SDK and re-send. This is the right path when the plan reflects the
   contract you actually want.
2. **Amend the plan.** The new shape is legitimate (a renamed property, a
   new optional field, a wider enum). Update the row with
   `POST /api/v1/cdp/tracking-plan` — it upserts by `event_name` — or from
   **Integrations → CDP → Tracking Plan** in the dashboard. Add the
   property rule, widen the `enum`, or drop the property from `required`.
3. **Drop the row from strict to soft.** If a producer you cannot
   force-update (a mobile SDK on an old version) is the one violating,
   flip the event back to `enforcement=soft`. Violations keep logging, the
   events are accepted, and you stop the data loss while you fix the
   producer. New rows should start soft for exactly this reason.

Pick (1) or (2) deliberately; falling back to (3) permanently means the
gate no longer gates.

## 5. Worked example — reproduce and resolve

Send a signed `track` whose payload violates the plan (the full signing
loop is in [CDP ingest signing](/guides/cdp-ingest-signing)):

```bash theme={null}
INGEST_ID="cdpsec_01J..."
SECRET="plaintext-from-post-api-v1-cdp-secrets"
BODY='{"event":"signed_up","userId":"user_42","properties":{"referrer":"organic"}}'
T=$(date +%s)
NONCE=$(openssl rand -hex 16)
SIG=$(printf '%s.%s.%s' "$T" "$NONCE" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')

curl -X POST "https://api.orbit.devotel.io/cdp/v1/$INGEST_ID/track" \
  -H "Content-Type: application/json" \
  -H "X-Orbit-CDP-Signature: v1=$SIG" \
  -H "X-Orbit-CDP-Timestamp: $T" \
  -H "X-Orbit-CDP-Nonce: $NONCE" \
  --data "$BODY"
```

The 422 envelope comes back shaped like section 2. Take the `path` and
`actual` from `details.violations` straight into the violations feed query,
fix per section 4, then re-send the corrected bytes. The rejected
`messageId` is never consumed — a corrected retry with your own
`messageId` is accepted cleanly.

## 6. What not to retry

* **Do not retry the same payload.** A plan violation is a deterministic
  tenant-owned decision — no backoff window, retry budget, or queue will
  change the verdict. A retry loop here just burns events against the same
  rule.
* **Do not drop the events to a dead letter and "fix later".** Strict
  rejection means the event is gone; fix the producer or the plan first,
  then re-send.
* **Do not weaken the plan to `soft` silently as the permanent fix.** That
  trades a visible 422 for invisible drift. If you flip to soft, track the
  violations feed until the producer is clean, then flip back.
* **Do not ask Support to relax the gate for you.** The plan and its
  enforcement level are tenant-owned policy — the platform cannot override
  them, by design.

## 7. Support bundle — what to send when you escalate

If the violations feed disagrees with what your producer claims to send
(suspected parse-time mutation, a proxy re-serializing properties), open a
ticket with:

* the ingest id from the request URL (`cdpsec_...`),
* the plan row id (`ctp_evt_...`) and the enforcement it ran,
* a violations-feed row (`ctp_vio_...`) or the raw 422 envelope with
  `meta.request_id`,
* the exact event body as sent over the wire (before any SDK
  re-serialization).

That is enough for Support to tell whether the divergence is on the wire
or in the plan row itself.

## See also

* [CDP tracking plan](/guides/cdp-tracking-plan) — declare events, pick
  soft vs strict, and read the violations feed.
* [CDP ingest signing](/guides/cdp-ingest-signing) — the HMAC contract that
  gets you past the 401 gate.
* [CDP API reference](/api-reference/endpoints/cdp) — the full ingest
  endpoint list and the 422 envelope shapes.
* [Error code reference](/reference/error-codes) — `CDP_UNAUTHORIZED`,
  `SCHEMA_VALIDATION_FAILED`, and `TRACKING_PLAN_VIOLATION` in the index.
* [Troubleshooting hub](/reference/troubleshooting-hub) — find this page in
  the hub's **CDP / event ingest** accordion.
