API Documentation

Complete reference for the Chat Projects REST API

Introduction

The Chat Projects REST API provides programmatic access to your chatbots using an standardinterface. This allows you to integrate chat functionality into your applications using familiar API patterns.

Base URL: https://ainvented.com/api

Authentication

All API requests require authentication using Bearer tokens. Include your API key in the Authorization header:

Authorization: Bearer YOUR_API_KEY

Security note

Keep your API keys secure and never expose them in client-side code. API keys provide full access to your chat projects.

Admin keys vs project-scoped keys

Every API key has one of two scopes. The scope is fixed at creation time and determines what the key can do.

Admin
Account-wide management key. Required for every endpoint under /v1/projects — listing, reading, updating, deleting, minting per-project keys, configuring webhooks, viewing consumption, and the reseller billing surface (payments, payment methods, auto-recharge).
Project-scoped
Bound to one project. Can use only the runtime endpoints — /v1/chat/completions, /v1/tasks, /v1/vector-stores, /v1/workflows — which silently resolve to the bound project. Cannot read or modify anything under /v1/projects(no list, read, update, delete, key management, webhooks, consumption, or billing). Intended for resellers to give their end-customers a key that uses the product without exposing the project-management surface.
Error code for the management surface

Every call to a /v1/projects endpoint (list, detail, nested api-keys / consumption / webhooks, and the reseller billing routes) with a project-scoped key returns:

  • admin_key_required — 403 permission_error. Use an Admin key to manage projects.
  • key_project_mismatch — 403 permission_error. Returned by vector-store store-targeted endpoints (/v1/vector-stores/{id}/...) when a project-scoped key targets a store that lives in a different project.

Rate Limits

API requests are rate limited using a token bucket algorithm. Rate limit information is included in response headers:

  • X-RateLimit-Limit: Maximum requests per time window
  • X-RateLimit-Remaining: Requests remaining in current window
  • X-RateLimit-Reset: Unix timestamp when limit resets

When rate limited, the API returns a 429 Too Many Requests response with a Retry-After header indicating seconds until reset.

Request Tracing & Observability

Every response from POST /v1/chat/completions — success or error, streaming or non-streaming — includes anX-Request-Id header (UUID). This id correlates the request with its usage events and, when the project is attached to an Observability project, with its captured trace. The header is purely additive — request and response bodies are unchanged.

  • Attach a chat project to an Observability project (sidebar → Observability) to see per-request traces: prompt, completion, model, tokens, cost, latency, TTFT and status.
  • Trace capture runs after the response is sent and never adds latency to your requests.
  • LLM-as-a-judge evaluations run against your project budget and appear in the project's consumption ledger like any other usage.
POST/v1/chat/completions

Create Chat Completion

Generate a chat completion response using your configured chat project. Supports both streaming and non-streaming modes.

Authentication

Bearer token required in Authorization header

Model tiers

Select the chat model per request with the model field (or set a project default via preconfigured_model):

  • ainvented-default — the standard model.
  • ainvented-low — a lower-cost, low-latency model. Best for high-volume, straightforward turns.
  • Omitting model uses the project's preconfigured default (or ainvented-default). Any unrecognized value is accepted and treated as ainvented-default — never rejected.
  • The selected tier is echoed back in the response model field. Per-tier pricing is on the pricing page.
Automatic fallback — you are billed for what is served
ainvented-low is a text model. A request that includes images, or whose prompt is too large for the low model, is transparently served by ainvented-default instead — no error, no change to your call. Billing always follows the model actually served: a fallback turn is charged at the default rate, never the low rate.
cURL — request the low tier
curl https://ainvented.com/api/v1/chat/completions \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"model":"ainvented-low",
       "messages":[{"role":"user","content":"Summarize: the meeting is moved to 3pm Friday."}]}'
# -> response.model echoes the requested tier; a text-only, in-budget turn is served + billed as ainvented-low

Streaming Response

When stream: true is set, the response uses Server-Sent Events (SSE). Each chunk follows this schema:

{
  "id": "string - Unique completion ID",
  "object": "string - 'chat.completion.chunk'",
  "created": "number - Unix timestamp",
  "model": "string - Model used",
  "choices": [
    {
      "index": "number - Choice index",
      "delta": {
        "role": "string (optional) - 'assistant' (first chunk only)",
        "content": "string (optional) - Incremental text content",
        "tool_calls": "array (optional) - Inline function-calling deltas: [{ index: number, id: string, type: 'function', function: { name: string, arguments: string } }]. Accumulate by `index`; the stream ends with a finish_reason 'tool_calls' chunk.",
        "images": "string[] (optional) - Emitted in a dedicated chunk when the model generates one or more images. URLs (or data: URLs). Never inlined as markdown in delta.content.",
        "image_meta": "object[] (optional) - Emitted IMMEDIATELY AFTER the delta.images chunk when the caller opted in via modalities/image. Same per-entry shape as response.image_meta (url, size, format, bytes, source, request_id, prompt_excerpt). Old clients can safely ignore unknown delta.* keys."
      },
      "finish_reason": "string | null - 'stop' (final text chunk), 'tool_calls' (after inline function-call deltas), else null"
    }
  ],
  "session_id": "string (final chunk only) - Session ID for future requests"
}

Session Management

Use the session_id parameter to maintain conversation context across requests:

  • First request — omit session_id; the response returns a new session ID.
  • Subsequent requests — include that session_id to continue the conversation.
  • Sessions are tied to your API key and keep the full conversation history.
  • Two ways to do multi-turn — pick one, don't combine them:
    • Stateless (recommended) — send the entire messages array (all prior turns + the new one) on every request. Your transcript IS the conversation history; the server does not add any stored history on top of it.
    • Stateful — send only the latest user message in messages plus the session_id; the server supplies the earlier turns from its store.
  • You don't need to do both: if messages already contains prior turns, the server automatically will not also re-load the stored session history, so context is never duplicated or double-counted toward usage. (Sending the full transcript AND relying on the stored history would otherwise repeat the same turns.)
  • /v1/chat/completions does not generate or expose chat session titles. (Title generation is suppressed upstream to minimize latency and cost.) Use the session_id returned in the response to correlate multi-turn conversations.

Project Selection

The API determines which project to use in this priority order:

  1. The API key's associated project (if scoped) — takes precedence over everything else.
  2. The project_id request field — only when the key is not scoped to a project.
  3. Your default project (first created).
Note
If your API key is scoped to a project, that project is always used and any project_id in the request is ignored — this enforces access control.

Project Enhancements: Skills & Guardrails

Every request automatically inherits the active Skills and Guardrails configured for the project in the AI Chat UI — injected as system context on every call, with no extra request fields.

🎯 Skills

Instruction presets that shape the AI's behaviour on every response (e.g. "always respond in bullet points"). Set via the Skills button in AI Chat.

🛡️ Guardrails

Content/tone rules that are always enforced (e.g. "never mention competitors"). Set via the Guardrails button in AI Chat.

Note
Skills and Guardrails are project-level and cannot be overridden per request — they apply equally to the chat UI and the Completions API.

Workflow Execution in Chat

Set run_workflow: true to run the project's configured workflow before the AI responds. The workflow output is injected as context so the AI can reason over real-time data from your pipeline.

cURL — workflow-backed completion
curl https://ainvented.com/api/v1/chat/completions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {"role": "user", "content": "Summarise the latest results"}
    ],
    "run_workflow": true
  }'
Python — workflow-backed completion
import requests

api_key = "your_api_key_here"
url = "https://ainvented.com/api/v1/chat/completions"

headers = {
    "Authorization": f"Bearer {api_key}",
    "Content-Type": "application/json"
}

# run_workflow: true triggers the project workflow before the AI responds.
# The workflow output is injected as context so the AI can reason over real data.
data = {
    "messages": [
        {"role": "user", "content": "Summarise the latest results"}
    ],
    "run_workflow": True
}

response = requests.post(url, headers=headers, json=data)
result = response.json()
print(result["choices"][0]["message"]["content"])
How it works
With run_workflow: true, the project's workflow graph runs first; its end-node output is injected as system_context before the model sees your message (the user message is available as the user_message input). Workflow errors are non-fatal — the AI still responds normally.

Tools Override (Selective Workflow Execution)

By default, all active project workflows run on every request and their output is injected as context. Use the tools field to control which run:

Default

No tools field → all active workflows auto-run (same as the chat UI).

Override

A tools array of names → only matching workflows run. Names look like workflow_<name>.

Disable

Pass "tools": [] → no workflows run for this request. Skills and guardrails still apply.

cURL — selective workflow execution
# Only execute specific workflows (override mode)
curl https://ainvented.com/api/v1/chat/completions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {"role": "user", "content": "tell me about Croatia"}
    ],
    "tools": [
      {"type": "function", "function": {"name": "workflow_country_cities"}}
    ]
  }'
Python — selective workflow execution
import requests

api_key = "your_api_key_here"
url = "https://ainvented.com/api/v1/chat/completions"

headers = {
    "Authorization": f"Bearer {api_key}",
    "Content-Type": "application/json"
}

# Override mode: only execute specific workflow tools
# Pass an empty tools array to disable all workflow execution
data = {
    "messages": [
        {"role": "user", "content": "tell me about Croatia"}
    ],
    "tools": [
        {"type": "function", "function": {"name": "workflow_country_cities"}}
    ]
}

response = requests.post(url, headers=headers, json=data)
result = response.json()
print(result["choices"][0]["message"]["content"])

Request Schema

