# Error codes

> Stable codes from the API, the SDKs and the panel login, and what to do.

Source: https://staging.patterna.nl/docs/reference-error-codes

Every API error has a stable `code`. Branch on the code, not on the message. Every error also carries the `request_id`; quote it when you ask for help. The error body is described in the [API overview](https://staging.patterna.nl/docs/api-overview).

The tables below list every code the engine returns on the integration paths (serve, ingest, users, flows, experiments, keys), with its status. "Fall back" means: show your own list and keep the page working; "fix" means a bug or a configuration mistake that a fallback must not hide.

## API: serve and ingest

| Code | Status | Meaning | What to do |
|---|---|---|---|
| `bad_request` | 400 | a missing or invalid field, **an unknown JSON field** (bodies are decoded strictly), an empty body or a body over 2 MiB; the `message` names the problem | fix the request |
| `unauthorized` | 401 | missing, revoked or expired key | fix the key |
| `wrong_scope` | 401 | a serve or ingest key on another scope's path (`pk_s_` serve, `pk_i_` ingest, `pk_a_` admin passes everywhere) | use the key of the right scope |
| `not_found` | 404 | a path the API does not have, answered in the usual error body | check the path and the `/v1` prefix |
| `unknown_surface` | 404 | no surface with that key | fix the surface key |
| `unknown_request` | 404 | `POST /interactions` (or `/abandon`) with a `request_id` that has no impression | report with the `request_id` from the slate; for your own list report nothing |
| `unknown_item` | 422 | an interaction for an `item_id` that is not in the slate; `details.unknown_item_ids` lists them | report only items from the slate |
| `displayed_position_required` | 422 | an interaction on a `device_resort` surface without `displayed_position` | send where the app showed the item |
| `bad_tester_id` | 422 | `X-Patterna-Tester` with a value other than `1` | send `1` or leave the header out |
| `webhook_not_configured` | 401 | a Shopify webhook for a tenant without `shopify_webhook_secret` | set the secret with `PATCH /settings` |
| `bad_signature` | 401 | a Shopify webhook whose `X-Shopify-Hmac-Sha256` does not match | check the secret in Shopify and in Patterna |
| `item_not_found` | 404 | a catalog call for an `external_id` and `kind` that do not exist | check the id and the `kind` query parameter |
| `surface_off` | 409 | the surface's status is `off` | fall back; expected while a surface is off |
| `not_shown` | 409 | `POST /recommendations/{request_id}/abandon` for a slate answered with `shown: false` | nothing to do: hide the block; the slate already counts in its arm |
| `mode_conflict` | 422 | a `user_id` on a `non_profiled` request; `PUT /users/{user_id}/profile` with mode `non_profiled`, or without `mode` for a user who opted out | fix: send only `session_id` in `non_profiled` mode; set the mode explicitly before a profile. A consent bug is a privacy bug |
| `mode_required` | 422 | `PUT /users/{user_id}/profile` without `mode` for a user Patterna does not know yet | send `mode` (`personalized` or `cohort`) with the profile, or set it first with `PUT /users/{user_id}/mode` |
| `no_candidates` | 422 | no published item of the surface's kinds and locale survives the flow's filters | fall back, then check the catalog sync, the locale and the filters |
| `pii_rejected` | 422 | a catalog item attribute named `email`, `name` or `ip` | remove the attribute |
| `rate_limited` | 429 | the key's token bucket is empty (serve 200/s, burst 400; ingest 50/s, burst 100); `Retry-After: 1` | fall back on serve; retry ingest later. Never retry `recommend` |
| `quota_exceeded` | 429 | the plan's monthly serve requests are used; `details` carries `quota`, `used` and `resets_at` (the 1st of next month, UTC). Interactions, events and the catalog keep working | show your own list until the reset; never retry `recommend`. Ask us to raise the quota; a `quota_warning` notification arrived at 90% |
| `internal` | 500 | an unexpected error; logged with the `request_id` | fall back; quote the `request_id` |
| `unavailable` | 503 | storage did not answer in time or could not be reached (a timed-out key lookup answers this, never 401); `Retry-After: 1` | fall back on serve, retry once elsewhere; alert if it persists |

Events are not rejected as a batch for content. `POST /events` answers `202` and lists problem events in `rejected[]` with a reason (`unknown type`, `invalid signature`, `user_id or session_id malformed`, …), and counts removed props in `dropped_fields`. See [Ingest](https://staging.patterna.nl/docs/api-ingest#response-202). Each entry in `rejected[]` also carries a stable `code`: `invalid_event`, `pii_rejected` (with `fields`, the paths, never the values), `invalid_signature`, `no_signing_secret` (a signed event while the tenant has no secret), `unknown_request`, `unknown_item` or `displayed_position_required`. The batch still answers `202`.

## API: admin

| Code | Status | Meaning | What to do |
|---|---|---|---|
| `bad_request` | 400 | also an invalid flow config: `config.generators must hold at least one generator`, `unknown generator kind …`, `ranker.kind must be linear, learned_linear or gbdt`, `ranker.kind lambdamart is not available yet`, `filters.diversity must be between 0 and 1`, `exploration.kind must be none or plackett_luce`, `exploration.share must be between 0 and 0.5`; an experiment with a `primary_metric` other than `tap_rate`, `second_item_rate`, `join_rate`, `engaged_after_tap` or `purchase_rate` | fix the config; see [Flow config reference](https://staging.patterna.nl/docs/reference-flow-config) |
| `forbidden_role` | 403 | a panel session (or key) whose role may not do this: viewers read only, editors configure, owners manage keys, people and secrets | ask an owner |
| `forbidden_scope` | 403 | an admin key tried to mint an admin key | an owner mints admin keys in the panel |
| `session_required` | 403 | an action tied to a person (approvals, saved views) called with a key | use the panel |
| `not_found` | 404 | the object does not exist for this tenant | check the id |
| `unknown_flow_version` | 404 | preview of a flow version that does not exist | check the version |
| `experiment_running` | 409 | the surface already has a running experiment | end it with `POST /experiments/{id}/end` first |
| `approval_required` | 409 | activating a flow on a live surface while the two-person rule is on | request an approval |
| `approval_pending` | 409 | an approval for the same change is already open | wait for it or reject it |
| `exists` | 409 | a surface, key or member with that name already exists | pick another name |
| `deletion_scheduled` | 409 | a workspace deletion is already scheduled | cancel it first |
| `unknown_items` | 422 | a curated list or gold set names items that are not in the catalog | fix the ids |
| `invalid_reason_template` | 400 | a flow's `reasons` names an unknown reason key or generator kind, locale or placeholder | use the keys and placeholders in [Reason keys](https://staging.patterna.nl/docs/reference-reason-keys) |
| `signing_secret_exists` | 409 | `POST /settings/signing-secret` when a secret already exists | rotate instead |
| `metric_not_applicable` | 400 | an experiment with `primary_metric: join_rate` on a surface without communities, or `purchase_rate` on a surface without products | pick a metric that fits the surface |

## TypeScript SDK

Every failure is a `PatternaError` with `code`, `op`, `status`, `requestId`, `details` and a `transient` getter.

| Code or class | Meaning | What to do |
|---|---|---|
| `consent_required` (`ConsentError`) | `recommend` without a mode, or an unknown mode | always pass the user's mode; a consent bug is a privacy bug |
| `consent_violation` (`ConsentError`) | a `non_profiled` request with a `user_id` or without a `session_id` | fix the caller; nothing was sent |
| `ValidationError` | bad input, for example an email or `auth0\|...` as user id, or a context key outside the allowlist | fix the caller; the id is never in the message |
| `TimeoutError` | the per-call budget ran out | show your own list |
| `NetworkError` | no answer | show your own list; idempotent calls were retried once |
| `ApiError` | the API answered with an error; `code` is the API's | see the API table |

`withFallback` falls back on every SDK error except `ConsentError` and `ValidationError`, which it rethrows. That includes an `ApiError` with status 400, 401, 404 or `mode_conflict`: log `code` and alert on those, so a bug does not hide behind your own list. The SDK has typed errors for `no_candidates`, `surface_off` and `unknown_surface`.

## Go SDK

Every error is `*Error{Code, Op, Status, RequestID, Message, Details}`. Compare with `errors.Is` or `patterna.IsAPIError(err, "pii_rejected")`.

| Sentinel | Meaning | What to do |
|---|---|---|
| `ErrTimeout` | the deadline passed (budget or a shorter caller deadline) | serve your own list |
| `ErrAborted` | the caller cancelled the context | nothing |
| `ErrCircuitOpen` | the breaker for this scope is open after 5 failures; it stays open 30 s | serve your own list; watch `OnBreakerChange` |
| `ErrQueueFull` | the interaction report queue (1024) is full; the report was dropped | check `DroppedReports()`; look for a slow API |
| `ErrUnknownSurface` | the API answered `404 unknown_surface`; `RecommendWithFallback` serves your own list and sets `rec.Cause` | a configuration bug: alert on it |
| `ErrUnknownItem` | matches the API code `unknown_item` (`422`, an interaction for an item not in the slate) whatever the status | report only items from the slate |
| `ErrItemNotFound` | matches the API code `item_not_found` (`404` from the catalog calls `GetItem`, `DeleteItem`, `SetItemStatus`, `SetItemTags`; SDK 0.4.1) | check the id and the `kind` |

Consent and opaque-id checks in Go fail before anything is sent, like in the TypeScript SDK.

## Panel login

These come from the panel's auth endpoints, which are not in the contract yet.

| Code | Meaning | What to do |
|---|---|---|
| `invalid_credentials` | wrong email or password | try again |
| `rate_limited` | too many attempts | wait a moment |
| `mfa_pending` | signed in, MFA not done yet (401 from `/auth/me`) | enter the MFA code |
| `mfa_invalid` | wrong MFA code | try again with a fresh code |
| `sso_<reason>` | the Orven one-click login failed; the suffix says why | sign in with email and password |
