Public API

Caiioo includes a REST API that lets you control everything programmatically: run agents, manage tools, schedule tasks, and more. The API lives on the same local server that powers the desktop app and browser bridge.

Base URL: http://localhost:3847/v1

Authentication: Two ways to authenticate, both gated by the API toggle in settings:

For external consumers (scripts, integrations, curl): Set an API access token in Settings > API Access, then use it as a Bearer token:

curl -H "Authorization: Bearer YOUR_API_TOKEN" http://localhost:3847/v1/providers

For the local app (automatic): The Caiioo desktop app, browser extensions, and mobile apps authenticate automatically via the existing relay auth header (x-relay-auth; HTTP headers are case-insensitive). No manual setup needed — the app handles this behind the scenes. Relay-auth requests bypass the Enable Public API toggle since they're already trusted; only bearer-token requests are gated by the toggle.

Setup:

  1. Open Caiioo Settings > API Access
  2. Toggle Enable Public API on
  3. Set an API access token (any string you choose — treat it like a password)
  4. Use that token in all API requests

The API is available on localhost and through the private relay. Check GET /v1/auth/info (no auth required) for current status and setup instructions.

Rate limits: every /v1/* request is rate-limited per client IP — 100 GET requests per minute for reads and 30 write requests per minute (POST / PATCH / DELETE) combined. Over-limit requests get 429. Webhook deliveries (POST /v1/webhooks/:id) are not bearer-authed and are not subject to these limits.

Profiles

A single device can hold multiple user profiles (e.g. personal + work). The API lets external scripts inspect the available profiles and switch the active one before doing other work. Profile creation, update, and delete are deliberately not exposed via the public API — those flows belong in the app's onboarding UI.

List profiles:

GET /v1/profiles

Returns { profiles: [...] } with one entry per non-deleted profile. Each entry includes id, name, email, avatarUrl, tier, accessibleModes, license, organization, preferences, onboardingComplete, createdAt, lastAccessedAt. Token-bearing fields (serviceCredentials, oauthConnections) and sync internals (vectorClock, lastModifiedBy) are stripped. Use /v1/connectors to inspect OAuth connections.

Get the active profile:

GET /v1/profiles/active

Returns { profile } for the profile currently in use by this server. All /v1/* resources scoped to a profile (threads, attachments, settings, skills) operate against this one.

Switch the active profile:

PUT /v1/profiles/active
Content-Type: application/json

{ "profileId": "user-uuid-from-list" }

Returns { profile } for the newly active profile. Returns 404 if the ID isn't a known profile.

Providers & Models

Discover which LLM providers are configured and what models are available.

List providers:

GET /v1/providers

Returns every supported provider type. Currently: anthropic, openai, google, openrouter, ollama, poe, mlx, perplexity, baseten, cloudflare. Each entry includes type, displayName, icon, requiresApiKey, hasApiKey, and a capabilities object with the flags below.

Capability flag Meaning
supportsVision Provider can accept image inputs
supportsPdfFile Provider can accept raw PDF file content blocks natively
supportsToolCalling Provider supports function/tool calling
supportsStreaming Provider streams tokens incrementally
supportsExtendedThinking Provider exposes a reasoning/thinking budget
supportsPromptCaching Provider supports prompt-cache directives
nativeReasoningBlocks Provider emits thinking as native message blocks (vs. text)
requiresThoughtSignature Provider requires signed thought tokens to be echoed back

Capability flags mirror each provider class's readonly capabilities field (see src/shared/providers/*-provider.ts) and are validated by a drift sentinel test — call this endpoint at runtime rather than hardcoding the matrix.

Notes for the BYOK providers:

  • perplexity exposes a curated list of Sonar models (Perplexity has no public /models endpoint).
  • cloudflare (AI Gateway) is BYOK + multi-vendor; the model list is determined by your gateway configuration and is returned as an empty array by /v1/providers/cloudflare/models. Use your own gateway catalog.

List models for a provider:

GET /v1/providers/openrouter/models

Returns the model catalog for that provider. Each model includes id, displayName, and contextLength where available. Returns 503 when the provider lacks an API key or its upstream catalog is unreachable.

Flat catalog across all providers:

GET /v1/models

Merges models from every configured provider into one list. Providers without API keys are skipped and listed in warnings.

Agents

Agents are the core of caiioo. Each agent is a Mode — a configured personality with its own system prompt, tools, variables, and skills.

List all agents:

GET /v1/agents

Returns builtin agents (Shopping, Workplace, General) and any custom agents you've created. Each is tagged with source: "builtin" or source: "custom".

Create a custom agent:

POST /v1/agents
Content-Type: application/json

{
  "id": "my-research-agent",
  "branding": {
    "name": "Research Agent",
    "description": "Searches the web and summarizes findings"
  },
  "defaultSettings": {
    "systemPrompt": "You are a research assistant. Always cite sources.",
    "enabledTools": { "web_browsing": true, "search_tools": true }
  },
  "settingLevels": {}
}

Returns 201 with the created agent. A vector clock is attached automatically for sync.

Update an agent:

PATCH /v1/agents/my-research-agent
Content-Type: application/json

{ "branding": { "name": "Research Agent", "description": "Updated description" } }

Merges the patch into the existing agent and bumps the vector clock. Builtin agents return 403 — they're read-only.

Delete an agent:

DELETE /v1/agents/my-research-agent

Soft-deletes via tombstone (syncs across devices). Returns 204.

Running Agents

This is the main event — invoke an agent to process a message.

Request body — supported fields

Field Required Description
agentId yes Builtin mode ID (e.g. general) or a custom agent ID
input.message yes User message text
input.attachments no Array of attachment IDs (already uploaded via /v1/attachments) to attach to this turn
input.variables no Per-run variable overrides merged into the agent's variable resolver
input.tabContext no Free-form string injected as page-context (used by the browser bridge)
input.messageId no Client-supplied message ID — useful for dedup if you retry
threadId no Existing thread to continue. If omitted, a new thread is created and returned
mode no "sync" or "async". Defaults to "async"

Synchronous Mode

Wait for the full response:

POST /v1/runs
Content-Type: application/json

{
  "agentId": "general",
  "input": { "message": "What's the weather in Paris today?" },
  "mode": "sync"
}

Returns 200 with { content, usage, status: "completed" } after the agent finishes. If the agent errors, returns 500 with { error, status: "error" }. Sync runs time out after 5 minutes with status: "error" if no terminal event is received — use async + SSE for anything that may take longer.

Asynchronous Mode

Fire and forget — useful for long-running tasks:

POST /v1/runs
Content-Type: application/json

{
  "agentId": "my-research-agent",
  "input": { "message": "Write a 2000-word analysis of renewable energy trends" },
  "mode": "async"
}

Returns 202 immediately with { runId, threadId, status: "running" }.

Poll for status:

GET /v1/runs/{runId}

Returns { run: { runId, threadId, agentId, status, createdAt, content?, usage?, error? } }. Status is one of running, completed, error, or cancelled.

Stream events in real time (SSE):

GET /v1/runs/{runId}/events

Returns a text/event-stream with every agent event as it happens: GENERATION_STARTED, STREAMING_CONTENT, tool calls, subagent activity, and the terminal event (GENERATION_COMPLETE, GENERATION_ERROR, or GENERATION_CANCELLED).

After the terminal event, the server emits one final SSE frame with event: terminal and an empty data payload as an explicit end-of-stream sentinel, then closes the connection. Clients should treat receipt of that frame (or a connection close) as the signal to stop reading.

If you subscribe after the run has already finished, the server replays one frame of type RUN_SNAPSHOT containing the full final record, followed by the event: terminal sentinel, then closes.

Cancel a run:

POST /v1/runs/{runId}/cancel

Returns { run: { ..., status: "cancelled" } }.

Threads

Threads are conversations. Every agent run happens within a thread, and threads persist across sessions. The API lets you list, read, create, and manage threads programmatically.

List all threads (metadata only):

GET /v1/threads

Returns { threads: [...] } for the current profile with messages stripped (use the detail endpoint when you need them). Every other field is preserved — id, title, createdAt, updatedAt, modeId, archived, usage stats, plus subAgentHistories, anonymizerSnapshot, threadToolApprovals, threadVariables, threadToolOverrides, messagingBinding, scheduledTaskId when set. (The richer payload is exclusive to the API — the WebSocket sidebar broadcast uses a tighter strip to stay under transport limits.)

Get a thread with full messages:

GET /v1/threads/{id}

Returns the complete thread including its messages array — every user message, assistant response, tool call, and tool result.

Get just the messages:

GET /v1/threads/{id}/messages

Returns only the messages array — lighter than the full thread object when you just need the conversation.

Create a thread:

POST /v1/threads
Content-Type: application/json

{ "title": "Research project", "modeId": "general" }

Returns 201 with { thread }. The new thread appears in the sidebar immediately (via WebSocket broadcast). The API never switches the app's active thread on create — call PUT /v1/threads/active separately if you want that.

Update a thread:

PATCH /v1/threads/{id}
Content-Type: application/json

{ "title": "Renamed project", "archived": true }

Updatable fields: title, modeId, archived, lastUsedModel. Changes broadcast to the sidebar in real time.

Delete a thread:

DELETE /v1/threads/{id}

Soft-deletes the thread (tombstone for sync). Returns 204. Deleted threads move to trash and can be recovered until trash is emptied.

Active thread:

GET /v1/threads/active            # Returns { threadId }
PUT /v1/threads/active            # Body: { "threadId": "..." }

Trash management:

GET /v1/threads/trash/count       # Returns { count }
POST /v1/threads/trash/empty      # Returns { deletedCount, protectedCount }

Protected threads (retained via the data retention toggle) are excluded from trash emptying.

Continuing a conversation via the API: To send a follow-up message to an existing thread, use POST /v1/runs with the thread's ID:

POST /v1/runs
Content-Type: application/json

{
  "agentId": "general",
  "threadId": "existing-thread-id",
  "input": { "message": "Follow up on that last point" },
  "mode": "sync"
}

The agent sees the full conversation history from the thread.

Attachments

Attachments are files linked to threads — screenshots, PDFs, documents, uploaded images, generated artifacts. The API lets you list, upload, download, and manage them.

List all attachments (metadata only):

GET /v1/attachments

Returns attachment metadata for the current profile. Heavy fields (dataUrl, extractedContent, extractedImages) are stripped — use the detail or content endpoints for those.

List attachments for a specific thread:

GET /v1/threads/{threadId}/attachments

Get attachment metadata:

GET /v1/attachments/{id}

Returns full metadata including extractedContent (OCR text, parsed markdown), contentType, fileName, size, and a hasContent flag. The raw binary is NOT included — use the /content endpoint for that.

Download attachment binary:

GET /v1/attachments/{id}/content

Returns the raw file with the correct Content-Type and Content-Disposition headers. Pipe this to a file:

curl -o output.pdf \
  -H "Authorization: Bearer $API_TOKEN" \
  http://localhost:3847/v1/attachments/{id}/content

Upload an attachment:

POST /v1/attachments
Content-Type: application/json

{
  "threadId": "thread-id",
  "type": "user_upload",
  "contentType": "application/pdf",
  "fileName": "report.pdf",
  "description": "Quarterly report",
  "dataUrl": "data:application/pdf;base64,JVBERi0xLjQ..."
}

The dataUrl is a base64-encoded data URL. Returns 201 with the new attachment ID. The attachment is linked to the specified thread.

Update attachment metadata:

PATCH /v1/attachments/{id}
Content-Type: application/json

{ "description": "Updated description", "fileName": "new-name.pdf" }

Delete an attachment:

DELETE /v1/attachments/{id}

Soft-deletes via tombstone. Returns 204.

MCP Servers

Manage your MCP (Model Context Protocol) server connections — the servers that give agents access to external tools and data sources.

List configured servers:

GET /v1/mcp-servers

Returns all MCP server configs for the current profile. Sensitive fields (authToken, env, credentialId) are stripped from the response.

Get a server's config:

GET /v1/mcp-servers/{id}

Add a new MCP server:

POST /v1/mcp-servers
Content-Type: application/json

{
  "id": "my-server",
  "name": "My MCP Server",
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/files"],
  "serverType": "local"
}

For remote HTTP servers, use "url" instead of "command":

{
  "id": "remote-server",
  "name": "Remote API",
  "url": "https://my-mcp-server.example.com/sse",
  "serverType": "remote"
}

Accepted optional fields on POST: command, args, env, url, description, transportType, authType, authToken, authHeader, headers, specType, specPath, timeoutMs, credentialId, oauthConnectionId, approval. Server-managed fields (customOAuth, hubPackageId, profileId, connectorId, teamPublished, teamOrgId, vectorClock, ...) cannot be set via the API.

Update a server:

PATCH /v1/mcp-servers/{id}
Content-Type: application/json

{ "name": "Renamed Server", "args": ["-y", "@mcp/server-v2"] }

Patchable fields mirror the POST allowlist (including credentialId and oauthConnectionId — handy for rotating an OAuth connection without delete-and-recreate). Server-managed fields stay read-only.

Enable/disable a server:

POST /v1/mcp-servers/{id}/toggle
Content-Type: application/json

{ "enabled": false }

Delete a server:

DELETE /v1/mcp-servers/{id}

Process Management

For local (stdio) MCP servers, you can manage the server process directly.

List running processes:

GET /v1/mcp-servers/processes

Returns running server processes with pid, startedAt, and running status.

Start a server:

POST /v1/mcp-servers/{id}/start

Reads the command/args/env from the server config and spawns the process. Returns the process status.

Stop a server:

POST /v1/mcp-servers/{id}/stop

Gracefully shuts down the server process (SIGTERM with fallback to SIGKILL).

Call a JSON-RPC method directly:

POST /v1/mcp-servers/{id}/call
Content-Type: application/json

{ "method": "tools/list", "params": {} }

Sends a raw JSON-RPC 2.0 request to the server and returns the result. Useful for debugging or calling methods not exposed through the tools API.

Tools & Toolkits

Browse and invoke the tools that agents use — web browsing, search, calendar, Gmail, Slate, and more.

List toolkits (grouped):

GET /v1/toolkits

Returns embedded tools grouped by category (Productivity, Search, Utilities, etc.) and any connected MCP servers as separate toolkits, each with their actions listed.

List all tools (flat):

GET /v1/tools
GET /v1/tools?source=embedded   # Only builtin tools
GET /v1/tools?source=mcp        # Only MCP server tools

Get tool details with input schema:

GET /v1/tools/calculator

Returns { tool: { name, displayName, description, source, category, inputSchema, actions?, requiredTier?, requiredRuntimes?, riskTier?, riskExplanation?, requiresApproval? } }.

Field Meaning
inputSchema JSON Schema for the tool's input — validate before invoking
actions Optional sub-actions (e.g. Slate read/write); each has id, displayName, description, optional requiredTier
requiredTier Minimum user tier (free if omitted). Public-API invocation refuses anything above free
requiredRuntimes Platforms where this tool is available (macos, ios, etc.); omit ⇒ everywhere
riskTier Consent risk: low (default), medium, high
riskExplanation Human-readable reason riskTier is elevated
requiresApproval true when the tool needs per-call user approval — public-API invocation refuses these

Invoke a tool directly:

POST /v1/tools/calculator/invoke
Content-Type: application/json

{ "input": { "expression": "sqrt(144) + 3^2" }, "threadId": "optional-thread-id" }

Returns { result }. Input is validated against the tool's schema — invalid input returns 422 with details. Pass an optional threadId if the tool needs thread-scoped context (e.g. attachment lookups).

Gating — what the public API refuses:

  • Tier-gated tools (metadata.requiredTier !== 'free') → 403. The agent loop checks tier against the user's session; the public API has no carried tier context, so the conservative policy is to refuse. Use POST /v1/runs with an agent that has the right tier instead.
  • Approval-required tools (metadata.approval.requiresApproval === true) → 403. There is no human in the loop to ask. Invoke via POST /v1/runs (which surfaces the prompt to the user) or set tool approval mode to approve_all in the app to bypass.
  • Remote MCP tools501 with guidance to use /v1/runs (they require the agent subprocess transport).

Vision and image-using tools: upload via POST /v1/attachments first, then pass the returned attachment ID inside input (e.g. { "input": { "attachment_id": "att_abc", "prompt": "What is in this image?" } }). The tool reads the binary from storage via threadId; you don't pass the bytes inline.

Connectors

Manage OAuth integrations — Google, Microsoft, GitHub, Notion, Slack, and more.

Browse available integrations:

GET /v1/connectors/catalog

Returns all registered OAuth providers with their name, category, and default scopes.

List your connected accounts:

GET /v1/connectors

Returns active connections for the current profile. Tokens are never exposed — only metadata (provider, email, status, scopes, timestamps).

Get a single connection:

GET /v1/connectors/{id}

Returns metadata for one connection (provider, email, status, scopes, timestamps). Tokens are never exposed.

Check connection health:

POST /v1/connectors/{id}/test

Returns { health: { status, isTokenExpired, canRefresh } }.

Remove a connection:

DELETE /v1/connectors/{id}

Creating new connections requires the interactive OAuth flow via the app UI or the /auth/* routes.

Triggers

Schedule agents to run automatically — daily briefings, weekly reports, interval-based monitoring.

Every trigger has a kind discriminator: schedule (default — fires on a clock), webhook (fires when an external service POSTs to the webhook path), or messaging (fires from a connected messaging adapter — e.g. inbound Slack/Discord messages matching a channel filter).

List triggers:

GET /v1/triggers

Returns { triggers: [...] }. Each entry includes its kind, schedule, and kind-specific fields (e.g. webhookSecret/webhookPath for webhook triggers, messagingAdapterId/messagingChannelFilter for messaging triggers).

Get a single trigger:

GET /v1/triggers/{id}

Create a scheduled trigger:

POST /v1/triggers
Content-Type: application/json

{
  "name": "Morning Briefing",
  "prompt": "Summarize my unread emails and today's calendar",
  "modeId": "general",
  "schedule": { "type": "daily", "time": "08:00" }
}

Supported schedule types:

  • { "type": "interval", "minutes": 60 } — every N minutes (min 15, max 1440)
  • { "type": "daily", "time": "09:00" } — daily at a specific time
  • { "type": "weekly", "day": "mon", "time": "09:00" } — weekly
  • { "type": "weekdays", "time": "08:30" } — Monday through Friday
  • { "type": "daysOfWeek", "days": ["mon", "wed", "fri"], "time": "10:00" } — specific days
  • { "type": "monthly", "dayOfMonth": 1, "time": "09:00" } — monthly
  • { "type": "manual" } — only when fired via API

Fire a trigger manually:

POST /v1/triggers/{id}/fire

Returns 202 with a threadId for the resulting run.

Update or delete:

PATCH /v1/triggers/{id}
DELETE /v1/triggers/{id}

Webhooks

Webhook triggers let external services (CI/CD, monitoring, form builders) trigger an agent run via HTTP.

Create a webhook trigger:

POST /v1/triggers
Content-Type: application/json

{
  "name": "Deploy hook",
  "prompt": "A deploy happened: {{webhook.body}}",
  "modeId": "general",
  "kind": "webhook"
}

Returns 201 with a webhookSecret and webhookPath. Store the secret — you'll need it to sign payloads.

Send a webhook:

# Compute HMAC-SHA256 of the raw request body
SIGNATURE=$(echo -n '{"repo":"my-app","branch":"main"}' | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}')

POST /v1/webhooks/{triggerId}
Content-Type: application/json
X-Webhook-Signature: $SIGNATURE

{"repo": "my-app", "branch": "main"}

The webhook endpoint does NOT require bearer auth — it uses HMAC verification instead. Returns 202 with the threadId of the dispatched run. The {{webhook.body}} placeholder in the trigger prompt is replaced with the raw request body.

Messaging triggers

Messaging triggers fire when a connected messaging adapter (e.g. a Slack or Discord integration installed via the Community Hub) receives an inbound message matching the trigger's filter. Create with kind: "messaging":

POST /v1/triggers
Content-Type: application/json

{
  "name": "Mention responder",
  "prompt": "Reply helpfully to: {{message.text}}",
  "modeId": "general",
  "kind": "messaging",
  "messagingAdapterId": "slack-team-acme",
  "messagingChannelFilter": "#support"
}

The adapter is responsible for dispatching matching events into the trigger; messagingChannelFilter is adapter-specific.

Custom Functions

Create your own tools that agents can call. Functions are written in JavaScript or Python and execute in a sandbox.

List functions:

GET /v1/functions

Create a function:

POST /v1/functions
Content-Type: application/json

{
  "name": "calculate_bmi",
  "description": "Calculate Body Mass Index from height and weight",
  "language": "javascript",
  "source": "return { bmi: (input.weightKg / (input.heightM * input.heightM)).toFixed(1) };",
  "inputSchema": {
    "type": "object",
    "properties": {
      "weightKg": { "type": "number" },
      "heightM": { "type": "number" }
    },
    "required": ["weightKg", "heightM"]
  }
}

JavaScript functions receive input and must return a result. Python functions set a result variable:

# Python example
result = {"bmi": round(input["weightKg"] / (input["heightM"] ** 2), 1)}

Execute a function directly:

POST /v1/functions/{id}/execute
Content-Type: application/json

{ "input": { "weightKg": 75, "heightM": 1.80 } }

Security: JavaScript runs in Node's vm sandbox (no filesystem or network access, 10s timeout). Python runs as a subprocess with a 30s timeout. Both validate input before execution.

Update or delete:

PATCH /v1/functions/{id}
DELETE /v1/functions/{id}

Skills

Skills are saved prompts that agents can reference and quick-launch from the blank-conversation view. Hub-installed skills (managed via package install) are NOT included here — only user-authored skills are listed and editable.

List skills:

GET /v1/skills

Returns { skills: [...] } of user-authored skills for the active profile. Tombstoned, hub-overlay, and sync-shadow skills are filtered out.

Get one skill:

GET /v1/skills/{id}

Create a skill:

POST /v1/skills
Content-Type: application/json

{
  "prompt": "Summarize the page in 3 bullet points.",
  "tags": ["utility", "summarize"],
  "isFavorite": true,
  "modes": ["general"],
  "displayName": "Quick Summary",
  "description": "Three-bullet TL;DR of the active page"
}

Only prompt is required. modes (optional) scopes the skill to specific agent IDs; omit for all modes. Returns 201 with the new skill (including server-assigned id, createdAt, updatedAt).

Update a skill:

PATCH /v1/skills/{id}
Content-Type: application/json

{ "prompt": "Updated prompt", "isFavorite": false }

Patchable fields: prompt, tags, modes, displayName, description, isFavorite. Attempts to patch a hub-overlay skill return 404.

Delete a skill:

DELETE /v1/skills/{id}

Soft-deletes via tombstone. Returns 204. Hub-overlay skills return 404 — uninstall the source package instead.

Workflows

Orchestrate multiple agents in a DAG (directed acyclic graph) — run steps in parallel where possible, and feed outputs from earlier steps into later ones.

Validate a workflow graph:

POST /v1/workflows/validate
Content-Type: application/json

{
  "graph": {
    "nodes": [
      { "id": "research", "agentId": "general", "prompt": "Research renewable energy trends" },
      { "id": "analyze", "agentId": "general", "prompt": "Research competitor pricing" },
      { "id": "report", "agentId": "general", "prompt": "Write a report combining: {{outputs.research}} and {{outputs.analyze}}", "dependsOn": ["research", "analyze"] }
    ]
  }
}

Returns { valid: true/false, errors: [...] }. Checks for cycles, duplicate IDs, and missing dependency references.

Execute a workflow:

POST /v1/workflows/execute
Content-Type: application/json

{
  "graph": {
    "nodes": [
      { "id": "research", "agentId": "general", "prompt": "Research renewable energy trends" },
      { "id": "summarize", "agentId": "general", "prompt": "Summarize: {{outputs.research}}", "dependsOn": ["research"] }
    ]
  }
}

Returns { status: "completed", outputs: { research: "...", summarize: "..." }, nodeResults: {...} }.

Independent nodes (no shared dependencies) run in parallel. The {{outputs.nodeId}} placeholder in a node's prompt is replaced with the content output of the named upstream node. Each node is a full agent run, so it can use tools, browse the web, and access all the capabilities of the target agent.

Knowledge Bases

Organize documents into searchable collections that agents can reference.

List knowledge bases:

GET /v1/knowledge/bases

Create a knowledge base:

POST /v1/knowledge/bases
Content-Type: application/json

{ "name": "Research Papers" }

Returns 201 with the new base ID.

Upload a document to a knowledge base:

POST /v1/knowledge/bases/{id}/documents
Content-Type: application/json

{
  "fileName": "research-paper.pdf",
  "contentType": "application/pdf",
  "dataUrl": "data:application/pdf;base64,JVBERi0xLjQ...",
  "description": "Renewable energy trends 2026"
}

The dataUrl field is a base64-encoded data URL. Returns 201 with the document metadata.

List documents in a knowledge base:

GET /v1/knowledge/bases/{id}/documents

Search within a knowledge base:

POST /v1/knowledge/bases/{id}/search
Content-Type: application/json

{ "query": "renewable energy" }

Returns matching documents based on file name and description. Semantic (vector) search is coming in a future release.

Delete a document or knowledge base:

DELETE /v1/knowledge/bases/{id}/documents/{docId}
DELETE /v1/knowledge/bases/{id}

Export & Import Agents

Share agents as portable packages — across devices, teams, or the Community Hub.

Export an agent:

POST /v1/agents/{id}/export

Returns a JSON package containing the agent definition, tool requirements (derived from enabled tools), connector requirements (which OAuth providers are needed), and trigger templates. Sync metadata is stripped — the package is a clean, self-contained blueprint.

Import an agent:

POST /v1/agents/import
Content-Type: application/json

{
  "package": {
    "$schema": "caiioo.agent.package/v1",
    "agent": {
      "id": "shared-research-agent",
      "branding": { "name": "Research Agent", "description": "From the team" },
      "defaultSettings": { "systemPrompt": "You research things." },
      "settingLevels": {}
    },
    "toolRequirements": [
      { "toolId": "web_browsing", "enabled": true },
      { "toolId": "search_tools", "enabled": true }
    ]
  }
}

Returns 201 with the installed agent. ID collisions with builtins or existing agents return 409.

Error Handling

The API uses standard HTTP status codes:

Code Meaning
200 Success
201 Created
202 Accepted (async operation started)
204 Deleted (no content)
400 Bad request — check the error field for details
401 Unauthorized — missing or invalid session secret
403 Forbidden — e.g., trying to modify a builtin agent
404 Not found
409 Conflict — e.g., agent ID already exists
422 Validation error — input doesn't match tool schema
429 Rate-limited — see the rate-limit budgets in the Authentication section
500 Server error — check the error field
501 Not implemented — feature exists but isn't available this way
503 Service unavailable — storage or provider not ready

All error responses include { "error": "human-readable message" }.

Quick Start Example

Here's a complete workflow: create an agent, run it, and stream the results.

# 1. Create a custom agent
curl -X POST http://localhost:3847/v1/agents \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "quick-summarizer",
    "branding": { "name": "Quick Summarizer" },
    "defaultSettings": {
      "systemPrompt": "Summarize any input concisely in 3 bullet points.",
      "enabledTools": { "web_browsing": true }
    },
    "settingLevels": {}
  }'

# 2. Run it asynchronously
RUN=$(curl -s -X POST http://localhost:3847/v1/runs \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "agentId": "quick-summarizer", "input": { "message": "Summarize https://en.wikipedia.org/wiki/Artificial_intelligence" }, "mode": "async" }')

RUN_ID=$(echo $RUN | jq -r '.runId')

# 3. Stream the events
curl -N http://localhost:3847/v1/runs/$RUN_ID/events \
  -H "Authorization: Bearer $API_TOKEN"

# 4. Export the agent for sharing
curl -X POST http://localhost:3847/v1/agents/quick-summarizer/export \
  -H "Authorization: Bearer $API_TOKEN"

This guide is maintained by the Caiioo team using Slate, our built-in editor.