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; for the
signing contract that gets you past the 401 gate first, see
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.422 SCHEMA_VALIDATION_FAILED— a workspace-authored event schema in strict mode rejected the payload’s JSON shape. Theerror.validation_errorsarray carries{ path, message, expected, received }per failure.422 TRACKING_PLAN_VIOLATION— the tracking plan rejected the event. Theerror.details.violationsarray tells you which rule.
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:
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; thetype field tells you
which one tripped:
missing_required— a property in the plan’srequiredlist arrived missing or null. The producer stopped sending it (or renamed it).type_mismatch— the value’s type differs from the plan’s rule (integersatisfies anumberrule).enum_mismatch— the value fell outside the plan’senumset.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.
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 chosestrict 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:
- 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.
- 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 byevent_name— or from Integrations → CDP → Tracking Plan in the dashboard. Add the property rule, widen theenum, or drop the property fromrequired. - 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.
5. Worked example — reproduce and resolve
Send a signedtrack whose payload violates the plan (the full signing
loop is in CDP ingest signing):
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
softsilently 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 withmeta.request_id, - the exact event body as sent over the wire (before any SDK re-serialization).
See also
- CDP tracking plan — declare events, pick soft vs strict, and read the violations feed.
- CDP ingest signing — the HMAC contract that gets you past the 401 gate.
- CDP API reference — the full ingest endpoint list and the 422 envelope shapes.
- Error code reference —
CDP_UNAUTHORIZED,SCHEMA_VALIDATION_FAILED, andTRACKING_PLAN_VIOLATIONin the index. - Troubleshooting hub — find this page in the hub’s CDP / event ingest accordion.