Error codes
Stable codes from the API, the SDKs and the panel login, and what to do.
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.
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. 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 |
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 |
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 |