{
  "model": "string",
  "messages": [
    {
      "role": "string",
      "content": "string | ContentPart[]"
    }
  ],
  "temperature": "number",
  "max_tokens": "number",
  "stream": "boolean",
  "project_id": "string",
  "prompt_cache": "boolean (optional, default true)",
  "prompt_cache_ttl_hours": "string|integer",
  "session_id": "string",
  "run_workflow": "boolean",
  "tools": "array",
  "modalities": "array",
  "image": "object",
  "connector_id": "string",
  "connector_to": "string",
  "connector_media_type": "string",
  "connector_media_url": "string",
  "connector_media_filename": "string",
  "connector_email_subject": "string",
  "tool_choice": "string|object"
}
model
stringoptional
Selects the chat model tier: 'ainvented-default' (the standard model) or 'ainvented-low' (a lower-cost model). When omitted, the project's preconfigured default model is used (or 'ainvented-default' if the project has none). For compatibility with standard chat-completion clients, any other/legacy value is accepted (never rejected) and treated as 'ainvented-default'. The value is echoed back in the response; pricing follows the selected tier (see the pricing page).
messages
array
array — fields below
role
stringrequired
'system' | 'user' | 'assistant'
content
string | ContentPart[]required
Message content (text string or array of content parts for multimodal).PUBLIC URL READING ("web search"): any public http(s) URL you include in the LAST user message's TEXT (e.g. 'summarize https://example.com/article') is automatically fetched and read by the assistant so it can answer about the page contents — no special parameter needed. CONTROLLED PER-PROJECT by the `web_search_enabled` setting (default true; set it when you create or update the project — see POST / PUT /api/v1/projects). When a project sets web_search_enabled=false, URLs typed in message text are NOT fetched. SCOPE: detection runs on the message TEXT ONLY and is completely independent of file/image/audio handling — `image_url` content parts, attached project files, and audio are always forwarded regardless of this setting; only text-mentioned URLs are affected. data: URLs and images still go through `image_url` content parts. Adds no latency to plain text-only messages (URLs are fetched upstream only when present) and does not change billing. Works in non-streaming and streaming.
temperature
numberoptional
0.0 to 2.0, default: 1.0
max_tokens
numberoptional
Maximum tokens to generate
stream
booleanoptional
Enable streaming response, default: false
project_id
stringoptional
Specific project ID (ignored if API key is scoped to a project)
prompt_cache
boolean (optional, default true)
Prompt caching. When enabled (the default), repeated prompt prefixes across nearby requests are served from cache and those cached input tokens are billed at a heavily discounted cached rate (see Pricing), so your balance lasts longer at no quality cost. Set false to opt THIS request out — cached input tokens are then billed at the full input rate. PRECEDENCE: this request field > the project's `prompt_cache_enabled` setting > platform default (ON). Caching never changes the response text; the discount applies only when the provider reports a cache hit (not guaranteed for short prompts). Adds no latency.
prompt_cache_ttl_hours
string|integeroptional
Managed Prompt Cache: keep this project's stable prompt prefix (system/assistant instructions of at least 1,024 tokens) in a dedicated cache for a guaranteed lifetime, so EVERY request reusing it gets the cached-input discount deterministically (not best-effort). Allowed values: 1, 6, 12 or 24 (hours), "auto", or null — "auto"/null defer to the project's managed-cache setting for this request (same as omitting the field); any other value returns 400. The cached stable prefix INCLUDES your `system` role message(s) — so sending a large, unchanging system prompt bills it at the cached rate on every reuse instead of re-charging it each turn (different system prompts are cached separately and never collide). Billing: cached storage is charged at $1.50 per 1M tokens per hour (pro-rated per cached prefix) plus a one-time cache-write at $0.40 per 1M tokens; break-even guidance: the cache pays for itself above roughly 5 reuses of the prefix per hour. PRECEDENCE: this request field > the project's `prompt_cache_ttl_hours` setting. Requires prompt caching enabled (`prompt_cache:false` wins). Identical configuration and pricing for both ainvented-default and ainvented-low - switching models changes nothing.
session_id
stringoptional
Existing session ID for context continuity. If you already send the full conversation in `messages` (stateless usage — you manage history client-side), the server uses your transcript as the history and does NOT additionally load this session's stored history, so context is never duplicated. Send only the latest message + session_id to instead have the server supply the prior turns.
run_workflow
booleanoptional
Execute the project's configured workflow and inject results as context before generating the response. Default: false
tools
arrayoptional
Standard function-tool definitions for workflow override mode. When provided, only matching project workflows are executed. When omitted, all active project workflows auto-execute. An empty array disables all workflow execution. Format: [{ type: 'function', function: { name, description?, parameters? } }]. SPECIAL: a tool named 'image_generation' opts this request into image output (same as modalities: ['image']); its `parameters` are merged into the `image` block.TASKS (when the Tasks feature is enabled for your project): a tool whose `function.name` matches a stored Task (created via POST /api/v1/tasks) SELECTS that task for this interaction — standard tool selection. By default, selected tasks run ASYNCHRONOUSLY after the reply is flushed (zero TTFT/TTLT impact) and analyze the conversation history to extract structured data / classify intent; opting into `task_wait:true` instead runs them inline and adds their latency (see Result-delivery modes). A tools name matching no task/workflow/tool is ignored (no error). Read results via GET /api/v1/tasks/{id}/runs.
modalities
arrayoptional
Output modalities hint. Allowed values: 'text' | 'image'.IMAGE GENERATION IS STRICTLY OPT-IN — when this field is OMITTED, the endpoint stays on the fast text-only path with the lowest TTFT/TTLT (no image-generation work is triggered). A first-time caller will always feel the fastest possible response. Semantics: omitted → text-only fast path (default); ['text'] → same as omitted, treated as explicit text-only; ['image'] → OPT-IN: image only (no text reply); ['text','image'] → OPT-IN: text + image for every request. Images returned by the model are still surfaced in response.images (subject to your project's image-generation setting).
image
objectoptional
Image-generation parameters. Applied only when 'image' is in modalities or a tool named 'image_generation' is provided. Fields: { n?: 1..4 (default 1), size?: '256x256'|'512x512'|'1024x1024'|'1024x1792'|'1792x1024' (default '1024x1024'), quality?: 'standard'|'hd' (default 'standard'), style?: 'vivid'|'natural' (default 'vivid'), response_format?: 'url'|'b64_json' (default 'url'), remove_background?: boolean (default false; available when background removal is enabled for your project) }. PRECEDENCE: per-request image.* > your project defaults (Configure pane) > built-in defaults. Per-request always wins.REFERENCE/PRODUCT IMAGES: any images you attach in the last user message's content as `image_url` parts (base64 data: URLs or http(s) URLs) are forwarded to the image generator as references, so it edits/uses YOUR product instead of inventing one. Works in both streaming and non-streaming.
connector_id
stringoptional
UUID of an active chat connector to ALSO dispatch the AI reply through (Telegram, WhatsApp, Slack, Email/Resend, Gmail, SMTP, Telnyx SMS). The /v1/chat/completions response is still returned normally; the connector dispatch is best-effort and non-fatal — a dispatch failure is logged but does not change the HTTP response.
connector_to
stringoptional
Recipient address for the connector dispatch. Format depends on the channel: '+15551234567' for SMS / WhatsApp / Telnyx, channel id like 'C0123456' for Slack, chat id integer for Telegram, RFC-822 email for Resend / Gmail / SMTP. Required when connector_id is set.
connector_media_type
stringoptional
Type of media to attach to the connector dispatch. Allowed: 'image' | 'audio' | 'voice' | 'document' | 'video'. Default 'image'. Only consumed when connector_media_url is also set.
connector_media_url
stringoptional
Explicit media URL to attach to the connector dispatch. Wins over auto-attach. When this field is omitted AND the response produced images, response.images[0] is auto-attached (when auto-attach is enabled for your project).
connector_media_filename
stringoptional
Filename for documents dispatched via document-capable connectors (Telegram document, email attachments). Only consumed when connector_media_type='document'.
connector_email_subject
stringoptional
Subject line for email/Gmail/SMTP dispatch. Defaults to a generic 'AI assistant reply' subject when omitted.
tool_choice
string|objectoptional
Standard tool-selection control, also used to control Tasks (when the Tasks feature is enabled for your project). Values: 'auto' (run task-tools named in `tools` PLUS any matching on_message/on_intent project tasks — an on_intent task auto-fires when the conversation matches one of its declared intents, e.g. cancel_order), 'none' (run no tasks), 'required' (run ONLY the task-tools named in `tools`), or { type: 'function', function: { name } } (run ONLY that one task). COST-SAFE DEFAULT: auto-trigger expansion (firing unnamed on_message/on_intent tasks) happens when tool_choice is EXPLICITLY 'auto'. If tool_choice is omitted, only the tasks you NAME in `tools` fire — an empty `tools:[]` or omitting both fields runs NO tasks by default (so passing tools:[] to disable workflows never incurs surprise task spend). TRANSPARENT AUTO-FIRE (opt-in per project): a project can be configured so an OMITTED tool_choice behaves as 'auto' — then a plain chat-completion request (no tools/tool_choice at all) automatically runs the project's on_message/on_intent tasks, so existing clients need no changes. Contact us to enable it for your project. By default task selection + execution are resolved asynchronously AFTER the reply (zero TTFT/TTLT impact); only the opt-in task_wait:true runs them inline and adds latency.

Response Schema

{
  "id": "string",
  "object": "string",
  "created": "number",
  "model": "string",
  "choices": [
    {
      "index": "number",
      "message": {
        "role": "string",
        "content": "string | null",
        "tool_calls": "array (only when the model calls inline function tools)"
      },
      "finish_reason": "string"
    }
  ],
  "response": {
    "images": "string[]",
    "image_meta": "object[] (only present when images were produced)"
  },
  "session_id": "string"
}
id
string
Unique completion ID
object
string
'chat.completion'
created
number
Unix timestamp
model
string
Model used
choices
array
array — fields below
index
number
Choice index
message
object
object — fields below
role
string
'assistant'
content
string | null
Generated text. null when the model emits tool_calls (function calling).
tool_calls
array (only when the model calls inline function tools)
[{ id: string, type: 'function', function: { name: string, arguments: string (JSON-encoded) } }]. Present with finish_reason 'tool_calls'.
finish_reason
string
'stop' | 'length' | 'content_filter' | 'tool_calls' (when the model called one or more inline function tools)
response
object
object — fields below
images
string[]
URLs (or data: URLs) of any images generated by the model. Empty array when no images were produced or image generation is disabled. Image URLs are NEVER inlined as markdown in choices[0].message.content; they are always returned in this dedicated field.
image_meta
object[] (only present when images were produced)
One entry per URL in `images`, same order. Fields per entry: { url: string, size: '256x256'|...|'1792x1024', format: string|null, bytes: number|null, source: string (origin of the image, e.g. 'background_removed'), request_id: string (uuid identifying the originating request), prompt_excerpt: string (first 80 chars of the originating prompt) }.
session_id
string
Session ID for future requests

Code Examples

Basic completion request with non-streaming response

import requests

api_key = "your_api_key_here"
url = "https://ainvented.com/api/v1/chat/completions"

headers = {
    "Authorization": f"Bearer {api_key}",
    "Content-Type": "application/json"
}

data = {
    "messages": [
        {"role": "user", "content": "Hello, how are you?"}
    ],
    "temperature": 0.7,
    "max_tokens": 150
}

response = requests.post(url, headers=headers, json=data)
result = response.json()
print(result["choices"][0]["message"]["content"])

Tasks — async history analysis

Tasks are reusable, client-defined units that asynchronously analyze a conversation's history to extract structured data or classify intent — without ever affecting the chat reply's latency. A task is an instruction prompt + a JSON-Schema output_schema + a trigger. Manage them via the REST endpoints below, and select them per interaction with the completions tools/tool_choice params (a task is a function tool referenced by name). Available when the Tasks feature is enabled for your project.

Endpoints

POST/api/v1/tasks
Create a task. Body: name, instructions, trigger_type, output_schema, and optional trigger_config, history_scope, model, action, enabled, scope. (See Triggers and Limits below.)
GET/api/v1/tasks
List tasks. Filters: status, trigger_type, enabled, limit, after. Your global tasks are included by default — pass ?include_global=false for project-only, or ?scope=global for globals only. Each task carries scope.
GET/api/v1/tasks/{id}
Retrieve one task (add ?include_deleted=true to see soft-deleted).
PUT/api/v1/tasks/{id}
Update a task — including pause/resume via enabled.
DELETE/api/v1/tasks/{id}
Soft delete (never hard-deletes; run history is retained).
POST/api/v1/tasks/{id}/run
Run a task synchronously now and return the completed run with its result. Body: messages?, session_id?, project_id?. Used by the tool_call delivery mode and for run-now / testing. Runs regardless of the task's enabled flag (manual operator override — enabled:false only pauses automatic triggers); the run is billed and audited normally.
GET/api/v1/tasks/{id}/runs
Run history — each run has result, status, confidence, request_id, source_session_id, token counts, completed_at. Filter to one conversation with ?session_id=…. (See Reading results.)

Triggers

A task's trigger_type decides when it can run:

  • manual — only runs when you explicitly name it (in tools) or call /run.
  • on_message — auto-runs on each interaction under tool_choice:'auto' (or on every request when transparent auto-fire is enabled for the project).
  • on_intenttrigger_config: { intents: [...] }. Under tool_choice:'auto' (or transparent auto-fire) the task auto-fires when the conversation matches one of its declared intents (e.g. cancel_order, refund). You can also name it explicitly in tools to force it on any turn.
  • scheduletrigger_config: { cron, timezone?, session_id? }.
The schedule trigger is currently unavailable
Creating or switching a task to trigger_type:'schedule' returns 400 schedule_disabled. Use on_message, on_intent, or manual meanwhile.

Scope — project vs. global

scope defaults to 'project'. Set it to 'global' to make the task available to all of your projects (it's user-scoped, so project_id is null).

A global task fires in /v1/chat/completions only when its name is explicitly listed in tools — it's never auto-expanded by on_message/on_intent under tool_choice:'auto' (that stays project-local). When it runs, the run binds to the calling project. A global task may not use trigger_type:'schedule' (no calling project at cron time) → 400 invalid_schedule_scope.

Selecting tasks from a completion

Name a task as a function tool — "tools":[{"type":"function","function":{"name":"<task name>"}}] — controlled by tool_choice (auto / none / required / {type:'function',function:{name}}). With both tools and tool_choice omitted, no tasks run by default (the cost-safe default). The project is resolved from your API key (or the project_id field) exactly like the rest of the endpoint — you never put a project id inside tools, and a task name resolves within the resolved project.

Transparent auto-fire (opt-in per project)
A project can be configured so an omitted tool_choice behaves as 'auto'. Then a plain chat-completion request — only model + messages, no tools or tool_choice — automatically runs the project's on_message/on_intent tasks. This lets existing chat-completion clients use Tasks with zero code changes. Contact us to enable it for your project. (Global tasks still fire only when explicitly named, even with auto-fire on.)

Inline function tools (standard function calling)

Separate from stored Tasks, you can use standard function calling: pass full function definitions inline on every request and let the model decide when to call them. Nothing is stored — if you don't send a definition on a request, it doesn't exist for that request.

  • Define functions in tools: {type:"function", function:{name, description, parameters /* JSON Schema */}}.
  • tool_choice: auto (model decides), none (never call), required (must call one), or {type:"function",function:{name}} (force one).
  • When the model calls a function, the reply has finish_reason:"tool_calls", message.content:null, and message.tool_calls[] (each with function.arguments as a JSON string). Parallel calls are supported.
  • You execute the function and continue the standard loop: resend with the prior {role:"assistant", tool_calls:[…]} and a {role:"tool", tool_call_id, name, content} result. The model then answers using the result.
  • Works with stream:true (tool calls arrive as delta.tool_calls then a final finish_reason:"tool_calls" chunk).
cURL — function-calling loop
# 1) Model decides to call get_weather
curl https://ainvented.com/api/v1/chat/completions \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"model":"ainvented-default",
       "messages":[{"role":"user","content":"Weather in Paris?"}],
       "tools":[{"type":"function","function":{"name":"get_weather",
         "description":"Get current weather",
         "parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}],
       "tool_choice":"auto"}'
# -> finish_reason "tool_calls"; message.tool_calls[0].function.arguments is a JSON string

# 2) Run the function yourself, then send the result back to finish the turn
curl https://ainvented.com/api/v1/chat/completions \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"model":"ainvented-default","messages":[
       {"role":"user","content":"Weather in Paris?"},
       {"role":"assistant","content":null,"tool_calls":[{"id":"call_1","type":"function",
         "function":{"name":"get_weather","arguments":"{\"city\":\"Paris\"}"}}]},
       {"role":"tool","tool_call_id":"call_1","name":"get_weather","content":"18C, sunny"}]}'
# -> a normal answer that uses "18C, sunny"
Inline tools vs. stored Tasks
Inline function tools are model-driven and ephemeral (you run the function and feed the result back). Stored Tasks are server-side history-analysis units you create once and reference by name. A tools entry whose name matches a stored Task is treated as that Task; any other function definition is an inline tool.

Result-delivery modes

How you get a task result back from /chat/completions:

async (default)
Task runs after the reply is flushed; fetch the result via /runs or a webhook. Zero TTFT/TTLT impact.
task_wait: true (opt-in)
The endpoint awaits the selected task(s) and returns them inline — a top-level task_results array (non-streaming), or a final delta.task_results SSE chunk before [DONE] (streaming). Adds the task's latency by choice.
task_mode: "tool_call"
Returns the task as a proposed tool_calls with finish_reason:"tool_calls"no AI call is made. You execute it via POST /api/v1/tasks/{id}/run and continue the standard tool loop with a {role:"tool", ...} message. Honors stream:true; takes precedence over task_wait.

Reading results & delivery (action)

After a completion selects a task, poll GET /api/v1/tasks/{id}/runs?session_id=… using the session_id the completion returned (it matches the run's source_session_id). A run with status:"success" is done. A run stuck running past ~10 min is auto-failed to error (timed_out) — just re-trigger.

Or have the result pushed when the run finishes via the task's action:

  • action.webhook_url — POSTs the structured payload (must be a public http(s) URL; private/loopback/metadata addresses are rejected for SSRF safety).
  • action.connector_id + action.connector_to — sends via a project chat connector (Telegram / WhatsApp / SMS / Email / Gmail / SMTP).
  • action.min_confidence — gates delivery (results below it are stored but not pushed).

Webhook takes precedence when both are set; delivery is best-effort and never affects the stored run. The delivered flag on each run reflects the outcome.

history_scope & limits

history_scope controls how much of the conversation the task reads:

  • { mode: 'full' } (default) — the entire transcript, with automatic map-reduce over long histories (bounded to the most recent ~144k tokens to cap per-run cost).
  • { mode: 'last_n', n: <1..1000> } — only the last N messages.
  • { mode: 'since', since: '<ISO>' } — falls back to full for the standard message shape (no per-message timestamps).

Limits: name ≤120 chars, unique per project (or per account for global tasks) and not a reserved name like image_generation; on a name clash the project-local task wins for that project. instructions ≤20000 chars; metadata ≤16 keys; output_schema nesting ≤32 levels. Unknown fields are rejected (strict).

Cost & quotas

Every task AI call is metered (one usage event per AI call); CRUD makes no AI call. Per-project guardrails cap active tasks and runs per minute & per day — over the cap, runs are skipped (no spend) and task creation returns 429 task_quota_exceeded.

Error codes

feature_disabled (403) — Tasks not enabled for this project.
task_not_found (404) — task id not found / not owned by your project.
invalid_request_error (400) — bad payload, invalid output_schema / cron / trigger_config. Includes schedule_disabled (schedule trigger is off) and invalid_schedule_scope (a global task can't use the schedule trigger).
task_quota_exceeded (429) — active-task cap reached.
duplicate_task_name (409) — a non-deleted task with that name already exists in the same scope (per project, or per account for global tasks). A deleted task's name becomes reusable.
An unknown task name in tools is silently ignored (not a 4xx) — selection resolves asynchronously after the reply.

Multimodal Content Support

The Chat Completions API supports multimodal content including base64-encoded images and audio files. You can send images and audio alongside text in your messages using the ContentPart array format.

Image Generation — Response Shape

When the model produces one or more images (e.g. a project configured for image generation, an image-modification workflow, or a text-to-image prompt), URLs to the generated images are returned in a dedicated field. Image URLs are never inlined as markdown in choices[0].message.content / delta.content.

Non-streaming (stream=false)

The top-level response includes a response.images array. Empty when no images were generated.

{
  "id": "chatcmpl-1736541234567",
  "object": "chat.completion",
  "created": 1736541234,
  "model": "default",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Here is the rendered image you requested."
      },
      "finish_reason": "stop"
    }
  ],
  "response": {
    "images": [
      "https://cdn.ainvented.com/generated/abc123.png",
      "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
    ]
  },
  "session_id": "sess_abc123"
}

Streaming (stream=true)

Generated images arrive in a dedicated chunk whose choices[0].delta is { images: string[] }. This chunk is interleaved with normal text-delta chunks; one chunk per batch of generated images. The final chunk has finish_reason: "stop" and a top-level session_id, followed by data: [DONE].

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1736541234,"model":"default","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1736541234,"model":"default","choices":[{"index":0,"delta":{"content":"Here is the rendered image you requested."},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1736541234,"model":"default","choices":[{"index":0,"delta":{"images":["https://cdn.ainvented.com/generated/abc123.png"]},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1736541234,"model":"default","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"session_id":"sess_abc123"}

data: [DONE]

Client tip: while iterating SSE chunks, treat delta.content (string) and delta.images (array) as independent fields that may appear in different chunks. Append delta.content to your running text buffer; collect delta.images into a separate array.

Content Types

Text Content

{
  "type": "'text'",
  "text": "string - Text content"
}

Image Content (URL or Base64)

{
  "type": "'image_url'",
  "image_url": {
    "url": "string - HTTP/HTTPS URL or data URL (data:image/<format>;base64,<data>)",
    "detail": "string (optional) - 'auto' | 'low' | 'high' - Image detail level for token calculation"
  }
}

Supported formats: JPEG, PNG, GIF, WebP
Size limit: 20MB (configurable)
Detail levels: 'low' (85 tokens), 'high' (tile-based), 'auto' (automatic)

Audio Content (Base64)

{
  "type": "'input_audio'",
  "input_audio": {
    "data": "string - Base64-encoded audio data",
    "format": "string - Audio format: 'wav' | 'mp3' | 'm4a' | 'flac' | 'ogg'"
  }
}

Supported formats: WAV, MP3, M4A, FLAC, OGG
Size limit: 25MB (configurable)

Base64 Image Examples

Python - Image Analysis

Python
import requests
import base64

api_key = "your_api_key_here"
url = "https://ainvented.com/api/v1/chat/completions"

# Read and encode image
with open("image.jpg", "rb") as image_file:
    image_data = base64.b64encode(image_file.read()).decode('utf-8')

headers = {
    "Authorization": f"Bearer {api_key}",
    "Content-Type": "application/json"
}

data = {
    "messages": [
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "What's in this image?"
                },
                {
                    "type": "image_url",
                    "image_url": {
                        "url": f"data:image/jpeg;base64,{image_data}",
                        "detail": "high"
                    }
                }
            ]
        }
    ],
    "max_tokens": 300
}

