Get started
Errors & limits
Every 4xx/5xx response is application/problem+json with a stable string code. Branch on the code — never on the human-readable title or detail.
Error codes
Every 4xx/5xx response carries application/problem+json with a stable string code extension. Branch on the code — never on title or detail strings.
| Code | HTTP | Meaning | Remediation |
|---|---|---|---|
| auth.unauthorized | 401 | Unauthorized The request did not include valid credentials. | Send an `Authorization: Bearer <jwt>` header, or `X-Api-Key: ak_…` (account) / `X-Api-Key: ik_…` (integration) for an API key. |
| auth.forbidden | 403 | Forbidden The credentials are valid but lack permission for this operation. | Confirm the user owns the resource or has the required role. |
| auth.invalid_credentials | 401 | Invalid credentials Email/password combination did not match any account. | Verify the credentials. Repeated failures are throttled by the per-IP login limit. |
| auth.api_key_invalid | 401 | API key invalid The provided API key did not match a known prefix, or its hash did not validate. | Re-copy the key from the API keys page. If you regenerated the key, the old value is permanently invalid. |
| auth.api_key_disabled | 401 | API key disabled The API key has been disabled by an administrator. | Regenerate the key from the persona's settings, or contact support if the disable was unexpected. |
| auth.not_owner | 403 | Not the resource owner The authenticated account is not the owner of the requested resource. | Log out and sign in with the account that owns this resource. |
| auth.origin_not_allowed | 403 | Origin not allowed The request originated from a domain that is not in the account's allowlist. | Add the calling domain to the account's allowed origins, or contact support. |
| auth.consent_required | 403 | Consent required The end user has not granted the consent required for this operation. | Prompt the end user to complete the consent flow before retrying. |
| auth.user_not_initialized | 403 | User not initialized The authenticated account is missing required setup data. | Complete account onboarding (profile, billing, etc.) and retry. |
| limits.rate_exceeded | 429 | Rate limit exceeded Too many requests. Check the `Retry-After` header for the recommended wait time. The `extensions.scope` field hints at the partition (`user`, `ip`, `session`, `api_key_slot`). | Honor `Retry-After`. If `scope` is `api_key_slot`, switching to the other key slot may unblock immediately. |
| limits.concurrent_session | 429 | Concurrent session limit The account already has the maximum number of active sessions. | Close another active session or upgrade the subscription tier. |
| limits.waiting_room_full | 503 | Waiting room full The waiting room queue is at capacity. | Back off and retry after a short delay. |
| limits.quota_exceeded | 402 | Quota exceeded The account has consumed its allotted quota (minutes, knowledge units, storage, etc.). | Upgrade the subscription tier or wait for the next billing cycle. |
| limits.subscription_required | 402 | Subscription required This operation requires an active paid subscription. | Purchase a subscription on the pricing page. |
| limits.session_time_limit | 409 | Session time-of-day limit Sessions cannot start at the current time per the persona's schedule. | Retry within the persona's configured availability window. |
| limits.session_interaction_limit | 429 | Session interaction limit (WebSocket close 4005) Returned only as WebSocket close 4005, never as HTTP. The live session reached the persona's per-session interaction cap (PublishSettings.SessionInteractionLimit, counted in billing units — Full mode = 2 per turn, Chat mode = 1). | End-user UI should show a session-ended message; starting a new session is allowed (subject to cross-session limits). |
| limits.session_duration_limit | 409 | Session duration limit (WebSocket close 4006) Returned only as WebSocket close 4006, never as HTTP. The live session exceeded the persona's per-session duration cap (PublishSettings.SessionDurationLimitSeconds, wall-clock — includes Standby/idle time). | End-user UI should show a session-ended message; starting a new session is allowed (subject to cross-session limits). |
| upload.file_too_large | 413 | File too large The uploaded file exceeds the per-endpoint size limit. | Compress the file or split it. See the endpoint reference for per-endpoint size caps. |
| upload.invalid_signature | 400 | Invalid file signature The file content does not match the declared content type (e.g. an `.exe` renamed to `.pdf`). | Verify the file is genuinely the type its extension claims. |
| upload.unsupported_type | 415 | Unsupported file type The file's content type is not accepted by this endpoint. | Convert the file to one of the supported types listed in the endpoint reference. |
| upload.page_limit_exceeded | 413 | Page limit exceeded The document exceeds the maximum page count for ingestion. | Split the document into smaller files. |
| resource.not_found | 404 | Resource not found No resource exists at the addressed URL, or it is not visible to the caller. | Verify the ID, then the caller's permissions. |
| resource.conflict | 409 | Resource conflict The operation conflicts with the resource's current state (duplicate, version skew, etc.). | Re-read the resource and retry with current state. |
| session.turn_in_flight | 409 | Turn already in flight A turn is already being processed for this session. /v1/chat serializes writers, so the second caller is rejected before anything persists. | Retry once the in-flight turn returns. Do not send concurrent turns for one session id. |
| session.owned_by_live | 409 | Session owned by a live surface A live (WebSocket) surface currently owns this session, so it cannot be driven over REST at the same time. | Continue the conversation over the live connection, or start a new session id for the REST caller. |
| brand.confirm_required | 409 | Confirm the brand delete The brand still has sources, so deleting it removes every document of every source. The delete is refused until the brand's display name is sent back as `?confirm=`. | Ask the owner to type the brand's name, then repeat the DELETE with `?confirm=<display name>`. The `detail` says how much would go. |
| brand.has_integration_streams | 409 | Brand has integration streams One or more of the brand's sources are an integration's streams. An integration is an account-level connection, so a brand delete does not sever it. | Move those streams to another brand (on the integration), or delete the integration first. |
| brand.onboarding_active | 409 | Onboarding still building into this brand An onboarding job is still building a persona and crawling knowledge under this brand. | Wait for the onboarding to finish, then delete. |
| brand.removal_busy | 409 | Another bulk knowledge job is running Only one bulk knowledge job runs per account at a time, and one is already queued or running. | Poll the running job at `GET /api/knowledge/documents/bulk-jobs/active` and retry when it completes. |
| knowledge.source_not_found | 404 | Source not found No source of this name exists in the account. A write never creates a source implicitly. | Create it first (`POST /v1/knowledge/sources { name, brand_id }`), then write into it. |
| knowledge.source_name_taken | 409 | Source name belongs to another brand A source of this name already exists under a different brand of the account. Names are unique per account. | Move the existing source to this brand (`PATCH /sources/{source}/brand`), or use another name. |
| knowledge.source_managed | 409 | Source is managed by an integration The source is one of an integration's streams: only that integration writes it, so it is read-only to the dashboard and the account key. That is what keeps it 1:1 with the connected site. | Change the content on the site and let it sync. To remove the stream, switch it off or delete the integration. |
| knowledge.source_type_taken | 409 | Source name belongs to a source of the other type A source of this name already exists under the same brand but holds the other thing — a catalog where files were asked for, or the reverse. A source's type is fixed when it is created, so this is a different source under a taken name. | Pick another name, or write into the existing source as what it already is. |
| knowledge.source_type_mismatch | 409 | Wrong kind of source for this write A source holds a catalog or files and never both. Typed entries go into a catalog; an uploaded file, page or post goes into a source of files. The route and the source disagree. | Send the write to a source of the matching type, or create one (`POST /v1/knowledge/sources { name, brand_id, type }`). |
| knowledge.too_many_entries | 422 | Too many images or links on one document A document carries at most `Knowledge:Acceptor:MaxLibraryEntries` images and as many links (15). The list sent was longer; nothing was dropped and nothing was written. | Send the first entries only — the primary image first — and keep the rest out of the library. |
| knowledge.source_confirm_required | 409 | Confirm the source delete The source still has documents, so deleting it removes them all with their category schemas. The delete is refused until the source's name is sent back as `?confirm=`. | Ask the owner to type the source name, then repeat the DELETE with `?confirm=<name>`. The `detail` says how many documents would go. |
| onboarding.crawl_active | 409 | Website crawl still filling this source An onboarding's crawl is queued or running into this source. A running crawl has no cancellation hook, and a queued one is minutes from writing. | Wait for the onboarding's knowledge track to finish (`GET /api/onboarding/jobs/{jobId}`), then delete. |
| onboarding.active | 409 | An onboarding is already running One onboarding at a time per account. The response carries the running job's `jobId`, `brandId` and `personaId`. | Attach to the running job (`GET /api/onboarding/jobs/{jobId}`) instead of starting another. |
| onboarding.website_unreadable | 422 | Website could not be read The scrape of the site's homepage failed — the site refused, timed out, or returned nothing usable. | Check the URL, or fill the brand in by hand (`POST /api/onboarding/jobs` with `brand` and no `website_url`). |
| onboarding.source_taken | 409 | The crawl's source is taken The source named by the site's host already exists as a catalog, or belongs to another brand of the account. | Free the name (rename or delete that source) or move it to this brand, then start again. |
| knowledge.removal_busy | 409 | Another bulk knowledge job is running Only one bulk knowledge job runs per account at a time, and one is already queued or running. | Poll the running job at `GET /api/knowledge/documents/bulk-jobs/active` and retry when it completes. |
| knowledge.summary_in_progress | 409 | Summary build already queued A build of this source's summary is already queued or running. | Read the state back in `GET /api/knowledge/sources`; request another rebuild after it completes. |
| knowledge.concurrent_write | 409 | Record created concurrently Another write created this record at the same moment, so this one was not saved. | Send the record again; it updates the record now. |
| knowledge.import_running | 409 | Import running The source is being imported right now. | Follow `status` and `import` in `GET /api/knowledge/sources` and ask again once the import is over. |
| knowledge.nothing_skipped | 409 | Nothing to import The source's last import skipped nothing, or its site sends what was skipped itself (a WordPress stream). | Nothing to do; `import.skipped` in `GET /api/knowledge/sources` shows what the plan left out. |
| integration.disconnected | 409 | Integration disconnected The integration was disconnected on personaizer.com, so its streams are frozen read-only and its key no longer writes. | Reconnect from the site's plugin settings; the same integration resumes with its history. |
| integration.closed | 409 | Closed to the site The site pushed into a door that is shut: the integration is disconnected, or the stream is switched off or was never switched on. | Drop the records you were about to push and call `POST /v1/integration/sync`; it says which streams are on. |
| integration.stream_taken | 409 | Stream name taken The source name this stream would use already belongs to another source of the account. | Rename or move the existing source on personaizer.com, then reconnect. |
| integration.reconcile_stale_generation | 409 | Reconcile generation stale The stream's full list names a generation older than the one last applied, so applying it would undo newer syncs. | Rebuild the list from the site's current state and send it with a newer generation. |
| integration.persona_of_other_brand | 409 | Persona belongs to another brand The widget persona chosen on the consent screen speaks for a different brand than the one approved. | Pick a persona of this brand, or move the persona to it first. |
| validation.failed | 400 | Validation failed One or more request fields failed validation. The `errors` extension contains per-field details. | Inspect `extensions.errors` and correct the highlighted fields. |
| server.error | 500 | Server error An unexpected server-side error occurred. | Retry with backoff. If the failure persists, contact support with the response's `traceId` extension if present. |
Rate limits
When a limit is exceeded the response is 429 Too Many Requests with a Retry-After header (seconds) and ProblemDetails body code = limits.rate_exceeded. The extensions.scope field tells you whether the partition was per-user, per-IP, or per-API-key-slot.
| Policy | Limit | Window | Partition | Purpose |
|---|---|---|---|---|
| AuthSensitive | 5 | 30 seconds | per IP scope: ip | Login, register, password reset, email confirmation. Strict per-IP limit so credential-stuffing campaigns from a single source get throttled fast. |
| AuthRefresh | 30 | 1 minute | per IP scope: ip | Refresh-token exchange. Looser than AuthSensitive but still per-IP to bound rotation abuse. |
| PersonaUploads | 20 | 1 minute | persona_or_user_or_ip scope: persona | Persona texture/asset uploads. Per-user when authenticated, per-IP otherwise. |
| KnowledgeUploadsRate internal services bypass | 60 | 1 minute | persona_or_user_or_ip scope: persona | Knowledge document uploads (PDF/DOCX/etc.). Limit is sourced from KnowledgeAcceptorSettings.RateLimit so the same knob controls accept-time check and throttle. |
| SubscriptionChange | 5 | 10 minutes | persona_or_user_or_ip scope: persona | Subscription tier changes and cancellations. Tight to prevent oscillation abuse. |
| MutationDefault internal services bypass | 300 | 1 minute | persona_or_user_or_ip scope: persona | Generic per-user safety net for state-changing endpoints (POST/PUT/PATCH/DELETE) without a stricter category-specific policy. Loose by design — never bites a real user, catches scripted abuse. |
| Global internal services bypass | 300 | 1 minute | per IP scope: ip | Per-IP safety net applied to every request. Loose by design — never bites a real user, catches scripted abuse on untagged endpoints. Stricter category-specific policies apply on top. |
| SessionSnapshot | 30 | 1 minute | per session scope: session | Snapshot uploads per active session (matches expected camera-capture cadence with headroom). Partitioned by sessionId. |
| SessionRead | 240 | 1 minute | per session scope: session | Per-session image / snapshot reads. Looser than upload — the same client may pull many cached assets. |
| SessionWebSocket | 30 | 1 minute | per IP scope: ip | WebSocket upgrade attempts per source IP. Bounds concurrent-connection abuse before a session is fully established. |
| V1Chat | 60 | 1 minute | api_key scope: api_key | Headless chat (POST /v1/chat) per persona API key. Quota (1 interaction per call) and the per-session turn lock bite first for sustained traffic; this bounds bursts. |
| V1CardClick | 60 | 1 minute | api_key scope: api_key | Card click tracking (POST /v1/cards/click) per persona API key. Free (no credit charge), so this rate limit is the only abuse guard. |
| WaitingRoomQueue | 30 | 1 minute | per user (per IP if anonymous) scope: user | Queue join / finalize / cancel actions. Per-user when authenticated, per-IP for anonymous guests so shared egress (corporate proxy, mobile CGN) doesn't share a bucket between legitimate users. |
| WaitingRoomQueueWs | 60 | 1 minute | per IP scope: ip | WebSocket upgrade attempts for queue position polling. Per-IP so one host can't open thousands of long-lived sockets to exhaust capacity. |