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.

CodeHTTPMeaningRemediation
auth.unauthorized401
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.forbidden403
Forbidden
The credentials are valid but lack permission for this operation.
Confirm the user owns the resource or has the required role.
auth.invalid_credentials401
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_invalid401
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_disabled401
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_owner403
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_allowed403
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_required403
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_initialized403
User not initialized
The authenticated account is missing required setup data.
Complete account onboarding (profile, billing, etc.) and retry.
limits.rate_exceeded429
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_session429
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_full503
Waiting room full
The waiting room queue is at capacity.
Back off and retry after a short delay.
limits.quota_exceeded402
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_required402
Subscription required
This operation requires an active paid subscription.
Purchase a subscription on the pricing page.
limits.session_time_limit409
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_limit429
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_limit409
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_large413
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_signature400
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_type415
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_exceeded413
Page limit exceeded
The document exceeds the maximum page count for ingestion.
Split the document into smaller files.
resource.not_found404
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.conflict409
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_flight409
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_live409
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_required409
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_streams409
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_active409
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_busy409
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_found404
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_taken409
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_managed409
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_taken409
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_mismatch409
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_entries422
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_required409
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_active409
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.active409
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_unreadable422
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_taken409
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_busy409
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_progress409
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_write409
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_running409
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_skipped409
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.disconnected409
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.closed409
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_taken409
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_generation409
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_brand409
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.failed400
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.error500
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.

PolicyLimitWindowPartitionPurpose
AuthSensitive530 secondsper IP
scope: ip
Login, register, password reset, email confirmation. Strict per-IP limit so credential-stuffing campaigns from a single source get throttled fast.
AuthRefresh301 minuteper IP
scope: ip
Refresh-token exchange. Looser than AuthSensitive but still per-IP to bound rotation abuse.
PersonaUploads201 minutepersona_or_user_or_ip
scope: persona
Persona texture/asset uploads. Per-user when authenticated, per-IP otherwise.
KnowledgeUploadsRate
internal services bypass
601 minutepersona_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.
SubscriptionChange510 minutespersona_or_user_or_ip
scope: persona
Subscription tier changes and cancellations. Tight to prevent oscillation abuse.
MutationDefault
internal services bypass
3001 minutepersona_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
3001 minuteper 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.
SessionSnapshot301 minuteper session
scope: session
Snapshot uploads per active session (matches expected camera-capture cadence with headroom). Partitioned by sessionId.
SessionRead2401 minuteper session
scope: session
Per-session image / snapshot reads. Looser than upload — the same client may pull many cached assets.
SessionWebSocket301 minuteper IP
scope: ip
WebSocket upgrade attempts per source IP. Bounds concurrent-connection abuse before a session is fully established.
V1Chat601 minuteapi_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.
V1CardClick601 minuteapi_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.
WaitingRoomQueue301 minuteper 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.
WaitingRoomQueueWs601 minuteper 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.