response = requests.post(url, headers=headers, json=data)
result = response.json()
print(result["choices"][0]["message"]["content"])

TypeScript - Image Analysis

TypeScript
const apiKey = "your_api_key_here";
const url = "https://ainvented.com/api/v1/chat/completions";

// Read and encode image (Node.js)
import fs from 'fs';
const imageBuffer = fs.readFileSync('image.jpg');
const imageBase64 = imageBuffer.toString('base64');

const response = await fetch(url, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${apiKey}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    messages: [
      {
        role: "user",
        content: [
          {
            type: "text",
            text: "What's in this image?"
          },
          {
            type: "image_url",
            image_url: {
              url: `data:image/jpeg;base64,${imageBase64}`,
              detail: "high"
            }
          }
        ]
      }
    ],
    max_tokens: 300
  })
});

const result = await response.json();
console.log(result.choices[0].message.content);

cURL - Image Analysis

Bash
# Using base64 command to encode image
IMAGE_BASE64=$(base64 -i image.jpg)

curl https://ainvented.com/api/v1/chat/completions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"messages\": [
      {
        \"role\": \"user\",
        \"content\": [
          {
            \"type\": \"text\",
            \"text\": \"What's in this image?\"
          },
          {
            \"type\": \"image_url\",
            \"image_url\": {
              \"url\": \"data:image/jpeg;base64,$IMAGE_BASE64\",
              \"detail\": \"high\"
            }
          }
        ]
      }
    ],
    \"max_tokens\": 300
  }"

Base64 Audio Examples

Python - Audio Transcription

Python
import requests
import base64

api_key = "your_api_key_here"
url = "https://ainvented.com/api/v1/chat/completions"

# Read and encode audio
with open("audio.wav", "rb") as audio_file:
    audio_data = base64.b64encode(audio_file.read()).decode('utf-8')

headers = {
    "Authorization": f"Bearer {api_key}",
    "Content-Type": "application/json"
}

data = {
    "messages": [
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "Transcribe this audio"
                },
                {
                    "type": "input_audio",
                    "input_audio": {
                        "data": audio_data,
                        "format": "wav"
                    }
                }
            ]
        }
    ],
    "max_tokens": 500
}

response = requests.post(url, headers=headers, json=data)
result = response.json()
print(result["choices"][0]["message"]["content"])

TypeScript - Audio Transcription

TypeScript
const apiKey = "your_api_key_here";
const url = "https://ainvented.com/api/v1/chat/completions";

// Read and encode audio (Node.js)
import fs from 'fs';
const audioBuffer = fs.readFileSync('audio.wav');
const audioBase64 = audioBuffer.toString('base64');

const response = await fetch(url, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${apiKey}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    messages: [
      {
        role: "user",
        content: [
          {
            type: "text",
            text: "Transcribe this audio"
          },
          {
            type: "input_audio",
            input_audio: {
              data: audioBase64,
              format: "wav"
            }
          }
        ]
      }
    ],
    max_tokens: 500
  })
});

const result = await response.json();
console.log(result.choices[0].message.content);

cURL - Audio Transcription

Bash
# Using base64 command to encode audio
AUDIO_BASE64=$(base64 -i audio.wav)

curl https://ainvented.com/api/v1/chat/completions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: "application/json" \
  -d "{
    \"messages\": [
      {
        \"role\": \"user\",
        \"content\": [
          {
            \"type\": \"text\",
            \"text\": \"Transcribe this audio\"
          },
          {
            \"type\": \"input_audio\",
            \"input_audio\": {
              \"data\": \"$AUDIO_BASE64\",
              \"format\": \"wav\"
            }
          }
        ]
      }
    ],
    \"max_tokens\": 500
  }"

Browser-Based File Upload

When working in the browser, use the FileReader API to convert files to base64:

JavaScript - Browser File Upload

JavaScript
// Browser example - Using FileReader API
const apiKey = "your_api_key_here";
const url = "https://ainvented.com/api/v1/chat/completions";

// Handle file input from user
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  
  // Read file as base64
  const reader = new FileReader();
  reader.onload = async () => {
    // Extract base64 data (remove data URL prefix)
    const base64Data = reader.result.split(',')[1];
    
    const response = await fetch(url, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${apiKey}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        messages: [
          {
            role: "user",
            content: [
              {
                type: "text",
                text: "What's in this image?"
              },
              {
                type: "image_url",
                image_url: {
                  url: `data:image/jpeg;base64,${base64Data}`,
                  detail: "high"
                }
              }
            ]
          }
        ],
        max_tokens: 300
      })
    });
    
    const result = await response.json();
    console.log(result.choices[0].message.content);
  };
  
  reader.readAsDataURL(file);
});

Security Warning: Never expose your API key in client-side code. Always proxy requests through your backend server to keep API keys secure.

Combined Image and Audio Example

You can send multiple types of content in a single request for rich multimodal interactions:

Python - Combined Image and Audio

Python
import requests
import base64

api_key = "your_api_key_here"
url = "https://ainvented.com/api/v1/chat/completions"

# Read and encode image
with open("chart.png", "rb") as image_file:
    image_data = base64.b64encode(image_file.read()).decode('utf-8')

# Read and encode audio
with open("narration.mp3", "rb") as audio_file:
    audio_data = base64.b64encode(audio_file.read()).decode('utf-8')

headers = {
    "Authorization": f"Bearer {api_key}",
    "Content-Type": "application/json"
}

data = {
    "messages": [
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "Analyze this chart and transcribe the audio narration"
                },
                {
                    "type": "image_url",
                    "image_url": {
                        "url": f"data:image/png;base64,{image_data}",
                        "detail": "high"
                    }
                },
                {
                    "type": "input_audio",
                    "input_audio": {
                        "data": audio_data,
                        "format": "mp3"
                    }
                }
            ]
        }
    ],
    "max_tokens": 1000
}

response = requests.post(url, headers=headers, json=data)
result = response.json()
print(result["choices"][0]["message"]["content"])

Validation and Error Handling

All multimodal content is validated before processing. The following validations are performed:

  • Base64 encoding: Data must contain only valid base64 characters (A-Z, a-z, 0-9, +, /, =)
  • Data URL format: Image data URLs must follow the format: data:image/<format>;base64,<data>
  • MIME type: Must be a supported image or audio format
  • Size limits: Content must not exceed configured size limits (20MB for images, 25MB for audio)
  • Required fields: All required fields must be present (e.g., 'data' and 'format' for audio)

Common Validation Errors

Invalid base64 encoding in image data

The base64 string contains invalid characters. Ensure proper encoding.

Malformed data URL: missing MIME type

Data URL must include MIME type: data:image/jpeg;base64,...

Image size (25.3MB) exceeds limit (20MB)

Reduce image size or compress before encoding.

Unsupported image format: bmp. Supported formats: jpeg, png, gif, webp

Convert image to a supported format.

Missing required field 'format' in input_audio

Audio content must include both 'data' and 'format' fields.

Token Estimation for Multimodal Content

Multimodal content consumes tokens based on content type and size:

Image Tokens

  • Low detail: Fixed 85 tokens per image
  • High detail: 85 + (number of 512px tiles × 170) tokens
  • Auto detail: Automatically determined based on image size

Audio Tokens

Estimated based on audio duration: approximately 2 tokens per second of audio. When duration cannot be determined, a conservative estimate is used based on file size.

Note: Token estimates are included in usage tracking and billing records. The response includes a breakdown of text, image, and audio tokens consumed.

Best Practices for Multimodal Content

  • Optimize file sizes: Compress images and audio before encoding to reduce token usage and improve response times
  • Use appropriate detail levels: Use 'low' detail for simple image recognition, 'high' for detailed analysis
  • Handle validation errors: Implement proper error handling for validation failures with retry logic
  • Monitor size limits: Stay well below size limits (80% threshold triggers warnings)
  • Mix content types: Combine text, images, and audio in a single message for rich interactions
  • Backward compatibility: Standard HTTP/HTTPS image URLs continue to work alongside base64 data URLs
POST/v1/workflows/execute

Execute Workflow

Execute a workflow project programmatically with custom input parameters. The workflow runs using the same logic as the UI, returning results formatted with node labels as keys.

Authentication

Bearer token required in Authorization header. API key must be scoped to a workflow project.

Input Parameters

Workflow input parameters match the format used in the UI's "Input Parameters" modal. All parameters are optional, but at least one should be provided:

  • input_vars: Object with key-value pairs for workflow variables
  • images: Array of base64-encoded image strings
  • audios: Array of base64-encoded audio strings
  • urls: Array of URL strings for web scraping or data fetching
  • url_inputs: Array of URL input objects with node-specific configuration
  • external_apis: Array of external API configuration objects

Output Format

The workflow output is formatted with node labels as keys, matching the format displayed in the UI toaster. Each node's output is accessible by its label:

Workflows may include Files and Multimodal RAG context nodes (configured in the functions‑workflow‑builder). They enrich downstream nodes with file URLs or vector‑store search results and are billed to your project as audited usage. They are internal to the workflow and do not change this endpoint's request or response contract.

{
  "success": true,
  "output": {
    "Web Scraper": {
      "articles": [
        {"title": "AI Breakthrough", "url": "..."},
        {"title": "ML Advances", "url": "..."}
      ]
    },
    "Summarizer": {
      "summary": "Recent AI developments include..."
    }
  },
  "execution_time_ms": 2341
}

Project Selection

The API determines which workflow project to execute based on this priority order:

  1. API key's associated project (if configured) - Takes precedence over all other options
  2. project_id parameter (if API key is not scoped to a project)
  3. Your default project (first created workflow project)

Note: For workflow execution, it's recommended to scope your API key to a specific workflow project. This ensures proper access control and prevents accidental execution of the wrong workflow.

Rate Limiting

Workflow execution uses the same rate limiting as the chat completions endpoint. Rate limit headers are included in all responses:

  • X-RateLimit-Limit: Maximum requests per time window
  • X-RateLimit-Remaining: Requests remaining in current window
  • X-RateLimit-Reset: Unix timestamp when limit resets

Error Responses

Workflow execution can return the following error codes:

{
  "error": {
    "message": "string - Human-readable error message",
    "type": "string - Error type (authentication_error, permission_denied, validation_error, etc.)",
    "code": "string - Specific error code"
  }
}
  • 401 Unauthorized: Invalid, missing, or revoked API key
  • 403 Forbidden: Project doesn't belong to your account or is disabled
  • 404 Not Found: Workflow project doesn't exist
  • 422 Validation Error: Invalid input parameters or malformed JSON
  • 429 Too Many Requests: Rate limit exceeded
  • 500 Server Error: Workflow execution failed or internal error

Example Success Response

{
  "success": true,
  "output": {
    "Web Scraper": {
      "articles": [
        {"title": "AI Breakthrough", "url": "https://example.com/article1"},
        {"title": "ML Advances", "url": "https://example.com/article2"}
      ]
    },
    "Summarizer": {
      "summary": "Recent AI developments include breakthrough in neural networks..."
    }
  },
  "execution_time_ms": 2341
}

Example Error Response

{
  "error": {
    "message": "Invalid API key. Please check your API key and try again.",
    "type": "invalid_request_error",
    "code": "authentication_error"
  }
}

Request Schema

{
  "project_id": "string",
  "input_vars": "object",
  "images": "string[]",
  "audios": "string[]",
  "urls": "string[]",
  "url_inputs": "array",
  "external_apis": "array"
}
project_id
stringoptional
Workflow project ID (ignored if API key is scoped to a project)
input_vars
objectoptional
Key-value pairs for workflow input variables
images
string[]optional
Array of base64-encoded image strings
audios
string[]optional
Array of base64-encoded audio strings
urls
string[]optional
Array of URL strings
url_inputs
arrayoptional
Array of URL input objects with nodeId, customLabel, urls, and enabled fields
external_apis
arrayoptional
Array of external API configuration objects

Response Schema

{
  "success": "boolean",
  "output": "object",
  "execution_time_ms": "number"
}
success
boolean
Execution success status
output
object
Workflow output with node labels as keys
execution_time_ms
number
Execution duration in milliseconds

Code Examples

Basic workflow execution with input variables and URLs

curl https://ainvented.com/api/v1/workflows/execute \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input_vars": {
      "search_query": "AI news",
      "max_results": 10
    },
    "urls": ["https://example.com/feed"]
  }'

Projects

Manage chat projects with their assigned API keys, two independent quotas (cents-budget and token-units quota), currency, alert thresholds, and hard-cap policies. Every monetized AI call routes through both quotas — when hard_cap is true and consumed >= budget, the gate refuses with 402 insufficient_balance; when token_hard_capis true and projected token usage would exceed token_limit, the gate refuses with 402 insufficient_token_quota— both checks happen before any billable work begins, so over-budget requests are never charged. The token check runs first; the response code lets clients tell the two refusals apart.

POST/api/v1/projects

Create chat project

Creates a chat project, its initial API key, and its initial budget in a single transaction. The full API key is returned ONCE; subsequent reads expose only the partial key.

Authentication

Bearer <api_key> — must be an Admin key (no project binding). See the Authentication section above for the scope rules.

Request Schema

