Product AI & Vision
How the PlatformXe AI gateway and vision service work — AI profiles, feature keys, limits, daily quotas, tool calling, knowledge retrieval, error codes and idempotency.
PlatformXe runs your product's AI traffic through one gateway, so policy, limits and spend are controlled in one place. Your app sends prompts; PlatformXe decides what each feature may do.
Scope
PlatformXe is an infrastructure API. The AI & Vision gateway is one more capability on the same API as messaging, storage and OCR, and it is deliberately narrow:
- Your app owns the prompt, the context and the decision. PlatformXe never decides for the caller, never executes a tool call (it returns the call; your code runs it) and never acts on a result.
- One provider-neutral contract:
complete,extract,analyze-roomand knowledge search. The provider and model behind a profile — Gemini, Grok or Claude today — are profile settings Caldera changes without a client change. - Zero data retention on every request: the provider keeps neither prompt nor output; PlatformXe keeps a usage row (tokens, cost, feature) and a short-lived audit detail that the retention job purges.
- Metered in naira: the gateway's cost for every call is recorded against your feature, and each profile's daily caps stop a runaway feature.
- Out of scope: training or hosting models, autonomous agents, and decisions taken on your behalf. PlatformXe routes, meters and polices your calls; it is not an AI product.
| Endpoint | Purpose | Scope | Rate tier |
|---|---|---|---|
POST /api/v1/ai/complete | One chat turn (Gemini, Grok or Claude) | ai:complete | 600/hr per key |
POST /api/v1/ai/extract | Prompts (and images) in, schema-validated JSON out | ai:complete | 600/hr per key |
GET /api/v1/ai/profile?featureKey= | Your profile for one feature + today's usage | ai:complete | 600/hr per key |
GET /api/v1/ai/usage?from=&to=&featureKey= | Your AI and vision usage | ai:complete | 600/hr per key |
POST /api/v1/vision/analyze-room | Property photo → room category + alt text | vision:analyze | 2,000/hr per key |
PUT / DELETE / GET /api/v1/ai/knowledge/documents | Index, delete and list your knowledge documents | ai:knowledge | 1,200/hr per key |
POST /api/v1/ai/knowledge/search | Hybrid search over your documents, with citations | ai:knowledge | 1,200/hr per key |
SDK reference: /sdk/ai and /sdk/vision. Machine-readable contract: the AI, AI Knowledge and Vision tags of the OpenAPI document.
AI profiles
Every call runs under an AI profile, selected by two values:
- the caller service — taken from your API key, never from the request body, so one app cannot borrow another app's profile;
featureKey— names the product use case (guest-concierge,room-vision,ai-canvas,document.freeform…).
A profile is set up by Caldera for your service. It holds:
| Setting | Effect |
|---|---|
status | ACTIVE or DISABLED. A disabled profile refuses with 403 AI_PROFILE_DISABLED; no profile at all is 403 AI_PROFILE_NOT_FOUND. |
allowedProviders / allowedModels | Which providers and models the feature may use (empty models list = the provider defaults). |
| Policy text | Appended after the PlatformXe baseline policy. You own your prompts; PlatformXe owns the policy. Withheld from the profile view. |
maxTokens, maxMessages, maxTotalChars | Per-call limits (see below). |
allowImages | Whether extract accepts images. |
allowTools | Whether complete accepts tools and tool-calling turns (see Tool calling). |
dailyTokenCap, dailyRequestCap | Daily caps per UTC day; null = no cap. |
| Output screening, PII redaction | Screen model replies before returning them; redact emails / phone numbers / ID numbers before they reach the model. |
GET /api/v1/ai/profile?featureKey=… (client.ai.profile) shows your profile's settings with today's usedTokensToday, usedRequestsToday and resetsAt (the next UTC midnight).
Feature keys
A featureKey is 1–64 lowercase letters, digits, . or -, starting with a letter or digit. It is required on complete and extract; analyze-room defaults to room-vision.
sourceApp is a deprecated alias for featureKey (types 3.5.0; removed in 4.0.0). It is only read when featureKey is absent. Send featureKey.
Limits
| Limit | Value |
|---|---|
Messages per complete turn | The profile's maxMessages, never more than 64 |
Characters per turn (all message content, or both extract prompts together) | The profile's maxTotalChars, never more than 256,000 |
Each extract prompt | 32,000 characters |
Images per extract | 4, each a public https url or base64 (≤ 8 MiB decoded, image/jpeg / image/png / image/webp) — only when the profile allows images |
analyze-room image | Exactly one of imageUrl (public https) / imageBase64 (≤ 8 MiB decoded) |
| Output tokens | The profile's maxTokens, never more than the deployment ceiling (256–32,768) |
extract temperature | 0–2, default 0.2 |
| Usage window | from / to at most 92 days apart; default the last 30 days |
A breached limit is 400 BAD_REQUEST.
Daily quotas
A profile's dailyTokenCap and dailyRequestCap are counted per UTC day. They are checked after the safety screen and before the model is called; a call that reaches the provider counts against them.
When a cap is reached the call is refused with 429 AI_QUOTA_EXCEEDED, a Retry-After header with the seconds until the next UTC midnight, and error.details = { metric, retryAfterSeconds }. It is distinct from the per-key rate tier (429 RATE_LIMITED, which resets within the hour). The official SDKs do not retry AI_QUOTA_EXCEEDED.
Usage and cost
Every call that reached a profile is recorded once — succeeded (OK), refused by the safety screen (REFUSED), blocked on output (BLOCKED), over quota (QUOTA_EXCEEDED), degraded (FALLBACK, vision) or failed (FAILED). GET /api/v1/ai/usage (client.ai.usage) returns your own usage grouped by feature, kind (ai / vision / embedding), provider and model, with token counts and cost in PlatformXe credits (1 credit = 1 kobo). Knowledge embeddings — at indexing time and for a search query — are recorded as kind embedding against the profile named by the request's featureKey.
Tool calling
With allowTools on the profile, POST /api/v1/ai/complete accepts tools (up to 16 function tools: name, description, JSON Schema parameters) and toolChoice (auto, none, required or { name }). The reply may carry toolCalls with finishReason: "tool_calls".
PlatformXe is a pipe for tools: it never executes a tool call. Your app runs each call through its own permission checks and audit, then sends the result back as a role: "tool" message (toolCallId) after the assistant message that carried the calls.
Tool traffic gets the same data handling as text: input screening and PII redaction cover tool results and earlier call arguments, output screening covers every returned call's arguments, and redaction placeholders are restored in the arguments you receive. Without allowTools, tools or tool messages are refused with 400 BAD_REQUEST ("tools not enabled for this feature").
Knowledge retrieval
/api/v1/ai/knowledge/* (scope ai:knowledge) indexes your own documents — policies, handbooks, knowledge pages — and searches them, so an assistant can answer from your data with citations.
- Indexing —
PUT /documentswithfeatureKey,collection, yoursourceRef,title, optionalurlandacl, and markdown or plaintext(≤ 400,000 characters). The text is split by heading into ~800-token chunks with a 100-token overlap; fenced code is never split. Re-sending the same text only updates the metadata. - ACLs —
aclholds opaque tags you own. A search sees a document when itsaclTagshold any of the document's tags; a document without tags is visible to every search of your service. - Search —
POST /searchwithfeatureKey,collections,query(≤ 1,000 characters),aclTagsandk(1–20, default 8). Candidates come from Postgres full-text and trigram (typo-tolerant) matching; when embeddings are available the query is embedded and cosine similarity is fused with the lexical ranks by reciprocal rank fusion (mode: "hybrid"), otherwisemode: "lexical". Every hit carriessourceRef,ordinalandheadingfor a citation. The query is screened like any AI input. - Embeddings — Google
text-embedding-005through the AI gateway with zero data retention. When the gateway is unavailable, documents are still indexed and searchable lexically; embeddings are filled in later. - Reconciliation —
GET /documents?collection=lists your documents with theircontentHash; delete what no longer exists on your side withDELETE /documents?collection=&sourceRef=.
Indexed text is stored exactly as sent: PlatformXe does not redact personal data at index time. Do not index Tier-1 / Tier-2 personal data. Documents are kept until you delete them.
Vision
POST /api/v1/vision/analyze-room classifies a property photo into one of the roomCategories you send (your own taxonomy) and, when wantAltText is true, writes alt text in the requested locale (en-NG, en-GB or en-US). The response carries roomCategory (or null), roomConfidence, altText, detectedFeatures, modelVersion, aiStatus and metrics.
A provider timeout or overload is not an error: the endpoint answers 200 with aiStatus: "FALLBACK" and a deterministic answer, so an upload flow never blocks on it.
Idempotency
analyze-room accepts an optional x-idempotency-key header (scoped to your API key):
| Situation | Response |
|---|---|
| First request with a key | Processed; the response is stored for 24 h |
| Same key again, after it finished | The stored response, with X-Idempotency-Cache: HIT |
| Same key while the first is still running | 409 CONFLICT |
The answer was a FALLBACK | Returned with X-Idempotency-Cache: skip and not stored — retrying the same key later reaches the provider again |
| The idempotency store is unavailable | 503 IDEMPOTENCY_UNAVAILABLE with Retry-After — nothing ran, retry is safe |
Error responses (400, 401, 403, 404, 422, 429 and 5xx) are never stored.
Error codes
Every error uses the standard envelope { "success": false, "error": { "code", "message", "details"? } }.
| Code | Status | Endpoints | Meaning |
|---|---|---|---|
BAD_REQUEST | 400 | all | Malformed body or query, or a limit breached |
UNAUTHORIZED | 401 | all | Missing, invalid or inactive API key |
FORBIDDEN | 403 | all | The key lacks ai:complete / ai:knowledge / vision:analyze, or its organization is inactive |
AI_PROFILE_NOT_FOUND | 403 | complete, extract, profile, analyze-room, knowledge PUT + search | No profile for your caller service + featureKey |
AI_PROFILE_DISABLED | 403 | complete, extract, analyze-room, knowledge PUT + search | The profile is switched off |
AI_POLICY_DENIED | 403 | complete, extract, knowledge search | The input was refused by the safety screen |
CONFLICT | 409 | analyze-room | The idempotency key is still in flight |
AI_OUTPUT_BLOCKED | 422 | complete, extract | The model reply was refused by output screening |
AI_OUTPUT_INVALID | 422 | extract | The reply did not match outputJsonSchema; details.path is the JSON Pointer of the first failing value |
RATE_LIMITED | 429 | all | The per-key rate tier is spent (Retry-After) |
AI_QUOTA_EXCEEDED | 429 | complete, extract, analyze-room | The profile's daily cap is reached (Retry-After to the next UTC midnight; details.metric) |
SERVER_ERROR | 500 | all | Unexpected failure |
AI_UPSTREAM_ERROR | 502 | complete, extract | The model provider failed or timed out |
AI_NOT_CONFIGURED | 503 | complete, extract | The AI gateway is not configured on that deployment |
VISION_NOT_CONFIGURED | 503 | analyze-room | No vision provider (AI gateway or Azure fallback) is configured on that deployment |
IDEMPOTENCY_UNAVAILABLE | 503 | analyze-room | The idempotency store is unavailable — nothing ran |
/api/v1/ocr/* now requires its own scope, ocr:run. Keys created with the default scopes carry ai:complete, vision:analyze and ocr:run for the services that use them; an older key with an explicit scope list needs them granted before it can call these routes.