Skip to main content

Compliance profile lifecycle errors

A compliance profile is the reusable KYC packet that every gated surface — number purchases, Sender-ID registrations, brands, and campaigns — draws from instead of re-asking for the same paperwork. Four error codes guard that packet across its lifecycle: two block a surface that references a profile, two block an operation on the profile itself. This page maps each code to the state that raises it and to the fix you can run yourself.
Compliance posture is tenant-owned: you create the profile, submit it, and decide what it references. Orbit enforces the lifecycle gates; the approval itself always comes from the carrier or regulator reviewing your packet.

The lifecycle state map

A profile moves through one fixed review lifecycle, with three side-exits:
  • draft — freely editable. Nothing submits to a carrier until you submit the profile, so fields, documents, and metadata may change.
  • pending_review — submitted and awaiting carrier or regulator review. The payload freezes: downstream submissions carry a copy of what we sent, and edits to the live packet could fork it from what each carrier holds.
  • approved — review cleared; the profile may open gated surfaces. Still frozen for mutation — once carriers act on the submitted packet, edits would fork our record from theirs.
  • locked (not a status — a guard) — any mutation or submit attempt on a non-draft answers 409 COMPLIANCE_PROFILE_LOCKED because the payload entered a downstream submission. Fix is clone-and-fix, below.
  • in use (a status-independent guard) — any asset (number purchase, sender, brand, or campaign) still referencing the profile answers 409 COMPLIANCE_PROFILE_IN_USE on delete or document-detach, because deletion would strand the downstream reference.
  • rejected / partially_rejected — the carrier refused the packet (or some of it). Editable again; rejected carries the notes to fix before re-submit.
  • expired — the carrier’s validity window lapsed. Treated like rejected for the gating surfaces; re-submit the profile to regain approval.
Only draft, rejected, and partially_rejected are editable; everything else is locked against mutation by design. The statuses gate surfaces in one direction only — a profile governs what it may be attached to, but every surface’s own gate (number pending_compliance, sender pending) is documented on Troubleshoot a pending number or Sender ID.

Map your symptom to the failing operation

Before you read the individual codes, sort the failure by which call you made:

The four codes

COMPLIANCE_PROFILE_REQUIRED — 422

Where it fires. A purchase or registration call on a surface that requires a profile: a number purchase into a regulated country (UK, DE, FR, and similar), a DIDWW DID-group placeholder row, a sender registration, a brand or campaign registration that requires KYC. Why. The surface checked your account for an approved profile covering that use case and country and found none — either because no profile exists yet, or because the existing ones are draft, pending_review, rejected, or expired. Fix. Two paths:
  1. You already have a suitable profile — pass its id. For a number purchase, send compliance_profile_id on the purchase request; for a sender or brand registration, attach the profile from its console surface (Settings → Compliance → Sender IDs / Brand identity). Some surfaces pick up the only approved profile automatically; the explicit id is always safer.
  2. You do not — create a profile (Settings → Compliance → Profiles, or POST /api/v1/compliance/compliance-profiles), fill its fields against the country’s checklist (see Country Compliance Requirements), submit it, and wait for approved. Only then re-run the blocked purchase.

COMPLIANCE_PROFILE_NOT_APPROVED — 422

Where it fires. The same attach path as above, when the request references a profile whose status is not approved. Why. The gate reads the profile’s status: draft, pending_review, rejected, partially_rejected, or expired all fail identical to a missing profile. A pending_review retry loop does nothing but waste lead time. Fix. Read the profile’s status first (the Profiles list in the dashboard, or GET /api/v1/compliance/compliance-profiles/:id).
  • pending_review — wait for the carrier’s review to complete; attach succeeds the moment it flips to approved. See the lead times on Troubleshoot a pending number or Sender ID.
  • rejected / partially_rejected — read the reviewer notes on the Profiles page, fix what they flagged on the still-editable draft, and re-submit. A re-submit moves the profile back to pending_review; only an approved packet clears this gate.
  • expired — treat as rejected: clone and re-submit (see LOCKED below for why clone is the path once a profile is frozen).

COMPLIANCE_PROFILE_LOCKED — 409

Where it fires. Any attempt to mutate a profile that has moved past draft: editing structured fields or metadata, updating the use-case or country, attaching or replacing documents, or re-submit. Why. Once a profile submits to providers, its payload freezes so the copy carriers hold and the copy Orbit reads stay in sync — an edit to a post-submit profile would fork those copies. draft, rejected, and partially_rejected stay editable precisely because nothing depends on them yet; everything else is locked by design. Fix. Clone-and-fix:
Clone copies the profile — name suffixed (copy), structured data, and document attachments — into a new draft with a new id and no provider submissions yet. Edit the clone, submit it, and re-attach it to every downstream asset the old profile fed (with POST /api/v1/numbers/:id/attach-compliance-profile for numbers, or the sender-ID / brand surfaces’ attach endpoints). This is also the path for expired profiles once you need to re-verify them.

COMPLIANCE_PROFILE_IN_USE — 409

Where it fires. DELETE on the profile, or a document-detach call on one of its underlying documents, when any active downstream entity still references the profile — a purchased number, a registered sender ID, a brand, or a campaign. Why. Deletion would strand the downstream asset without its KYC backing, so the API refuses and names the conflict. Fix. Detach downstream first, then retry the delete. Walk your numbers, sender IDs, brands, and campaigns; for each asset referencing the profile, either attach a different approved profile or retire the asset. The conflict check runs server-side, so run the detach walk from the console surfaces each asset exposes. Only when nothing references the profile does the delete (or the document-detach) succeed.

When a non-approved profile is acceptable

Three lawful uses survive the NOT_APPROVED gate:
  1. Draft-time assembly — while you fill the profile against its country’s checklist, nothing is blocked: COMPLIANCE_PROFILE_REQUIRED answers only the surface that wants an approved reference, not the profile itself.
  2. Attach by explicit compliance_profile_id with optimistic reference — some surfaces accept a profile id and queue against pending_compliance until approval lands (see the pending-gated runbook for the numbers flow). Referencing a non-approved profile goes through as long as the surface supports queuing; the 422 fires when the surface requires an approved profile now.
  3. Clone-and-fix cycles — editing the clone and re-submitting replaces the old profile gradually; the old asset keeps serving traffic until you re-attach, so an asset mid-swap is not an approved profile failure.
Outside these lanes, only approved opens the gate.

Retry class map

From Error Codes: each class permits a different retry behaviour, so classify before you loop. None of the four is a transient fault, and none carries details.retry_after — the 409-class conflicts are resolved by state, not by waiting.

Escalate with the full payload

If a profile-side error persists past the fix above, open a support ticket carrying:
  • The profile id (cprof_…), its current status, and the operation you attempted (attach / mutate / delete).
  • The error code and HTTP status from the response envelope.
  • The referencing asset id (num_…, sender id, brand id, or campaign id) for REQUIRED / NOT_APPROVED / IN_USE.
  • The meta.request_id from the failed response — the durable handle support uses to pull the attempt directly.
  • For LOCKED: the id of the clone you created and its own status.
With those five, the reviewer reads the profile’s state, its downstream references, and the exact request without a second round-trip.