{
  "name": "string (required, 1..40)",
  "type": "\"chat\" | \"function\" | \"canvas\" (optional, default \"chat\") — also accepts legacy integers 1=chat, 2=function, 3=canvas. Any other value returns 400 invalid_request_error with param=\"type\".",
  "budget_cents": "integer cents (optional, default 0)",
  "currency": "\"USD\" | \"EUR\" | \"GBP\" | \"CAD\" | \"AUD\" (optional, default \"USD\")",
  "alert_thresholds": "integer[] (optional, strictly ascending, max 20, each 1..100, default [50,80,95,100])",
  "hard_cap": "boolean (optional, default true) — refuse the request when consumed >= budget",
  "token_limit": "integer (optional, default 0) — max tokens this project can consume. 0 = unlimited.",
  "token_hard_cap": "boolean (optional, default false) — when true, refuse with 402 insufficient_token_quota once consumed_tokens + estTokens > token_limit",
  "web_search_enabled": "boolean (optional, default true) — \"web search\". When true, public http(s) URLs the end-user types in their message TEXT are fetched and read by the assistant in /v1/chat/completions. Set false to disable: only message-text URLs are suppressed — attached project files, `image_url` parts, and audio are always forwarded either way. Does not change billing.",
  "prompt_cache_enabled": "boolean (optional, default true) — prompt caching for this project. When enabled (the default), repeated prompt prefixes are served from cache and billed at the discounted cached rate, lowering consumption at no quality cost. Set false to opt this project out — its requests bill cached input tokens at the full input rate. A per-request `prompt_cache` field overrides this per call.",
  "prompt_cache_ttl_hours": "string|integer|null — Managed Prompt Cache for this project, a single unified selector: 1, 6, 12 or 24 (fixed hours), \"auto\" (hand the lifetime to the optimizer, which continuously picks the best TTL to maximize this project's net cache savings), or null to disable (default). Keeps the project prompt prefix (>=1,024 tokens) cached for a guaranteed lifetime so every reuse bills at the cached rate. Storage: $1.50 per 1M tokens/hour + $0.40 per 1M one-time write. Same options and pricing for both models.",
  "preconfigured_model": "'ainvented-default' | 'ainvented-low' (optional, default 'ainvented-default') — the project's DEFAULT chat model tier, used by /v1/chat/completions when the caller omits the `model` field. An explicit request `model` always overrides this. 'ainvented-low' is the lower-cost tier (see the pricing page)."
}
name
string (required, 1..40)
type
"chat" | "function" | "canvas" (optional, default "chat") — also accepts legacy integers 1=chat, 2=function, 3=canvas. Any other value returns 400 invalid_request_error with param="type".
budget_cents
integer cents (optional, default 0)
currency
"USD" | "EUR" | "GBP" | "CAD" | "AUD" (optional, default "USD")
alert_thresholds
integer[] (optional, strictly ascending, max 20, each 1..100, default [50,80,95,100])
hard_cap
boolean (optional, default true) — refuse the request when consumed >= budget
token_limit
integer (optional, default 0) — max tokens this project can consume. 0 = unlimited.
token_hard_cap
boolean (optional, default false) — when true, refuse with 402 insufficient_token_quota once consumed_tokens + estTokens > token_limit
web_search_enabled
boolean (optional, default true) — "web search". When true, public http(s) URLs the end-user types in their message TEXT are fetched and read by the assistant in /v1/chat/completions. Set false to disable: only message-text URLs are suppressed — attached project files, `image_url` parts, and audio are always forwarded either way. Does not change billing.
prompt_cache_enabled
boolean (optional, default true) — prompt caching for this project. When enabled (the default), repeated prompt prefixes are served from cache and billed at the discounted cached rate, lowering consumption at no quality cost. Set false to opt this project out — its requests bill cached input tokens at the full input rate. A per-request `prompt_cache` field overrides this per call.
prompt_cache_ttl_hours
string|integer|null — Managed Prompt Cache for this project, a single unified selector: 1, 6, 12 or 24 (fixed hours), "auto" (hand the lifetime to the optimizer, which continuously picks the best TTL to maximize this project's net cache savings), or null to disable (default). Keeps the project prompt prefix (>=1,024 tokens) cached for a guaranteed lifetime so every reuse bills at the cached rate. Storage: $1.50 per 1M tokens/hour + $0.40 per 1M one-time write. Same options and pricing for both models.optional
preconfigured_model
'ainvented-default' | 'ainvented-low' (optional, default 'ainvented-default') — the project's DEFAULT chat model tier, used by /v1/chat/completions when the caller omits the `model` field. An explicit request `model` always overrides this. 'ainvented-low' is the lower-cost tier (see the pricing page).

Response Schema

{
  "id": "uuid",
  "name": "string",
  "type": "integer — DEPRECATED. Kept for backward compatibility with pre-2026-05 clients. Use type_name instead.",
  "type_name": "\"chat\" | \"function\" | \"canvas\" — canonical project kind.",
  "api_key": {
    "id": "uuid",
    "key": "full key — returned only once",
    "partialKey": "last 4 chars"
  },
  "budget": {
    "budget_cents": "number",
    "consumed_cents": "number",
    "remaining_cents": "number",
    "remaining_pct": "number",
    "currency": "string",
    "hard_cap": "boolean",
    "alert_thresholds": "number[]",
    "fired_thresholds": "number[]",
    "token_limit": "number",
    "consumed_tokens": "number",
    "remaining_tokens": "number",
    "remaining_tokens_pct": "number",
    "token_hard_cap": "boolean",
    "fired_token_thresholds": "number[]"
  },
  "web_search_enabled": "boolean — effective value (true unless explicitly created with web_search_enabled:false)",
  "prompt_cache_enabled": "boolean — effective value (true unless explicitly set to false)",
  "prompt_cache_ttl_hours": "string|integer|null — managed-cache value: \"auto\" (optimizer-managed), a fixed TTL in hours, or null (not configured)",
  "preconfigured_model": "string — effective default model tier ('ainvented-default' unless created with 'ainvented-low')",
  "created_at": "ISO timestamp"
}
id
uuid
name
string
type
integer — DEPRECATED. Kept for backward compatibility with pre-2026-05 clients. Use type_name instead.
type_name
"chat" | "function" | "canvas" — canonical project kind.
api_key
object
object — fields below
id
uuid
key
full key — returned only once
partialKey
last 4 chars
budget
object
object — fields below
budget_cents
number
consumed_cents
number
remaining_cents
number
remaining_pct
number
currency
string
hard_cap
boolean
alert_thresholds
number[]
fired_thresholds
number[]
token_limit
number
consumed_tokens
number
remaining_tokens
number
remaining_tokens_pct
number
token_hard_cap
boolean
fired_token_thresholds
number[]
web_search_enabled
boolean — effective value (true unless explicitly created with web_search_enabled:false)
prompt_cache_enabled
boolean — effective value (true unless explicitly set to false)
prompt_cache_ttl_hours
string|integer|null — managed-cache value: "auto" (optimizer-managed), a fixed TTL in hours, or null (not configured)
preconfigured_model
string — effective default model tier ('ainvented-default' unless created with 'ainvented-low')
created_at
ISO timestamp

Code Examples

Create a chat project with a $250 budget AND a 1 000 000-token hard cap. Using the canonical string for `type`.

curl -X POST $BASE/api/v1/projects \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "qa-prod-1",
    "type": "chat",
    "budget_cents": 25000,
    "currency": "USD",
    "alert_thresholds": [50, 80, 95, 100],
    "hard_cap": true,
    "token_limit": 1000000,
    "token_hard_cap": true
  }'
GET/api/v1/projects

List projects for the authenticated user

Returns all non-deleted projects owned by the API key's user, with a budget summary per row.

Authentication

Bearer <api_key>

Response Schema

{
  "data": [
    {
      "id": "uuid",
      "name": "string",
      "type": "integer — DEPRECATED. Use type_name.",
      "type_name": "\"chat\" | \"function\" | \"canvas\"",
      "disabled": "boolean",
      "created_at": "ISO",
      "prompt_cache_ttl_hours": "string|integer|null — managed-cache value: \"auto\" (optimizer-managed), a fixed TTL in hours, or null (off)",
      "budget": {
        "budget_cents": "number",
        "consumed_cents": "number",
        "currency": "string",
        "hard_cap": "boolean",
        "token_limit": "number",
        "consumed_tokens": "number",
        "remaining_tokens": "number",
        "remaining_tokens_pct": "number",
        "token_hard_cap": "boolean"
      }
    }
  ]
}
data
array
array — fields below
id
uuid
name
string
type
integer — DEPRECATED. Use type_name.
type_name
"chat" | "function" | "canvas"
disabled
boolean
created_at
ISO
prompt_cache_ttl_hours
string|integer|null — managed-cache value: "auto" (optimizer-managed), a fixed TTL in hours, or null (off)
budget
object
object — fields below
budget_cents
number
consumed_cents
number
currency
string
hard_cap
boolean
token_limit
number
consumed_tokens
number
remaining_tokens
number
remaining_tokens_pct
number
token_hard_cap
boolean

Code Examples

curl $BASE/api/v1/projects -H "Authorization: Bearer $API_KEY"
PUT/api/v1/projects/cache-ttl

Bulk: set the managed-cache value on ALL your projects

Batch-applies one Managed Prompt Cache value to every project of your account in a single call — the API twin of the partner console's 'apply to all projects' button. Accepts a fixed TTL, 'auto' (hand every project to the optimizer), or null (off). Projects without a settings row get one created. Safe at any scale: a project whose prompts are never reused mints no cache and pays no storage (creation requires the prefix to be seen 3 times within 10 minutes).

Authentication

Bearer <api_key> — must be an Admin key (no project binding).

Request Schema

{
  "prompt_cache_ttl_hours": "string|integer|null — 1, 6, 12 or 24 hours, \"auto\" (optimizer-managed on every project), or null to disable on every project. Same semantics, minimum prompt size (1,024 tokens) and pricing ($1.50 per 1M tokens/hour storage + $0.40 per 1M one-time write) as the per-project setting."
}
prompt_cache_ttl_hours
string|integer|null — 1, 6, 12 or 24 hours, "auto" (optimizer-managed on every project), or null to disable on every project. Same semantics, minimum prompt size (1,024 tokens) and pricing ($1.50 per 1M tokens/hour storage + $0.40 per 1M one-time write) as the per-project setting.required

Response Schema

{
  "updated": "integer — number of projects the value was applied to",
  "prompt_cache_ttl_hours": "string|integer|null — the applied value"
}
updated
integer — number of projects the value was applied to
prompt_cache_ttl_hours
string|integer|null — the applied value

Code Examples

curl -X PUT $BASE/api/v1/projects/cache-ttl \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt_cache_ttl_hours": "auto"}'
GET/api/v1/projects/{projectId}

Read a project (with counts)

Returns the project, its full budget snapshot, and component counts (connectors, skills, workflows). Soft-deleted projects return 404 unless ?include_deleted=true is supplied.

Authentication

Bearer <api_key> — must own the project.

Response Schema

{
  "id": "uuid",
  "name": "string",
  "type": "integer — DEPRECATED. Use type_name.",
  "type_name": "\"chat\" | \"function\" | \"canvas\"",
  "disabled": "boolean",
  "created_at": "ISO",
  "budget": "BudgetSnapshot",
  "prompt_cache_ttl_hours": "string|integer|null — the project's managed-cache value: \"auto\" (optimizer-managed), a fixed TTL in hours, or null (not configured / off)",
  "counts": {
    "connector": "number",
    "skill": "number",
    "workflow": "number"
  }
}
id
uuid
name
string
type
integer — DEPRECATED. Use type_name.
type_name
"chat" | "function" | "canvas"
disabled
boolean
created_at
ISO
budget
BudgetSnapshot
prompt_cache_ttl_hours
string|integer|null — the project's managed-cache value: "auto" (optimizer-managed), a fixed TTL in hours, or null (not configured / off)
counts
object
object — fields below
connector
number
skill
number
workflow
number

Code Examples

curl $BASE/api/v1/projects/$PID -H "Authorization: Bearer $API_KEY"
PUT/api/v1/projects/{projectId}

Update a project

Patches name, budget_cents, currency, alert_thresholds, hard_cap, disabled, token_limit, token_hard_cap, web_search_enabled, prompt_cache_enabled, and prompt_cache_ttl_hours. Currency cannot be changed once consumed_cents > 0 (returns 409). Managed cache is a single unified selector: setting prompt_cache_ttl_hours to 'auto' hands the lifetime to the optimizer; a number/null sets a fixed TTL and turns auto off — there is no separate flag and no manual/auto interlock. Updating alert_thresholds re-arms BOTH fired_thresholds and fired_token_thresholds; updating token_limit re-arms fired_token_thresholds. Lowering token_limit below current consumed_tokens is allowed and is not retroactive — the next request is evaluated against the new limit. Empty patch returns 400. Audit log emits project.token_limit.set and project.token_limit.cap_toggle, project.web_search.toggle, project.prompt_caching.toggle, and project.prompt_cache_ttl.set, when the corresponding fields change.

Authentication

Bearer <api_key> — must own the project.

Request Schema

{
  "name": "string (optional, 1..40)",
  "budget_cents": "integer",
  "currency": "\"USD\" | \"EUR\" | \"GBP\" | \"CAD\" | \"AUD\"",
  "alert_thresholds": "integer[] (optional, strictly ascending, max 20)",
  "hard_cap": "boolean",
  "disabled": "boolean",
  "token_limit": "integer (optional, ≥ 0) — 0 means unlimited",
  "token_hard_cap": "boolean",
  "web_search_enabled": "boolean — enable/disable fetching of public URLs typed in message TEXT (\"web search\") for this project. Only message-text URLs are affected; files, image_url parts, and audio are unaffected.",
  "prompt_cache_enabled": "boolean — enable/disable prompt caching for this project. When false, requests bill cached input tokens at the full input rate.",
  "prompt_cache_ttl_hours": "string|integer|null — the unified Managed Prompt Cache selector: set a fixed TTL (1|6|12|24), \"auto\" (hand the lifetime to the optimizer), or null to disable. $1.50 per 1M tokens/hour storage while active. Selecting a fixed value/off turns auto off and vice-versa — no interlock.",
  "preconfigured_model": "'ainvented-default' | 'ainvented-low' — set the project's DEFAULT chat model tier (used when /v1/chat/completions omits `model`). 'ainvented-default' clears any override back to the standard model."
}
name
string (optional, 1..40)
budget_cents
integeroptional
currency
"USD" | "EUR" | "GBP" | "CAD" | "AUD"optional
alert_thresholds
integer[] (optional, strictly ascending, max 20)
hard_cap
booleanoptional
disabled
booleanoptional
token_limit
integer (optional, ≥ 0) — 0 means unlimited
token_hard_cap
booleanoptional
web_search_enabled
boolean — enable/disable fetching of public URLs typed in message TEXT ("web search") for this project. Only message-text URLs are affected; files, image_url parts, and audio are unaffected.optional
prompt_cache_enabled
boolean — enable/disable prompt caching for this project. When false, requests bill cached input tokens at the full input rate.optional
prompt_cache_ttl_hours
string|integer|null — the unified Managed Prompt Cache selector: set a fixed TTL (1|6|12|24), "auto" (hand the lifetime to the optimizer), or null to disable. $1.50 per 1M tokens/hour storage while active. Selecting a fixed value/off turns auto off and vice-versa — no interlock.optional
preconfigured_model
'ainvented-default' | 'ainvented-low' — set the project's DEFAULT chat model tier (used when /v1/chat/completions omits `model`). 'ainvented-default' clears any override back to the standard model.optional

Response Schema

{
  "id": "uuid",
  "name": "string",
  "disabled": "boolean",
  "budget": "BudgetSnapshot (includes token fields)",
  "web_search_enabled": "boolean — echoed only when web_search_enabled was included in the patch",
  "prompt_cache_enabled": "boolean — echoed only when prompt_cache_enabled was included in the patch",
  "prompt_cache_ttl_hours": "string|integer|null — echoed only when included in the patch (\"auto\" | fixed hours | null)",
  "preconfigured_model": "string — echoed only when preconfigured_model was included in the patch"
}
id
uuid
name
string
disabled
boolean
budget
BudgetSnapshot (includes token fields)
web_search_enabled
boolean — echoed only when web_search_enabled was included in the patch
prompt_cache_enabled
boolean — echoed only when prompt_cache_enabled was included in the patch
prompt_cache_ttl_hours
string|integer|null — echoed only when included in the patch ("auto" | fixed hours | null)
preconfigured_model
string — echoed only when preconfigured_model was included in the patch

Code Examples

Raise budget, tighten alerts, and switch to a 500 000-token hard cap.

curl -X PUT $BASE/api/v1/projects/$PID \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "budget_cents": 50000,
    "alert_thresholds": [25, 50, 75, 100],
    "token_limit": 500000,
    "token_hard_cap": true
  }'
DELETE/api/v1/projects/{projectId}

