API Documentation
Build, deploy, and observe Ainvented agents with a clear REST API. Start with one request, then add sessions, tools, files, tasks, and production controls as you need them.
Send your first message
Create an API key, keep it on your server, and make one chat-completion request.
- 1Create a key
Open API Key Management and copy your key once. Never expose it in browser code.
- 2Call the API from your server
export API_KEY="your_api_key_here" curl https://ainvented.com/api/v1/chat/completions \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "messages": [{"role": "user", "content": "Hello from Ainvented"}] }' - 3Read the answer
The generated text is in
choices[0].message.content. Save the returnedsession_idwhen you want Ainvented to maintain conversation history.
Introduction
The Ainvented REST API gives your applications programmatic access to agents and their projects through a consistent interface. Use familiar HTTP patterns while Ainvented manages model access, sessions, tools, context, and observability.
API origin: https://ainvented.com
Authentication
Ainvented v1 API requests use Bearer tokens unless an endpoint is explicitly marked public or browser-session only. Include your API key in the Authorization header:
Authorization: Bearer YOUR_API_KEYSecurity 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.
/v1/projects — listing, reading, updating, deleting, minting per-project keys, configuring webhooks, viewing consumption, and the partner payment surface (payments, payment methods, auto-recharge). /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 partner-enabled accounts to give their clients a key that uses the product without exposing the project-management surface.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 windowX-RateLimit-Remaining: Requests remaining in current windowX-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.
/api/v1/modelsList Models
List the model tiers this account can use. Uses the standard list-models shape, so a compatible client library or model picker populates itself with no Ainvented-specific code.
Authentication
Bearer token required in Authorization header
Response Schema
JSON type shape
{
"object": "string",
"data": [
{
"id": "string",
"object": "string",
"created": "number",
"owned_by": "string"
}
]
}Fields6
objectdataidobjectcreatedowned_byCode Examples
Every tier you may pass as `model`
curl https://ainvented.com/api/v1/models \
-H "Authorization: Bearer $API_KEY"
# {
# "object": "list",
# "data": [
# { "id": "ainvented-default", "object": "model", "created": 1735689600, "owned_by": "ainvented" },
# { "id": "ainvented-low", "object": "model", "created": 1751328000, "owned_by": "ainvented" },
# { "id": "ainvented-saga", "object": "model", "created": 1783036800, "owned_by": "ainvented" },
# { "id": "ainvented-epic", "object": "model", "created": 1783036800, "owned_by": "ainvented" }
# ]
# }Returns a single model object, or 404 if the tier is not available to you
curl https://ainvented.com/api/v1/models/ainvented-low \
-H "Authorization: Bearer $API_KEY"
# { "id": "ainvented-low", "object": "model", "created": 1751328000, "owned_by": "ainvented" }
# An unavailable or unknown name returns 404:
# { "error": { "message": "The model 'not-a-tier' does not exist",
# "type": "invalid_request_error", "param": "model", "code": "model_not_found" } }Populate a model picker from the live list
import requests
BASE = "https://ainvented.com/api/v1"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
models = requests.get(f"{BASE}/models", headers=headers).json()
for m in models["data"]:
print(m["id"]) # ainvented-default, ainvented-low, ainvented-saga, ainvented-epic
one = requests.get(f"{BASE}/models/ainvented-epic", headers=headers).json()
print(one["owned_by"]) # ainvented
# Any standard chat-completions client library works too: point its base URL at
# BASE and its "list models" call returns exactly the tiers above./api/v1/chat/completionsCreate 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 model. Best for high-volume, cost-sensitive turns.ainvented-saga— an advanced reasoning model. Best for complex analysis and long-form work.ainvented-epic— the highest-capability model. Best for the hardest reasoning tasks.ainvented-liteandliteare additional names for the standard tier — same routing, same price asainvented-default.- The bare words
low,saga,epic,liteanddefaultare accepted too (case-insensitively), for clients whose settings offer a single free-text "Model ID" box.GET /api/v1/models/{id}accepts every one of these spellings and resolves it to the canonical entry. - Omitting
modeluses the project's preconfigured default (orainvented-default). Any unrecognized value is accepted and treated asainvented-default— never rejected. - The response
modelfield echoes the canonical name of the tier you selected —epiccomes back asainvented-epic, never your raw string.GET /api/v1/modelsis the authoritative list. Per-tier pricing is on the pricing page.
Select how much reasoning each request gets with the reasoning_effort field (or set a project default via preconfigured_effort): low, medium, high, xhigh or max. Omitting it keeps the tier's standard effort, so a higher effort is always something you opt into — it buys answer quality with time-to-first-token and completion tokens. It applies identically to streaming and non-streaming requests.
- The same five levels work on every tier — each is mapped onto what the serving model actually supports, so you never have to know which engine is behind a tier.
- For drop-in compatibility,
reasoning_effort: "minimal"and"none"are also accepted and treated aslow, and an explicitnullis treated as if the field were omitted — so a request written against a standard chat-completions client works unchanged. - Any other value returns
400 invalid_request_errorwithparam="reasoning_effort". It is rejected before the request reaches a model, so an invalid value never costs you anything. - Reasoning tokens are billed inside
completion_tokens— never as an extra charge — and are reported underusage.completion_tokens_details.reasoning_tokenswhen the serving model breaks them out. - A higher effort spends more of the same
max_tokensbudget thinking, so a capped request can return less visible text atmaxthan atlow. Raisemax_tokensalongside the effort.
curl https://ainvented.com/api/v1/chat/completions \
-H "Authorization: Bearer $API_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-lowStreaming 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",
"context_truncation": "object (final chunk only, and only when part of the request had to be left out) - Same shape as the non-streaming `context_truncation` field. It rides the FINAL chunk because the upstream reports the trim after the generated text."
}Coding agents & other standard chat-completions clients
This endpoint is a drop-in target for any client that speaks the standard chat-completions protocol. Point the client's Base URL at https://ainvented.com/api/v1, paste an API key, and set the Model ID to one of the tiers above.
Agent clients differ from chat clients in one way that matters: they ship a largesystem prompt that defines a strict response protocol — "every reply must be exactly one tool call", replies formatted as tagged blocks — and they reject any reply that does not follow it, re-sending the whole conversation as a retry. For those clients the caller's system prompt must be the only thing shaping the answer, and it is:
- Whenever a request carries a
systemmessage, that message is authoritative: the platform's conversational framing and tone rules are not layered on top of it. You do not have to ask for this and there is nothing to configure. - Your project's configured agent prompt still applies, ahead of your per-request instructions, so anything you set up in the project is preserved and your request wins on any point the two disagree about.
- Native function
toolswork exactly as before, andtool_callscome back in the usual shape — the two are independent. - Send
system_authority: "merge"on a request to pin the previous behaviour.
Screenshots and other images an agent attaches are accepted as image_url content parts in JPEG, PNG, GIF, WebP, BMP, TIFF or AVIF (an editor or browser screenshot is usually WebP). An unsupported format is a 400, not a retryable error — retrying the same image will not help; convert it first.
curl https://ainvented.com/api/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"model":"ainvented-epic",
"stream":true,
"stream_options":{"include_usage":true},
"messages":[
{"role":"system","content":"You are an agent. Every reply MUST be exactly one <tool> block and nothing else."},
{"role":"user","content":"List the files in the project root."}
]}'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_idto 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
messagesarray (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
messagesplus thesession_id; the server supplies the earlier turns from its store.
- Stateless (recommended) — send the entire
- You don't need to do both: if
messagesalready 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/completionsdoes not generate or expose chat session titles. (Title generation is suppressed upstream to minimize latency and cost.) Use thesession_idreturned in the response to correlate multi-turn conversations.
Project Selection
The API determines which project to use in this priority order:
- The API key's associated project (if scoped) — takes precedence over everything else.
- The
project_idrequest field — only when the key is not scoped to a project. - Your default project (first created).
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.
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 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
}'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"])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.
# 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"}}
]
}'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
JSON type shape
{
"model": "string",
"messages": [
{
"role": "string",
"content": "string | ContentPart[]"
}
],
"temperature": "number",
"max_tokens": "number",
"reasoning_effort": "string",
"system_authority": "string",
"stream": "boolean",
"stream_options": "object",
"project_id": "string",
"safety_identifier": "string",
"prompt_cache": "boolean",
"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"
}Fields26
modelmessagesrolecontenttemperaturemax_tokensreasoning_effortsystem_authoritystreamstream_optionsproject_idsafety_identifierprompt_cacheprompt_cache_ttl_hourssession_idrun_workflowtoolsmodalitiesimageconnector_idconnector_toconnector_media_typeconnector_media_urlconnector_media_filenameconnector_email_subjecttool_choiceResponse Schema
JSON type shape
{
"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"
}
],
"usage": {
"prompt_tokens": "number",
"completion_tokens": "number",
"total_tokens": "number",
"prompt_tokens_details": "object",
"completion_tokens_details": "object"
},
"response": {
"images": "string[]",
"image_meta": "object[] (only present when images were produced)"
},
"session_id": "string",
"context_truncation": "object (present ONLY when part of the request had to be left out)"
}Fields22
idobjectcreatedmodelchoicesindexmessagerolecontenttool_callsfinish_reasonusageprompt_tokenscompletion_tokenstotal_tokensprompt_tokens_detailscompletion_tokens_detailsresponseimagesimage_metasession_idcontext_truncationCode 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"])Streaming completion with Server-Sent Events
import json
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": "Tell me a story"}
],
"stream": True
}
response = requests.post(url, headers=headers, json=data, stream=True)
response.raise_for_status()
for line in response.iter_lines():
if not line.startswith(b"data: "):
continue
payload = line[6:]
if payload == b"[DONE]":
break
chunk = json.loads(payload)
content = chunk["choices"][0]["delta"].get("content")
if content:
print(content, end="", flush=True)Basic cURL example
curl https://ainvented.com/api/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": "Hello!"}
],
"temperature": 0.7
}'Using fetch API
const apiKey = "your_api_key_here";
const url = "https://ainvented.com/api/v1/chat/completions";
const response = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
messages: [
{ role: "user", content: "Hello, how are you?" }
],
temperature: 0.7,
max_tokens: 150
})
});
const result = await response.json();
console.log(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
/api/v1/tasksname, instructions, trigger_type, output_schema, and optional trigger_config, history_scope, model, action, enabled, scope. (See Triggers and Limits below.)/api/v1/tasksstatus, 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./api/v1/tasks/{id}?include_deleted=true to see soft-deleted)./api/v1/tasks/{id}enabled./api/v1/tasks/{id}/api/v1/tasks/{id}/runresult. 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./api/v1/tasks/{id}/runsresult, status, confidence, request_id, source_session_id, prompt_tokens/completion_tokens, 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 (intools) or call/run.on_message— auto-runs on each interaction undertool_choice:'auto'(or on every request when transparent auto-fire is enabled for the project).on_intent—trigger_config: { intents: [...] }. Undertool_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 intoolsto force it on any turn.schedule—trigger_config: { cron, timezone?, session_id? }.
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).
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.
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, andmessage.tool_calls[](each withfunction.argumentsas 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 asdelta.tool_callsthen a finalfinish_reason:"tool_calls"chunk).
# 1) Model decides to call get_weather
curl https://ainvented.com/api/v1/chat/completions \
-H "Authorization: Bearer $API_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 $API_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"Result-delivery modes
How you get a task result back from /chat/completions:
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 tofullfor 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.
Each run carries the tokens it was charged for on prompt_tokens and completion_tokens, so GET /api/v1/tasks/{id}/runs reconciles 1:1 with what the project's consumption reports for those runs. A run that never reached the model — refused by the budget gate, or a task that no longer exists — reports 0, which is exactly what it cost. A retried run reports the sum of its attempts.
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.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 - THREE ACCEPTED FORMS, detected automatically from the value: (1) an http(s) URL, (2) a data URL (data:image/<format>;base64,<data>), or (3) bare base64 with no prefix. Base64 can never begin 'http://' or 'data:' — ':' is outside the base64 alphabet — so the detection is exact, not a guess. Accepted formats: jpeg, png, gif, webp, bmp, tiff, avif — the same set for all three forms. Anything other than jpeg/png is converted losslessly on ingest at its ORIGINAL dimensions, so the same picture costs the same input tokens whichever format you send it in. An http(s) URL is identified from the BYTES it returns, not from its Content-Type header, so a correct image served as 'application/octet-stream' works and an HTML error page served as 'image/png' does not. That download is capped at the same size limit as an inline image and is abandoned as soon as it passes it. A URL must be publicly reachable: private, loopback and cloud-metadata addresses are refused (invalid_url). An unaccepted format or an oversized file is a 400 (unsupported_format / image_too_large) and is never billed; a value that is none of the three forms is a 400 (invalid_image_url); a URL that is simply unreachable is skipped and the rest of the message is answered. BILLING IS IDENTICAL ACROSS THE THREE FORMS — a linked image costs exactly what the same image costs inline.",
"detail": "string (optional) - 'auto' | 'low' | 'high' - Image detail level for token calculation"
}
}Supported formats: JPEG, PNG, GIF, WebP, BMP, TIFF, AVIF
Size limit: 20MB (configurable)
Detail levels: 'low' (85 tokens), 'high' (tile-based), 'auto' (automatic)
Note: anything other than JPEG/PNG is converted losslessly on ingest, keeping its original dimensions — so the same picture costs the same input tokens in any accepted format. Animated GIF/WebP contribute their first frame.
Audio Content (URL or Base64)
{
"type": "'input_audio'",
"input_audio": {
"data": "string - THREE ACCEPTED FORMS, detected automatically from the value, exactly as for image_url.url: (1) an http(s) URL to the audio file, (2) a data URL (data:audio/<format>;base64,<data>), or (3) bare base64. A URL is downloaded server-side, capped at the same size limit as inline audio, and identified from the BYTES it returns rather than from its Content-Type, so an HTML error page served as 'audio/mpeg' is refused (unsupported_format) instead of being transcribed and charged to you. A URL must be publicly reachable: private, loopback and cloud-metadata addresses are refused (invalid_url). An oversized file is a 400 (audio_too_large) and is never billed. If the audio was the ONLY content in the message and it could not be retrieved, the request is refused (media_unavailable) rather than answered with an empty prompt you would still be billed for. BILLING IS IDENTICAL ACROSS THE THREE FORMS — a linked clip costs exactly what the same clip costs inline.",
"format": "string - Audio format: 'wav' | 'mp3' | 'm4a' | 'flac' | 'ogg'. REQUIRED with bare base64 (nothing else names the type). OPTIONAL with an http(s) URL or a data URL, which describe themselves — supply it anyway and it is still validated the same way."
}
}Supported formats: WAV, MP3, M4A, FLAC, OGG
Size limit: 25MB (configurable)
URL or Base64 — one rule for both fields
image_url.url and input_audio.data each accept the same three forms, and the form is detected from the value you send — there is no extra parameter to set. Base64 can never begin http:// or data:, so the detection is exact.
{
"model": "ainvented-default",
"messages": [
{
"role": "user",
"content": [
{ "type": "text", "text": "Describe the picture, then transcribe the audio." },
// (1) http(s) URL — downloaded server-side, capped, identified by its bytes
{ "type": "image_url", "image_url": { "url": "https://example.com/photo.jpg" } },
// (2) data URL — the classic inline form
{ "type": "image_url", "image_url": { "url": "data:image/png;base64,iVBORw0KGgo..." } },
// (3) bare base64 — no prefix, type read from the bytes
{ "type": "image_url", "image_url": { "url": "iVBORw0KGgo..." } },
// Audio takes the same three forms in input_audio.data.
// "format" is OPTIONAL here: the downloaded bytes name the type.
{ "type": "input_audio", "input_audio": { "data": "https://example.com/note.mp3" } },
// "format" is REQUIRED with bare base64 — nothing else names the type.
{ "type": "input_audio", "input_audio": { "data": "UklGRiQ...", "format": "wav" } }
]
}
]
}Billing is identical across the three forms. A linked image or clip costs exactly what the same file costs inline — a URL is not a discount.
A URL is read as bytes, not as a header. A correct file served as application/octet-stream works; an HTML error page served as audio/mpeg is refused with unsupported_format instead of being transcribed and billed.format is required only for bare base64. A URL and a data URL describe themselves; send it anyway and it is validated the same way.
URLs must be publicly reachable. Private, loopback and cloud-metadata addresses are refused with invalid_url.
Errors: invalid_image_url, invalid_url, unsupported_format, image_too_large, audio_too_large, media_unavailable — all 400s, none of them billed. A URL that is merely unreachable is skipped and the rest of the message is answered as usual.
Base64 Image Examples
Python - Image Analysis
Pythonimport 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
TypeScriptconst 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# Portable on macOS and GNU/Linux
IMAGE_BASE64=$(base64 < image.jpg | tr -d '\n')
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
Pythonimport 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
TypeScriptconst 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# Portable on macOS and GNU/Linux
AUDIO_BASE64=$(base64 < audio.wav | tr -d '\n')
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 — read the file locally, then send it to YOUR server.
// Your server adds the Ainvented API key and forwards the request securely.
const url = "/api/my-agent";
// 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: {
"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
Pythonimport 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: svg+xml. Supported formats: jpeg, png, gif, webp, bmp, tiff, avif
Convert image to a supported format. This is a 400 — the request never reaches a model, so it is not billed and retrying the same image will not succeed.
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
/api/v1/workflows/executeExecute 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 variablesimages: Array of base64-encoded image stringsaudios: Array of base64-encoded audio stringsurls: Array of URL strings for web scraping or data fetchingurl_inputs: Array of URL input objects with node-specific configurationexternal_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:
- API key's associated project (if configured) - Takes precedence over all other options
project_idparameter (if API key is not scoped to a project)- 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 windowX-RateLimit-Remaining: Requests remaining in current windowX-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
JSON type shape
{
"project_id": "string",
"input_vars": "object",
"images": "string[]",
"audios": "string[]",
"urls": "string[]",
"url_inputs": "array",
"external_apis": "array"
}Fields7
project_idinput_varsimagesaudiosurlsurl_inputsexternal_apisResponse Schema
JSON type shape
{
"success": "boolean",
"output": "object",
"execution_time_ms": "number"
}Fields3
successoutputexecution_time_msCode 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"]
}'Execute workflow with error handling
import requests
api_key = "your_api_key_here"
url = "https://ainvented.com/api/v1/workflows/execute"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
data = {
"input_vars": {
"search_query": "AI news",
"max_results": 10
},
"urls": ["https://example.com/feed"]
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
if result["success"]:
print("Workflow output:", result["output"])
print(f"Execution time: {result['execution_time_ms']}ms")
else:
print("Error:", result["error"])Execute workflow using fetch API
const apiKey = "your_api_key_here";
const url = "https://ainvented.com/api/v1/workflows/execute";
const response = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
input_vars: {
search_query: "AI news",
max_results: 10
},
urls: ["https://example.com/feed"]
})
});
const result = await response.json();
if (result.success) {
console.log("Workflow output:", result.output);
console.log(`Execution time: ${result.execution_time_ms}ms`);
} else {
console.error("Error:", result.error);
}Type-safe workflow execution
interface WorkflowRequest {
input_vars?: Record<string, any>;
images?: string[];
audios?: string[];
urls?: string[];
url_inputs?: Array<{
nodeId: string;
customLabel?: string;
urls: string[];
enabled: boolean;
}>;
external_apis?: Array<{
nodeId?: string;
name: string;
url: string;
method: string;
headers: string;
body: string;
}>;
}
interface WorkflowResponse {
success: boolean;
output: Record<string, any>;
execution_time_ms: number;
}
const apiKey = "your_api_key_here";
const url = "https://ainvented.com/api/v1/workflows/execute";
const request: WorkflowRequest = {
input_vars: {
search_query: "AI news",
max_results: 10
},
urls: ["https://example.com/feed"]
};
const response = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify(request)
});
const result: WorkflowResponse = await response.json();
if (result.success) {
console.log("Workflow output:", result.output);
console.log(`Execution time: ${result.execution_time_ms}ms`);
}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.
export BASE="https://ainvented.com"
export API_KEY="your_admin_api_key"
export PID="your_project_id"
# The account-wide bulk endpoints below refuse a project-scoped key with 403, so their
# examples name the variable ADMIN_KEY to make that requirement obvious. Same value.
export ADMIN_KEY="$API_KEY"/api/v1/projectsCreate 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
JSON type shape
{
"name": "string",
"type": "\"chat\" | \"function\" | \"canvas\"",
"budget_cents": "integer cents",
"currency": "\"USD\" | \"EUR\" | \"GBP\" | \"CAD\" | \"AUD\"",
"alert_thresholds": "integer[]",
"hard_cap": "boolean",
"token_limit": "integer",
"token_hard_cap": "boolean",
"web_search_enabled": "boolean",
"prompt_cache_enabled": "boolean",
"prompt_cache_ttl_hours": "string|integer|null",
"preconfigured_model": "'ainvented-default' | 'ainvented-low' | 'ainvented-saga' | 'ainvented-epic'",
"preconfigured_effort": "'low' | 'medium' | 'high' | 'xhigh' | 'max'"
}Fields13
nametypebudget_centscurrencyalert_thresholdshard_captoken_limittoken_hard_capweb_search_enabledprompt_cache_enabledprompt_cache_ttl_hourspreconfigured_modelpreconfigured_effortResponse Schema
JSON type shape
{
"id": "uuid",
"name": "string",
"type": "integer",
"type_name": "\"chat\" | \"function\" | \"canvas\"",
"api_key": {
"id": "uuid",
"key": "full key",
"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",
"prompt_cache_enabled": "boolean",
"prompt_cache_ttl_hours": "string|integer|null",
"preconfigured_model": "string",
"preconfigured_effort": "string|null",
"created_at": "ISO timestamp"
}Fields29
idnametypetype_nameapi_keyidkeypartialKeybudgetbudget_centsconsumed_centsremaining_centsremaining_pctcurrencyhard_capalert_thresholdsfired_thresholdstoken_limitconsumed_tokensremaining_tokensremaining_tokens_pcttoken_hard_capfired_token_thresholdsweb_search_enabledprompt_cache_enabledprompt_cache_ttl_hourspreconfigured_modelpreconfigured_effortcreated_atCode 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
}'Submitting an unsupported value returns a structured error listing the allowed values.
curl -X POST $BASE/api/v1/projects \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "x", "type": "pigeon"}'
# Response — 400 Bad Request
# {
# "error": {
# "message": "Invalid project type. Allowed string values: \"chat\", \"function\", \"canvas\"; legacy integer values: 1, 2, 3.",
# "type": "invalid_request_error",
# "param": "type"
# }
# }/api/v1/projectsList 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
JSON type shape
{
"data": [
{
"id": "uuid",
"name": "string",
"type": "integer",
"type_name": "\"chat\" | \"function\" | \"canvas\"",
"disabled": "boolean",
"created_at": "ISO",
"prompt_cache_ttl_hours": "string|integer|null",
"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"
}
}
]
}Fields18
dataidnametypetype_namedisabledcreated_atprompt_cache_ttl_hoursbudgetbudget_centsconsumed_centscurrencyhard_captoken_limitconsumed_tokensremaining_tokensremaining_tokens_pcttoken_hard_capCode Examples
curl $BASE/api/v1/projects -H "Authorization: Bearer $API_KEY"/api/v1/projects/cache-ttlBulk: set the managed-cache value on all (or selected) projects
Batch-applies one Managed Prompt Cache value to every project of your account — or to a chosen subset — 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). Audit log emits one project.prompt_cache_ttl.set row per project whose value actually changed.
Authentication
Bearer <api_key> — must be an Admin key (no project binding).
Request Schema
JSON type shape
{
"prompt_cache_ttl_hours": "string|integer|null",
"project_ids": "array"
}Fields2
prompt_cache_ttl_hoursproject_idsResponse Schema
JSON type shape
{
"updated": "integer",
"prompt_cache_ttl_hours": "string|integer|null"
}Fields2
updatedprompt_cache_ttl_hoursCode 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"}'curl -X PUT $BASE/api/v1/projects/cache-ttl \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt_cache_ttl_hours": 6, "project_ids": ["<uuid-1>", "<uuid-2>"]}'/api/v1/projects/defaultsBulk: set the default model and reasoning effort on all (or selected) projects
Batch-applies a default model and/or a default reasoning effort to every project of your account — or to a chosen subset — in a single call, instead of one PUT per project. Both fields are the values used when a request omits `model` / `reasoning_effort`; a request that names either is unaffected. Send at least one of the two (an empty patch returns 400). Projects without a settings row get one created. BILLING: the default model decides which tier's input, cached-input and output rate an unnamed-model request is billed at, and a higher default effort spends more output tokens per turn — on a request that sets its own max_tokens, that cap bounds the visible answer only and thinking tokens are billed on top of it. Channel (connector) messages and scheduled tasks cannot name a model or an effort, so the EFFORT default always applies to them. The MODEL default does not: outside this endpoint the platform does not forward a tier upstream, so connector, scheduled-task, evaluation and vector-store traffic is served by the standard model while still being ledgered at the project's configured tier. Pinning a premium tier therefore re-prices that traffic without changing what serves it — set a premium default only on projects whose traffic arrives through this endpoint. Audit log emits one project.preconfigured_model.set and/or project.preconfigured_effort.set row per project whose stored value actually changed, so every repricing stays attributable to the account that made it; re-applying a value a project already holds writes no row.
Authentication
Bearer <api_key> — must be an Admin key (no project binding).
Request Schema
JSON type shape
{
"preconfigured_model": "string",
"preconfigured_effort": "string|null",
"project_ids": "array"
}Fields3
preconfigured_modelpreconfigured_effortproject_idsResponse Schema
JSON type shape
{
"updated": "integer",
"preconfigured_model": "string",
"preconfigured_effort": "string|null"
}Fields3
updatedpreconfigured_modelpreconfigured_effortCode Examples
curl -X PUT $BASE/api/v1/projects/defaults \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"preconfigured_model": "ainvented-low", "preconfigured_effort": "medium"}'curl -X PUT $BASE/api/v1/projects/defaults \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"preconfigured_effort": null, "project_ids": ["<uuid-1>", "<uuid-2>"]}'/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
JSON type shape
{
"id": "uuid",
"name": "string",
"type": "integer",
"type_name": "\"chat\" | \"function\" | \"canvas\"",
"disabled": "boolean",
"created_at": "ISO",
"budget": "BudgetSnapshot",
"default_model": "string",
"web_search_enabled": "boolean",
"prompt_cache_enabled": "boolean",
"prompt_cache_ttl_hours": "string|integer|null",
"preconfigured_effort": "string|null",
"counts": {
"connector": "number",
"skill": "number",
"workflow": "number"
}
}Fields16
idnametypetype_namedisabledcreated_atbudgetdefault_modelweb_search_enabledprompt_cache_enabledprompt_cache_ttl_hourspreconfigured_effortcountsconnectorskillworkflowCode Examples
curl $BASE/api/v1/projects/$PID -H "Authorization: Bearer $API_KEY"/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, prompt_cache_ttl_hours, and preconfigured_effort. 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, project.prompt_cache_ttl.set, and project.preconfigured_effort.set, when the corresponding fields change.
Authentication
Bearer <api_key> — must own the project.
Request Schema
JSON type shape
{
"name": "string",
"budget_cents": "integer",
"currency": "\"USD\" | \"EUR\" | \"GBP\" | \"CAD\" | \"AUD\"",
"alert_thresholds": "integer[]",
"hard_cap": "boolean",
"disabled": "boolean",
"token_limit": "integer",
"token_hard_cap": "boolean",
"web_search_enabled": "boolean",
"prompt_cache_enabled": "boolean",
"prompt_cache_ttl_hours": "string|integer|null",
"preconfigured_model": "'ainvented-default' | 'ainvented-low' | 'ainvented-saga' | 'ainvented-epic'",
"preconfigured_effort": "'low' | 'medium' | 'high' | 'xhigh' | 'max' | null"
}Fields13
namebudget_centscurrencyalert_thresholdshard_capdisabledtoken_limittoken_hard_capweb_search_enabledprompt_cache_enabledprompt_cache_ttl_hourspreconfigured_modelpreconfigured_effortResponse Schema
JSON type shape
{
"id": "uuid",
"name": "string",
"disabled": "boolean",
"budget": "BudgetSnapshot (includes token fields)",
"web_search_enabled": "boolean",
"prompt_cache_enabled": "boolean",
"prompt_cache_ttl_hours": "string|integer|null",
"preconfigured_model": "string",
"preconfigured_effort": "string|null"
}Fields9
idnamedisabledbudgetweb_search_enabledprompt_cache_enabledprompt_cache_ttl_hourspreconfigured_modelpreconfigured_effortCode 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
}'/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.
/api/v1/projects/{projectId}/paymentsCreate 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 collects the customer's email (Stripe emails them a receipt for this payment and for every later auto-reload charge), lets them 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 <admin_api_key> — must be an account-wide Admin key, must own the project, and your account must be approved for payment collection.
Request Schema
JSON type shape
{
"amount_cents": "integer",
"currency": "\"USD\" (literal; any other value → 400)",
"description": "string",
"idempotency_key": "string",
"credit_target": "\"user_balance\" | \"project_budget\" . Embedded and under-$10 hosted flows default to \"user_balance\". For backward compatibility, the AInvented-hosted $10+ checkout defaults to this path project's \"project_budget\". An explicit value is always honored.",
"budget_project_id": "uuid",
"hosted_checkout": "boolean . 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 a required email field (so Stripe receipts the payment and every later auto-reload charge) and 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",
"cancel_url": "url",
"auto_reload": "object"
}Fields10
amount_centscurrencydescriptionidempotency_keycredit_targetbudget_project_idhosted_checkoutsuccess_urlcancel_urlauto_reloadResponse Schema
JSON type shape
{
"payment_id": "uuid",
"stripe_client_secret": "pi_*_secret_*",
"checkout_url": "the page to redirect the customer to (hosted_checkout=true only). For amounts ≥ $10 this is OUR checkout page",
"checkout_session_id": "cs_*",
"stripe_payment_intent_id": "pi_*",
"status": "PaymentStatus enum (REQUIRES_PAYMENT_METHOD, PROCESSING, SUCCEEDED, ...)",
"amount_cents": "integer",
"currency": "\"USD\"",
"description": "string | null",
"credit_target": "\"user_balance\" | \"project_budget\"",
"credited_budget_project_id": "uuid | null",
"created_at": "ISO timestamp",
"idempotent_replay": "boolean",
"auto_reload": "object | absent"
}Fields14
payment_idstripe_client_secretcheckout_urlcheckout_session_idstripe_payment_intent_idstatusamount_centscurrencydescriptioncredit_targetcredited_budget_project_idcreated_atidempotent_replayauto_reloadCode Examples
Easiest integration: get a checkout_url and redirect your customer. For $10+ they land on OUR checkout page — enter an email (Stripe receipts every charge to it), 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.)Create a $50.00 payment, then your frontend redeems the client_secret with Stripe.js Elements + your publishable key.
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",
"description": "Order #12345 — Acme Co.",
"idempotency_key": "order-12345"
}'When your account is not yet approved for payment collection.
# Response — 403 Forbidden
# {
# "error": {
# "message": "This account is not approved for payment collection. Contact ainvented at contact@ainvented.com to request access to the payments endpoints.",
# "type": "permission_error",
# "code": "payments_not_enabled"
# }
# }
#
# To request access, email contact@ainvented.com with:
# - your ainvented user id
# - a brief description of your business
# - the project id you want to charge from# Response — 400 Bad Request
# {
# "error": {
# "message": "Invalid literal value, expected \"USD\"",
# "type": "invalid_request_error",
# "param": "currency"
# }
# }The same idempotency_key was used for a different flow, amount, description, credit destination, hosted URL, or auto-reload choice. Mint a fresh key for a new payment, or resend the original request to receive it as a replay.
# Response — 400 Bad Request
# {
# "error": {
# "message": "idempotency_key was used for a different payment request. Use a fresh key or resend the original request.",
# "type": "invalid_request_error",
# "param": "idempotency_key",
# "code": "idempotency_key_reused"
# }
# }The project was soft-deleted. New payments are blocked; existing payment rows remain readable.
# Response — 403 Forbidden
# {
# "error": {
# "message": "Project is disabled",
# "type": "permission_error",
# "code": "project_disabled"
# }
# }Stripe was unreachable, returned a 5xx, or rejected the request. Safe to retry with the same idempotency_key.
# Response — 502 Bad Gateway
# {
# "error": {
# "message": "Payment provider error — please retry",
# "type": "payment_provider_error"
# }
# }Your frontend mounts Stripe Elements with the client_secret + ainvented's publishable key. No Stripe secret key is ever in the browser.
const stripe = Stripe('pk_live_AINVENTED_PUBLISHABLE_KEY');
const elements = stripe.elements({ clientSecret });
elements.create('payment').mount('#payment-element');
document.querySelector('#submit').addEventListener('click', async () => {
const { error } = await stripe.confirmPayment({
elements,
confirmParams: { return_url: 'https://your-site.com/return' },
redirect: 'if_required',
});
if (error) console.error(error.message);
});/api/v1/projects/{projectId}/paymentsList 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
JSON type shape
{
"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)"
}Fields5
query.statusquery.limitquery.offsetquery.fromquery.toResponse Schema
JSON type shape
{
"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",
"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"
}
}Fields19
datapayment_idamount_centsamount_received_centscurrencystatusdescriptioncredit_targetcredited_budget_project_idstripe_payment_intent_iderror_codeerror_messagecreated_atpaid_atrefunded_atpagelimitoffsethas_moreCode Examples
curl "$BASE/api/v1/projects/$PID/payments?status=SUCCEEDED&limit=50" -H "Authorization: Bearer $API_KEY"/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
JSON type shape
{
"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"
}Fields13
payment_idproject_idamount_centsamount_received_centscurrencystatusdescriptionstripe_payment_intent_iderror_codeerror_messagecreated_atpaid_atrefunded_atCode 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 enters an email for Stripe receipts, can set up auto-reload, and returns 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.
/api/v1/projects/{projectId}/payment-methodsSave 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
JSON type shape
{
"hosted_checkout": "boolean . true → returns a Stripe-hosted checkout_url (no client_secret, no publishable key needed).",
"success_url": "url",
"cancel_url": "url"
}Fields3
hosted_checkoutsuccess_urlcancel_urlResponse Schema
JSON type shape
{
"client_secret": "string",
"checkout_url": "https://checkout.stripe.com/…",
"customer_id": "string",
"publishable_key": "string | null"
}Fields4
client_secretcheckout_urlcustomer_idpublishable_keyCode 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_..." } }curl -X POST "$BASE/api/v1/projects/$PID/payment-methods" -H "Authorization: Bearer $API_KEY"/api/v1/projects/{projectId}/payment-methodsList 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
JSON type shape
{
"id": "string (pm_…)",
"brand": "string | null",
"last4": "string | null",
"exp_month": "integer | null",
"exp_year": "integer | null"
}Fields5
idbrandlast4exp_monthexp_yearCode Examples
curl "$BASE/api/v1/projects/$PID/payment-methods" -H "Authorization: Bearer $API_KEY"/api/v1/projects/{projectId}/auto-rechargeProject 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
JSON type shape
{
"enabled": "boolean (POST: required",
"threshold_cents": "integer",
"recharge_cents": "integer (optional*, 1000..99,999,999",
"payment_method_id": "string"
}Fields4
enabledthreshold_centsrecharge_centspayment_method_idResponse Schema
JSON type shape
{
"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"
}Fields7
project_idthreshold_centsrecharge_centsactivepayment_method_idlast_triggered_atupdated_atCode 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_..."}'curl -X POST "$BASE/api/v1/projects/$PID/auto-recharge" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"enabled": false}'curl "$BASE/api/v1/projects/$PID/auto-recharge" -H "Authorization: Bearer $API_KEY"
curl -X DELETE "$BASE/api/v1/projects/$PID/auto-recharge" -H "Authorization: Bearer $API_KEY"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 includesamount_received_cents,paid_at,stripe_payment_intent_id.payment.failed— fired on each failed attempt (Stripe allows retries). Payload includeserror_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).
Attach a customer's own WhatsApp Business account to one of your projects. You call one endpoint, get back a URL, and hand it to your customer — they sign in with Meta and pick their number. Neither you nor they ever type a WhatsApp access token, a phone number ID or a verify token; Ainvented obtains them from Meta server-side and stores them encrypted.
Once connected, a WhatsApp connector appears on the project and works on its own: inbound messages reach that project's assistant and replies go out from the customer's own number. There is no webhook for you to configure — one app-level endpoint serves every customer and routes each message by phone number to the project that owns it.
/api/v1/projects/{projectId}/whatsapp/connectCreate a connect link
Mints a one-shot URL that walks your customer through Meta's Embedded Signup and attaches the resulting WhatsApp Business account to this project. Same shape as hosted checkout: you get a URL and hand it over — no SDK, no Meta app of your own, no webhook to configure. The link is single-use and expires after 7 days.
Authentication
Bearer <admin_api_key> — must be an account-wide Admin key (no project binding) and must own the project.
Errors
- 403 admin_key_required — the key is bound to a project. Everything under
/v1/projects/**is account management and needs an Admin key. - 403 project_disabled — the project is soft-deleted.
- 400 — bad project id, or a
return_urlthat is not http(s). - 503 whatsapp_not_configured — WhatsApp onboarding is not switched on for this deployment. Never a client bug; retry later.
Request Schema
JSON type shape
{
"connector_name": "string",
"return_url": "url"
}Fields2
connector_namereturn_urlResponse Schema
JSON type shape
{
"connect_url": "the URL to hand your customer",
"connect_token": "the token embedded in that URL; use it to correlate with the list endpoint",
"project_id": "uuid",
"status": "\"pending\" on creation",
"connector_name": "string | null",
"return_url": "string | null",
"expires_at": "ISO timestamp"
}Fields7
connect_urlconnect_tokenproject_idstatusconnector_namereturn_urlexpires_atCode Examples
Send the returned connect_url to your customer however you like: an email, a button in your own dashboard, a QR code. It requires no Ainvented login on their side.
curl -X POST "$BASE/api/v1/projects/$PID/whatsapp/connect" \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"connector_name": "Acme Support",
"return_url": "https://acme.example/settings/whatsapp?done=1"
}'
# → { "connect_url": "https://ainvented.com/connect/whatsapp/DlhaNJ_Jyaf...",
# "connect_token": "DlhaNJ_Jyaf...",
# "status": "pending",
# "expires_at": "2026-08-31T20:17:38.262Z" }/api/v1/projects/{projectId}/whatsapp/connectCheck connect links
Lists the connect links issued for this project, newest first, so you can tell whether a customer finished. Poll it after handing over a link, or read it on demand when your customer returns to your app.
Authentication
Bearer <admin_api_key> — same requirements as the POST.
The listing never returns connect_url again. A link is a bearer capability — whoever holds it can attach a WhatsApp account to that project — so it is issued once and never re-exposed. To get another, POST again.
Request Schema
JSON type shape
{
"status": "\"pending\" | \"completed\" | \"failed\"",
"limit": "integer",
"offset": "integer"
}Fields3
statuslimitoffsetResponse Schema
JSON type shape
{
"data[].connect_token": "the token from the POST response",
"data[].status": "\"pending\" (issued, unused) | \"completed\" (connected) | \"failed\" (an attempt failed; the link still works and they can retry) | \"expired\" (past expires_at)",
"data[].connector_id": "uuid | null",
"data[].connector_name": "string | null",
"data[].error_message": "string | null",
"data[].created_at": "ISO timestamp",
"data[].expires_at": "ISO timestamp",
"data[].completed_at": "ISO timestamp | null",
"page": "{ limit, offset, has_more }"
}Fields9
data[].connect_tokendata[].statusdata[].connector_iddata[].connector_namedata[].error_messagedata[].created_atdata[].expires_atdata[].completed_atpageCode Examples
curl "$BASE/api/v1/projects/$PID/whatsapp/connect?status=completed" \
-H "Authorization: Bearer $ADMIN_KEY"What your customer goes through
The page you send them is hosted by Ainvented and needs no account. They sign in with Meta, choose or create a WhatsApp Business account, and verify a phone number. Meta asks them for a payment method: as a technology provider there is no credit line to share, so WhatsApp bills each customer directly for their own message usage.
Limits worth designing around
- The number must be free. A phone number already registered on the WhatsApp Business API cannot be onboarded, and your customer must be able to receive Meta's verification code on it.
- One number, one project. A number already active on a different project is refused with
phone_number_in_use. - Reconnecting is the same call. If the same number comes back to the same project, the existing connector is refreshed in place rather than duplicated.
- The access token does not expire, so your customers are never asked to reconnect on a schedule.
- 24-hour window. WhatsApp only allows free-form replies within 24 hours of the end user's message. The assistant answers immediately so this rarely bites, but it means it cannot open a conversation without an approved template.
- Templates are not managed through this API yet.
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.
/api/v1/projects/{projectId}/api-keysCreate 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
JSON type shape
{
"label": "string"
}Fields1
labelResponse Schema
JSON type shape
{
"id": "uuid",
"key": "full key",
"partialKey": "last 4 chars",
"label": "string",
"projectId": "uuid",
"createdAt": "ISO"
}Fields6
idkeypartialKeylabelprojectIdcreatedAtCode 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" }'/api/v1/projects/{projectId}/api-keysList 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
JSON type shape
{
"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"
}Fields11
keysidlabelpartialKeyprojectIdcreatedAtlastUsedAtrevokedAtrequestCounttokenUsagetotalCode Examples
curl "$BASE/api/v1/projects/$PID/api-keys?include_revoked=true" -H "Authorization: Bearer $API_KEY"/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.
/api/v1/projects/{projectId}/consumptionRead 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
JSON type shape
{
"from": "ISO timestamp",
"to": "ISO timestamp",
"group_by": "\"day\" | \"component\" | \"model\"",
"component_type": "\"completion\" | \"workflow\" | \"skill\" | \"assistant\" | \"connector\"",
"model": "\"ainvented-default\" | \"ainvented-low\" | \"ainvented-saga\" | \"ainvented-epic\" | \"(none)\""
}Fields5
fromtogroup_bycomponent_typemodelResponse Schema
JSON type shape
{
"project_id": "uuid",
"currency": "string",
"budget_cents": "number",
"consumed_cents": "number",
"remaining_cents": "number",
"remaining_pct": "number",
"budget_micros": "number",
"consumed_micros": "number",
"remaining_micros": "number",
"exhausted": "boolean",
"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",
"cached_tokens": "number",
"cost_cents": "number",
"cost_micros": "number"
},
"by_component": {
"completion": {
"events": "number",
"prompt_tokens": "number",
"completion_tokens": "number",
"cached_tokens": "number",
"total_tokens": "number",
"cost_cents": "number"
},
"workflow": "...same shape...",
"skill": "...",
"assistant": "...",
"connector": "..."
},
"tokens_by_component": {
"completion": "number",
"workflow": "number",
"skill": "number",
"assistant": "number",
"connector": "number"
},
"by_model": {
"ainvented-default": {
"events": "number",
"prompt_tokens": "number",
"completion_tokens": "number",
"cached_tokens": "number",
"total_tokens": "number",
"cost_cents": "number",
"cost_micros": "number"
},
"ainvented-low": "...same shape...",
"(none)": "...auxiliary calls with no billed model..."
},
"series": [
{
"bucket": "string",
"prompt_tokens": "number",
"completion_tokens": "number",
"cached_tokens": "number",
"total_tokens": "number",
"cost_cents": "number",
"cost_micros": "number"
}
]
}Fields66
project_idcurrencybudget_centsconsumed_centsremaining_centsremaining_pctbudget_microsconsumed_microsremaining_microsexhaustedhard_capalert_thresholdsfired_thresholdstoken_limitconsumed_tokensremaining_tokensremaining_tokens_pcttoken_hard_capfired_token_thresholdswindowfromtototalsprompt_tokenscompletion_tokenstotal_tokenscached_tokenscost_centscost_microsby_componentcompletioneventsprompt_tokenscompletion_tokenscached_tokenstotal_tokenscost_centsworkflowskillassistantconnectortokens_by_componentcompletionworkflowskillassistantconnectorby_modelainvented-defaulteventsprompt_tokenscompletion_tokenscached_tokenstotal_tokenscost_centscost_microsainvented-low(none)seriesbucketprompt_tokenscompletion_tokenscached_tokenstotal_tokenscost_centscost_microsCode Examples
Last 7 days, grouped by component, only workflow events.
FROM=$(node -p "new Date(Date.now() - 7*864e5).toISOString()")
curl "$BASE/api/v1/projects/$PID/consumption?from=$FROM&group_by=component&component_type=workflow" \
-H "Authorization: Bearer $API_KEY"Scope the whole report (totals, by_component, by_model, series) to a single model.
curl "$BASE/api/v1/projects/$PID/consumption?model=ainvented-low" \
-H "Authorization: Bearer $API_KEY"/api/v1/projects/{projectId}/balanceRead 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
JSON type shape
{
"project_id": "uuid",
"budget_cents": "number",
"consumed_cents": "number",
"remaining_cents": "number",
"budget_micros": "number",
"consumed_micros": "number",
"remaining_micros": "number",
"exhausted": "boolean",
"remaining_pct": "number",
"currency": "string",
"hard_cap": "boolean",
"token_limit": "number",
"consumed_tokens": "number",
"remaining_tokens": "number",
"token_hard_cap": "boolean"
}Fields15
project_idbudget_centsconsumed_centsremaining_centsbudget_microsconsumed_microsremaining_microsexhaustedremaining_pctcurrencyhard_captoken_limitconsumed_tokensremaining_tokenstoken_hard_capCode 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.
/api/promotions/resolveResolve 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
JSON type shape
{
"code": "string",
"slug": "string"
}Fields2
codeslugResponse Schema
JSON type shape
{
"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"
}
}Fields13
validreasonoffercodeofferKindcreditAmountdiscountPercentdiscountCapCentsminTopupCentsslugappliesAtSignupappliesAtReloadexpiresAtCode 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
# }
# }Resolve a landing-page slug (slug wins when both are sent).
curl "$BASE/api/promotions/resolve?slug=welcome"
# Same response shape as resolving by code. The /promo/welcome landing
# page uses exactly this call to render the offer.Unknown or expired offers are 200s with valid:false; only a missing code AND slug is a 400.
# An unknown code is NOT an HTTP error — the lookup outcome is in the body:
curl "$BASE/api/promotions/resolve?code=NO-SUCH-CODE"
# Response — 200 OK
# { "valid": false, "reason": "not_found", "offer": null }
# An expired (or inactive / not-started / fully-redeemed) offer still returns
# its display-safe summary so a landing page can render an honest message:
# { "valid": false, "reason": "expired", "offer": { "code": "SUMMER24", ... } }
# Omitting BOTH query parameters is the only 400:
curl "$BASE/api/promotions/resolve"
# Response — 400 Bad Request
# { "error": { "message": "code or slug is required", "type": "invalid_request" } }/api/billing/redeem-promoRedeem 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
JSON type shape
{
"code": "string"
}Fields1
codeResponse Schema
JSON type shape
{
"success": "boolean",
"offerKind": "string",
"creditAmount": "string",
"newBalance": "string",
"discountPercent": "string | null",
"message": "string"
}Fields6
successofferKindcreditAmountnewBalancediscountPercentmessageCode 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."
# }This endpoint reports errors as a flat { "error": "<message>" } body.
# Errors use a flat { "error": "<message>" } body on this endpoint.
# Response — 401 Unauthorized (no browser session)
# { "error": "Authentication required" }
# Response — 404 Not Found (unknown code)
# { "error": "Invalid promo code." }
# Response — 400 Bad Request (each returned as its own message):
# { "error": "You have already redeemed this promo code." }
# { "error": "This promo code is no longer active." }
# { "error": "This promo code has expired." }
# { "error": "This promo code is not active yet." }
# { "error": "This promo code has been fully redeemed." }
# Response — 429 Too Many Requests (more than 10 attempts/minute)/api/referrals/meYour 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
JSON type shape
{
"data": {
"asPromoter": "boolean",
"shareUrl": "string | null",
"referredCount": "number",
"commission": {
"accruedCashCents": "integer",
"paidCashCents": "integer",
"autoCreditedCents": "integer",
"currency": "string"
}
},
"generatedAt": "string"
}Fields10
dataasPromotershareUrlreferredCountcommissionaccruedCashCentspaidCashCentsautoCreditedCentscurrencygeneratedAtCode 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"
# }curl "$BASE/api/referrals/me"
# Response — 401 Unauthorized (no browser session)
# { "error": { "message": "Authentication required", "type": "auth_error" } }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.
Node.js client (example)
Node 18+ can call the REST endpoints directly with its built-in fetch. This complete example creates a store, adds reusable text, checks every HTTP response, and runs a semantic query—no extra client library required:
const headers = {
Authorization: `Bearer ${process.env.API_KEY}`,
'Content-Type': 'application/json',
}
async function request(path, options = {}) {
const response = await fetch(`https://ainvented.com${path}`, { headers, ...options })
if (!response.ok) throw new Error(`Ainvented API ${response.status}: ${await response.text()}`)
return response.json()
}
const store = await request('/api/v1/vector-stores', {
method: 'POST', body: JSON.stringify({ name: 'product-docs' }),
})
await request(`/api/v1/vector-stores/${store.id}/documents`, {
method: 'POST', body: JSON.stringify({ text: 'Ainvented can store reusable knowledge.' }),
})
const result = await request(`/api/v1/vector-stores/${store.id}/query`, {
method: 'POST', body: JSON.stringify({ query: 'How does it work?', top_k: 5, answer: true }),
})
console.log(result.answer, result.results)/api/v1/vector-storesCreate / 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
JSON type shape
{
"name": "string",
"description": "string",
"project_id": "uuid"
}Fields3
namedescriptionproject_idResponse Schema
JSON type shape
{
"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"
}Fields9
idnamedescriptiontagstatusproject_iddocument_countcreated_atupdated_atCode 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"}'List active stores (?status=archived|all, ?limit, ?cursor, ?order).
curl "$BASE/api/v1/vector-stores" -H "Authorization: Bearer $API_KEY"/api/v1/vector-stores/{id}/documentsAdd / 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.
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.
Request Schema
JSON type shape
{
"text": "string",
"url": "string (url)",
"file (multipart)": "binary",
"title": "string",
"tags": "string[]",
"chunk_size": "int",
"chunk_overlap": "int",
"metadata": "object",
"generate_metadata": "boolean"
}Fields9
texturlfile (multipart)titletagschunk_sizechunk_overlapmetadatagenerate_metadataResponse Schema
JSON type shape
{
"id": "string",
"store_id": "string",
"kind": "\"text\"|\"url\"|\"file\"",
"title": "string|null",
"doc_uid": "string|null",
"source": "string|null",
"char_count": "number",
"chunk_count": "number",
"embed_tokens": "number",
"status": "\"ready\"|\"failed\"",
"error_code": "string|null",
"created_at": "ISO"
}Fields12
idstore_idkindtitledoc_uidsourcechar_countchunk_countembed_tokensstatuserror_codecreated_atCode 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}'Attach filterable key→value metadata to every chunk (optional).
curl -X POST "$BASE/api/v1/vector-stores/$SID/documents" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"text":"Q4 revenue grew 12%.","metadata":{"team":"finance","year":2024}}'Opt in to LLM-extracted topics/summary/keywords/entities (extra audited cost). Merged into every chunk and filterable.
curl -X POST "$BASE/api/v1/vector-stores/$SID/documents" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"text":"TaskFlow Pro is $12/seat; invoices are emailed monthly.","generate_metadata":true}'Multipart file upload.
curl -X POST "$BASE/api/v1/vector-stores/$SID/documents" \
-H "Authorization: Bearer $API_KEY" -F 'file=@/path/to/doc.pdf'Multipart `metadata` form field — a JSON-object string. Malformed/non-object values are ignored (never an error).
curl -X POST "$BASE/api/v1/vector-stores/$SID/documents" \
-H "Authorization: Bearer $API_KEY" \
-F 'file=@/path/to/doc.pdf' -F 'metadata={"team":"finance","year":2024}'/api/v1/vector-stores/{id}/queryQuery — 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 not_exists. exists and not_exists test for the presence/absence of the key and take 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.
Request Schema
JSON type shape
{
"query": "string",
"top_k": "int",
"threshold": "number",
"optimize_query": "boolean",
"tags": "string[]",
"answer": "boolean (optional",
"max_tokens": "int",
"metadata_filter": "Array<{ key: string, op?: \"eq\"|\"neq\"|\"in\"|\"contains\"|\"gt\"|\"gte\"|\"lt\"|\"lte\"|\"exists\"|\"not_exists\", value?: any }> . AND-ed pre-filters applied BEFORE the semantic search; op defaults to \"eq\"; \"exists\"/\"not_exists\" take no value; \"in\" takes an array. Optional & backward-compatible.",
"text_contains": "string"
}Fields9
querytop_kthresholdoptimize_querytagsanswermax_tokensmetadata_filtertext_containsResponse Schema
JSON type shape
{
"results": "[{ text, title, source, score, metadata, ... }]",
"count": "number",
"answer": "string (only when answer:true)",
"usage": {
"total_tokens": "number"
}
}Fields5
resultscountanswerusagetotal_tokensCode 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}'Map-reduce pre-filter: AND-ed metadata predicates + a text substring, applied before the semantic search (optional).
curl -X POST "$BASE/api/v1/vector-stores/$SID/query" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"query":"revenue","top_k":5,
"metadata_filter":[{"key":"team","op":"eq","value":"finance"},
{"key":"year","op":"gte","value":2024}],
"text_contains":"Q4"}'Search + grounded answer.
curl -X POST "$BASE/api/v1/vector-stores/$SID/query" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"query":"Summarize the docs","answer":true,"max_tokens":150}'/api/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 — precisely that one, even when other documents share its filename. Both are soft (the record is preserved with status=archived) — never a destructive delete.
Authentication
Bearer <api_key>
Deleting one document among duplicates
Uploading the same file twice creates two documents, with the same title and source but different ids. Each carries its own doc_uid, and a delete matches on that — so removing one copy never touches the other, and collateral_archived comes back 0.
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
JSON type shape
{
"id": "string",
"status": "\"archived\"",
"deleted": "true",
"collateral_archived": "number (document delete only)"
}Fields4
idstatusdeletedcollateral_archivedCode Examples
Archive a store.
curl -X DELETE "$BASE/api/v1/vector-stores/$SID" -H "Authorization: Bearer $API_KEY"Removes exactly this document, whatever else shares its filename.
curl -X DELETE "$BASE/api/v1/vector-stores/$SID/documents/$DOC_ID" \
-H "Authorization: Bearer $API_KEY"
# => {"id":"...","status":"archived","deleted":true,"collateral_archived":0}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.
/api/v1/projects/{projectId}/webhooksRegister 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
JSON type shape
{
"url": "string",
"events": "(\"budget.threshold\" | \"budget.exhausted\" | \"tokens.threshold\" | \"tokens.exhausted\" | \"usage.recorded\")[]"
}Fields2
urleventsResponse Schema
JSON type shape
{
"id": "uuid",
"url": "string",
"events": "string[]",
"is_active": "boolean",
"secret": "string",
"created_at": "ISO"
}Fields6
idurleventsis_activesecretcreated_atCode 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"] }'/api/v1/projects/{projectId}/webhooksList 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
JSON type shape
{
"data": [
{
"id": "uuid",
"url": "string",
"events": "string[]",
"is_active": "boolean",
"created_at": "ISO",
"last_delivery_at": "ISO | null",
"failure_count": "number"
}
]
}Fields8
dataidurleventsis_activecreated_atlast_delivery_atfailure_countCode Examples
curl $BASE/api/v1/projects/$PID/webhooks -H "Authorization: Bearer $API_KEY"/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
JSON type shape
{
"url": "string",
"events": "string[]",
"is_active": "boolean",
"rotate_secret": "boolean"
}Fields4
urleventsis_activerotate_secretResponse Schema
JSON type shape
{
"id": "uuid",
"url": "string",
"events": "string[]",
"is_active": "boolean",
"secret": "string (only present when rotate_secret was true)"
}Fields5
idurleventsis_activesecretCode 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 }'/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"/api/v1/projects/{projectId}/webhooks/{webhookId}/testTest 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
JSON type shape
{
"delivered": "boolean",
"attempts": "number"
}Fields2
deliveredattemptsCode 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.thresholdX-Ainvented-Delivery— unique delivery UUIDX-Ainvented-Signature—sha256=<hex>over the raw bodyUser-Agent—Ainvented-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"
}
}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).
context_length_exceeded (param: "messages") — the prompt is larger than the selected model tier's input budget and could not be trimmed without cutting your own messages. The message states how many input tokens the request needs and how many are available. This refusal is NOT billed: it is decided before the model is called, so no tokens are consumed and no usage is recorded. Retry with fewer or shorter messages, fewer attached files, or a tier with more headroom. Requests that can be trimmed succeed instead and report what was dropped on the context_truncation response field.
Image-generation specific codes (all return 400 with type invalid_request_error):
image_modality_unsupported—modalitiescontained a value other thantext/image.invalid_modalities—modalitieswas null or not an array.image_count_out_of_range—image.nwas outside 1..4 or not an integer.image_size_unsupported—image.sizewas not one of the five allowed values.invalid_quality/invalid_style/invalid_response_format— correspondingimage.*field had an unsupported value.invalid_remove_background—image.remove_backgroundwas not a boolean.invalid_image_block—imagewas 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 thecodefield of animage_generation_failedenvelope.
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.
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
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.
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.
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.
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.
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.
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
- API Key Management - Create and manage your API keys
