REST API

Server-side endpoints for knowledge management, persona reads, and subscription limits. Authenticate with an API key and call these from your backend to integrate PersonAIzer into your own flows.

Base URL: https://core.personaizer.com

Authentication

All API requests require an API key. Create and manage keys in the dashboard at Settings → API Keys. The full key is shown only once at creation — store it securely on your server and never expose it in client-side code or version control. Keys can be individually disabled or deleted from the dashboard at any time.

Request header

X-Api-Key: pa_AbCdEfGhIjKlMnOpQrStUv

Example request

curl https://core.personaizer.com/api/knowledge/docs \
  -H "X-Api-Key: pa_AbCdEfGhIjKlMnOpQrStUv"

Zero-downtime key rotation

1

Create a second key from the dashboard — Settings → API Keys → New key

2

Update your integration to send the new key

3

Disable the old key from the dashboard, verify no traffic, then delete it

Restricted endpoints: Account settings, billing, and API key management only accept a browser session — API keys receive 403 auth.forbidden on those routes by design. All knowledge, persona, and subscription endpoints accept API keys.

Identifiers — always use external_id

Every knowledge document has two identifiers: an internal server-generated UUID and a caller-supplied external_id. Always use external_id — it is stable across re-uploads, survives content updates, and is the only identifier exposed in the public API.

Recommended

# Your own stable slug — survives re-uploads, human-readable
PUT /api/knowledge/docs
{
  "items": [
    { "external_id": "product-SKU-9821", "content": "..." },
    { "external_id": "article-how-to-reset", "content": "..." }
  ]
}

Avoid

# ✗ Unstable — row 42 today could mean a different product tomorrow.
#   Timestamp-based IDs create duplicates on every re-upload.
{ "external_id": "42", ... }
{ "external_id": "1747123456789", ... }
Format1–128 characters matching ^[A-Za-z0-9._:\-]{1,128}$. Use your own stable slug: product SKU, CMS record ID, article slug.
Requiredexternal_id is required for every item in the batch endpoint. It is the only way to later update, delete, or activate a document for a persona.
IdempotentUploading the same external_id twice with identical content is free — the server returns status: "no_op" and skips re-embedding. Safe for cron re-pushes and webhook retries.

Two upload patterns

The API offers two endpoints with different processing semantics. Choose based on volume and whether you want immediate availability or batch control.

Batch upsert

PUT /api/knowledge/docs

Accepts up to 100 documents per call. Docs land as staged — saved but not queued for embedding. Use for bulk catalog syncs where you want to control when embedding runs.

  • Full create-or-update (upsert) semantics keyed on external_id
  • Docs stay staged until you trigger processing from the dashboard
  • No-op items (unchanged content) return instantly at zero cost
# Upload a catalog — docs land as "staged", Tasks ignores them.
PUT /api/knowledge/docs
{
  "items": [
    { "external_id": "sku-001", "content": "Blue running shoes, size 10, $89." },
    { "external_id": "sku-002", "content": "Red trail runners, size 9, $109." }
  ]
}

# Response — docs are staged, not yet embedded:
# { "created": 2, "updated": 0, "no_op": 0, "failed": 0,
#   "results": [
#     { "external_id": "sku-001", "status": "created" },
#     { "external_id": "sku-002", "status": "created" }
#   ] }

Single upsert

PUT /api/knowledge/docs/by-external-id/{external_id}

Upserts one document and immediately queues it for embedding. Use for real-time updates — a product price change, a new article — where you want the content available as quickly as possible.

  • Returns re_embedded: true when content changed
  • Returns re_embedded: false for unchanged content (free)
  • HTTP 201 on first call, 200 on subsequent updates
# Single upsert — immediately queued for embedding.
PUT /api/knowledge/docs/by-external-id/sku-001
{
  "content": "Blue running shoes, size 10, now $79."
}

# Response: HTTP 200 (existing) or 201 (new)
# { "external_id": "sku-001", "status": "updated", "re_embedded": true }

Tip: For large initial catalog imports use the batch endpoint, then trigger processing from the dashboard once your data looks correct. For ongoing real-time sync (webhooks, event-driven updates) use the single endpoint.

Persona activation

A knowledge document is only surfaced in a persona's context if it is activated for that persona. Activation is the join between a document and a persona — a document can be active for many personas or none. Only embedded (fully processed) documents participate in retrieval; staging or activating a staged document is valid but it won't appear in conversations until embedding completes.

Replace the full activation list

# Replace the full activation list for a persona.
PUT /api/personas/{persona_id}/knowledge
{
  "external_ids": ["sku-001", "sku-002", "article-how-to-reset"]
}

Replaces the persona's entire activation list. Any document not in external_ids is deactivated.

Add documents without disrupting existing activations

# Add docs without touching the existing list (additive).
POST /api/personas/{persona_id}/knowledge
{
  "external_ids": ["sku-003", "sku-004"]
}

# Response: { "added": 2, "skipped": 0 }

Additive — existing activations are untouched. Already-active documents are counted as skipped, not duplicated.

Remove a single document

# Remove a single doc from a persona — idempotent.
DELETE /api/personas/{persona_id}/knowledge/sku-003

Idempotent — returns 204 No Content whether or not the document was active.

GET/api/personas

List all personas for your account.

GET/api/personas/{persona_id}

Get a single persona by ID.

GET/api/personas/{persona_id}/knowledge

List external_ids currently activated for a persona.

Checking your limits

Before large uploads, check your remaining quota. Knowledge Units (Ku) measure embedded content volume; Storage Units (Su) measure raw file storage in MB.

curl https://core.personaizer.com/api/subscription/limits \
  -H "X-Api-Key: pa_AbCdEfGhIjKlMnOpQrStUv"

# { "usage": { "knowledgeUnitsUsed": 12480, "knowledgeUnitsLimit": 100000,
#              "storageUnitsUsed": 4.2, "storageUnitsLimit": 512 } }

Uploading beyond your limit returns 402 knowledge.quota_exceeded. The batch endpoint checks quota upfront — if the batch would exceed your limit the entire request is rejected, nothing is written.

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 `Authorization: Bearer pa_live_…` / `X-Api-Key: pa_live_…` 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 slot has been disabled by the account owner.
Switch the application to the other key slot (Primary ↔ Secondary), or regenerate the disabled slot to re-enable it.
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.
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 minuteper user (per IP if anonymous)
scope: user
Persona texture/asset uploads. Per-user when authenticated, per-IP otherwise.
KnowledgeUploadsRate
internal services bypass
601 minuteper user (per IP if anonymous)
scope: user
Knowledge document uploads (PDF/DOCX/etc.). Limit is sourced from KnowledgeAcceptorSettings.RateLimit so the same knob controls accept-time check and throttle.
SubscriptionChange510 minutesper user (per IP if anonymous)
scope: user
Subscription tier changes and cancellations. Tight to prevent oscillation abuse.
MutationDefault
internal services bypass
3001 minuteper user (per IP if anonymous)
scope: user
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.
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.