Soft delete a project

Sets disabled=true, revokes all API keys (sets revokedAt), and deactivates all webhooks. Idempotent: a second call returns 410. Subsequent reads return 404 unless ?include_deleted=true is supplied. The DB row is preserved for audit.

Authentication

Bearer <api_key> — must own the project.

Code Examples

curl -X DELETE $BASE/api/v1/projects/$PID -H "Authorization: Bearer $API_KEY"

Payments

Collect payments from your end-clients into ainvented's Stripe account. By default the equivalent USD amount is credited to your user balance; optionally set credit_target: "project_budget"to credit a project budget instead. USD only. Refunds happen via the Stripe dashboard and are mirrored read-only — they never debit your balance. You can also configure per-project auto-recharge(below) to top a project budget up automatically when it runs low.

Credit model — two independent ledgers

Your user balance and each project budget are separate prepaid pools — they do not roll into each other. A project_budget payment funds only that project (it does not raise your user balance). A project with a budget is self-metered: its AI usage draws down its own budget, and when that budget is exhausted the project is blocked (HTTP 402 budget_exhausted) — it does not fall back to your user balance. Check a project's remaining budget any time viaGET /api/v1/projects/{projectId}budget.remaining_cents. Budgets & payments apply to every project type (chat, function, canvas) — not chat-only.

Access required

Your account must be approved for payment collection (admin-only opt-in) before you can call these endpoints. To request access, email contact@ainvented.com with the user id you want enrolled and a brief description of your business.

Until approved, every call returns 403 permission_error with code: "payments_not_enabled".

No Stripe key is ever shared with you

The POST endpoint returns a Stripe client_secret — a one-shot token bound to a single PaymentIntent. Your frontend uses it with Stripe.js Elements + ainvented's publishable key.

The client_secret cannot create new charges, refund existing ones, or read any data outside the specific PaymentIntent it's bound to.

POST/api/v1/projects/{projectId}/payments

Create a payment

Creates a Stripe PaymentIntent on ainvented's Stripe account and returns a client_secret your frontend uses with Stripe.js. Amount is the smallest USD unit (cents) and must be between 50 ($0.50, Stripe's USD floor) and 99,999,999 ($999,999.99). EASIEST INTEGRATION: pass hosted_checkout:true to get a checkout_url instead — redirect your customer to it and you need NO Stripe key at all (not even the publishable key). For amounts ≥ $10, that checkout_url is OUR customer checkout page, which also lets the customer set up auto-reload (toggle ON by default, they can opt out) and return to your site after paying — you don't have to send anything extra. See hosted_checkout + auto_reload below.

Authentication

Bearer <api_key> — must own the project AND your account must be approved for payment collection.

Request Schema

{
  "amount_cents": "integer (required, 50..99,999,999)",
  "currency": "\"USD\" (literal; any other value → 400)",
  "description": "string (optional, max 280 chars) — shows in Stripe dashboard",
  "idempotency_key": "string (optional, max 255 chars) — replayed within 24h returns the existing payment",
  "credit_target": "\"user_balance\" | \"project_budget\" (optional, default \"user_balance\"). When \"project_budget\", the successful payment credits a project budget instead of your user balance.",
  "budget_project_id": "uuid — the project budget to credit when credit_target=\"project_budget\". Defaults to this path project; a different project must be one you also own (else 403/404).",
  "hosted_checkout": "boolean (optional, default false). true → response returns a checkout_url; redirect your customer there. You need NO Stripe key, not even the publishable key. BY DEFAULT (amount_cents ≥ $10) this returns OUR customer checkout page with an opt-out auto-reload toggle, so your customers can set up auto-reload without you sending anything. success_url/cancel_url are honored on that page as a \"Return to site\" button + a back link (they no longer force the plain Stripe page). The only case that stays on the plain Stripe-hosted page is amount_cents < $10 (too small to carry auto-reload). To keep our page but start the toggle OFF (opt-in), send auto_reload:{enabled:false}; to customize the default amounts, send your own auto_reload object.",
  "success_url": "url (optional, hosted_checkout only) — where the customer returns after paying. On our checkout page it becomes a \"Return to site\" button; on the plain Stripe page it is a redirect ({CHECKOUT_SESSION_ID} substituted).",
  "cancel_url": "url (optional, hosted_checkout only) — a \"back\" link on our checkout page / a redirect on the Stripe page.",
  "auto_reload": "object — configure project auto-reload alongside this top-up: { enabled?: boolean (defaults true — on the checkout page the auto-reload toggle starts ON so the customer opts OUT; send enabled:false to offer it opt-in), threshold_cents?, recharge_cents?, payment_method_id? }. Behavior depends on the flow: (a) EMBEDDED (no hosted_checkout) → the config is applied server-side immediately and echoed as auto_reload.applied; money/card fields merge onto any existing config. (b) HOSTED_CHECKOUT + auto_reload → checkout_url points at OUR customer checkout page (not Stripe): the CUSTOMER reviews/adjusts the auto-reload settings there, the combo is validated BEFORE they pay, and auto-reload is applied only when they complete payment — the response returns auto_reload {applied:false, pending:true, defaults:{…}}. For the hosted-checkout flow the top-up amount_cents AND the auto-reload amount each have a $10.00 (1000-cent) minimum. Omitted ⇒ no auto_reload key (unchanged). A config-write failure NEVER fails the payment."
}
amount_cents
integer (required, 50..99,999,999)
currency
"USD" (literal; any other value → 400)
description
string (optional, max 280 chars) — shows in Stripe dashboard
idempotency_key
string (optional, max 255 chars) — replayed within 24h returns the existing payment
credit_target
"user_balance" | "project_budget" (optional, default "user_balance"). When "project_budget", the successful payment credits a project budget instead of your user balance.
budget_project_id
uuid — the project budget to credit when credit_target="project_budget". Defaults to this path project; a different project must be one you also own (else 403/404).optional
hosted_checkout
boolean (optional, default false). true → response returns a checkout_url; redirect your customer there. You need NO Stripe key, not even the publishable key. BY DEFAULT (amount_cents ≥ $10) this returns OUR customer checkout page with an opt-out auto-reload toggle, so your customers can set up auto-reload without you sending anything. success_url/cancel_url are honored on that page as a "Return to site" button + a back link (they no longer force the plain Stripe page). The only case that stays on the plain Stripe-hosted page is amount_cents < $10 (too small to carry auto-reload). To keep our page but start the toggle OFF (opt-in), send auto_reload:{enabled:false}; to customize the default amounts, send your own auto_reload object.
success_url
url (optional, hosted_checkout only) — where the customer returns after paying. On our checkout page it becomes a "Return to site" button; on the plain Stripe page it is a redirect ({CHECKOUT_SESSION_ID} substituted).
cancel_url
url (optional, hosted_checkout only) — a "back" link on our checkout page / a redirect on the Stripe page.
auto_reload
object — configure project auto-reload alongside this top-up: { enabled?: boolean (defaults true — on the checkout page the auto-reload toggle starts ON so the customer opts OUT; send enabled:false to offer it opt-in), threshold_cents?, recharge_cents?, payment_method_id? }. Behavior depends on the flow: (a) EMBEDDED (no hosted_checkout) → the config is applied server-side immediately and echoed as auto_reload.applied; money/card fields merge onto any existing config. (b) HOSTED_CHECKOUT + auto_reload → checkout_url points at OUR customer checkout page (not Stripe): the CUSTOMER reviews/adjusts the auto-reload settings there, the combo is validated BEFORE they pay, and auto-reload is applied only when they complete payment — the response returns auto_reload {applied:false, pending:true, defaults:{…}}. For the hosted-checkout flow the top-up amount_cents AND the auto-reload amount each have a $10.00 (1000-cent) minimum. Omitted ⇒ no auto_reload key (unchanged). A config-write failure NEVER fails the payment.optional

Response Schema

{
  "payment_id": "uuid",
  "stripe_client_secret": "pi_*_secret_* — one-shot, bound to this PaymentIntent. (embedded flow only; absent when hosted_checkout=true)",
  "checkout_url": "the page to redirect the customer to (hosted_checkout=true only). For amounts ≥ $10 this is OUR checkout page — https://<app>/checkout/<token> (auto-reload + Return-to-site); for < $10 it is a Stripe-hosted https://checkout.stripe.com/… page.",
  "checkout_session_id": "cs_* — the Stripe Checkout Session id (hosted_checkout=true only)",
  "stripe_payment_intent_id": "pi_* — the Stripe PI id",
  "status": "PaymentStatus enum (REQUIRES_PAYMENT_METHOD, PROCESSING, SUCCEEDED, ...)",
  "amount_cents": "integer",
  "currency": "\"USD\"",
  "description": "string | null",
  "credit_target": "\"user_balance\" | \"project_budget\" — where the payment will be credited on success",
  "credited_budget_project_id": "uuid | null — the funded project budget (null for user_balance)",
  "created_at": "ISO timestamp",
  "idempotent_replay": "boolean — true when this request replayed an existing payment",
  "auto_reload": "object | absent — present only when you sent auto_reload. Embedded flow: { applied: boolean, config?: {…}, error?: {…} } (applied=false = payment succeeded but config not written, explicit). Hosted-checkout flow: { applied:false, pending:true, defaults:{…} } — the customer sets it on the checkout_url page and it is applied on payment completion."
}
payment_id
uuid
stripe_client_secret
pi_*_secret_* — one-shot, bound to this PaymentIntent. (embedded flow only; absent when hosted_checkout=true)
checkout_url
the page to redirect the customer to (hosted_checkout=true only). For amounts ≥ $10 this is OUR checkout page — https://<app>/checkout/<token> (auto-reload + Return-to-site); for < $10 it is a Stripe-hosted https://checkout.stripe.com/… page.
checkout_session_id
cs_* — the Stripe Checkout Session id (hosted_checkout=true only)
stripe_payment_intent_id
pi_* — the Stripe PI id
status
PaymentStatus enum (REQUIRES_PAYMENT_METHOD, PROCESSING, SUCCEEDED, ...)
amount_cents
integer
currency
"USD"
description
string | null
credit_target
"user_balance" | "project_budget" — where the payment will be credited on success
credited_budget_project_id
uuid | null — the funded project budget (null for user_balance)
created_at
ISO timestamp
idempotent_replay
boolean — true when this request replayed an existing payment
auto_reload
object | absent — present only when you sent auto_reload. Embedded flow: { applied: boolean, config?: {…}, error?: {…} } (applied=false = payment succeeded but config not written, explicit). Hosted-checkout flow: { applied:false, pending:true, defaults:{…} } — the customer sets it on the checkout_url page and it is applied on payment completion.

Code Examples

Easiest integration: get a checkout_url and redirect your customer. For $10+ they land on OUR checkout page — pay + (by default) opt into auto-reload — then a "Return to site" button sends them to success_url. You send nothing extra; you never touch Stripe.

curl -X POST $BASE/api/v1/projects/$PID/payments \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_cents": 5000,
    "currency": "USD",
    "credit_target": "project_budget",
    "hosted_checkout": true,
    "success_url": "https://your-app.com/thanks",
    "cancel_url": "https://your-app.com/cart"
  }'
# → { "payment_id": "...",
#     "checkout_url": "https://ainvented.com/checkout/<token>",   # OUR page (amount ≥ $10)
#     "auto_reload": { "applied": false, "pending": true,
#                      "defaults": { "enabled": true, "threshold_cents": 1000, "recharge_cents": 2000 } }, ... }
# Redirect your customer to checkout_url. No publishable key, no Stripe.js.
# (Want the toggle OFF by default? add  "auto_reload": {"enabled": false}.
#  Amounts under $10 return a plain Stripe checkout_url with no auto-reload.)
GET/api/v1/projects/{projectId}/payments

List payments for a project

Returns payments ordered by created_at DESC. Supports pagination + status / date filters.

Authentication

Bearer <api_key> — must own the project AND your account must be approved for payment collection.

Request Schema

{
  "query.status": "optional PaymentStatus enum (REQUIRES_PAYMENT_METHOD|PROCESSING|REQUIRES_ACTION|SUCCEEDED|FAILED|CANCELED|REFUNDED)",
  "query.limit": "integer 1..200 (default 20)",
  "query.offset": "integer ≥ 0 (default 0)",
  "query.from": "ISO timestamp (inclusive)",
  "query.to": "ISO timestamp (exclusive)"
}
query.status
optional PaymentStatus enum (REQUIRES_PAYMENT_METHOD|PROCESSING|REQUIRES_ACTION|SUCCEEDED|FAILED|CANCELED|REFUNDED)
query.limit
integer 1..200 (default 20)
query.offset
integer ≥ 0 (default 0)
query.from
ISO timestamp (inclusive)
query.to
ISO timestamp (exclusive)

Response Schema

{
  "data": [
    {
      "payment_id": "uuid",
      "amount_cents": "integer",
      "amount_received_cents": "integer (0 until succeeded)",
      "currency": "\"USD\"",
      "status": "PaymentStatus enum",
      "description": "string | null",
      "credit_target": "\"user_balance\" | \"project_budget\"",
      "credited_budget_project_id": "uuid | null — the funded project budget",
      "stripe_payment_intent_id": "string",
      "error_code": "string | null",
      "error_message": "string | null",
      "created_at": "ISO",
      "paid_at": "ISO | null",
      "refunded_at": "ISO | null"
    }
  ],
  "page": {
    "limit": "integer",
    "offset": "integer",
    "has_more": "boolean"
  }
}
data
array
array — fields below
payment_id
uuid
amount_cents
integer
amount_received_cents
integer (0 until succeeded)
currency
"USD"
status
PaymentStatus enum
description
string | null
credit_target
"user_balance" | "project_budget"
credited_budget_project_id
uuid | null — the funded project budget
stripe_payment_intent_id
string
error_code
string | null
error_message
string | null
created_at
ISO
paid_at
ISO | null
refunded_at
ISO | null
page
object
object — fields below
limit
integer
offset
integer
has_more
boolean

Code Examples

curl "$BASE/api/v1/projects/$PID/payments?status=SUCCEEDED&limit=50" -H "Authorization: Bearer $API_KEY"
GET/api/v1/projects/{projectId}/payments/{paymentId}

Read a single payment

Returns the full payment shape. Returns 404 if the payment is not in this project (cross-project lookups are not allowed).

Authentication

Bearer <api_key> — must own the project AND your account must be approved for payment collection.

Response Schema

{
  "payment_id": "uuid",
  "project_id": "uuid",
  "amount_cents": "integer",
  "amount_received_cents": "integer",
  "currency": "\"USD\"",
  "status": "PaymentStatus enum",
  "description": "string | null",
  "stripe_payment_intent_id": "string",
  "error_code": "string | null",
  "error_message": "string | null",
  "created_at": "ISO",
  "paid_at": "ISO | null",
  "refunded_at": "ISO | null"
}
payment_id
uuid
project_id
uuid
amount_cents
integer
amount_received_cents
integer
currency
"USD"
status
PaymentStatus enum
description
string | null
stripe_payment_intent_id
string
error_code
string | null
error_message
string | null
created_at
ISO
paid_at
ISO | null
refunded_at
ISO | null

Code Examples

curl "$BASE/api/v1/projects/$PID/payments/$PMT_ID" -H "Authorization: Bearer $API_KEY"

You never need a Stripe key

ainvented's Stripe secret key stays on our servers — you never see or handle it. Easiest path (zero Stripe keys): pass hosted_checkout: true to/payments or/payment-methods and you get back acheckout_url — just redirect your customer there. For a $10+ payment it's our checkout page (the customer can also set up auto-reload and return to your site); smaller payments and the card-save flow use Stripe's hosted page. Either way you need no Stripe key at all, not even the publishable key. Prefer to stay on your own page? Use the embedded flow instead: confirm the returnedstripe_client_secret client-side with your public publishable key. Everything is authorized with your ainvented API key only.

POST/api/v1/projects/{projectId}/payment-methods

Save a card (SetupIntent or hosted checkout)

Starts the save-card flow so a card can later back project auto-recharge. Default (embedded): ainvented creates a Stripe customer + SetupIntent server-side and returns a client_secret you confirm with Stripe.js + the PUBLIC publishable key. EASIEST: pass hosted_checkout:true to get a Stripe-hosted checkout_url (setup-mode) — redirect your customer there and you need NO Stripe key at all. Either way the saved pm_… attaches to your customer and is reusable across all your projects. You never touch ainvented's Stripe secret key.

Authentication

Bearer <api_key> — must own the project AND your account must be approved for payment collection.

Request Schema

{
  "hosted_checkout": "boolean (optional, default false). true → returns a Stripe-hosted checkout_url (no client_secret, no publishable key needed).",
  "success_url": "url (optional, hosted_checkout only) — redirect after the card is saved.",
  "cancel_url": "url (optional, hosted_checkout only) — redirect on cancel."
}
hosted_checkout
boolean (optional, default false). true → returns a Stripe-hosted checkout_url (no client_secret, no publishable key needed).
success_url
url (optional, hosted_checkout only) — redirect after the card is saved.
cancel_url
url (optional, hosted_checkout only) — redirect on cancel.

Response Schema

{
  "client_secret": "string — embedded flow: confirm client-side with Stripe.js + publishable key (absent when hosted_checkout=true)",
  "checkout_url": "https://checkout.stripe.com/… — hosted flow: redirect your customer to save a card (hosted_checkout=true only)",
  "customer_id": "string — your ainvented Stripe customer id",
  "publishable_key": "string | null — the public pk_… to use for embedded confirmation"
}
client_secret
string — embedded flow: confirm client-side with Stripe.js + publishable key (absent when hosted_checkout=true)
checkout_url
https://checkout.stripe.com/… — hosted flow: redirect your customer to save a card (hosted_checkout=true only)
customer_id
string — your ainvented Stripe customer id
publishable_key
string | null — the public pk_… to use for embedded confirmation

Code Examples

Redirect your customer to checkout_url to save a card; it then backs per-project auto-recharge.

curl -X POST "$BASE/api/v1/projects/$PID/payment-methods" \
  -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
  -d '{"hosted_checkout": true, "success_url": "https://your-app.com/card-saved"}'
# → { "data": { "checkout_url": "https://checkout.stripe.com/c/pay/cs_...", "customer_id": "cus_..." } }
GET/api/v1/projects/{projectId}/payment-methods

List saved cards

Returns the cards saved on your ainvented Stripe customer (so you can pick a pm_… for auto-recharge). Returns [] if you have not saved a card yet.

Authentication

Bearer <api_key> — must own the project AND be approved for payment collection.

Response Schema

{
  "id": "string (pm_…)",
  "brand": "string | null",
  "last4": "string | null",
  "exp_month": "integer | null",
  "exp_year": "integer | null"
}
id
string (pm_…)
brand
string | null
last4
string | null
exp_month
integer | null
exp_year
integer | null

Code Examples

curl "$BASE/api/v1/projects/$PID/payment-methods" -H "Authorization: Bearer $API_KEY"
POST/api/v1/projects/{projectId}/auto-recharge

Project auto-reload (create / get / update / disable)

Configure automatic top-up (auto-reload) of a PROJECT BUDGET. When the project’s remaining budget (budget_cents − consumed_cents) drops below threshold_cents, ainvented charges your saved card off-session for recharge_cents and credits the project budget. Four verbs: POST create/enable/partial-patch (enabled required; money/card fields merge onto any existing config; 201 new, 200 update); PUT upsert (full OR partial; money fields optional and merged; 200); GET returns the config (or null); DELETE soft-disables (active=false). A lightweight toggle is just a POST with enabled=false or enabled=true. The payment_method_id (pm_…) must already be saved via POST /payment-methods. recharge_cents must be ≥ threshold_cents, and enabling requires a saved card. Each recharge is recorded as a project payment (credit_target=project_budget), is charge-idempotent per 60s window, and fires no AI call.

Authentication

Bearer <admin api_key> — must own the project AND be approved for payment collection. (Admin key required; project-scoped keys get 403 admin_key_required.)

Request Schema

{
  "enabled": "boolean (POST: required — the on/off toggle. PUT: optional, defaults true)",
  "threshold_cents": "integer (optional, 0..99,999,999) — recharge when remaining budget falls below this. Merges onto the existing config when omitted.",
  "recharge_cents": "integer (optional*, 1000..99,999,999 — minimum $10.00) — amount charged + credited on each recharge. *Required to persist a config; must be ≥ threshold_cents. Merges when omitted.",
  "payment_method_id": "string (optional*, pm_…) — saved Stripe payment method. *Required to enable. Merges when omitted."
}
enabled
boolean (POST: required — the on/off toggle. PUT: optional, defaults true)
threshold_cents
integer (optional, 0..99,999,999) — recharge when remaining budget falls below this. Merges onto the existing config when omitted.
recharge_cents
integer (optional*, 1000..99,999,999 — minimum $10.00) — amount charged + credited on each recharge. *Required to persist a config; must be ≥ threshold_cents. Merges when omitted.
payment_method_id
string (optional*, pm_…) — saved Stripe payment method. *Required to enable. Merges when omitted.

Response Schema

{
  "project_id": "uuid",
  "threshold_cents": "integer",
  "recharge_cents": "integer",
  "active": "boolean",
  "payment_method_id": "string | null",
  "last_triggered_at": "ISO | null",
  "updated_at": "ISO | null"
}
project_id
uuid
threshold_cents
integer
recharge_cents
integer
active
boolean
payment_method_id
string | null
last_triggered_at
ISO | null
updated_at
ISO | null

Code Examples

curl -X POST "$BASE/api/v1/projects/$PID/auto-recharge" \
  -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
  -d '{"enabled": true, "threshold_cents": 1000, "recharge_cents": 5000, "payment_method_id": "pm_..."}'

Webhook events

Subscribe via POST /api/v1/projects/{id}/webhooks with events: ['payment.succeeded', 'payment.failed'] in the body.

  • payment.succeeded — fired once when the PaymentIntent reaches the terminal succeeded state. Payload includes amount_received_cents, paid_at, stripe_payment_intent_id.
  • payment.failed — fired on each failed attempt (Stripe allows retries). Payload includes error_code, error_message.

No DELETE endpoint. Refunds are not supported via API. If you need to refund a customer, use the Stripe dashboard; the row's status will mirror to REFUNDED but your balance is NOT debited (manual reconciliation).

Project API keys

Each project may have multiple API keys, each scoped to that project. Keys are hashed with Argon2id; only the last 4 characters (partialKey) are stored for display. The full key is returned exactly once at creation.

POST/api/v1/projects/{projectId}/api-keys

Create an API key under a project

Creates a new ApiKey row scoped to the project. Returns the full key once.

Authentication

Bearer <api_key> — must own the project.

Request Schema

{
  "label": "string (required, 1..100, trimmed)"
}
label
string (required, 1..100, trimmed)

Response Schema

{
  "id": "uuid",
  "key": "full key — returned only once",
  "partialKey": "last 4 chars",
  "label": "string",
  "projectId": "uuid",
  "createdAt": "ISO"
}
id
uuid
key
full key — returned only once
partialKey
last 4 chars
label
string
projectId
uuid
createdAt
ISO

Code Examples

curl -X POST $BASE/api/v1/projects/$PID/api-keys \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "label": "prod-server-1" }'
GET/api/v1/projects/{projectId}/api-keys

List API keys for a project

Returns active keys by default. Append ?include_revoked=true to also see revoked keys.

Authentication

Bearer <api_key> — must own the project.

Response Schema

{
  "keys": [
    {
      "id": "uuid",
      "label": "string",
      "partialKey": "last 4 chars",
      "projectId": "uuid",
      "createdAt": "ISO",
      "lastUsedAt": "ISO | null",
      "revokedAt": "ISO | null",
      "requestCount": "number",
      "tokenUsage": "string (BigInt as string)"
    }
  ],
  "total": "number"
}
keys
array
array — fields below
id
uuid
label
string
partialKey
last 4 chars
projectId
uuid
createdAt
ISO
lastUsedAt
ISO | null
revokedAt
ISO | null
requestCount
number
tokenUsage
string (BigInt as string)
total
number

Code Examples

curl "$BASE/api/v1/projects/$PID/api-keys?include_revoked=true" -H "Authorization: Bearer $API_KEY"
DELETE/api/v1/projects/{projectId}/api-keys/{keyId}

Revoke an API key

Sets revokedAt on the key. Subsequent calls using that key return 401. Already-revoked → 410. Cross-project mismatch → 404. Usage history is retained.

Authentication

Bearer <api_key> — must own the project.

Code Examples

curl -X DELETE $BASE/api/v1/projects/$PID/api-keys/$KEY_ID -H "Authorization: Bearer $API_KEY"

Consumption

Aggregates token + cost usage across your project. Each billable sub-call (completion, workflow, skill, assistant, connector) is recorded as its own usage event, so the reported totals always match what your project actually consumed.

GET/api/v1/projects/{projectId}/consumption

Read consumption breakdown

Returns a window of token + cost usage with a per-component breakdown and a time series. Window may not exceed 90 days. Defaults to the last 30 days UTC.

Authentication

Bearer <api_key> — must own the project.

Request Schema

{
  "from": "ISO timestamp (optional, default now-30d)",
  "to": "ISO timestamp (optional, default now)",
  "group_by": "\"day\" | \"component\" | \"model\" (optional, default \"day\")",
  "component_type": "\"completion\" | \"workflow\" | \"skill\" | \"assistant\" | \"connector\" — filters the whole response to one component",
  "model": "\"ainvented-default\" | \"ainvented-low\" | \"(none)\" — filters the whole response to one model"
}
from
ISO timestamp (optional, default now-30d)
to
ISO timestamp (optional, default now)
group_by
"day" | "component" | "model" (optional, default "day")
component_type
"completion" | "workflow" | "skill" | "assistant" | "connector" — filters the whole response to one componentoptional
model
"ainvented-default" | "ainvented-low" | "(none)" — filters the whole response to one modeloptional

Response Schema

{
  "project_id": "uuid",
  "currency": "string",
  "budget_cents": "number",
  "consumed_cents": "number",
  "remaining_cents": "number",
  "remaining_pct": "number",
  "budget_micros": "number — exact budget (10000 micros = 1 cent)",
  "consumed_micros": "number — EXACT consumption; consumed_cents is floor(consumed_micros/10000)",
  "remaining_micros": "number — exact remaining (clamped ≥ 0)",
  "exhausted": "boolean — true when the hard-cap budget is fully spent (matches the 402 gate)",
  "hard_cap": "boolean",
  "alert_thresholds": "number[]",
  "fired_thresholds": "number[]",
  "token_limit": "number",
  "consumed_tokens": "number",
  "remaining_tokens": "number",
  "remaining_tokens_pct": "number",
  "token_hard_cap": "boolean",
  "fired_token_thresholds": "number[]",
  "window": {
    "from": "ISO",
    "to": "ISO"
  },
  "totals": {
    "prompt_tokens": "number",
    "completion_tokens": "number",
    "total_tokens": "number",
    "cost_cents": "number",
    "cost_micros": "number"
  },
  "by_component": {
    "completion": {
      "events": "number",
      "total_tokens": "number",
      "cost_cents": "number"
    },
    "workflow": "...same shape...",
    "skill": "...",
    "assistant": "...",
    "connector": "..."
  },
  "tokens_by_component": {
    "completion": "number — total tokens for completion events in the window",
    "workflow": "number",
    "skill": "number",
    "assistant": "number",
    "connector": "number"
  },
  "by_model": {
    "ainvented-default": {
      "events": "number",
      "prompt_tokens": "number — input tokens for this model",
      "completion_tokens": "number — output tokens for this model",
      "total_tokens": "number",
      "cost_cents": "number"
    },
    "ainvented-low": "...same shape...",
    "(none)": "...auxiliary calls with no billed model..."
  },
  "series": [
    {
      "bucket": "string",
      "total_tokens": "number",
      "cost_cents": "number"
    }
  ]
}
project_id
uuid
currency
string
budget_cents
number
consumed_cents
number
remaining_cents
number
remaining_pct
number
budget_micros
number — exact budget (10000 micros = 1 cent)
consumed_micros
number — EXACT consumption; consumed_cents is floor(consumed_micros/10000)
remaining_micros
number — exact remaining (clamped ≥ 0)
exhausted
boolean — true when the hard-cap budget is fully spent (matches the 402 gate)
hard_cap
boolean
alert_thresholds
number[]
fired_thresholds
number[]
token_limit
number
consumed_tokens
number
remaining_tokens
number
remaining_tokens_pct
number
token_hard_cap
boolean
fired_token_thresholds
number[]
window
object
object — fields below
from
ISO
to
ISO
totals
object
object — fields below
prompt_tokens
number
completion_tokens
number
total_tokens
number
cost_cents
number
cost_micros
number
by_component
object
object — fields below
completion
object
object — fields below
events
number
total_tokens
number
cost_cents
number
workflow
...same shape...
skill
...
assistant
...
connector
...
tokens_by_component
object
object — fields below
completion
number — total tokens for completion events in the window
workflow
number
skill
number
assistant
number
connector
number
by_model
object
object — fields below
ainvented-default
object
object — fields below
events
number
prompt_tokens
number — input tokens for this model
completion_tokens
number — output tokens for this model
total_tokens
number
cost_cents
number
ainvented-low
...same shape...
(none)
...auxiliary calls with no billed model...
series
array
array — fields below
bucket
string
total_tokens
number
cost_cents
number

Code Examples

Last 7 days, grouped by component, only workflow events.

curl "$BASE/api/v1/projects/$PID/consumption?from=$(date -u -v-7d +%FT%TZ)&group_by=component&component_type=workflow" \
  -H "Authorization: Bearer $API_KEY"
GET/api/v1/projects/{projectId}/balance

Read a project's authoritative balance

Single source of truth for 'does this key/project have money?'. Money is reported at floored-cent precision (budget_cents/consumed_cents/remaining_cents, backward-compatible) AND exact micro precision (budget_micros/consumed_micros/remaining_micros; 10000 micros = 1 cent). The `exhausted` flag is computed from the SAME comparison the request-time gate uses, so polling this never disagrees with the 402 a key actually receives. IMPORTANT: a project-scoped hard-cap key spends ONLY this project budget — it never falls back to the user balance. Not admin-gated: a project-scoped key may read its own balance.

Authentication

Bearer <api_key> — must own the project.

Response Schema

{
  "project_id": "uuid",
  "budget_cents": "number",
  "consumed_cents": "number — floor(consumed_micros/10000)",
  "remaining_cents": "number",
  "budget_micros": "number",
  "consumed_micros": "number — exact",
  "remaining_micros": "number",
  "exhausted": "boolean — true when consumed_micros >= budget_micros for a hard cap",
  "remaining_pct": "number",
  "currency": "string",
  "hard_cap": "boolean",
  "token_limit": "number",
  "consumed_tokens": "number",
  "remaining_tokens": "number",
  "token_hard_cap": "boolean"
}
project_id
uuid
budget_cents
number
consumed_cents
number — floor(consumed_micros/10000)
remaining_cents
number
budget_micros
number
consumed_micros
number — exact
remaining_micros
number
exhausted
boolean — true when consumed_micros >= budget_micros for a hard cap
remaining_pct
number
currency
string
hard_cap
boolean
token_limit
number
consumed_tokens
number
remaining_tokens
number
token_hard_cap
boolean

Code Examples

Check whether a project-scoped key still has budget before relying on it.

curl "$BASE/api/v1/projects/$PID/balance" -H "Authorization: Bearer $API_KEY"

Promotions

Promo codes and referral share links. GET /api/promotions/resolve is fully public (it powers the /promo/<slug> landing pages) and returns a display-safe offer summary. Redeeming a code and reading your referral stats are browser-session endpoints: they authenticate with the session cookie of your signed-in ainvented account — not available with API keys. Money fields follow the platform conventions: offer credit amounts are USD dollars; caps, minimums and commission totals are integer cents.

GET/api/promotions/resolve

Resolve a promo code or share-link slug

Resolves a promo code or a share-link landing slug to a display-safe offer summary. Lookups always answer HTTP 200 with { valid, reason, offer } so a landing page can render the outcome honestly — including why an offer is not redeemable ('not_found' | 'inactive' | 'not_started' | 'expired' | 'fully_redeemed'). offer is null only when reason is 'not_found'. The only 400 is omitting both query parameters. Rate-limited per IP (30 requests/minute).

Authentication

None — public endpoint. No API key and no session required.

Request Schema

{
  "code": "string",
  "slug": "string"
}
code
stringoptional
Query parameter. The promo code to resolve (case-insensitive). One of code or slug is required — omitting both returns 400 invalid_request.
slug
stringoptional
Query parameter. The share-link landing slug to resolve (compared lowercased). When both code and slug are provided, slug wins.

Response Schema

{
  "valid": "boolean",
  "reason": "string | null",
  "offer": {
    "code": "string",
    "offerKind": "string",
    "creditAmount": "number | null",
    "discountPercent": "number | null",
    "discountCapCents": "integer | null",
    "minTopupCents": "integer | null",
    "slug": "string | null",
    "appliesAtSignup": "boolean",
    "appliesAtReload": "boolean",
    "expiresAt": "string | null"
  }
}
valid
booleanrequired
true when the offer exists and is currently redeemable.
reason
string | nullrequired
Why the offer is NOT redeemable: 'not_found' | 'inactive' | 'not_started' | 'expired' | 'fully_redeemed'. null when valid is true.
offer
object
object — fields below
code
stringrequired
The canonical promo code.
offerKind
stringrequired
'credit' (adds balance instantly at redemption) | 'reload_discount' (a bonus applied to a future qualifying top-up).
creditAmount
number | nullrequired
USD dollars credited on redemption for a 'credit' offer; null for other kinds.
discountPercent
number | nullrequired
Bonus percent applied to a qualifying top-up for a 'reload_discount' offer; null for 'credit' offers.
discountCapCents
integer | nullrequired
Upper bound of the reload bonus in integer cents; null for 'credit' offers.
minTopupCents
integer | nullrequired
Minimum top-up in integer cents before the offer applies; null when there is no minimum.
slug
string | nullrequired
Landing-page slug — the offer's shareable page is /promo/<slug>. null when the offer has no landing page.
appliesAtSignup
booleanrequired
Whether the offer applies when a new user signs up with the code.
appliesAtReload
booleanrequired
Whether the offer applies at reload (top-up) time.
expiresAt
string | nullrequired
ISO timestamp after which the offer expires; null when it never expires.

Code Examples

Resolve a promo code (case-insensitive).

curl "$BASE/api/promotions/resolve?code=WELCOME10"

# Response — 200 OK
# {
#   "valid": true,
#   "reason": null,
#   "offer": {
#     "code": "WELCOME10",
#     "offerKind": "credit",
#     "creditAmount": 10,
#     "discountPercent": null,
#     "discountCapCents": null,
#     "minTopupCents": null,
#     "slug": "welcome",
#     "appliesAtSignup": true,
#     "appliesAtReload": true,
#     "expiresAt": null
#   }
# }
POST/api/billing/redeem-promo

Redeem a promo code

Redeems a promo code for the signed-in user. A 'credit' offer credits your USD balance instantly; a 'reload_discount' offer records a claim that is applied automatically to your next qualifying top-up. Each user can redeem a given code at most once, and redemption caps are enforced atomically. Same-origin requests only (CSRF protection), rate-limited to 10 attempts/minute per user.

Authentication

Session cookie required (browser session) — not available with API keys.

Request Schema

{
  "code": "string"
}
code
stringrequired
The promo code to redeem. Minimum 4 characters; letters, digits and hyphens only; case-insensitive.

Response Schema

{
  "success": "boolean",
  "offerKind": "string",
  "creditAmount": "string",
  "newBalance": "string",
  "discountPercent": "string | null",
  "message": "string"
}
success
booleanrequired
true when the redemption (or discount claim) was recorded.
offerKind
stringrequired
'credit' | 'reload_discount'.
creditAmount
stringoptional
USD dollars credited, as a decimal string. Present only for 'credit' offers.
newBalance
stringoptional
Your updated USD balance, as a decimal string. Present only for 'credit' offers.
discountPercent
string | nulloptional
The claimed bonus percent, as a decimal string. Present only for 'reload_discount' offers.
message
stringoptional
Human-readable confirmation. Present only for 'reload_discount' offers.

Code Examples

Redeem a credit or reload-discount code with your browser session.

# Runs with your signed-in browser session cookie (see Authentication above).
curl -X POST $BASE/api/billing/redeem-promo \
  -H "Content-Type: application/json" \
  -b "authjs.session-token=<your browser session cookie>" \
  -d '{ "code": "WELCOME10" }'

# Response — 200 OK (credit offer: balance credited instantly)
# { "success": true, "offerKind": "credit", "creditAmount": "10", "newBalance": "12.50" }

# Response — 200 OK (reload_discount offer: claim saved, applied on your next top-up)
# {
#   "success": true,
#   "offerKind": "reload_discount",
#   "discountPercent": "20",
#   "message": "Discount saved — it will apply to your next reload."
# }
GET/api/referrals/me

Your referral stats & share link

Returns the signed-in user's referral summary for the Share & Earn view: whether you are enrolled as a promoter, your shareable link, how many users you have referred, and your commission totals in integer cents. Enrollment is automatic when the Share & Earn program is enabled (your first call provisions your link); otherwise it is admin-managed. Non-enrolled users receive asPromoter:false with zeroed totals — never an error. The response shape is identical in every case.

Authentication

Session cookie required (browser session) — not available with API keys.

Response Schema

{
  "data": {
    "asPromoter": "boolean",
    "shareUrl": "string | null",
    "referredCount": "number",
    "commission": {
      "accruedCashCents": "integer",
      "paidCashCents": "integer",
      "autoCreditedCents": "integer",
      "currency": "string"
    }
  },
  "generatedAt": "string"
}
data
object
object — fields below
asPromoter
booleanrequired
true when you are enrolled as a promoter. Enrollment is admin-managed — when false every other field is its zero/null state.
shareUrl
string | nullrequired
Your shareable referral link: a /promo/<slug> landing page when a landing-page offer exists, otherwise a tracked /r/<id> redirect that forwards visitors to signup. null when not enrolled.
referredCount
numberrequired
Users attributed to you. Attribution is first-wins: a user counts for at most one referrer, forever.
commission
object
object — fields below
accruedCashCents
integerrequired
Commission earned and owed to you, awaiting cash payout. Integer cents.
paidCashCents
integerrequired
Commission already paid out to you in cash. Integer cents.
autoCreditedCents
integerrequired
Commission automatically credited to your platform balance. Integer cents.
currency
stringrequired
Always 'USD'.
generatedAt
stringrequired
ISO timestamp of when the response was generated.

Code Examples

Read your own referral stats (enrolled and not-enrolled shapes shown).

# Runs with your signed-in browser session cookie (see Authentication above).
curl "$BASE/api/referrals/me" \
  -b "authjs.session-token=<your browser session cookie>"

# Response — 200 OK (enrolled promoter)
# {
#   "data": {
#     "asPromoter": true,
#     "shareUrl": "https://ainvented.com/r/9c1f4c1e-0000-4000-8000-000000000000",
#     "referredCount": 3,
#     "commission": {
#       "accruedCashCents": 1500,
#       "paidCashCents": 200,
#       "autoCreditedCents": 0,
#       "currency": "USD"
#     }
#   },
#   "generatedAt": "2026-07-01T12:00:00.000Z"
# }

# Response — 200 OK (not enrolled)
# {
#   "data": { "asPromoter": false, "shareUrl": null, "referredCount": 0,
#             "commission": { "accruedCashCents": 0, "paidCashCents": 0,
#                             "autoCreditedCents": 0, "currency": "USD" } },
#   "generatedAt": "2026-07-01T12:00:00.000Z"
# }

Vector Stores

Use Ainvented as a vector store. A store is a named collection of documents in your project's vector space. Ingest documents (text, URL, or file) — they are chunked and embedded — then run semantic search, optionally with a RAG-generated answer.

Cost
Creating, renaming, and listing a store is free. add document (embedding) and query (query embedding, plus generation when answer:true) consume tokens and are metered against your project's budget and token quota. There is no zero-token billable path.

Node.js client (example)

A small, zero-dependency example client can wrap every endpoint below — its methods map 1:1 to the REST calls and it throws a typed VectorStoreApiError (carrying status and code) on any non-2xx response. Save it as vectorStoreClient.mjs and call it like this:

quickstart.mjs — Node 18+, create → ingest → search
import { VectorStoreClient } from './vectorStoreClient.mjs'

// baseUrl defaults to https://ainvented.com
const vs = new VectorStoreClient({ apiKey: process.env.API_KEY })

const store = await vs.createStore({ name: 'product-docs' })
await vs.addText(store.id, 'Ainvented can be used as a vector store.')
await vs.addFile(store.id, './manual.pdf')          // any supported type

const { results, answer } = await vs.query(
  store.id, 'how do I use Ainvented as a vector store?', { topK: 5, answer: true })
console.log(answer, results)
POST/v1/vector-stores

Create / list / get stores

Create a store (local, zero-cost — reserves a tag). GET /v1/vector-stores lists; GET /v1/vector-stores/{id} retrieves; POST /v1/vector-stores/{id} updates name/description; all are local.

Authentication

Bearer <api_key>

Request Schema

{
  "name": "string (required, 1–200)",
  "description": "string (optional, ≤500)",
  "project_id": "uuid (optional; ignored when the API key is project-scoped)"
}
name
string (required, 1–200)
description
string (optional, ≤500)
project_id
uuid (optional; ignored when the API key is project-scoped)

Response Schema

{
  "id": "string (cuid)",
  "name": "string",
  "description": "string|null",
  "tag": "string (vs:<id>)",
  "status": "\"active\" | \"archived\"",
  "project_id": "uuid",
  "document_count": "number",
  "created_at": "ISO",
  "updated_at": "ISO"
}
id
string (cuid)
name
string
description
string|null
tag
string (vs:<id>)
status
"active" | "archived"
project_id
uuid
document_count
number
created_at
ISO
updated_at
ISO

Code Examples

Create a store.

curl -X POST "$BASE/api/v1/vector-stores" \
  -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
  -d '{"name":"product-docs","description":"Docs for RAG"}'
POST/v1/vector-stores/{id}/documents

Add / list documents

Add a document via JSON (text or url) or multipart (file ≤100MB). Raw text is stored as a .txt document. Embedding tokens are metered. Very large text-heavy files may take up to 10 minutes to embed. GET lists documents (?status=ready|pending|failed|archived|all).

Authentication

Bearer <api_key>

Per-chunk metadata (optional)

Attach arbitrary metadata (a JSON object of key→value pairs) when adding a document and it is stored on every chunk of that document. You can then narrow a search to matching chunks with the query metadata_filter (see below). Pass it as a JSON body field for text/url adds, or as a multipart metadata form field (a JSON-object string) for file uploads. A malformed or non-object multipart metadata value is silently ignored — never an error.

Optional & backward-compatible
metadata is fully optional. Omitting it sends the exact same request as before and ingests with no chunk metadata. Metadata is stored alongside the chunk and adds no model/LLM cost — embedding tokens are identical to the same add without metadata.

Supported file types

Uploads are chunked + embedded (text is extracted; audio is transcribed; images are embedded for visual search). Unknown extensions are ingested as best-effort plain text. Max 100 MB. Very large text-heavy files may take up to 10 minutes to embed.

  • 📄 Text & Office: .pdf .docx .pptx .xlsx .xls .csv .json .jsonl .txt .md
  • 💻 Code & config: .py .js .ts .tsx .jsx .java .cpp .c .h .cs .php .rb .go .rs .swift .kt .scala .r .sh .bash .sql .xml .yaml .yml .toml .ini .cfg .conf .css .scss .sass .less
  • 🖼️ Images: .png .jpg .jpeg .gif .webp .bmp .tiff .svg
  • 🔊 Audio: .mp3 .wav .m4a .ogg .flac .aac
  • 🌐 Web: pass url (JSON body) for an HTTP/HTTPS page — text plus extracted images/audio.
How images & audio behave
Audio is transcribed to text and then chunked like any document. An image is embedded for cross-modal (text→image) retrieval and stored with doc_type:"image", char_count:0 and chunk_count:0 — it carries no text chunk, so it matches on visual similarity, not keywords. Use an on-topic query and a low/zero threshold so a purely visual image ranks alongside text chunks; each result includes image_b64 for rendering.

Request Schema

{
  "text": "string — raw text to embed (one of text/url/file)",
  "url": "string (url) — fetch + embed remote content",
  "file (multipart)": "binary — documents, code/config, images, audio, or ANY extension (best-effort text), ≤100MB",
  "title": "string (optional, ≤300)",
  "tags": "string[] (optional, extra tags ≤20)",
  "chunk_size": "int",
  "chunk_overlap": "int",
  "metadata": "object — key→value pairs applied to EVERY chunk of this document; filter on them later with query metadata_filter. JSON body field for text/url; multipart `metadata` form field (a JSON-object string) for file. Optional & backward-compatible — omit it and the request is unchanged.",
  "generate_metadata": "boolean (optional, default false) — when true, an LLM extracts semantic metadata for the document (topics, summary, keywords, entities, language) and merges it into every chunk's filterable metadata. This makes an extra, fully-audited model call billed cost-plus alongside embeddings; omit/false adds no cost. Multipart: `generate_metadata=true` form field."
}
text
string — raw text to embed (one of text/url/file)
url
string (url) — fetch + embed remote content
file (multipart)
binary — documents, code/config, images, audio, or ANY extension (best-effort text), ≤100MB
title
string (optional, ≤300)
tags
string[] (optional, extra tags ≤20)
chunk_size
intoptional
chunk_overlap
intoptional
metadata
object — key→value pairs applied to EVERY chunk of this document; filter on them later with query metadata_filter. JSON body field for text/url; multipart `metadata` form field (a JSON-object string) for file. Optional & backward-compatible — omit it and the request is unchanged.optional
generate_metadata
boolean (optional, default false) — when true, an LLM extracts semantic metadata for the document (topics, summary, keywords, entities, language) and merges it into every chunk's filterable metadata. This makes an extra, fully-audited model call billed cost-plus alongside embeddings; omit/false adds no cost. Multipart: `generate_metadata=true` form field.

Response Schema

{
  "id": "string",
  "store_id": "string",
  "kind": "\"text\"|\"url\"|\"file\"",
  "title": "string|null",
  "source": "string|null",
  "char_count": "number",
  "chunk_count": "number",
  "embed_tokens": "number",
  "status": "\"ready\"|\"failed\"",
  "error_code": "string|null",
  "created_at": "ISO"
}
id
string
store_id
string
kind
"text"|"url"|"file"
title
string|null
source
string|null
char_count
number
chunk_count
number
embed_tokens
number
status
"ready"|"failed"
error_code
string|null
created_at
ISO

Code Examples

Embed a text blob.

curl -X POST "$BASE/api/v1/vector-stores/$SID/documents" \
  -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
  -d '{"text":"Ainvented can be used as a vector store.","chunk_size":800,"chunk_overlap":100}'
POST/v1/vector-stores/{id}/query

Query — semantic search (+ optional RAG answer)

Embeds the query and returns ranked chunks. Set answer:true to also generate a grounded answer from the retrieved chunks (a second, billable generation step). optimize_query:true adds a billable query rewrite.

Authentication

Bearer <api_key>

Map-reduce pre-filters (optional)

metadata_filter is an array of AND-ed predicates (≤20), each { key, op?, value? }. Supported op values: eq (default) neq in contains gt gte lt lte exists. exists tests for the key and takes no value; in takes an array of values. text_contains (≤500 chars) keeps only chunks whose text contains that substring. Both are applied before the semantic search (the “map” step), then the usual top_k/threshold ranking runs over what survives.

Optional, backward-compatible & no extra cost
Both fields are optional. Omitting them reproduces the exact legacy search(identical request and behaviour). Filtering happens upstream before the vector search, so it adds no model/LLM cost — the query embedding is unchanged whether or not you filter. Each query is billed exactly once against the calling project.

Request Schema

{
  "query": "string (required, ≤10000)",
  "top_k": "int (optional, 1–100)",
  "threshold": "number (optional, 0–1 similarity cutoff)",
  "optimize_query": "boolean (optional; extra cost)",
  "tags": "string[] (optional, extra tag filter)",
  "answer": "boolean (optional — generate a RAG answer)",
  "max_tokens": "int (optional, answer length cap)",
  "metadata_filter": "Array<{ key: string, op?: \"eq\"|\"neq\"|\"in\"|\"contains\"|\"gt\"|\"gte\"|\"lt\"|\"lte\"|\"exists\", value?: any }> (optional, ≤20). AND-ed pre-filters applied BEFORE the semantic search; op defaults to \"eq\"; \"exists\" takes no value; \"in\" takes an array. Optional & backward-compatible.",
  "text_contains": "string (optional, ≤500) — keep only chunks whose text contains this substring (applied before the semantic search). Optional & backward-compatible."
}
query
string (required, ≤10000)
top_k
int (optional, 1–100)
threshold
number (optional, 0–1 similarity cutoff)
optimize_query
boolean (optional; extra cost)
tags
string[] (optional, extra tag filter)
answer
boolean (optional — generate a RAG answer)
max_tokens
int (optional, answer length cap)
metadata_filter
Array<{ key: string, op?: "eq"|"neq"|"in"|"contains"|"gt"|"gte"|"lt"|"lte"|"exists", value?: any }> (optional, ≤20). AND-ed pre-filters applied BEFORE the semantic search; op defaults to "eq"; "exists" takes no value; "in" takes an array. Optional & backward-compatible.
text_contains
string (optional, ≤500) — keep only chunks whose text contains this substring (applied before the semantic search). Optional & backward-compatible.

Response Schema

{
  "results": "[{ text, title, source, score, metadata, ... }] — each chunk now carries the metadata it was ingested with (object, may be empty/absent)",
  "count": "number",
  "answer": "string (only when answer:true)",
  "usage": {
    "total_tokens": "number — embedding (+ generation when answer:true)"
  }
}
results
[{ text, title, source, score, metadata, ... }] — each chunk now carries the metadata it was ingested with (object, may be empty/absent)
count
number
answer
string (only when answer:true)
usage
object
object — fields below
total_tokens
number — embedding (+ generation when answer:true)

Code Examples

Top-3 semantic search.

curl -X POST "$BASE/api/v1/vector-stores/$SID/query" \
  -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
  -d '{"query":"How do I use Ainvented as a vector store?","top_k":3}'
DELETE/v1/vector-stores/{id}

Soft delete (store or document)

DELETE /v1/vector-stores/{id} archives the store and removes its documents from search. DELETE /v1/vector-stores/{id}/documents/{docId} removes a single document. Both are soft (the record is preserved with status=archived) — never a destructive delete.

Authentication

Bearer <api_key>

Cost & metering

Adding a document, searching, and generating an answer each consume tokens that are metered against your project's budget and token quota — one usage event per billable call, so your Consumption totals always match what you actually used. Local operations (create, rename, list, soft delete) are free.

Response Schema

{
  "id": "string",
  "status": "\"archived\"",
  "deleted": "true"
}
id
string
status
"archived"
deleted
true

Code Examples

Archive a store.

curl -X DELETE "$BASE/api/v1/vector-stores/$SID" -H "Authorization: Bearer $API_KEY"

Webhooks

Register HTTPS endpoints to receive notifications when a project crosses a budget threshold (50%, 80%, 95%, 100% by default), exhausts its budget, or — opt-in — on every recorded usage event. Deliveries are signed with HMAC-SHA256 over the raw body using the secret returned at registration. Each delivery is idempotent on a payload hash so retries do not duplicate. URLs must be https:// and cannot resolve to RFC1918 / loopback / link-local / multicast addresses.

POST/api/v1/projects/{projectId}/webhooks

Register a webhook

Creates a webhook registration. The signing secret is returned ONCE in the response and is never readable again — store it securely.

Authentication

Bearer <api_key> — must own the project. Project must not be disabled.

Request Schema

{
  "url": "string (required, https://, no credentials, ≤2048 chars, public IP only)",
  "events": "(\"budget.threshold\" | \"budget.exhausted\" | \"tokens.threshold\" | \"tokens.exhausted\" | \"usage.recorded\")[] (optional, default [\"budget.threshold\",\"budget.exhausted\"])"
}
url
string (required, https://, no credentials, ≤2048 chars, public IP only)
events
("budget.threshold" | "budget.exhausted" | "tokens.threshold" | "tokens.exhausted" | "usage.recorded")[] (optional, default ["budget.threshold","budget.exhausted"])

Response Schema

{
  "id": "uuid",
  "url": "string",
  "events": "string[]",
  "is_active": "boolean",
  "secret": "string — 64 hex chars; returned only once",
  "created_at": "ISO"
}
id
uuid
url
string
events
string[]
is_active
boolean
secret
string — 64 hex chars; returned only once
created_at
ISO

Code Examples

curl -X POST $BASE/api/v1/projects/$PID/webhooks \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://hooks.your-app.com/budget", "events": ["budget.threshold","budget.exhausted"] }'
GET/api/v1/projects/{projectId}/webhooks

List webhooks

Active by default. Append ?include_disabled=true to also see soft-deleted webhooks. Secrets are NEVER returned by list.

Authentication

Bearer <api_key> — must own the project.

Response Schema

{
  "data": [
    {
      "id": "uuid",
      "url": "string",
      "events": "string[]",
      "is_active": "boolean",
      "created_at": "ISO",
      "last_delivery_at": "ISO | null",
      "failure_count": "number"
    }
  ]
}
data
array
array — fields below
id
uuid
url
string
events
string[]
is_active
boolean
created_at
ISO
last_delivery_at
ISO | null
failure_count
number

Code Examples

curl $BASE/api/v1/projects/$PID/webhooks -H "Authorization: Bearer $API_KEY"
PUT/api/v1/projects/{projectId}/webhooks/{webhookId}

Update a webhook (incl. rotate secret)

Patches url, events, or is_active. Pass rotate_secret: true to issue a new signing secret — the new secret is returned ONCE in the response and the old one stops verifying immediately. Empty patch returns 400.

Authentication

Bearer <api_key> — must own the project.

Request Schema

{
  "url": "string (optional, https-only same rules as register)",
  "events": "string[]",
  "is_active": "boolean",
  "rotate_secret": "boolean"
}
url
string (optional, https-only same rules as register)
events
string[]optional
is_active
booleanoptional
rotate_secret
booleanoptional

Response Schema

{
  "id": "uuid",
  "url": "string",
  "events": "string[]",
  "is_active": "boolean",
  "secret": "string (only present when rotate_secret was true)"
}
id
uuid
url
string
events
string[]
is_active
boolean
secret
string (only present when rotate_secret was true)

Code Examples

curl -X PUT $BASE/api/v1/projects/$PID/webhooks/$WH_ID \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "rotate_secret": true }'
DELETE/api/v1/projects/{projectId}/webhooks/{webhookId}

Soft delete a webhook

Sets is_active=false. Idempotent: a second call returns 410. Past deliveries (ProjectWebhookDelivery rows) are retained for audit.

Authentication

Bearer <api_key> — must own the project.

Code Examples

curl -X DELETE $BASE/api/v1/projects/$PID/webhooks/$WH_ID -H "Authorization: Bearer $API_KEY"
POST/api/v1/projects/{projectId}/webhooks/{webhookId}/test

Test fire a webhook

Sends a synthetic budget.threshold payload to the registered URL with the same signing scheme as production deliveries. Useful for verifying signature handling on the receiving side.

Authentication

Bearer <api_key> — must own the project. Webhook must be active.

Response Schema

{
  "delivered": "boolean",
  "attempts": "number"
}
delivered
boolean
attempts
number

Code Examples

curl -X POST $BASE/api/v1/projects/$PID/webhooks/$WH_ID/test \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Webhook payload & signature

Every delivery sends the following headers and a JSON body. Verify the signature using HMAC-SHA256 over the raw request body with the webhook's secret.

  • X-Ainvented-Event — event type, e.g. budget.threshold
  • X-Ainvented-Delivery — unique delivery UUID
  • X-Ainvented-Signaturesha256=<hex> over the raw body
  • User-AgentAinvented-Webhook/1.0
{
  "id": "<delivery uuid>",
  "event": "budget.threshold",
  "occurred_at": "2026-04-29T10:30:00Z",
  "project_id": "<uuid>",
  "data": {
    "currency": "USD",
    "budget_cents": 25000,
    "consumed_cents": 20000,
    "remaining_cents": 5000,
    "remaining_pct": 20.0,
    "threshold_pct": 80,
    "by_component": {
      "completion": { "events": 412, "total_tokens": 88000, "cost_cents": 9100 },
      "workflow":   { "events": 33,  "total_tokens": 21000, "cost_cents": 4200 },
      "skill":      { "events": 71,  "total_tokens": 11334, "cost_cents": 2500 },
      "assistant":  { "events": 18,  "total_tokens":  3000, "cost_cents": 1520 }
    }
  }
}

Token-quota events (tokens.threshold / tokens.exhausted) fire when consumed_tokens crosses one of the project's alert_thresholds percentages of token_limit. Each threshold fires at most once per re-arm cycle (re-arm happens automatically when token_limit or alert_thresholds is updated). Payload shape:

{
  "id": "<delivery uuid>",
  "event": "tokens.threshold",
  "occurred_at": "2026-05-14T16:00:00Z",
  "project_id": "<uuid>",
  "data": {
    "currency": "USD",
    "token_limit":      1000000,
    "consumed_tokens":   800000,
    "remaining_tokens":  200000,
    "remaining_pct":     20.0,
    "threshold_pct":     80,
    "by_component": {
      "completion": { "events": 412, "total_tokens": 660000, "cost_cents": 9100 },
      "workflow":   { "events": 33,  "total_tokens":  90000, "cost_cents": 4200 }
    }
  }
}

Reliability: 3 retries with exponential backoff (1 s, 4 s, 16 s). Each delivery is recorded in ProjectWebhookDelivery; the payloadHash column makes deliveries idempotent so retries never duplicate. Webhooks are auto-disabled after 20 consecutive failures.

Error Codes

All errors follow the following error schema format:

{
  "error": {
    "message": "string - Human-readable error message",
    "type": "string - Error type (see Error Codes section)",
    "param": "string (optional) - Parameter that caused the error",
    "code": "string (optional) - Specific error code"
  }
}
400

Bad Request

Type: invalid_request_error

Description: The request is malformed or contains invalid parameters. This includes validation errors for multimodal content (invalid base64, unsupported formats, size limits exceeded) and for image-generation inputs (see image-gen specific codes below).

Image-generation specific codes (all return 400 with type invalid_request_error):

  • image_modality_unsupportedmodalities contained a value other than text/image.
  • invalid_modalitiesmodalities was null or not an array.
  • image_count_out_of_rangeimage.n was outside 1..4 or not an integer.
  • image_size_unsupportedimage.size was not one of the five allowed values.
  • invalid_quality / invalid_style / invalid_response_format — corresponding image.* field had an unsupported value.
  • invalid_remove_backgroundimage.remove_background was not a boolean.
  • invalid_image_blockimage was not a plain object.

502 image-generation specific codes (with type service_unavailable):

  • image_generation_failed — image generation returned a non-OK status, an empty image list, or fewer images than requested (partial failure = failure).
  • image_background_removal_failed — background removal returned a non-OK status or an invalid response (when background removal is enabled for your project).
  • empty_response / partial_failure / upstream_<status> / fetch_error — finer-grained sub-codes that appear in the code field of an image_generation_failed envelope.

Handling: Check the error message and param field to identify which parameter is invalid. Validate your request against the schema. For multimodal content, ensure proper base64 encoding, supported formats, and size limits.

401

Unauthorized

Type: authentication_error

Description: Authentication failed. Invalid, missing, or revoked API key.

Handling: Verify your API key is correct and hasn't been revoked. Ensure the Authorization header is properly formatted: Bearer YOUR_API_KEY

403

Forbidden

Type: permission_error

Description: Insufficient permissions to access the requested resource. Image-gen specific: image_generation_disabled fires when the request explicitly opted in to image output via modalities/tools but image generation is disabled for the project.

Handling: Verify you own the project you're trying to access. Check that your API key has permission for the requested operation. For image_generation_disabled, enable image generation in the AI Chat → Configure pane → "Image Generation" toggle, or remove 'image' from modalities.

404

Not Found

Type: not_found_error

Description: The requested resource doesn't exist or doesn't belong to your API key.

Handling: Verify the project ID or session ID exists and belongs to your API key. For sessions, ensure you're using a session ID from a previous response.

429

Too Many Requests

Type: rate_limit_error

Description: Rate limit exceeded for your API key.

Handling: Implement exponential backoff. Check the Retry-After header for seconds until reset. Monitor rate limit headers to avoid hitting limits.

429

Server At Capacity

Type: server_overloaded

Description: Distinct from the per-key rate limit above. Under sustained high concurrency a node may briefly shed new requests to protect the requests already in flight (load-shedding / admission control). The request was not processed, reached no AI model, and incurred no token cost — it is fully safe to retry.

Handling: Honor the Retry-After header and retry with backoff. Because the request did nothing, retrying does not double-bill.

500

Internal Server Error

Type: server_error

Description: An unexpected error occurred on the server.

Handling: Retry the request with exponential backoff. If the error persists, contact support with the error ID from the response.

503

Service Unavailable

Type: service_unavailable

Description: The service is temporarily unavailable due to maintenance or overload.

Handling: Retry after a delay. Check status page for maintenance announcements.

Best Practices

  • Error Handling: Always implement proper error handling for all HTTP status codes. Parse error responses to provide meaningful feedback to users.
  • Rate Limiting: Monitor rate limit headers and implement exponential backoff when approaching limits.
  • Session Management: Reuse session IDs for multi-turn conversations to maintain context. Store session IDs securely on your backend.
  • Streaming: Use streaming mode for better user experience with long responses. Handle SSE parsing correctly and display tokens as they arrive.
  • Security: Never expose API keys in client-side code. Always make API calls from your backend. Rotate keys regularly and revoke compromised keys immediately.
  • Timeouts: Set appropriate timeouts for your requests (recommended: 60 seconds for completions).
  • Multimodal Content: Compress images and audio before encoding to reduce token usage. Use appropriate detail levels for images ('low' for simple recognition, 'high' for detailed analysis). Monitor size limits and handle validation errors gracefully.

Additional Resources