diff --git a/.vitepress/config.ts b/.vitepress/config.ts
index 29042de2..99be4071 100644
--- a/.vitepress/config.ts
+++ b/.vitepress/config.ts
@@ -244,13 +244,9 @@ export default defineConfig({
'README.md',
'CONTRIBUTING.md',
'DEPLOYMENT.md',
+ // Retired Embed, Environment, Endpoint, Deployment, and Site Agent pages live here.
'_archived/**',
- // This legacy guide uses dashboard-only /default/v1 routes and an unpublished embed API.
- 'guides/site-agent-integration.md',
- 'use-cases/**',
'api-reference/webhooks.md',
- 'api-reference/embeds/**',
- 'api-reference/environments/**',
'agents/endpoint-quickstart.md',
'admin/api-keys.md',
'setup/cli.md',
@@ -284,6 +280,9 @@ export default defineConfig({
// canonicalized/noindexed in favor of Official Native API.
.filter((item) => ![
'agents/deployments',
+ 'api-reference/endpoints/',
+ 'api-reference/deployments/',
+ 'api-reference/credentials/',
'store/models',
'api-reference/volcengine-contents-generations',
].includes(item.url.replace(/^\//, '')))
@@ -303,7 +302,7 @@ export default defineConfig({
['link', { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' }],
['link', { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap' }],
['meta', { property: 'og:title', content: 'SandBase Docs - Connect Your Agent to the Real World' }],
- ['meta', { property: 'og:description', content: 'Learn how to use Setup, call Models and APIs, build Agents, publish Services, create Schedules, and review Sessions in SandBase.' }],
+ ['meta', { property: 'og:description', content: 'Learn how to use Setup, call Models and APIs, build Agents, deploy Services, create Schedules, and review Sessions in SandBase.' }],
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:site_name', content: 'SandBase' }],
['meta', { property: 'og:image', content: 'https://www.sandbase.ai/og-default.png' }],
diff --git a/.vitepress/sidebar.ts b/.vitepress/sidebar.ts
index 5eafafcd..4aa5ba44 100644
--- a/.vitepress/sidebar.ts
+++ b/.vitepress/sidebar.ts
@@ -79,8 +79,9 @@ export const docsSidebar: DefaultTheme.SidebarItem[] = [
items: [
{ text: 'Overview', link: '/agents/' },
{ text: 'Define agent', link: '/agents/agent-api' },
- { text: 'Tools & credentials', link: '/agents/mcp-tools' },
- { text: 'Credentials for tools', link: '/agents/api-credentials' },
+ { text: 'Skills & MCP tools', link: '/agents/mcp-tools' },
+ { text: 'MCP credentials', link: '/agents/api-credentials' },
+ { text: 'OpenAI compatibility', link: '/agents/openai-compatibility' },
],
},
{
@@ -123,9 +124,14 @@ export const apiReferenceSidebar: DefaultTheme.SidebarItem[] = [
{ text: 'List Agents', link: '/api-reference/agents/list' },
{ text: 'Get Agent', link: '/api-reference/agents/get' },
{ text: 'Update Agent', link: '/api-reference/agents/update' },
+ { text: 'Replace Agent (CAS)', link: '/api-reference/agents/replace' },
+ { text: 'Delete Agent', link: '/api-reference/agents/delete' },
{ text: 'Archive Agent', link: '/api-reference/agents/archive' },
+ { text: 'Unarchive Agent', link: '/api-reference/agents/unarchive' },
+ { text: 'Restore Agent Version', link: '/api-reference/agents/restore' },
{ text: 'List Agent Versions', link: '/api-reference/agents/versions' },
{ text: 'Get Agent Version', link: '/api-reference/agents/get-version' },
+ { text: 'Agent Catalog', link: '/api-reference/agents/catalog' },
],
},
{
@@ -136,54 +142,53 @@ export const apiReferenceSidebar: DefaultTheme.SidebarItem[] = [
{ text: 'Create Session', link: '/api-reference/sessions/create' },
{ text: 'List Sessions', link: '/api-reference/sessions/list' },
{ text: 'Get Session', link: '/api-reference/sessions/get' },
- { text: 'Update Session', link: '/api-reference/sessions/update' },
+ { text: 'Update Metadata', link: '/api-reference/sessions/update' },
+ { text: 'Rename Session', link: '/api-reference/sessions/rename' },
{ text: 'Delete Session', link: '/api-reference/sessions/delete' },
- { text: 'Send Events', link: '/api-reference/sessions/send-events' },
+ { text: 'Send Input', link: '/api-reference/sessions/send-events' },
{ text: 'Stream Events', link: '/api-reference/sessions/stream' },
- { text: 'List Events', link: '/api-reference/sessions/list-events' },
+ { text: 'List Items', link: '/api-reference/sessions/items' },
+ { text: 'List Turns', link: '/api-reference/sessions/turns' },
+ { text: 'Get Turn', link: '/api-reference/sessions/get-turn' },
+ { text: 'Cancel Turn', link: '/api-reference/sessions/cancel' },
{ text: 'Archive Session', link: '/api-reference/sessions/archive' },
+ { text: 'Unarchive Session', link: '/api-reference/sessions/unarchive' },
+ { text: 'Reconnect', link: '/api-reference/sessions/reconnect' },
+ { text: 'Snapshot Stream', link: '/api-reference/sessions/stream-snapshot' },
+ { text: 'Upload File', link: '/api-reference/sessions/upload-file' },
+ { text: 'List Files', link: '/api-reference/sessions/list-files' },
+ { text: 'Download File', link: '/api-reference/sessions/download-file' },
+ { text: 'Delete File', link: '/api-reference/sessions/delete-file' },
],
},
{
text: 'Services',
collapsed: true,
items: [
- { text: 'Overview', link: '/api-reference/endpoints/' },
- { text: 'Create Service', link: '/api-reference/endpoints/#create-a-service' },
- { text: 'List Services', link: '/api-reference/endpoints/#list-services' },
- { text: 'Get Service', link: '/api-reference/endpoints/#get-a-service' },
- { text: 'Update Service', link: '/api-reference/endpoints/#update-a-service' },
- { text: 'Delete Service', link: '/api-reference/endpoints/#delete-a-service' },
- { text: 'Invoke with REST', link: '/api-reference/endpoints/#invoke-with-rest' },
- { text: 'Invoke through MCP', link: '/api-reference/endpoints/mcp' },
- { text: 'Invoke with ACP', link: '/api-reference/endpoints/acp' },
+ { text: 'Overview', link: '/api-reference/services/' },
+ { text: 'Create Service', link: '/api-reference/services/create' },
+ { text: 'List Services', link: '/api-reference/services/list' },
+ { text: 'Get Service', link: '/api-reference/services/get' },
+ { text: 'Update Service', link: '/api-reference/services/update' },
+ { text: 'Pause, Resume, Archive', link: '/api-reference/services/lifecycle' },
+ { text: 'Invoke Service', link: '/api-reference/services/invoke' },
+ { text: 'Runs', link: '/api-reference/services/runs' },
+ { text: 'Notifications', link: '/api-reference/services/#notifications' },
],
},
{
text: 'Schedules',
collapsed: true,
items: [
- { text: 'Overview', link: '/api-reference/deployments/' },
- { text: 'Create Schedule', link: '/api-reference/deployments/create' },
- { text: 'List Schedules', link: '/api-reference/deployments/list' },
- { text: 'Get Schedule', link: '/api-reference/deployments/get' },
- { text: 'Update Schedule', link: '/api-reference/deployments/update' },
- { text: 'Delete Schedule', link: '/api-reference/deployments/delete' },
- { text: 'Archive Schedule', link: '/api-reference/deployments/archive' },
- { text: 'Pause Schedule', link: '/api-reference/deployments/pause' },
- { text: 'Resume Schedule', link: '/api-reference/deployments/unpause' },
- { text: 'Test Feishu Notification', link: '/api-reference/deployments/test-feishu-notification' },
- {
- text: 'Schedule runs',
- collapsed: true,
- items: [
- { text: 'Trigger Schedule', link: '/api-reference/deployments/run' },
- { text: 'List Runs for Schedule', link: '/api-reference/deployments/list-runs' },
- { text: 'Get Run for Schedule', link: '/api-reference/deployments/get-run' },
- { text: 'List All Schedule Runs', link: '/api-reference/deployments/list-all-runs' },
- { text: 'Get Schedule Run by ID', link: '/api-reference/deployments/get-global-run' },
- ],
- },
+ { text: 'Overview', link: '/api-reference/schedules/' },
+ { text: 'Create Schedule', link: '/api-reference/schedules/create' },
+ { text: 'List Schedules', link: '/api-reference/schedules/list' },
+ { text: 'Get Schedule', link: '/api-reference/schedules/get' },
+ { text: 'Update Schedule', link: '/api-reference/schedules/update' },
+ { text: 'Update Timing', link: '/api-reference/schedules/timing' },
+ { text: 'Pause, Resume, Archive', link: '/api-reference/schedules/lifecycle' },
+ { text: 'Trigger Run', link: '/api-reference/schedules/run' },
+ { text: 'Runs', link: '/api-reference/schedules/runs' },
],
},
{
@@ -196,18 +201,33 @@ export const apiReferenceSidebar: DefaultTheme.SidebarItem[] = [
{ text: 'Get Skill', link: '/api-reference/skills/get' },
{ text: 'Update Skill', link: '/api-reference/skills/update' },
{ text: 'Delete Skill', link: '/api-reference/skills/delete' },
+ { text: 'Skill Versions', link: '/api-reference/skills/versions' },
+ { text: 'Skill Catalog', link: '/api-reference/skills/catalog' },
],
},
{
- text: 'Credentials',
+ text: 'MCP Connections',
collapsed: true,
items: [
- { text: 'Overview', link: '/api-reference/credentials/' },
- { text: 'Create Credential', link: '/api-reference/credentials/#create-a-credential' },
- { text: 'List Credentials', link: '/api-reference/credentials/#list-credentials' },
- { text: 'Get Credential', link: '/api-reference/credentials/#get-a-credential' },
- { text: 'Update Credential', link: '/api-reference/credentials/#update-a-credential' },
- { text: 'Rotate Credential', link: '/api-reference/credentials/#rotate-a-credential' },
+ { text: 'Overview', link: '/api-reference/mcp-connections/' },
+ { text: 'Create Connection', link: '/api-reference/mcp-connections/#create-a-connection' },
+ { text: 'List Connections', link: '/api-reference/mcp-connections/#list-connections' },
+ { text: 'Get Connection', link: '/api-reference/mcp-connections/#get-a-connection' },
+ { text: 'Replace Connection', link: '/api-reference/mcp-connections/#replace-a-connection' },
+ { text: 'Enable Connection', link: '/api-reference/mcp-connections/#enable-a-connection' },
+ { text: 'Disable Connection', link: '/api-reference/mcp-connections/#disable-a-connection' },
+ ],
+ },
+ {
+ text: 'Secrets',
+ collapsed: true,
+ items: [
+ { text: 'Overview', link: '/api-reference/secrets/' },
+ { text: 'Create Secret', link: '/api-reference/secrets/#create-a-secret' },
+ { text: 'List Secrets', link: '/api-reference/secrets/#list-secrets' },
+ { text: 'Get Secret', link: '/api-reference/secrets/#get-a-secret' },
+ { text: 'Rotate Secret', link: '/api-reference/secrets/#rotate-a-secret' },
+ { text: 'Revoke Secret', link: '/api-reference/secrets/#revoke-a-secret' },
],
},
{
diff --git a/.vitepress/theme/HomePage.vue b/.vitepress/theme/HomePage.vue
index 5b06b0bc..bcea924f 100644
--- a/.vitepress/theme/HomePage.vue
+++ b/.vitepress/theme/HomePage.vue
@@ -17,7 +17,7 @@
01
Use in my AI tool
-
Choose Models, APIs, or published Agents. Run setup once. Use them in Codex, Claude, Cursor, or Kiro.
+
Choose Models, APIs, or Agent Services. Run setup once. Use them in Codex, Claude, Cursor, or Kiro.
@@ -31,7 +31,7 @@
03
Build an Agent
-
Define the Agent, test runs, then publish it for API access or schedule it.
+
Define the Agent, test it in a Session, then deploy it as a Service or Schedule.
@@ -86,7 +86,7 @@
01
Setup
- Add Models, APIs, and published Agents to the AI app you already use.
+ Add Models, APIs, and Agent Services to the AI app you already use.
- Choose tools
- Run one setup command
@@ -106,10 +106,10 @@
03
Build an Agent
- Turn instructions, Models, APIs, and Skills into repeatable work.
+ Turn instructions, Models, Skills, and MCP tools into repeatable work.
- Define and test
- - Publish as an API
+ - Deploy as a Service
- Schedule recurring runs
@@ -120,7 +120,7 @@
CONSOLE FLOW
Build the Agent first. Connect it when it is ready.
- Agents are where you define and test the work. Published Agents and Schedules are ways to make that work usable outside the builder.
+ Agents are where you define and test the work. Services and Schedules run a pinned Agent version outside the builder, and every Run is recorded as a Session.
-
2
Publish Agent
+
2
Deploy as a Service
@@ -169,16 +169,16 @@
/v1/models · /v1/chat/completions
List models, inspect pricing and capabilities, and call them directly.
-
+
SERVICES
Services
- /v1/endpoints
- Publish a tested Agent so apps and API clients can invoke it.
+ /v1/services
+ Deploy a tested Agent version so apps and API clients can invoke it and track each Run.
-
+
SCHEDULES
Schedules
- /v1/deployments
+ /v1/schedules
Run an existing Agent on a recurring schedule and review each result.
diff --git a/.vitepress/theme/SiteFooter.vue b/.vitepress/theme/SiteFooter.vue
index 7eb71f67..2b920823 100644
--- a/.vitepress/theme/SiteFooter.vue
+++ b/.vitepress/theme/SiteFooter.vue
@@ -38,9 +38,9 @@ const footerSections = [
title: 'Build',
links: [
{ href: `${SITE}/console/agents`, label: 'Build Agent' },
- { href: `${SITE}/console/endpoints`, label: 'Published Agents' },
+ { href: `${SITE}/console/endpoints`, label: 'Services' },
{ href: `${SITE}/console/deployments`, label: 'Schedules' },
- { href: `${SITE}/console/sessions`, label: 'Runs' },
+ { href: `${SITE}/console/sessions`, label: 'Sessions' },
],
},
{
diff --git a/.vitepress/theme/generatedApiReferenceSpecs.ts b/.vitepress/theme/generatedApiReferenceSpecs.ts
index 85f5fc88..184d7024 100644
--- a/.vitepress/theme/generatedApiReferenceSpecs.ts
+++ b/.vitepress/theme/generatedApiReferenceSpecs.ts
@@ -59,1131 +59,97 @@ export const generatedApiReferenceSpecs: Record = {
"code": "{\n \"items\": [\n {\n \"id\": \"opaque-task-id\",\n \"model\": \"openai/gpt-5.4\",\n \"type\": \"llm\",\n \"execution_mode\": \"sync\",\n \"status\": \"completed\",\n \"latency_ms\": 842,\n \"user_cost\": \"0.001250\",\n \"prompt_tokens\": 120,\n \"completion_tokens\": 48,\n \"total_tokens\": 168,\n \"cached_tokens\": 0,\n \"cache_creation_tokens\": 0,\n \"created_at\": \"2026-08-24T00:00:00Z\",\n \"completed_at\": \"2026-08-24T00:00:01Z\",\n \"api_key_prefix\": \"sk-1234567\"\n }\n ],\n \"total\": 1,\n \"page\": 1,\n \"page_size\": 20\n}"
}
},
- "endpoints/acp": {
- "title": "Invoke Service with ACP",
- "operation": "Services",
- "method": "POST",
- "path": "/v1/endpoints/{endpoint_id}/acp",
- "description": "Experimental ACP-over-HTTP transport. initialize, session/new, and session/cancel return JSON-RPC JSON; session/prompt streams newline-delimited JSON-RPC messages.",
- "groups": [
- {
- "title": "Path parameters",
- "fields": [
- { "name": "endpoint_id", "type": "string", "required": true, "description": "Active Service configured with the acp protocol." }
- ]
- },
- {
- "title": "JSON-RPC request",
- "fields": [
- { "name": "jsonrpc", "type": "string", "required": false, "description": "Use 2.0." },
- { "name": "id", "type": "string | number | null", "required": false, "description": "Client identifier echoed in the response." },
- { "name": "method", "type": "string", "required": true, "description": "initialize, session/new, session/prompt, or session/cancel." },
- { "name": "params", "type": "object", "required": false, "description": "Method-specific parameters. Prompt and cancel require sessionId." }
- ]
- }
- ],
- "examples": [
- {
- "label": "Initialize",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/endpoints/ep_01.../acp -H \"Authorization: Bearer $SANDBASE_API_KEY\" -H \"Content-Type: application/json\" -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}'"
- },
- {
- "label": "Create session",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/endpoints/ep_01.../acp -H \"Authorization: Bearer $SANDBASE_API_KEY\" -H \"Content-Type: application/json\" -d '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"session/new\",\"params\":{}}'"
- }
- ],
- "response": {
- "status": "200 OK",
- "code": "{\n \"jsonrpc\": \"2.0\",\n \"id\": 2,\n \"result\": {\n \"sessionId\": \"sess_01...\"\n }\n}"
- },
- "notes": [
- { "title": "Prompt streaming", "description": "session/prompt returns application/x-ndjson session/update notifications followed by a final result containing stopReason." },
- { "title": "Experimental transport", "description": "ACP-over-HTTP may change. ACP Session IDs are canonical SandBase Session IDs." }
- ]
- },
- "deployments/archive": {
- "title": "Archive Schedule",
- "operation": "Schedules",
- "method": "POST",
- "path": "/v1/deployments/{deployment_id}/archive",
- "description": "Archive a Deployment that should no longer be triggered.",
- "groups": [
- {
- "title": "Path parameters",
- "fields": [
- {
- "name": "deployment_id",
- "type": "string",
- "required": true,
- "description": "Identifier for deployment id."
- }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/deployments/depl_01.../archive \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
- }
- ]
- },
- "deployments/create": {
- "title": "Create Schedule",
- "operation": "Schedules",
- "method": "POST",
- "path": "/v1/deployments",
- "description": "Create a manually triggered or scheduled Deployment from an existing Agent or a declarative runtime definition. JSON and YAML are supported.",
- "groups": [
- {
- "title": "Advanced request body",
- "fields": [
- {
- "name": "name",
- "type": "string",
- "required": true,
- "description": "Display name for the Deployment."
- },
- {
- "name": "agent_id",
- "type": "string",
- "required": true,
- "description": "Agent to run."
- },
- {
- "name": "initial_events",
- "type": "array",
- "required": true,
- "description": "At least one valid user.message event."
- }
- ]
- },
- {
- "title": "Declarative request body",
- "fields": [
- { "name": "name", "type": "string", "required": true, "description": "Display name for the Deployment." },
- { "name": "runtime", "type": "string", "required": true, "description": "Enabled declarative runtime. This release supports hermes." },
- { "name": "initial_events", "type": "array", "required": true, "description": "At least one valid user.message event." },
- { "name": "model", "type": "string | object", "required": false, "description": "Optional model override for the generated Agent." },
- { "name": "system", "type": "string", "required": false, "description": "Optional system instructions for the generated Agent." },
- { "name": "skills", "type": "array | null", "required": false, "description": "Up to 20 vendor/plugin references." },
- { "name": "schedule", "type": "object | null", "required": false, "description": "Optional cron schedule." },
- { "name": "timeout_policy", "type": "object", "required": false, "description": "Optional hard and inactivity timeout settings." }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/deployments \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Daily customer brief\",\n \"agent_id\": \"agent_01...\",\n \"environment_id\": \"env_01...\",\n \"initial_events\": [{\n \"type\": \"user.message\",\n \"content\": [{\"type\":\"text\",\"text\":\"Prepare today’s brief.\"}]\n }],\n \"schedule\": {\n \"type\": \"cron\",\n \"expression\": \"0 9 * * *\",\n \"timezone\": \"Asia/Shanghai\"\n }\n }'"
- },
- {
- "label": "Declarative YAML",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/deployments -H \"Authorization: Bearer $SANDBASE_API_KEY\" -H \"Content-Type: application/yaml\" --data-binary $'name: Daily customer brief\\nruntime: hermes\\ninitial_events:\\n - type: user.message\\n content:\\n - type: text\\n text: Prepare today’s brief.'"
- }
- ]
- },
- "deployments/get": {
- "title": "Get Schedule",
- "operation": "Schedules",
- "method": "GET",
- "path": "/v1/deployments/{deployment_id}",
- "description": "Retrieve one Deployment and its resolved Agent and runtime binding.",
- "groups": [
- {
- "title": "Path parameters",
- "fields": [
- {
- "name": "deployment_id",
- "type": "string",
- "required": true,
- "description": "Identifier for deployment id."
- }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl https://api.sandbase.ai/v1/deployments/depl_01... \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
- }
- ]
- },
- "deployments/get-run": {
- "title": "Get Schedule Run",
- "operation": "Schedules",
- "method": "GET",
- "path": "/v1/deployments/{deployment_id}/runs/{drun_id}",
- "description": "Retrieve one durable DeploymentRun owned by the requested Deployment.",
- "groups": [
- {
- "title": "Path parameters",
- "fields": [
- {
- "name": "deployment_id",
- "type": "string",
- "required": true,
- "description": "Deployment that owns the DeploymentRun."
- },
- {
- "name": "drun_id",
- "type": "string",
- "required": true,
- "description": "DeploymentRun identifier beginning with drun_."
- }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl https://api.sandbase.ai/v1/deployments/depl_01.../runs/drun_01... \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
- }
- ],
- "response": {
- "status": "200 OK",
- "code": "{\n \"id\": \"drun_01...\",\n \"type\": \"deployment_run\",\n \"deployment_id\": \"depl_01...\",\n \"agent\": {\"id\":\"agent_01...\",\"type\":\"agent\",\"version\":1},\n \"trigger_context\": {\"type\":\"manual\"},\n \"session_id\": \"sess_01...\",\n \"error\": null,\n \"created_at\": \"2026-08-04T09:00:00Z\"\n}"
- },
- "errors": [
- {
- "status": 404,
- "type": "not_found",
- "description": "The DeploymentRun does not exist or does not belong to the Deployment."
- },
- {
- "status": 409,
- "type": "deployment_trigger_in_progress",
- "description": "The DeploymentRun has not resolved yet."
- }
- ]
- },
- "deployments/list-runs": {
- "title": "List Schedule Runs",
- "operation": "Schedules",
- "method": "GET",
- "path": "/v1/deployments/{deployment_id}/runs",
- "description": "List durable DeploymentRun records for one Deployment.",
- "groups": [
- {
- "title": "Path parameters",
- "fields": [
- {
- "name": "deployment_id",
- "type": "string",
- "required": true,
- "description": "Identifier for deployment id."
- }
- ]
- },
- {
- "title": "Query parameters",
- "fields": [
- {
- "name": "limit",
- "type": "integer",
- "required": false,
- "default": "20",
- "description": "Page size from 1 to 1000. Pass it explicitly when following nested-list cursors."
- },
- {
- "name": "page",
- "type": "string",
- "required": false,
- "description": "Opaque cursor returned in the previous response's next_page field."
- }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl \"https://api.sandbase.ai/v1/deployments/depl_01.../runs?limit=20\" \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
- }
- ],
- "notes": [
- {
- "title": "Nested-list pagination",
- "description": "Set limit explicitly. A full page includes next_page; because the current endpoint detects cursors from page fullness, following the final full-page cursor can return an empty page."
- }
- ]
- },
- "deployments/list": {
- "title": "List Schedules",
- "operation": "Schedules",
- "method": "GET",
- "path": "/v1/deployments",
- "description": "List Deployments in the current organization.",
- "groups": [
- {
- "title": "Query parameters",
- "fields": [
- { "name": "agent_id", "type": "string", "required": false, "description": "Filter by owning Agent ID." },
- { "name": "status", "type": "string[]", "required": false, "description": "Repeat to include active and/or paused Deployments." },
- { "name": "include_archived", "type": "boolean", "required": false, "default": "false", "description": "Include archived Deployments." },
- { "name": "created_at[gte]", "type": "string · RFC 3339", "required": false, "description": "Return Deployments created at or after this timestamp." },
- { "name": "created_at[lte]", "type": "string · RFC 3339", "required": false, "description": "Return Deployments created at or before this timestamp." },
- { "name": "limit", "type": "integer", "required": false, "default": "20", "description": "Number of Deployments to return, from 1 to 100." },
- { "name": "page", "type": "string", "required": false, "description": "Opaque cursor from a previous response's next_page field." }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl \"https://api.sandbase.ai/v1/deployments?limit=20\" \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
- }
- ]
- },
- "deployments/pause": {
- "title": "Pause Schedule",
- "operation": "Schedules",
- "method": "POST",
- "path": "/v1/deployments/{deployment_id}/pause",
- "description": "Pause scheduled triggers for a Deployment.",
- "groups": [
- {
- "title": "Path parameters",
- "fields": [
- {
- "name": "deployment_id",
- "type": "string",
- "required": true,
- "description": "Identifier for deployment id."
- }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/deployments/depl_01.../pause \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
- }
- ]
- },
- "deployments/run": {
- "title": "Trigger Schedule",
- "operation": "Schedules",
- "method": "POST",
- "path": "/v1/deployments/{deployment_id}/runs",
- "description": "Create a durable DeploymentRun and attempt to create exactly one new Session.",
- "groups": [
- {
- "title": "Path parameters",
- "fields": [
- {
- "name": "deployment_id",
- "type": "string",
- "required": true,
- "description": "Identifier for deployment id."
- }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/deployments/depl_01.../runs \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Idempotency-Key: daily-acme-2026-08-03\" \\\n -d '{}'"
- }
- ]
- },
- "deployments/unpause": {
- "title": "Resume Schedule",
- "operation": "Schedules",
- "method": "POST",
- "path": "/v1/deployments/{deployment_id}/unpause",
- "description": "Resume scheduled triggers for a paused Deployment.",
- "groups": [
- {
- "title": "Path parameters",
- "fields": [
- {
- "name": "deployment_id",
- "type": "string",
- "required": true,
- "description": "Identifier for deployment id."
- }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/deployments/depl_01.../unpause \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
- }
- ]
- },
- "deployments/test-feishu-notification": {
- "title": "Test Feishu Notification",
- "operation": "Schedules",
- "method": "POST",
- "path": "/v1/deployments/{deployment_id}/notifications/feishu/test",
- "description": "Send a fixed SandBase test message to the Feishu webhook already saved on a Deployment. No URL or custom message is accepted.",
- "groups": [
- {
- "title": "Path parameters",
- "fields": [
- { "name": "deployment_id", "type": "string", "required": true, "description": "Deployment identifier." }
- ]
- },
- {
- "title": "Request body",
- "description": "Omit the body or send an empty JSON object.",
- "fields": []
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/deployments/depl_01.../notifications/feishu/test \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{}'"
- }
- ],
- "response": {
- "status": "200 OK",
- "code": "{\n \"delivered\": true\n}"
- },
- "notes": [
- { "title": "Saved target only", "description": "Configure notification_settings.feishu_webhook_url through the Deployment update API first. The secret URL is never returned." }
- ]
- },
- "deployments/update": {
- "title": "Update Schedule",
- "operation": "Schedules",
- "method": "PATCH",
- "path": "/v1/deployments/{deployment_id}",
- "description": "Update mutable Deployment settings and bindings.",
- "groups": [
- {
- "title": "Path parameters",
- "fields": [
- {
- "name": "deployment_id",
- "type": "string",
- "required": true,
- "description": "Identifier for deployment id."
- }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl -X PATCH https://api.sandbase.ai/v1/deployments/depl_01... \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Weekday customer brief\",\n \"schedule\": {\"type\":\"cron\",\"expression\":\"0 9 * * 1-5\",\"timezone\":\"Asia/Shanghai\"}\n }'"
- }
- ]
- },
- "models/audio": {
- "title": "Audio (TTS/STT)",
- "operation": "Models",
- "method": "POST",
- "path": "/v1/run",
- "description": "Run a text-to-speech or speech-to-text model. Asynchronous audio models may include webhook_url for a task callback.",
- "groups": [
- {
- "title": "Request body",
- "schema": "ImageEditParams",
- "fields": [
- { "name": "model", "type": "string", "required": true, "description": "Use gpt-image-2 on this endpoint." },
- { "name": "prompt", "type": "string", "required": true, "description": "Text description of the requested edit." },
- { "name": "image", "type": "file | file[]", "required": true, "description": "One or more source image files. Repeat the multipart field for multiple images." },
- { "name": "mask", "type": "file", "required": false, "description": "Optional mask image for inpainting." },
- { "name": "n", "type": "integer", "required": false, "description": "Number of edited images to return when supported." },
- { "name": "size", "type": "string", "required": false, "description": "Output dimensions supported by the model." },
- { "name": "quality", "type": "string", "required": false, "description": "Output quality supported by the model." },
- { "name": "background", "type": "string", "required": false, "description": "Output background setting." },
- { "name": "input_fidelity", "type": "string", "required": false, "description": "How closely supported models should preserve source-image details." },
- { "name": "output_format", "type": "string", "required": false, "description": "Output encoding such as png, webp, or jpeg." },
- { "name": "output_compression", "type": "integer", "required": false, "description": "Output compression level from 0 to 100." },
- { "name": "user", "type": "string", "required": false, "description": "Provider-compatible end-user identifier." }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/run \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"bytedance/seed-speech/tts/2.0\",\"text\":\"Hello, welcome to SandBase!\"}'"
- },
- {
- "label": "Python",
- "language": "python",
- "code": "response = client.post(\"/v1/run\", json={\n \"model\": \"bytedance/seed-speech/tts/2.0\",\n \"text\": \"Hello, welcome to SandBase!\"\n})\nprint(response.json()[\"outputs\"][0][\"url\"])"
- }
- ],
- "response": { "status": "200 OK", "code": "{\n \"id\": \"run_01...\",\n \"status\": \"completed\",\n \"outputs\": [{ \"url\": \"https://media.sandbase.ai/audio/output.mp3\" }]\n}" }
- },
- "assets/upload": {
- "title": "Upload Media",
- "operation": "Assets",
- "method": "POST",
- "path": "/v1/upload",
- "description": "Upload an image, audio, or video file and receive a stored URL for a supported model input.",
- "groups": [
- {
- "title": "Multipart form",
- "fields": [
- { "name": "file", "type": "binary", "required": true, "description": "Image up to 20 MB, audio up to 50 MB, or video up to 500 MB. Supported formats are listed below." },
- { "name": "type", "type": "image | audio | video", "required": false, "description": "Optional category hint. MIME detection from the file still controls validation." }
- ]
- },
- {
- "title": "Supported MIME types",
- "fields": [
- { "name": "Image", "type": "string", "required": false, "description": "image/jpeg, image/png, image/webp, image/gif — maximum 20 MB." },
- { "name": "Audio", "type": "string", "required": false, "description": "audio/mpeg, audio/mp4, audio/wav, audio/x-wav, audio/ogg, audio/aac, audio/flac — maximum 50 MB." },
- { "name": "Video", "type": "string", "required": false, "description": "video/mp4, video/quicktime, video/webm — maximum 500 MB." }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/upload \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -F \"file=@reference.mp4\" \\\n -F \"type=video\""
- }
- ],
- "response": {
- "status": "200 OK",
- "code": "{\n \"url\": \"https://media.sandbase.ai/uploads/2026/08/550e8400-e29b-41d4-a716-446655440000.mp4\",\n \"filename\": \"reference.mp4\",\n \"size\": 1048576,\n \"type\": \"video/mp4\",\n \"content_type\": \"video/mp4\"\n}"
- },
- "notes": [
- { "title": "MIME detection", "description": "The server detects the file type from its bytes and filename. The optional type field does not force a MIME type." }
- ]
- },
- "assets/create": {
- "title": "Register Asset",
- "operation": "Assets",
- "method": "POST",
- "path": "/v1/assets",
- "description": "Register a publicly accessible media URL and receive a persistent asset:// reference.",
- "groups": [
- {
- "title": "Request body",
- "fields": [
- { "name": "url", "type": "string", "required": true, "description": "Non-empty source URL forwarded to the Asset provider. The provider must be able to fetch it." },
- { "name": "asset_type", "type": "Image | Video | Audio", "required": true, "description": "Case-sensitive asset type." },
- { "name": "name", "type": "string", "required": false, "description": "Optional display name." }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/assets \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"url\": \"https://example.com/tea-ad.mp4\",\n \"asset_type\": \"Video\",\n \"name\": \"Tea ad reference video\"\n }'"
- }
- ],
- "response": {
- "status": "200 OK",
- "code": "{\n \"id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n \"asset_url\": \"asset://asset-20260710150403-hx4hf\",\n \"asset_type\": \"Video\",\n \"name\": \"Tea ad reference video\",\n \"created_at\": \"2026-07-10T15:04:03Z\"\n}"
- }
- },
- "assets/get": {
- "title": "Get Asset",
- "operation": "Assets",
- "method": "GET",
- "path": "/v1/assets/{asset_id}",
- "description": "Retrieve Asset metadata and a temporary signed download URL.",
- "groups": [
- {
- "title": "Path parameters",
- "fields": [
- { "name": "asset_id", "type": "string", "required": true, "description": "External Asset ID, without the asset:// prefix." }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl https://api.sandbase.ai/v1/assets/asset-20260710150403-hx4hf \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
- }
- ],
- "response": {
- "status": "200 OK",
- "code": "{\n \"id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n \"external_id\": \"asset-20260710150403-hx4hf\",\n \"asset_url\": \"asset://asset-20260710150403-hx4hf\",\n \"status\": \"Active\",\n \"asset_type\": \"Video\",\n \"download_url\": \"https://cdn.example.com/signed-url\",\n \"created_at\": \"2026-07-10T15:04:03Z\"\n}"
- },
- "notes": [
- { "title": "Provider-managed URL", "description": "The asset:// value is the reusable Asset reference. download_url is provider-managed and can be empty; query this endpoint again for its current value." }
- ]
- },
- "models/embedding": {
- "title": "Embeddings",
- "operation": "Models",
- "method": "POST",
- "path": "/v1/embeddings",
- "description": "OpenAI-compatible vector embeddings API. Input forms, dimensions, encoding, and additional fields remain subject to the selected model and provider.",
- "groups": [
- {
- "title": "Request body",
- "fields": [
- {
- "name": "model",
- "type": "string",
- "required": true,
- "description": "Embedding model ID, for example alibaba/text-embedding-v4"
- },
- {
- "name": "input",
- "type": "string | string[] | integer[] | integer[][]",
- "required": true,
- "description": "Text, token IDs, or a batch of texts or token-ID arrays to embed"
- },
- {
- "name": "dimensions",
- "type": "integer",
- "required": false,
- "description": "Output vector dimension when supported by the selected model; there is no universal dimension list"
- },
- {
- "name": "encoding_format",
- "type": "string",
- "required": false,
- "description": "Embedding encoding format. Provider support determines whether float or base64 is available"
- }
- ]
- }
- ],
- "examples": [
- {
- "label": "HTTP",
- "language": "http",
- "code": "POST https://api.sandbase.ai/v1/embeddings\nAuthorization: Bearer sk-your-api-key\nContent-Type: application/json\n\n{\"model\":\"alibaba/text-embedding-v4\",\"input\":\"SandBase provides a unified API for AI models, agents, and developer tools.\",\"dimensions\":1024,\"encoding_format\":\"float\"}"
- },
- {
- "label": "Python",
- "language": "python",
- "code": "from openai import OpenAI\n\nclient = OpenAI(\n api_key=\"sk-YOUR_KEY\",\n base_url=\"https://api.sandbase.ai/v1\",\n)\n\nresponse = client.embeddings.create(\n model=\"alibaba/text-embedding-v4\",\n input=\"SandBase provides a unified API for AI models, agents, and developer tools.\",\n dimensions=1024,\n encoding_format=\"float\",\n)\n\nembedding = response.data[0].embedding\nprint(f\"Dimensions: {len(embedding)}\")"
- },
- {
- "label": "TypeScript",
- "language": "typescript",
- "code": "import OpenAI from 'openai';\n\nconst client = new OpenAI({\n apiKey: 'sk-YOUR_KEY',\n baseURL: 'https://api.sandbase.ai/v1',\n});\n\nconst response = await client.embeddings.create({\n model: 'alibaba/text-embedding-v4',\n input: 'SandBase provides a unified API for AI models, agents, and developer tools.',\n dimensions: 1024,\n encoding_format: 'float',\n});\n\nconsole.log(response.data[0].embedding.length);"
- }
- ],
- "response": {
- "status": "200 OK",
- "code": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"embedding\",\n \"index\": 0,\n \"embedding\": [0.0123, -0.0045, 0.0312]\n }\n ],\n \"model\": \"alibaba/text-embedding-v4\",\n \"usage\": {\n \"prompt_tokens\": 16,\n \"total_tokens\": 16\n }\n}"
- }
- },
- "models/get": {
- "title": "Get Model",
- "operation": "Models",
- "method": "GET",
- "path": "/v1/models/{id_or_name}",
- "description": "Get model details by UUID or full logical name. Name lookup returns enabled catalog models; UUID lookup can return a disabled record with enabled set to false. Protocol-ingress-only models are never returned.",
- "groups": [
- {
- "title": "Path parameters",
- "fields": [
- {
- "name": "id_or_name",
- "type": "string",
- "required": true,
- "description": "Model UUID or full logical model name, for example openai/gpt-4o. Names may contain slashes."
- }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl \"https://api.sandbase.ai/v1/models/openai/gpt-4o\" \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
- }
- ],
- "response": {
- "status": "200 OK",
- "code": "{\n \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"name\": \"openai/gpt-4o\",\n \"display_name\": \"GPT-4o\",\n \"vendor\": \"OpenAI\",\n \"description\": \"Most capable GPT-4 model with vision...\",\n \"type\": \"llm\",\n \"capability_tags\": [\"chat\", \"streaming\", \"vision\", \"tools\", \"json_mode\", \"cache\"],\n \"skills\": [\"chat\", \"streaming\", \"vision\", \"tools\", \"json_mode\", \"cache\"],\n \"unified_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"prompt\": {\n \"type\": \"string\"\n }\n }\n },\n \"execution_mode\": \"chat\",\n \"supported_modes\": [\"chat\"],\n \"run_count\": 12500,\n \"sort_order\": 10,\n \"published_at\": \"2026-07-07T00:00:00Z\",\n \"vendor_slug\": \"openai\",\n \"model_slug\": \"gpt-4o\",\n \"enabled\": true,\n \"featured\": true,\n \"created_at\": \"2026-07-07T00:00:00Z\",\n \"model_card\": {\n \"base_price\": \"0.000000\",\n \"prompt_token_price\": \"0.0000025\",\n \"completion_token_price\": \"0.000010\",\n \"cache_read_multiplier\": \"0.5\",\n \"cache_write_multiplier\": \"1.0\",\n \"cache_write_1h_multiplier\": null,\n \"reasoning_price\": null,\n \"price_formula\": \"$input_tokens * 0.0000025 + $output_tokens * 0.000010\",\n \"markup_ratio\": \"1.0000\",\n \"readme\": \"Model usage notes...\",\n \"cover_url\": \"https://...\"\n },\n \"examples\": []\n}"
- }
- },
- "models/image": {
- "title": "Image Generation",
- "operation": "Models",
- "method": "POST",
- "path": "/v1/run",
- "description": "Generate images using AI models like FLUX, Stable Diffusion, DALL-E, and more.",
- "groups": [
- {
- "title": "Request body",
- "fields": [
- {
- "name": "model",
- "type": "string",
- "required": true,
- "description": "Current model identifier, for example bfl/flux-1/schnell."
- },
- {
- "name": "prompt",
- "type": "string",
- "required": true,
- "description": "Text description of the image"
- },
- {
- "name": "aspect_ratio",
- "type": "string",
- "required": false,
- "description": "Output aspect ratio. Flux 1 Schnell defaults to 16:9."
- },
- {
- "name": "guidance_scale",
- "type": "number",
- "required": false,
- "description": "Prompt guidance strength from 1 to 20."
- },
- {
- "name": "output_format",
- "type": "jpeg | png",
- "required": false,
- "description": "Output image format. Flux 1 Schnell defaults to jpeg."
- },
- {
- "name": "mode",
- "type": "string",
- "required": false,
- "description": "Set to async only when the selected model supports asynchronous execution"
- },
- {
- "name": "webhook_url",
- "type": "string",
- "required": false,
- "description": "Public HTTPS callback URL for this async task only"
- }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/run \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"bfl/flux-1/schnell\",\"prompt\":\"A futuristic city at sunset\",\"aspect_ratio\":\"1:1\"}'"
- },
- {
- "label": "Python",
- "language": "python",
- "code": "response = client.post(\"/v1/run\", json={\n \"model\": \"bfl/flux-1/schnell\",\n \"prompt\": \"A futuristic city at sunset, cyberpunk style\",\n \"aspect_ratio\": \"1:1\"\n})\nprint(response.json()[\"outputs\"][0][\"url\"])"
- }
- ],
- "response": { "status": "200 OK", "code": "{\n \"id\": \"run_01...\",\n \"status\": \"completed\",\n \"outputs\": [{ \"url\": \"https://media.sandbase.ai/images/output.png\" }]\n}" }
- },
- "models/list": {
- "title": "List Models",
- "operation": "Models",
- "method": "GET",
- "path": "/v1/models",
- "description": "List enabled logical models in the OpenAI-compatible model-list format.",
- "groups": [
- {
- "title": "Query parameters",
- "fields": [
- {
- "name": "q",
- "type": "string",
- "required": false,
- "description": "Fuzzy search over model name, display name, vendor, and description."
- },
- {
- "name": "vendor",
- "type": "string",
- "required": false,
- "description": "Filter by vendor, for example OpenAI or Anthropic."
- },
- {
- "name": "type",
- "type": "string",
- "required": false,
- "description": "Filter by model type. Defaults to llm."
- },
- {
- "name": "order",
- "type": "string",
- "required": false,
- "description": "Sort order: sort_order, most-popular, newest, or name."
- }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl \"https://api.sandbase.ai/v1/models\" \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
- }
- ],
- "response": {
- "status": "200 OK",
- "code": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"openai/gpt-4o\",\n \"object\": \"model\",\n \"created\": 1783353600,\n \"owned_by\": \"openai\"\n }\n ]\n}"
- }
- },
- "models/video": {
- "title": "Video Generation",
+ "models/audio": {
+ "title": "Audio (TTS/STT)",
"operation": "Models",
"method": "POST",
"path": "/v1/run",
- "description": "Generate videos using AI models. Video generation is asynchronous — you submit a request and poll for results.",
- "groups": [
- {
- "title": "Request body",
- "fields": [
- {
- "name": "model",
- "type": "string",
- "required": true,
- "description": "Current model identifier, for example kwaivgi/kling-video/3.0/turbo/standard/text-to-video."
- },
- {
- "name": "prompt",
- "type": "string",
- "required": true,
- "description": "Text description of the video"
- },
- {
- "name": "duration",
- "type": "number",
- "required": false,
- "description": "Video duration in seconds"
- },
- {
- "name": "image_url",
- "type": "string",
- "required": false,
- "description": "Reference image for image-to-video"
- },
- {
- "name": "webhook_url",
- "type": "string",
- "required": false,
- "description": "Public HTTPS callback URL for this async task only"
- }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/run \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"kwaivgi/kling-video/3.0/turbo/standard/text-to-video\",\"prompt\":\"A cat playing piano\",\"duration\":5}'"
- },
- {
- "label": "Python",
- "language": "python",
- "code": "# 1. Submit generation request\nresponse = client.post(\"/v1/run\", json={\n \"model\": \"kwaivgi/kling-video/3.0/turbo/standard/text-to-video\",\n \"prompt\": \"A cat playing piano\",\n \"duration\": 5,\n \"webhook_url\": \"https://hooks.example.com/sandbase/task?token=opaque-token\"\n})\ngeneration_id = response.json()[\"id\"]\n\n# 2. SandBase posts the terminal event to webhook_url.\n# You may still poll the existing result endpoint as a fallback.\nimport time\nwhile True:\n result = client.get(f\"/v1/run/{generation_id}\").json()\n if result[\"status\"] in (\"completed\", \"failed\", \"timeout\"):\n print(result)\n break\n time.sleep(5)"
- }
- ],
- "response": { "status": "202 Accepted", "code": "{\n \"id\": \"run_01...\",\n \"status\": \"pending\",\n \"model\": \"kwaivgi/kling-video/3.0/turbo/standard/text-to-video\"\n}" }
- },
- "sessions/archive": {
- "title": "Archive Session",
- "operation": "Sessions",
- "method": "POST",
- "path": "/v1/sessions/{session_id}/archive",
- "description": "Archive a Session while preserving its record.",
- "groups": [
- {
- "title": "Path parameters",
- "fields": [
- {
- "name": "session_id",
- "type": "string",
- "required": true,
- "description": "Identifier for session id."
- }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/sessions/sess_01.../archive \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
- }
- ]
- },
- "sessions/create": {
- "title": "Create Session",
- "operation": "Sessions",
- "method": "POST",
- "path": "/v1/sessions",
- "description": "Create a stateful execution of an Agent. SandBase resolves runtime binding internally.",
+ "description": "Run a text-to-speech or speech-to-text model. Asynchronous audio models may include webhook_url for a task callback.",
"groups": [
{
"title": "Request body",
+ "schema": "ImageEditParams",
"fields": [
- {
- "name": "agent",
- "type": "string",
- "required": true,
- "description": "Agent ID used by this Session."
- },
- {
- "name": "title",
- "type": "string",
- "required": false,
- "description": "Human-readable Session title."
- },
- {
- "name": "metadata",
- "type": "object",
- "required": false,
- "description": "Application metadata returned with the Session."
- }
- ]
- }
- ],
- "examples": [
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/sessions \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"agent\": \"agent_01...\",\n \"environment_id\": \"env_01...\",\n \"title\": \"Customer research\"\n }'"
- }
- ],
- "response": {
- "status": "200 OK",
- "code": "{\n \"id\": \"sess_01...\",\n \"type\": \"session\",\n \"agent_id\": \"agent_01...\",\n \"agent_version\": 1,\n \"agent_version_id\": \"av_01...\",\n \"agent\": { \"id\": \"agent_01...\", \"type\": \"agent\", \"version\": 1 },\n \"environment_id\": \"env_01...\",\n \"source\": \"direct\",\n \"title\": \"Customer research\",\n \"status\": \"idle\",\n \"metadata\": {},\n \"archived_at\": null,\n \"created_at\": \"2026-08-03T10:00:00Z\",\n \"updated_at\": \"2026-08-03T10:00:00Z\"\n}"
- }
- },
- "sessions/get": {
- "title": "Get Session",
- "operation": "Sessions",
- "method": "GET",
- "path": "/v1/sessions/{session_id}",
- "description": "Retrieve a session by ID, including current status and agent reference.",
- "groups": [
- {
- "title": "Path parameters",
- "fields": [
- {
- "name": "session_id",
- "type": "string",
- "required": true,
- "description": "The session ID (sess...)"
- }
+ { "name": "model", "type": "string", "required": true, "description": "Use gpt-image-2 on this endpoint." },
+ { "name": "prompt", "type": "string", "required": true, "description": "Text description of the requested edit." },
+ { "name": "image", "type": "file | file[]", "required": true, "description": "One or more source image files. Repeat the multipart field for multiple images." },
+ { "name": "mask", "type": "file", "required": false, "description": "Optional mask image for inpainting." },
+ { "name": "n", "type": "integer", "required": false, "description": "Number of edited images to return when supported." },
+ { "name": "size", "type": "string", "required": false, "description": "Output dimensions supported by the model." },
+ { "name": "quality", "type": "string", "required": false, "description": "Output quality supported by the model." },
+ { "name": "background", "type": "string", "required": false, "description": "Output background setting." },
+ { "name": "input_fidelity", "type": "string", "required": false, "description": "How closely supported models should preserve source-image details." },
+ { "name": "output_format", "type": "string", "required": false, "description": "Output encoding such as png, webp, or jpeg." },
+ { "name": "output_compression", "type": "integer", "required": false, "description": "Output compression level from 0 to 100." },
+ { "name": "user", "type": "string", "required": false, "description": "Provider-compatible end-user identifier." }
]
}
],
"examples": [
- {
- "label": "Python",
- "language": "python",
- "code": "from anthropic import Anthropic\n\nclient = Anthropic(\n api_key=\"sk-YOUR_KEY\",\n base_url=\"https://api.sandbase.ai\"\n)\n\nsession = client.beta.sessions.retrieve(\n session_id=\"sess_01abc...\"\n)\nprint(f\"Status: {session.status}\")"
- },
- {
- "label": "TypeScript",
- "language": "typescript",
- "code": "const session = await client.beta.sessions.retrieve('sess_01abc...');\nconsole.log(`Status: ${session.status}`);"
- },
{
"label": "cURL",
"language": "bash",
- "code": "curl https://api.sandbase.ai/v1/sessions/sess_01abc... \\\n -H \"Authorization: Bearer sk-YOUR_KEY\""
- }
- ],
- "response": {
- "status": "200 OK",
- "code": "{\n \"id\": \"sess_01abc...\",\n \"type\": \"session\",\n \"status\": \"idle\",\n \"title\": \"Research task\",\n \"agent_id\": \"agent_01HqR2k7...\",\n \"agent_version\": 1,\n \"agent_version_id\": \"av_01...\",\n \"agent\": { \"id\": \"agent_01HqR2k7...\", \"type\": \"agent\", \"version\": 1 },\n \"environment_id\": \"env_01abc...\",\n \"source\": \"direct\",\n \"metadata\": { \"user_id\": \"usr_123\", \"task\": \"research\" },\n \"archived_at\": null,\n \"created_at\": \"2026-05-29T10:00:00Z\",\n \"updated_at\": \"2026-05-29T10:05:00Z\"\n}"
- }
- },
- "sessions/list-events": {
- "title": "List Events",
- "operation": "Sessions",
- "method": "GET",
- "path": "/v1/sessions/{session_id}/events",
- "description": "List the event history of a session.",
- "groups": [
- {
- "title": "Path parameters",
- "fields": [
- {
- "name": "session_id",
- "type": "string",
- "required": true,
- "description": "The session ID (sess...)"
- }
- ]
+ "code": "curl -X POST https://api.sandbase.ai/v1/run \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"bytedance/seed-speech/tts/2.0\",\"text\":\"Hello, welcome to SandBase!\"}'"
},
- {
- "title": "Event object",
- "fields": [
- {
- "name": "id",
- "type": "string",
- "required": false,
- "description": "Event ID (sevt...)"
- },
- {
- "name": "type",
- "type": "string",
- "required": false,
- "description": "Event type (see below)"
- },
- {
- "name": "content",
- "type": "object[]",
- "required": false,
- "description": "Typed content blocks carried by the event."
- },
- {
- "name": "stop_reason",
- "type": "object | null",
- "required": false,
- "description": "Reason execution stopped, when present."
- },
- {
- "name": "model_used",
- "type": "string | null",
- "required": false,
- "description": "Model used to produce the event, when applicable."
- },
- {
- "name": "tokens_in",
- "type": "integer",
- "required": false,
- "description": "Input tokens for this event (0 when N/A)"
- },
- {
- "name": "tokens_out",
- "type": "integer",
- "required": false,
- "description": "Output tokens for this event (0 when N/A)"
- },
- {
- "name": "duration_ms",
- "type": "integer | null",
- "required": false,
- "description": "Processing duration in milliseconds, when measured."
- },
- {
- "name": "processed_at",
- "type": "string | null",
- "required": false,
- "description": "RFC 3339 processing timestamp, when available."
- },
- {
- "name": "created_at",
- "type": "string",
- "required": false,
- "description": "RFC 3339 timestamp"
- }
- ]
- }
- ],
- "examples": [
{
"label": "Python",
"language": "python",
- "code": "from anthropic import Anthropic\n\nclient = Anthropic(\n api_key=\"sk-YOUR_KEY\",\n base_url=\"https://api.sandbase.ai\"\n)\n\npage = client.beta.sessions.events.list(session_id=\"sess_01abc...\")\nfor event in page.data:\n print(f\"{event.type}: {event.id}\")"
- },
- {
- "label": "cURL",
- "language": "bash",
- "code": "curl \"https://api.sandbase.ai/v1/sessions/sess_01abc.../events\" \\\n -H \"Authorization: Bearer sk-YOUR_KEY\""
+ "code": "response = client.post(\"/v1/run\", json={\n \"model\": \"bytedance/seed-speech/tts/2.0\",\n \"text\": \"Hello, welcome to SandBase!\"\n})\nprint(response.json()[\"outputs\"][0][\"url\"])"
}
],
- "response": {
- "status": "200 OK",
- "code": "{\n \"data\": [\n {\n \"id\": \"sevt_01abc...\",\n \"type\": \"user.message\",\n \"content\": [{ \"type\": \"text\", \"text\": \"Where is my order #1234?\" }],\n \"stop_reason\": null,\n \"model_used\": null,\n \"tokens_in\": 0,\n \"tokens_out\": 0,\n \"duration_ms\": null,\n \"processed_at\": \"2026-05-29T10:00:00Z\",\n \"created_at\": \"2026-05-29T10:00:00Z\"\n },\n {\n \"id\": \"sevt_02def...\",\n \"type\": \"agent.message\",\n \"content\": [{ \"type\": \"text\", \"text\": \"Let me look up order #1234 for you.\" }],\n \"stop_reason\": null,\n \"model_used\": \"claude-sonnet-4\",\n \"tokens_in\": 1200,\n \"tokens_out\": 80,\n \"duration_ms\": 1840,\n \"processed_at\": \"2026-05-29T10:00:02Z\",\n \"created_at\": \"2026-05-29T10:00:02Z\"\n }\n ]\n}"
- }
- },
- "sessions/list": {
- "title": "List Sessions",
- "operation": "Sessions",
- "method": "GET",
- "path": "/v1/sessions",
- "description": "List Agent Sessions for your organization with filtering and cursor pagination.",
- "groups": [
- {
- "title": "Query parameters",
- "fields": [
- {
- "name": "agent_id",
- "type": "string",
- "required": false,
- "description": "Filter Sessions created with this Agent ID"
- },
- {
- "name": "agent_version",
- "type": "integer",
- "required": false,
- "description": "Filter by Agent version. Only applies when agent_id is also set."
- },
- {
- "name": "include_archived",
- "type": "boolean",
- "required": false,
- "description": "Include archived Sessions. Default false."
- },
- {
- "name": "created_at[gte]",
- "type": "string",
- "required": false,
- "description": "Only Sessions created at or after this RFC 3339 time."
- },
- {
- "name": "created_at[lte]",
- "type": "string",
- "required": false,
- "description": "Only Sessions created at or before this RFC 3339 time."
- },
- {
- "name": "limit",
- "type": "integer",
- "required": false,
- "description": "Page size. Default 20, max 100."
- },
- {
- "name": "page",
- "type": "string",
- "required": false,
- "description": "Opaque cursor from a previous response's next_page field."
- }
+ "response": { "status": "200 OK", "code": "{\n \"id\": \"run_01...\",\n \"status\": \"completed\",\n \"outputs\": [{ \"url\": \"https://media.sandbase.ai/audio/output.mp3\" }]\n}" }
+ },
+ "assets/upload": {
+ "title": "Upload Media",
+ "operation": "Assets",
+ "method": "POST",
+ "path": "/v1/upload",
+ "description": "Upload an image, audio, or video file and receive a stored URL for a supported model input.",
+ "groups": [
+ {
+ "title": "Multipart form",
+ "fields": [
+ { "name": "file", "type": "binary", "required": true, "description": "Image up to 20 MB, audio up to 50 MB, or video up to 500 MB. Supported formats are listed below." },
+ { "name": "type", "type": "image | audio | video", "required": false, "description": "Optional category hint. MIME detection from the file still controls validation." }
+ ]
+ },
+ {
+ "title": "Supported MIME types",
+ "fields": [
+ { "name": "Image", "type": "string", "required": false, "description": "image/jpeg, image/png, image/webp, image/gif — maximum 20 MB." },
+ { "name": "Audio", "type": "string", "required": false, "description": "audio/mpeg, audio/mp4, audio/wav, audio/x-wav, audio/ogg, audio/aac, audio/flac — maximum 50 MB." },
+ { "name": "Video", "type": "string", "required": false, "description": "video/mp4, video/quicktime, video/webm — maximum 500 MB." }
]
}
],
"examples": [
- {
- "label": "Python",
- "language": "python",
- "code": "from anthropic import Anthropic\n\nclient = Anthropic(\n api_key=\"sk-YOUR_KEY\",\n base_url=\"https://api.sandbase.ai\"\n)\n\npage = client.beta.sessions.list(agent_id=\"agent_01HqR2k7...\", limit=20)\nfor session in page.data:\n print(f\"{session.id} — {session.status} — {session.title}\")"
- },
{
"label": "cURL",
"language": "bash",
- "code": "curl \"https://api.sandbase.ai/v1/sessions?agent_id=agent_01HqR2k7...&created_at%5Bgte%5D=2026-05-01T00%3A00%3A00Z&limit=20\" \\\n -H \"Authorization: Bearer sk-YOUR_KEY\""
+ "code": "curl -X POST https://api.sandbase.ai/v1/upload \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -F \"file=@reference.mp4\" \\\n -F \"type=video\""
}
],
"response": {
"status": "200 OK",
- "code": "{\n \"data\": [\n {\n \"id\": \"sess_01abc...\",\n \"type\": \"session\",\n \"status\": \"idle\",\n \"title\": \"Research task\",\n \"agent_id\": \"agent_01HqR2k7...\",\n \"agent_version\": 1,\n \"environment_id\": \"env_01abc...\",\n \"metadata\": {},\n \"archived_at\": null,\n \"created_at\": \"2026-05-29T10:00:00Z\",\n \"updated_at\": \"2026-05-29T10:02:00Z\"\n }\n ],\n \"next_page\": \"page_eyJjIjoiMjAyNi0wNS0yOVQxMDowMDowMFoiLCJpIjoic2Vzc18wMSJ9\"\n}"
- }
+ "code": "{\n \"url\": \"https://media.sandbase.ai/uploads/2026/08/550e8400-e29b-41d4-a716-446655440000.mp4\",\n \"filename\": \"reference.mp4\",\n \"size\": 1048576,\n \"type\": \"video/mp4\",\n \"content_type\": \"video/mp4\"\n}"
+ },
+ "notes": [
+ { "title": "MIME detection", "description": "The server detects the file type from its bytes and filename. The optional type field does not force a MIME type." }
+ ]
},
- "sessions/send-events": {
- "title": "Send Session Events",
- "operation": "Sessions",
+ "assets/create": {
+ "title": "Register Asset",
+ "operation": "Assets",
"method": "POST",
- "path": "/v1/sessions/{session_id}/events",
- "description": "Send a message or interrupt event to a Session.",
+ "path": "/v1/assets",
+ "description": "Register a publicly accessible media URL and receive a persistent asset:// reference.",
"groups": [
{
- "title": "Path parameters",
+ "title": "Request body",
"fields": [
- {
- "name": "session_id",
- "type": "string",
- "required": true,
- "description": "Identifier for session id."
- }
+ { "name": "url", "type": "string", "required": true, "description": "Non-empty source URL forwarded to the Asset provider. The provider must be able to fetch it." },
+ { "name": "asset_type", "type": "Image | Video | Audio", "required": true, "description": "Case-sensitive asset type." },
+ { "name": "name", "type": "string", "required": false, "description": "Optional display name." }
]
}
],
@@ -1191,127 +157,117 @@ export const generatedApiReferenceSpecs: Record = {
{
"label": "cURL",
"language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/sessions/sess_01.../events \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"events\": [{\n \"type\": \"user.message\",\n \"content\": [{\"type\": \"text\", \"text\": \"Summarize this account.\"}]\n }]\n }'"
+ "code": "curl -X POST https://api.sandbase.ai/v1/assets \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"url\": \"https://example.com/tea-ad.mp4\",\n \"asset_type\": \"Video\",\n \"name\": \"Tea ad reference video\"\n }'"
}
- ]
+ ],
+ "response": {
+ "status": "200 OK",
+ "code": "{\n \"id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n \"asset_url\": \"asset://asset-20260710150403-hx4hf\",\n \"asset_type\": \"Video\",\n \"name\": \"Tea ad reference video\",\n \"created_at\": \"2026-07-10T15:04:03Z\"\n}"
+ }
},
- "sessions/stream": {
- "title": "Stream Events",
- "operation": "Sessions",
+ "assets/get": {
+ "title": "Get Asset",
+ "operation": "Assets",
"method": "GET",
- "path": "/v1/sessions/{session_id}/events/stream",
- "description": "Replay persisted Session Events, then follow newly persisted Events via Server-Sent Events (SSE).",
+ "path": "/v1/assets/{asset_id}",
+ "description": "Retrieve Asset metadata and a temporary signed download URL.",
"groups": [
{
"title": "Path parameters",
"fields": [
- {
- "name": "session_id",
- "type": "string",
- "required": true,
- "description": "The session ID (sess...)"
- }
+ { "name": "asset_id", "type": "string", "required": true, "description": "External Asset ID, without the asset:// prefix." }
]
}
],
"examples": [
{
- "label": "TEXT",
- "language": "text",
- "code": "session.status_running\n -> agent.tool_use -> agent.tool_result (zero or more)\n -> agent.message (zero or more)\nsession.status_idle (stop_reason: end_turn)"
- },
- {
- "label": "Python",
- "language": "python",
- "code": "from anthropic import Anthropic\n\nclient = Anthropic(\n api_key=\"sk-YOUR_KEY\",\n base_url=\"https://api.sandbase.ai\"\n)\n\nfor event in client.beta.sessions.events.stream(session_id=\"sess_01abc...\"):\n if event.type == \"agent.message\":\n for block in event.content:\n print(block.text, end=\"\")\n elif event.type == \"session.status_idle\":\n print(f\"\\n[done: {event.stop_reason.type}]\")\n break"
- },
- {
- "label": "Raw SSE",
- "language": "python",
- "code": "import json, httpx\n\nwith httpx.stream(\"GET\",\n \"https://api.sandbase.ai/v1/sessions/sess_01abc.../events/stream\",\n headers={\"Authorization\": \"Bearer sk-YOUR_KEY\"}\n) as response:\n for line in response.iter_lines():\n if line.startswith(\"data: \"):\n event = json.loads(line[6:])\n if \"type\" in event:\n print(event[\"type\"])"
+ "label": "cURL",
+ "language": "bash",
+ "code": "curl https://api.sandbase.ai/v1/assets/asset-20260710150403-hx4hf \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
}
],
"response": {
"status": "200 OK",
- "code": "id: sevt_01...\ndata: {\"id\":\"sevt_01...\",\"type\":\"session.status_running\",\"processed_at\":\"2026-05-29T10:00:00Z\",\"created_at\":\"2026-05-29T10:00:00Z\"}\n\n: heartbeat\n\nid: sevt_02...\ndata: {\"id\":\"sevt_02...\",\"type\":\"agent.tool_use\",\"content\":{\"name\":\"web_search\",\"input\":{\"query\":\"order 1234\"}},\"processed_at\":\"2026-05-29T10:00:01Z\",\"created_at\":\"2026-05-29T10:00:01Z\"}\n\nid: sevt_03...\ndata: {\"id\":\"sevt_03...\",\"type\":\"agent.message\",\"content\":[{\"type\":\"text\",\"text\":\"Your order #1234 shipped yesterday.\"}],\"processed_at\":\"2026-05-29T10:00:03Z\",\"created_at\":\"2026-05-29T10:00:03Z\"}"
- }
+ "code": "{\n \"id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n \"external_id\": \"asset-20260710150403-hx4hf\",\n \"asset_url\": \"asset://asset-20260710150403-hx4hf\",\n \"status\": \"Active\",\n \"asset_type\": \"Video\",\n \"download_url\": \"https://cdn.example.com/signed-url\",\n \"created_at\": \"2026-07-10T15:04:03Z\"\n}"
+ },
+ "notes": [
+ { "title": "Provider-managed URL", "description": "The asset:// value is the reusable Asset reference. download_url is provider-managed and can be empty; query this endpoint again for its current value." }
+ ]
},
- "skills/create": {
- "title": "Create Skill",
- "operation": "Skills",
+ "models/embedding": {
+ "title": "Embeddings",
+ "operation": "Models",
"method": "POST",
- "path": "/v1/skills",
- "description": "Create a reusable Skill resource from an uploaded bundle or Git source.",
+ "path": "/v1/embeddings",
+ "description": "OpenAI-compatible vector embeddings API. Input forms, dimensions, encoding, and additional fields remain subject to the selected model and provider.",
"groups": [
{
"title": "Request body",
"fields": [
{
- "name": "name",
+ "name": "model",
"type": "string",
"required": true,
- "description": "Display name, up to 100 characters."
- },
- {
- "name": "description",
- "type": "string",
- "required": false,
- "description": "Description, up to 1,000 characters."
+ "description": "Embedding model ID, for example alibaba/text-embedding-v4"
},
{
- "name": "categories",
- "type": "string[]",
- "required": false,
- "description": "Category labels."
+ "name": "input",
+ "type": "string | string[] | integer[] | integer[][]",
+ "required": true,
+ "description": "Text, token IDs, or a batch of texts or token-ID arrays to embed"
},
{
- "name": "skill_file_url",
- "type": "string",
+ "name": "dimensions",
+ "type": "integer",
"required": false,
- "description": "URL returned by /v1/skills/files. Required when git_url is absent."
+ "description": "Output vector dimension when supported by the selected model; there is no universal dimension list"
},
{
- "name": "git_url",
+ "name": "encoding_format",
"type": "string",
"required": false,
- "description": "Git source URL. Required when skill_file_url is absent."
- },
- {
- "name": "preview_image_urls",
- "type": "string[]",
- "required": false,
- "description": "Preview image URLs."
+ "description": "Embedding encoding format. Provider support determines whether float or base64 is available"
}
]
}
],
"examples": [
{
- "label": "cURL",
- "language": "bash",
- "code": "curl -X POST https://api.sandbase.ai/v1/skills \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"release-notes\",\n \"description\": \"Draft concise release notes\",\n \"categories\": [\"writing\"],\n \"skill_file_url\": \"https://media.sandbase.ai/_private/.../release-notes.zip\"\n }'"
+ "label": "HTTP",
+ "language": "http",
+ "code": "POST https://api.sandbase.ai/v1/embeddings\nAuthorization: Bearer sk-your-api-key\nContent-Type: application/json\n\n{\"model\":\"alibaba/text-embedding-v4\",\"input\":\"SandBase provides a unified API for AI models, agents, and developer tools.\",\"dimensions\":1024,\"encoding_format\":\"float\"}"
+ },
+ {
+ "label": "Python",
+ "language": "python",
+ "code": "from openai import OpenAI\n\nclient = OpenAI(\n api_key=\"sk-YOUR_KEY\",\n base_url=\"https://api.sandbase.ai/v1\",\n)\n\nresponse = client.embeddings.create(\n model=\"alibaba/text-embedding-v4\",\n input=\"SandBase provides a unified API for AI models, agents, and developer tools.\",\n dimensions=1024,\n encoding_format=\"float\",\n)\n\nembedding = response.data[0].embedding\nprint(f\"Dimensions: {len(embedding)}\")"
+ },
+ {
+ "label": "TypeScript",
+ "language": "typescript",
+ "code": "import OpenAI from 'openai';\n\nconst client = new OpenAI({\n apiKey: 'sk-YOUR_KEY',\n baseURL: 'https://api.sandbase.ai/v1',\n});\n\nconst response = await client.embeddings.create({\n model: 'alibaba/text-embedding-v4',\n input: 'SandBase provides a unified API for AI models, agents, and developer tools.',\n dimensions: 1024,\n encoding_format: 'float',\n});\n\nconsole.log(response.data[0].embedding.length);"
}
],
"response": {
- "status": "201 Created",
- "code": "{\n \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"name\": \"acme/release-notes\",\n \"display_name\": \"release-notes\",\n \"vendor_slug\": \"acme\",\n \"plugin_slug\": \"release-notes\",\n \"icon_url\": \"\",\n \"preview_urls\": [],\n \"created_at\": \"2026-08-03T10:00:00Z\"\n}"
+ "status": "200 OK",
+ "code": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"embedding\",\n \"index\": 0,\n \"embedding\": [0.0123, -0.0045, 0.0312]\n }\n ],\n \"model\": \"alibaba/text-embedding-v4\",\n \"usage\": {\n \"prompt_tokens\": 16,\n \"total_tokens\": 16\n }\n}"
}
},
- "skills/delete": {
- "title": "Delete Skill",
- "operation": "Skills",
- "method": "DELETE",
- "path": "/v1/skills/{skill_id}",
- "description": "Soft-delete a Skill owned by the current organization.",
+ "models/get": {
+ "title": "Get Model",
+ "operation": "Models",
+ "method": "GET",
+ "path": "/v1/models/{id_or_name}",
+ "description": "Get model details by UUID or full logical name. Name lookup returns enabled catalog models; UUID lookup can return a disabled record with enabled set to false. Protocol-ingress-only models are never returned.",
"groups": [
{
"title": "Path parameters",
"fields": [
{
- "name": "skill_id",
+ "name": "id_or_name",
"type": "string",
"required": true,
- "description": "Skill UUID."
+ "description": "Model UUID or full logical model name, for example openai/gpt-4o. Names may contain slashes."
}
]
}
@@ -1320,29 +276,65 @@ export const generatedApiReferenceSpecs: Record = {
{
"label": "cURL",
"language": "bash",
- "code": "curl -X DELETE https://api.sandbase.ai/v1/skills/550e8400-e29b-41d4-a716-446655440000 \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
+ "code": "curl \"https://api.sandbase.ai/v1/models/openai/gpt-4o\" \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
}
],
"response": {
"status": "200 OK",
- "code": "{\n \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"deleted\": true\n}"
+ "code": "{\n \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"name\": \"openai/gpt-4o\",\n \"display_name\": \"GPT-4o\",\n \"vendor\": \"OpenAI\",\n \"description\": \"Most capable GPT-4 model with vision...\",\n \"type\": \"llm\",\n \"capability_tags\": [\"chat\", \"streaming\", \"vision\", \"tools\", \"json_mode\", \"cache\"],\n \"skills\": [\"chat\", \"streaming\", \"vision\", \"tools\", \"json_mode\", \"cache\"],\n \"unified_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"prompt\": {\n \"type\": \"string\"\n }\n }\n },\n \"execution_mode\": \"chat\",\n \"supported_modes\": [\"chat\"],\n \"run_count\": 12500,\n \"sort_order\": 10,\n \"published_at\": \"2026-07-07T00:00:00Z\",\n \"vendor_slug\": \"openai\",\n \"model_slug\": \"gpt-4o\",\n \"enabled\": true,\n \"featured\": true,\n \"created_at\": \"2026-07-07T00:00:00Z\",\n \"model_card\": {\n \"base_price\": \"0.000000\",\n \"prompt_token_price\": \"0.0000025\",\n \"completion_token_price\": \"0.000010\",\n \"cache_read_multiplier\": \"0.5\",\n \"cache_write_multiplier\": \"1.0\",\n \"cache_write_1h_multiplier\": null,\n \"reasoning_price\": null,\n \"price_formula\": \"$input_tokens * 0.0000025 + $output_tokens * 0.000010\",\n \"markup_ratio\": \"1.0000\",\n \"readme\": \"Model usage notes...\",\n \"cover_url\": \"https://...\"\n },\n \"examples\": []\n}"
}
},
- "skills/get": {
- "title": "Get Skill",
- "operation": "Skills",
- "method": "GET",
- "path": "/v1/skills/{skill_id}",
- "description": "Retrieve one Skill and its source metadata.",
+ "models/image": {
+ "title": "Image Generation",
+ "operation": "Models",
+ "method": "POST",
+ "path": "/v1/run",
+ "description": "Generate images using AI models like FLUX, Stable Diffusion, DALL-E, and more.",
"groups": [
{
- "title": "Path parameters",
+ "title": "Request body",
"fields": [
{
- "name": "skill_id",
+ "name": "model",
+ "type": "string",
+ "required": true,
+ "description": "Current model identifier, for example bfl/flux-1/schnell."
+ },
+ {
+ "name": "prompt",
"type": "string",
"required": true,
- "description": "Skill UUID."
+ "description": "Text description of the image"
+ },
+ {
+ "name": "aspect_ratio",
+ "type": "string",
+ "required": false,
+ "description": "Output aspect ratio. Flux 1 Schnell defaults to 16:9."
+ },
+ {
+ "name": "guidance_scale",
+ "type": "number",
+ "required": false,
+ "description": "Prompt guidance strength from 1 to 20."
+ },
+ {
+ "name": "output_format",
+ "type": "jpeg | png",
+ "required": false,
+ "description": "Output image format. Flux 1 Schnell defaults to jpeg."
+ },
+ {
+ "name": "mode",
+ "type": "string",
+ "required": false,
+ "description": "Set to async only when the selected model supports asynchronous execution"
+ },
+ {
+ "name": "webhook_url",
+ "type": "string",
+ "required": false,
+ "description": "Public HTTPS callback URL for this async task only"
}
]
}
@@ -1351,35 +343,49 @@ export const generatedApiReferenceSpecs: Record = {
{
"label": "cURL",
"language": "bash",
- "code": "curl https://api.sandbase.ai/v1/skills/550e8400-e29b-41d4-a716-446655440000 \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
+ "code": "curl -X POST https://api.sandbase.ai/v1/run \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"bfl/flux-1/schnell\",\"prompt\":\"A futuristic city at sunset\",\"aspect_ratio\":\"1:1\"}'"
+ },
+ {
+ "label": "Python",
+ "language": "python",
+ "code": "response = client.post(\"/v1/run\", json={\n \"model\": \"bfl/flux-1/schnell\",\n \"prompt\": \"A futuristic city at sunset, cyberpunk style\",\n \"aspect_ratio\": \"1:1\"\n})\nprint(response.json()[\"outputs\"][0][\"url\"])"
}
],
- "response": {
- "status": "200 OK",
- "code": "{\n \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"name\": \"acme/release-notes\",\n \"display_name\": \"release-notes\",\n \"vendor_slug\": \"acme\",\n \"plugin_slug\": \"release-notes\",\n \"description\": \"Draft concise release notes\",\n \"categories\": [\"writing\"],\n \"icon_url\": \"\",\n \"preview_urls\": [],\n \"skill_file_url\": \"https://media.sandbase.ai/_private/.../release-notes.zip\",\n \"git_url\": \"\",\n \"created_at\": \"2026-08-03T10:00:00Z\",\n \"updated_at\": \"2026-08-03T10:00:00Z\"\n}"
- }
+ "response": { "status": "200 OK", "code": "{\n \"id\": \"run_01...\",\n \"status\": \"completed\",\n \"outputs\": [{ \"url\": \"https://media.sandbase.ai/images/output.png\" }]\n}" }
},
- "skills/list": {
- "title": "List Skills",
- "operation": "Skills",
+ "models/list": {
+ "title": "List Models",
+ "operation": "Models",
"method": "GET",
- "path": "/v1/skills",
- "description": "List Skills owned by the current organization.",
+ "path": "/v1/models",
+ "description": "List enabled logical models in the OpenAI-compatible model-list format.",
"groups": [
{
"title": "Query parameters",
"fields": [
{
- "name": "page",
- "type": "integer",
+ "name": "q",
+ "type": "string",
"required": false,
- "description": "Page number. Default 1. Values below 1 are normalized to 1."
+ "description": "Fuzzy search over model name, display name, vendor, and description."
},
{
- "name": "page_size",
- "type": "integer",
+ "name": "vendor",
+ "type": "string",
+ "required": false,
+ "description": "Filter by vendor, for example OpenAI or Anthropic."
+ },
+ {
+ "name": "type",
+ "type": "string",
+ "required": false,
+ "description": "Filter by model type. Defaults to llm."
+ },
+ {
+ "name": "order",
+ "type": "string",
"required": false,
- "description": "Items per page. The server applies its configured maximum."
+ "description": "Sort order: sort_order, most-popular, newest, or name."
}
]
}
@@ -1388,64 +394,53 @@ export const generatedApiReferenceSpecs: Record = {
{
"label": "cURL",
"language": "bash",
- "code": "curl \"https://api.sandbase.ai/v1/skills?page=1&page_size=20\" \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
+ "code": "curl \"https://api.sandbase.ai/v1/models\" \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\""
}
],
"response": {
"status": "200 OK",
- "code": "{\n \"data\": [\n {\n \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"name\": \"acme/release-notes\",\n \"display_name\": \"release-notes\",\n \"vendor_slug\": \"acme\",\n \"plugin_slug\": \"release-notes\",\n \"description\": \"Draft concise release notes\",\n \"icon_url\": \"\",\n \"preview_urls\": [],\n \"created_at\": \"2026-08-03T10:00:00Z\",\n \"updated_at\": \"2026-08-03T10:00:00Z\"\n }\n ],\n \"total\": 1,\n \"page\": 1,\n \"page_size\": 20\n}"
+ "code": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"openai/gpt-4o\",\n \"object\": \"model\",\n \"created\": 1783353600,\n \"owned_by\": \"openai\"\n }\n ]\n}"
}
},
- "skills/update": {
- "title": "Update Skill",
- "operation": "Skills",
- "method": "PUT",
- "path": "/v1/skills/{skill_id}",
- "description": "Update Skill metadata or replace its uploaded bundle.",
+ "models/video": {
+ "title": "Video Generation",
+ "operation": "Models",
+ "method": "POST",
+ "path": "/v1/run",
+ "description": "Generate videos using AI models. Video generation is asynchronous — you submit a request and poll for results.",
"groups": [
- {
- "title": "Path parameters",
- "fields": [
- {
- "name": "skill_id",
- "type": "string",
- "required": true,
- "description": "Skill UUID."
- }
- ]
- },
{
"title": "Request body",
"fields": [
{
- "name": "name",
+ "name": "model",
"type": "string",
- "required": false,
- "description": "Replacement display name, up to 100 characters. An omitted value becomes empty."
+ "required": true,
+ "description": "Current model identifier, for example kwaivgi/kling-video/3.0/turbo/standard/text-to-video."
},
{
- "name": "description",
+ "name": "prompt",
"type": "string",
- "required": false,
- "description": "Replacement description, up to 1,000 characters. An omitted value becomes empty."
+ "required": true,
+ "description": "Text description of the video"
},
{
- "name": "categories",
- "type": "string[]",
+ "name": "duration",
+ "type": "number",
"required": false,
- "description": "Replacement category list. An omitted value becomes empty."
+ "description": "Video duration in seconds"
},
{
- "name": "skill_file_url",
+ "name": "image_url",
"type": "string",
"required": false,
- "description": "Replacement uploaded Skill bundle URL."
+ "description": "Reference image for image-to-video"
},
{
- "name": "preview_image_urls",
- "type": "string[]",
+ "name": "webhook_url",
+ "type": "string",
"required": false,
- "description": "Replacement preview image list."
+ "description": "Public HTTPS callback URL for this async task only"
}
]
}
@@ -1454,13 +449,15 @@ export const generatedApiReferenceSpecs: Record = {
{
"label": "cURL",
"language": "bash",
- "code": "curl -X PUT https://api.sandbase.ai/v1/skills/550e8400-e29b-41d4-a716-446655440000 \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"release-notes\",\n \"description\": \"Draft release notes with a concise changelog\",\n \"categories\": [\"writing\", \"product\"]\n }'"
+ "code": "curl -X POST https://api.sandbase.ai/v1/run \\\n -H \"Authorization: Bearer $SANDBASE_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"kwaivgi/kling-video/3.0/turbo/standard/text-to-video\",\"prompt\":\"A cat playing piano\",\"duration\":5}'"
+ },
+ {
+ "label": "Python",
+ "language": "python",
+ "code": "# 1. Submit generation request\nresponse = client.post(\"/v1/run\", json={\n \"model\": \"kwaivgi/kling-video/3.0/turbo/standard/text-to-video\",\n \"prompt\": \"A cat playing piano\",\n \"duration\": 5,\n \"webhook_url\": \"https://hooks.example.com/sandbase/task?token=opaque-token\"\n})\ngeneration_id = response.json()[\"id\"]\n\n# 2. SandBase posts the terminal event to webhook_url.\n# You may still poll the existing result endpoint as a fallback.\nimport time\nwhile True:\n result = client.get(f\"/v1/run/{generation_id}\").json()\n if result[\"status\"] in (\"completed\", \"failed\", \"timeout\"):\n print(result)\n break\n time.sleep(5)"
}
],
- "response": {
- "status": "200 OK",
- "code": "{\n \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"name\": \"acme/release-notes\",\n \"display_name\": \"release-notes\",\n \"vendor_slug\": \"acme\",\n \"plugin_slug\": \"release-notes\",\n \"description\": \"Draft release notes with a concise changelog\",\n \"categories\": [\"writing\", \"product\"],\n \"icon_url\": \"\",\n \"preview_urls\": [],\n \"skill_file_url\": \"https://media.sandbase.ai/_private/.../release-notes.zip\",\n \"git_url\": \"\",\n \"created_at\": \"2026-08-03T10:00:00Z\",\n \"updated_at\": \"2026-08-03T10:05:00Z\"\n}"
- }
+ "response": { "status": "202 Accepted", "code": "{\n \"id\": \"run_01...\",\n \"status\": \"pending\",\n \"model\": \"kwaivgi/kling-video/3.0/turbo/standard/text-to-video\"\n}" }
},
"images/generations": {
"title": "Generate Image",
diff --git a/api-reference/deployments/archive.md b/_archived/api-reference/deployments/archive.md
similarity index 100%
rename from api-reference/deployments/archive.md
rename to _archived/api-reference/deployments/archive.md
diff --git a/api-reference/deployments/create.md b/_archived/api-reference/deployments/create.md
similarity index 100%
rename from api-reference/deployments/create.md
rename to _archived/api-reference/deployments/create.md
diff --git a/api-reference/deployments/delete.md b/_archived/api-reference/deployments/delete.md
similarity index 100%
rename from api-reference/deployments/delete.md
rename to _archived/api-reference/deployments/delete.md
diff --git a/api-reference/deployments/get-global-run.md b/_archived/api-reference/deployments/get-global-run.md
similarity index 100%
rename from api-reference/deployments/get-global-run.md
rename to _archived/api-reference/deployments/get-global-run.md
diff --git a/api-reference/deployments/get-run.md b/_archived/api-reference/deployments/get-run.md
similarity index 100%
rename from api-reference/deployments/get-run.md
rename to _archived/api-reference/deployments/get-run.md
diff --git a/api-reference/deployments/get.md b/_archived/api-reference/deployments/get.md
similarity index 100%
rename from api-reference/deployments/get.md
rename to _archived/api-reference/deployments/get.md
diff --git a/api-reference/deployments/list-all-runs.md b/_archived/api-reference/deployments/list-all-runs.md
similarity index 100%
rename from api-reference/deployments/list-all-runs.md
rename to _archived/api-reference/deployments/list-all-runs.md
diff --git a/api-reference/deployments/list-runs.md b/_archived/api-reference/deployments/list-runs.md
similarity index 100%
rename from api-reference/deployments/list-runs.md
rename to _archived/api-reference/deployments/list-runs.md
diff --git a/api-reference/deployments/list.md b/_archived/api-reference/deployments/list.md
similarity index 100%
rename from api-reference/deployments/list.md
rename to _archived/api-reference/deployments/list.md
diff --git a/api-reference/deployments/pause.md b/_archived/api-reference/deployments/pause.md
similarity index 100%
rename from api-reference/deployments/pause.md
rename to _archived/api-reference/deployments/pause.md
diff --git a/api-reference/deployments/run.md b/_archived/api-reference/deployments/run.md
similarity index 100%
rename from api-reference/deployments/run.md
rename to _archived/api-reference/deployments/run.md
diff --git a/api-reference/deployments/test-feishu-notification.md b/_archived/api-reference/deployments/test-feishu-notification.md
similarity index 100%
rename from api-reference/deployments/test-feishu-notification.md
rename to _archived/api-reference/deployments/test-feishu-notification.md
diff --git a/api-reference/deployments/unpause.md b/_archived/api-reference/deployments/unpause.md
similarity index 100%
rename from api-reference/deployments/unpause.md
rename to _archived/api-reference/deployments/unpause.md
diff --git a/api-reference/deployments/update.md b/_archived/api-reference/deployments/update.md
similarity index 100%
rename from api-reference/deployments/update.md
rename to _archived/api-reference/deployments/update.md
diff --git a/api-reference/embeds/create.md b/_archived/api-reference/embeds/create.md
similarity index 100%
rename from api-reference/embeds/create.md
rename to _archived/api-reference/embeds/create.md
diff --git a/api-reference/embeds/delete.md b/_archived/api-reference/embeds/delete.md
similarity index 100%
rename from api-reference/embeds/delete.md
rename to _archived/api-reference/embeds/delete.md
diff --git a/api-reference/embeds/get.md b/_archived/api-reference/embeds/get.md
similarity index 100%
rename from api-reference/embeds/get.md
rename to _archived/api-reference/embeds/get.md
diff --git a/api-reference/embeds/index.md b/_archived/api-reference/embeds/index.md
similarity index 100%
rename from api-reference/embeds/index.md
rename to _archived/api-reference/embeds/index.md
diff --git a/api-reference/embeds/list.md b/_archived/api-reference/embeds/list.md
similarity index 100%
rename from api-reference/embeds/list.md
rename to _archived/api-reference/embeds/list.md
diff --git a/api-reference/embeds/update.md b/_archived/api-reference/embeds/update.md
similarity index 100%
rename from api-reference/embeds/update.md
rename to _archived/api-reference/embeds/update.md
diff --git a/api-reference/embeds/usage.md b/_archived/api-reference/embeds/usage.md
similarity index 100%
rename from api-reference/embeds/usage.md
rename to _archived/api-reference/embeds/usage.md
diff --git a/api-reference/endpoints/acp.md b/_archived/api-reference/endpoints/acp.md
similarity index 100%
rename from api-reference/endpoints/acp.md
rename to _archived/api-reference/endpoints/acp.md
diff --git a/api-reference/endpoints/mcp.md b/_archived/api-reference/endpoints/mcp.md
similarity index 100%
rename from api-reference/endpoints/mcp.md
rename to _archived/api-reference/endpoints/mcp.md
diff --git a/api-reference/environments/index.md b/_archived/api-reference/environments/index.md
similarity index 100%
rename from api-reference/environments/index.md
rename to _archived/api-reference/environments/index.md
diff --git a/guides/site-agent-integration.md b/_archived/guides/site-agent-integration.md
similarity index 100%
rename from guides/site-agent-integration.md
rename to _archived/guides/site-agent-integration.md
diff --git a/use-cases/index.md b/_archived/use-cases/index.md
similarity index 100%
rename from use-cases/index.md
rename to _archived/use-cases/index.md
diff --git a/use-cases/site-agent-copilot.md b/_archived/use-cases/site-agent-copilot.md
similarity index 100%
rename from use-cases/site-agent-copilot.md
rename to _archived/use-cases/site-agent-copilot.md
diff --git a/agents/agent-api.md b/agents/agent-api.md
index 757babc3..4ef23cea 100644
--- a/agents/agent-api.md
+++ b/agents/agent-api.md
@@ -1,51 +1,95 @@
---
title: Define an Agent
-description: Configure a SandBase Agent with a model, instructions, APIs, Skills, and API credentials.
+description: Create and update a versioned Agent with the Agents API, including fields, limits, versions, and archive.
---
# Define an Agent
-An Agent starts as a configuration.
+Create an Agent with `POST /v1/agents`. The API follows the OpenAI Agents protocol, so you can use plain HTTP or the OpenAI SDK's `client.beta.agents` with `base_url="https://api.sandbase.ai/v1"`. The `OpenAI-Beta` header is optional; if you send it, it must be `agents=v1`. See [OpenAI compatibility](/agents/openai-compatibility) for what the SDK supports.
-You define what it should do, which Model it should use, and which capabilities it can access.
+## Create an Agent
-## Minimal Agent shape
+```bash
+curl -X POST https://api.sandbase.ai/v1/agents \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: create-research-agent-1" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Research the question, compare at least two sources, and cite them.",
+ "metadata": {"team": "research"}
+ }'
+```
+
+```python
+# Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+import os
+from openai import OpenAI
-```yaml
-name: customer-research
-model: deepseek/deepseek-v4-flash
-instructions: |
- Research a company and return a concise customer brief.
-tools:
- - web_search
- - page_scraping
-skills:
- - sandbase/research-report
-credentials:
- - SERPER_API_KEY
+client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+agent = client.beta.agents.create(
+ model="deepseek/deepseek-v4-flash",
+ name="Research assistant",
+ instructions="Research the question, compare at least two sources, and cite them.",
+ metadata={"team": "research"},
+)
+print(agent.id, agent.version) # agt_..., 1
```
-The exact UI can change, but the idea stays the same:
+The response is `201 Created` with the Agent: an `agt_` ID, `version: 1`, `row_version`, `status: "active"`, and `projection_status`. A new version is prepared for execution in the background; start Sessions once `projection_status` is `ready`.
+
+`Idempotency-Key` is optional. When you omit it the service generates one, so a retried request can create a second Agent. Send your own stable key when you retry.
+
+## Fields
+
+The body is strict JSON (up to 1 MiB). Unknown fields return 400.
-- **Model** decides how the Agent thinks.
-- **Instructions** tell it what outcome to produce.
-- **APIs** connect it to real-world data and actions.
-- **Skills** give it reusable know-how.
-- **Credentials** keep API keys and other secrets outside the prompt. SandBase supplies them to the runtime as environment variables.
+| Field | Required | Limits and values |
+|---|---|---|
+| `model` | Yes | Model ID, up to 200 characters |
+| `name` | No | Up to 128 characters |
+| `instructions` | No | Up to 256 KiB |
+| `description` | No | Up to 4096 characters |
+| `runtime_profile` | No | `codex` (default), `claude`, or `mcode` |
+| `skills` | No | `[{"skill_id": "skl_…", "version": null}]`; see [Skills](/agents/mcp-tools#skills) |
+| `mcp_connections` | No | Up to 32 references; see [MCP connections](/agents/mcp-tools#mcp-connections) |
+| `tools` | No | Up to 64 `function` tools and at most one `web_search` tool |
+| `text` | No | `format.type` `text` or `json_schema` (object schema); `verbosity` `low`, `medium`, or `high` |
+| `reasoning` | No | `effort` `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`; `summary` `concise`, `detailed`, or `auto` |
+| `service_tier` | No | `auto` (default), `default`, `flex`, `priority`, or `fast` |
+| `metadata` | No | Up to 16 string pairs; keys up to 64 and values up to 512 characters |
+| `note` | No | Change note stored on the version |
-## Test before publishing
+## Current limits
-Run the Agent from the Console first.
+- `web_search` must use `"mode": "disabled"` for Sessions, Services, and Schedules to start.
+- `function` tools can be saved, but Runs that need the client to return a tool result (`requires_action`) are not supported yet. A Session in that state returns `502 executor_contract_error`. Give the Agent external capabilities through [MCP connections](/agents/mcp-tools#mcp-connections) instead.
+- `multi_agent` must be omitted or `null`.
-Each test creates a Session. Use its event history to inspect accepted input, Agent responses, errors, and tool activity emitted during the run.
+## Update an Agent
+
+There are two update forms. Both create a new version.
+
+- `POST /v1/agents/{agent_id}` is the OpenAI SDK update. Send only the fields you want to change. No `row_version` is needed, so the last writer wins. `description` and `note` are not accepted in this form.
+- `PATCH /v1/agents/{agent_id}` is the compare-and-set update. It requires the `row_version` you last read and returns 409 if the Agent changed in the meantime. Explicit `null` values are rejected.
+
+```bash
+curl -X PATCH https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"row_version": 2, "instructions": "Answer in five bullet points with cited sources.", "note": "Shorter answers"}'
+```
-## Publish only when it works
+## Versions, archive, and restore
-When the Agent is tested, publish a selected version as a callable Service, or create a Schedule for manual or recurring runs.
+- `GET /v1/agents/{agent_id}/versions` lists immutable snapshots; `GET /v1/agents/{agent_id}/versions/{version}` reads one.
+- `POST /v1/agents/{agent_id}/restore` with `{"source_version": 1, "row_version": 3}` copies an earlier configuration into a new current version.
+- `POST /v1/agents/{agent_id}/archive` and `/unarchive` take `{"row_version": …}`. Archived Agents cannot start new Sessions, Services, or Schedules.
+- `DELETE /v1/agents/{agent_id}` archives the Agent. Agents are never hard-deleted.
## Next steps
-- [Tools and credentials](/agents/mcp-tools)
-- [Sessions](/agents/sessions)
-- [Services](/agents/services)
-- [Schedules](/agents/schedules)
+- [Skills and MCP tools](/agents/mcp-tools)
+- [Run the Agent in a Session](/agents/sessions)
+- [Agents API reference](/api-reference/agents/)
diff --git a/agents/api-credentials.md b/agents/api-credentials.md
index 03349b3c..087a2bfe 100644
--- a/agents/api-credentials.md
+++ b/agents/api-credentials.md
@@ -1,50 +1,55 @@
---
-title: Credentials for Agent tools
-description: Securely store and scope API keys, tokens, and other credentials used by SandBase Agents and Services.
+title: Credentials for MCP tools
+description: Store write-only Bearer tokens as Secrets so Agents can call authenticated MCP servers.
---
-# Credentials for Agent tools
+# Credentials for MCP tools
-Credentials are the API keys, tokens, connection strings, and other private values an Agent needs to use external services.
+When an MCP server requires authentication, store its Bearer token as a **Secret** and attach the Secret to an [MCP connection](/agents/mcp-tools#mcp-connections). The execution environment sends the token only to that server.
-## How credentials work
+## How Secrets work
-SandBase stores each credential as an encrypted, write-only value. The Console and public API expose its name, scope, status, and a masked hint—not the saved value. At execution time, the runtime resolves active credentials that match the Agent or Service configuration.
+- `kind` is always `mcp_bearer`: a Bearer token for one MCP server.
+- Each Secret is bound to one exact `server_url`. A connection can use it only when its `server_url` matches. To use another URL, create another Secret.
+- The token is write-only. The API returns the Secret's name, URL, status, and `row_version`, never the token.
+- Secrets are not environment variables. They are not injected into the Agent runtime, and Agents cannot read them.
-## Managing credentials
+Earlier versions of SandBase stored workspace-, Agent-, or Service-scoped credentials and injected them as environment variables such as `GITHUB_TOKEN`. That model has been retired and its records are not migrated. Recreate the values you still need as Secrets on MCP connections.
-### In the Console
+## With the API
-Navigate to **Developer → Credentials**.
+Create, rotate, and revoke Secrets with the Secrets API. This guide documents the API contract; the Console's **Developer → Credentials** page is not the reference for Secret behavior.
-When adding a credential, choose the narrowest useful scope:
+Create a Secret. `Idempotency-Key` is required for every Secret write.
-| Scope | Use it for |
-|---|---|
-| Workspace | A value available to eligible Agents in the current workspace |
-| Agent | A value associated with one Agent ID |
-| Service | A value available to a published Service workflow |
+```bash
+curl -X POST https://api.sandbase.ai/v1/secrets \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: github-mcp-token" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "GitHub MCP token", "kind": "mcp_bearer", "server_url": "https://mcp.example.com/github", "token": "'"$GITHUB_MCP_TOKEN"'"}'
+```
-Saved values cannot be read back. To replace one, use the credential's **Manage** action and rotate it. A credential can be disabled; disabled records remain visible as metadata.
+The response is `201 Created` with a `sec_` ID and `status` (`pending` while the token is stored, then `active`). Use the ID as `secret_id` when you [create the MCP connection](/api-reference/mcp-connections/#create-a-connection).
-### Common Variables
+| Task | Request |
+|---|---|
+| List Secrets | `GET /v1/secrets` |
+| Read one | `GET /v1/secrets/{secret_id}` |
+| Rotate the token | `POST /v1/secrets/{secret_id}/rotate` with `{"token": "…", "row_version": …}` |
+| Revoke | `DELETE /v1/secrets/{secret_id}` (returns 204; status becomes `disabled`) |
-| Variable | Example | Purpose |
-|----------|---------|---------|
-| `GITHUB_TOKEN` | `ghp_xxxx` | Authenticate with GitHub API |
-| `DATABASE_URL` | `postgres://...` | Connect to a database |
-| `SLACK_WEBHOOK` | `https://hooks.slack.com/...` | Post notifications |
-| `OPENAI_API_KEY` | `sk-...` | Call OpenAI directly from a tool |
+Rotation keeps the same Secret and URL. New Sessions use the new token; Sessions that already started keep the credential they froze. Revocation is permanent and blocks both new Sessions and further input to existing Sessions that use the Secret.
## Security
-- Values are encrypted before storage and omitted from credential responses
-- The Console shows only masked metadata after creation
-- Scopes control where a credential can be selected; use Agent scope when a workspace-wide value is unnecessary
-- Keep secrets out of prompts, instructions, test input, and manually authored event payloads
+- Create and rotate Secrets from a trusted backend, never from browser code.
+- Read tokens from your own secret store or environment, as in `$GITHUB_MCP_TOKEN` above, rather than pasting them into scripts.
+- Keep tokens out of instructions, Session input, metadata, and Skill bundles.
+- Revoke a Secret as soon as its token may have leaked, then create a new one and replace the connection.
## Next steps
-- [Define an Agent](/agents/agent-api) — create an Agent that uses credentials
-- [Tools and credentials](/agents/mcp-tools) — capabilities that consume these secrets
-- [Services](/agents/services) — publish an Agent with its credentials available at runtime
+- [Skills and MCP tools](/agents/mcp-tools)
+- [Secrets API](/api-reference/secrets/)
+- [MCP Connections API](/api-reference/mcp-connections/)
diff --git a/agents/deployments.md b/agents/deployments.md
index f61067f0..4ed74cd5 100644
--- a/agents/deployments.md
+++ b/agents/deployments.md
@@ -1,15 +1,13 @@
---
title: Services and Schedules
-description: Compatibility guide linking the current Services and Schedules documentation.
+description: The Endpoints and Deployments APIs have been retired. Use Services and Schedules.
robots: noindex,follow
---
# Services and Schedules
-This page is retained for existing links. SandBase now uses separate product terms for the two delivery modes:
+This page is kept for existing links. The Endpoints and Deployments APIs have been retired. Use Services and Schedules:
-- [Services](/agents/services) publish Agents through the **Services API** (`/v1/endpoints`).
-- [Schedules](/agents/schedules) trigger repeatable work through the **Schedules API**, whose compatibility path remains `/v1/deployments`.
-- [Sessions](/agents/sessions) contain persistent Agent interaction and event history.
-
-The Service API path remains `/v1/endpoints`; Deployment, DeploymentRun, and Session resource names remain unchanged.
+- [Services](/agents/services) invoke a pinned Agent version on demand. Each invocation returns a Run.
+- [Schedules](/agents/schedules) run a pinned Agent version on a cron expression or on demand.
+- [Sessions](/agents/sessions) hold the conversation and output of every Run.
diff --git a/agents/index.md b/agents/index.md
index f9776064..613006a4 100644
--- a/agents/index.md
+++ b/agents/index.md
@@ -1,84 +1,76 @@
---
title: Build Agent
-description: Define, test, publish, and schedule reusable Agents in SandBase.
+description: Define, test, and deploy reusable, versioned Agents in SandBase as Sessions, Services, and Schedules.
---
# Build Agent
-Build Agent is where you define and test an Agent.
+Build Agent is where you define and test an Agent, then decide how it runs.
-An Agent is a reusable workflow. It can use a Model, instructions, APIs, Skills, and credentials to do real work.
+An Agent is a saved, versioned configuration: a Model, instructions, a runtime profile, Skills, and MCP connections to external tools. Saving an Agent does not run it.
## What an Agent is
-An Agent is not just a prompt.
-
-It is a saved configuration that answers four questions:
-
-| Question | Example |
+| Question | Where it lives |
|---|---|
-| What should it do? | Research a company and return a sourced report |
-| How should it think? | Use a reasoning Model with clear instructions |
-| What can it use? | Search API, scrape API, internal Skills |
-| How does it run? | Manual test, Service, or Schedule |
+| What should it do? | `instructions` |
+| Which model does the work? | `model`, a model ID from the [Models API](/api-reference/models/) |
+| What does it know how to do? | [Skills](/agents/mcp-tools#skills), versioned instruction bundles |
+| Which external tools can it call? | [MCP connections](/agents/mcp-tools#mcp-connections), with [credentials](/agents/api-credentials) when the server needs a token |
+| How does it run? | A Session, a Service, or a Schedule |
-## Build first, connect later
+Every effective change creates a new immutable version. You can list versions and restore an earlier one at any time.
-SandBase separates building from delivery:
+## Three ways to run an Agent
-1. **Build Agent** — define and test the Agent.
-2. **Services** — expose a tested Agent to an application or another supported client.
-3. **Schedules** — run a tested Agent on a timer.
-4. **Sessions** — inspect persistent Agent interaction and event history.
+| Mode | Use it when | What you get |
+|---|---|---|
+| [Session](/agents/sessions) | You want an interactive conversation or a test run | A `ses_` Session you send input to and stream events from |
+| [Service](/agents/services) | An app or another tool should invoke the Agent on demand | A `run_` Run per invocation, each with its own Session |
+| [Schedule](/agents/schedules) | The Agent should run on a cron schedule | A `run_` Run per firing or manual trigger, each with its own Session |
-This keeps the workflow easy to understand: build the Agent once, then decide how it should be used.
+Services and Schedules are pinned to an Agent version when you create them (the current version if you do not choose one). They stay pinned to that version until you update their `agent_version`, so you can keep editing the Agent without changing what is deployed.
-## Agent lifecycle: Draft → Test → Service or Schedule → Session
+## Agent lifecycle: Draft → Test in a Session → Deploy → Inspect Runs
### 1. Draft
-Create an Agent with a clear name, outcome, Model, instructions, and expected output. Add APIs when it needs data or actions, Skills when it needs reusable know-how, and credentials when an API requires authentication.
-
-Start with the smallest useful workflow. A focused Agent is easier to test, observe, and reuse than one broad prompt that tries to do everything.
-
-### 2. Test
-
-Run the draft in the Console with representative input. Inspect the final output, tool calls, timing, errors, and cost. Refine instructions and tools until both successful and failure cases behave predictably.
-
-Do not publish a draft just because one happy-path test succeeded. Test missing input, unavailable tools, empty results, and output-format requirements.
+Create an Agent in the Console under **Build → Agents**, or with [`POST /v1/agents`](/agents/agent-api). Start with the smallest useful workflow and add Skills and MCP connections only when the instructions need them.
-### 3. Publish
+### 2. Test in a Session
-Publish a tested Agent version as a Service when an application or another supported client needs a stable callable interface. The Service stays pinned to that version until you deliberately upgrade it, while the Agent can continue to evolve separately.
+Create a Session for the Agent, send representative input, and read the output, tool activity, and errors. Test missing input, unavailable tools, empty results, and output-format requirements, not only the happy path.
-### 4. Schedule
+### 3. Deploy as a Service or Schedule
-Create a Schedule when the Agent should run later or repeatedly. Choose the Agent/version, cadence, input, and delivery behavior. A Schedule does not require a Service. Use Schedules for recurring reports, monitoring, synchronization, and routine operations.
+Create a Service when an application or MCP client should call the tested version. Create a Schedule when it should run repeatedly. Both are pinned to the version you tested.
-### 5. Inspect Sessions
+### 4. Inspect Runs and Sessions
-A direct or Service interaction is represented by a Session. Every Schedule trigger creates a separate DeploymentRun record and, on success, a new Session. Inspect Session events for Agent input, output, tool activity, and errors.
+Every Service invocation and every Schedule firing creates a Run. A Run has a `status` (`pending`, `succeeded`, `failed`, `cancelled`, or `skipped` for Schedules) and a `session_id` that you can open with the Sessions API to read the full conversation.
## Production checklist
-- instructions define the expected result and failure behavior
-- only necessary APIs and Skills are enabled
-- required credentials are configured in **Developer → Credentials**, rather than copied into prompts or Agent instructions
-- representative inputs and failure cases have been tested
-- a stable version is published before external use
-- scheduled work has an owner and a clear destination
-- Sessions and Schedule trigger records are reviewed after launch
+- `model` uses a current model ID from the Models API.
+- Skills are pinned to a version when behavior must not change silently.
+- MCP connections expose only the tools the Agent needs, using `allowed_tools`.
+- Tokens are stored as [Secrets](/agents/api-credentials), never written into instructions, input, or metadata.
+- Service and Schedule writes send an `Idempotency-Key`, and retries reuse it.
+- Run `status` and `error_code` are checked after launch, and scheduled work has an owner.
-## Store Agents
+## Catalog Agents
-Store Agents are examples and best practices.
+The public [Agent catalog](/api-reference/agents/catalog) lists Agents published by SandBase. You can read a catalog Agent, but to change it, clone it into your organization first with `POST /v1/agents/catalog/{agent_id}/clone`. The clone is a new private Agent that you own and can deploy.
-You can test a public Agent, but if you need to change it, clone it into your workspace first. The cloned version becomes your Agent.
+::: tip Using the OpenAI SDK
+Agents, Sessions, and Skills follow the OpenAI Agents API, so the official OpenAI SDK works with `base_url="https://api.sandbase.ai/v1"`. See [OpenAI compatibility](/agents/openai-compatibility) for verified examples and differences.
+:::
## Next steps
- [Define an Agent](/agents/agent-api)
-- [Tools and credentials](/agents/mcp-tools)
+- [OpenAI compatibility](/agents/openai-compatibility)
+- [Skills and MCP tools](/agents/mcp-tools)
- [Sessions](/agents/sessions)
- [Services](/agents/services)
- [Schedules](/agents/schedules)
diff --git a/agents/mcp-tools.md b/agents/mcp-tools.md
index 741fd610..47fbdc2d 100644
--- a/agents/mcp-tools.md
+++ b/agents/mcp-tools.md
@@ -1,61 +1,93 @@
---
-title: Tools and credentials
-description: Give a SandBase Agent the capabilities and API credentials it needs.
+title: Skills and MCP tools
+description: Give a SandBase Agent reusable Skills and remote MCP tools, and control which versions and tools each Session uses.
---
-# Tools and credentials
+# Skills and MCP tools
-APIs give your Agent access to real-world data and actions.
+An Agent gets capabilities in two ways:
-Choose capabilities in the Agent builder. SandBase handles how supported capabilities are made available to the runtime.
+- **Skills** teach the Agent how to do a job. A Skill is a versioned ZIP bundle of instructions and resources.
+- **MCP connections** let the Agent call tools on a remote MCP server over HTTPS, optionally authenticated with a [Secret](/agents/api-credentials).
-## What to add
+Most useful Agents combine both: an MCP server fetches data or takes actions, and a Skill describes how to use the results.
-Add tools when your Agent needs to:
+## Skills
-- search the web
-- read pages
-- call a SaaS API
-- fetch social or business data
-- create or transform media
-- trigger an external workflow
+1. Package a folder that contains `SKILL.md` with `name` and `description` frontmatter, and zip the folder itself (`zip -r release-notes.zip release-notes/`, 5 MiB maximum). The ZIP must have that folder at its top level; a `SKILL.md` at the ZIP root returns `400 invalid_request`.
+2. Upload it with `POST /v1/skills` as the multipart field `files`. The response contains an `skl_` ID and `default_version: "1"`.
+3. Reference it in the Agent's `skills` array.
-## Add a capability
+```json
+{
+ "skills": [
+ { "skill_id": "skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57", "version": null }
+ ]
+}
+```
-1. Browse **APIs** or **Skills** in Store.
-2. Open the Agent draft.
-3. Add only the capabilities required by its instructions.
-4. Add the required values under **Developer → Credentials**. SandBase resolves matching credentials when the Agent runs.
-5. Test the Agent and inspect its Session events and tool activity.
+- `"version": null` uses the Skill's `default_version`. A string such as `"2"` pins one version.
+- The version is resolved and frozen when each Session starts. Changing the default later affects new Sessions only.
+- Upload a new version with `POST /v1/skills/{skill_id}/versions`, and switch the default with `default=true` or `POST /v1/skills/{skill_id}`.
+- A disabled Skill makes new Sessions of Agents that reference it fail with `400 skill_not_found`.
-Protocol-level integration details are documented in the [API Reference](/api-reference/), not required for the normal Agent-building flow.
+You can also browse public Skills in the [Skills catalog](/api-reference/skills/catalog). See the [Skills API](/api-reference/skills/) for the full walkthrough.
-## Skills vs APIs
+## MCP connections
-APIs do things.
+1. If the MCP server needs a Bearer token, [create a Secret](/agents/api-credentials) for its exact URL.
+2. Register the server with `POST /v1/mcp-connections`:
-Skills teach the Agent how to do a job.
+ ```bash
+ curl -X POST https://api.sandbase.ai/v1/mcp-connections \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: github-mcp" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "GitHub", "server_url": "https://mcp.example.com/github", "secret_id": "sec_9b1d3f5a-7c9e-5b2d-8f4a-6c8e0a2d4f61"}'
+ ```
-For example:
+3. Reference the connection in the Agent's `mcp_connections` array:
-- Web Search API: fetch search results
-- Research Skill: decide how to search, compare sources, and write the report
+ ```json
+ {
+ "mcp_connections": [
+ {
+ "connection_id": "mcp_1f3d5b7a-9c2e-5f4a-8b6d-0e2c4a6f8b13",
+ "server_label": "github",
+ "allowed_tools": ["search_issues", "get_issue"],
+ "required": true
+ }
+ ]
+ }
+ ```
-Most useful Agents use both.
+| Field | Meaning |
+|---|---|
+| `connection_id` | `mcp_` connection ID in your organization |
+| `server_label` | Starts with a letter; letters, digits, `_`, and `-`; up to 64 characters; unique within the Agent |
+| `allowed_tools` | `null` allows every tool the server offers; `[]` allows none; a list allows only those names |
+| `required` | Whether the Session must load this server |
-## API credentials
+An Agent can reference up to 32 connections. The connection must be `active` when a Session starts.
-Credentials are private values used to authenticate an API, such as `GITHUB_TOKEN`, `SLACK_WEBHOOK`, or `SERPER_API_KEY`.
+::: info Differs from OpenAI
+OpenAI's Agents API declares MCP servers inline as `tools: [{"type": "mcp", ...}]`. SandBase returns `400` for that form: register the server as a connection resource and reference it in `mcp_connections`. The OpenAI SDK has no methods for `/v1/mcp-connections` or `/v1/secrets`, so call them with `client.post(...)` and pass `mcp_connections` through `extra_body` (Python) or the request body (Node.js). See [OpenAI compatibility](/agents/openai-compatibility#mcp-connections-with-the-sdk).
+:::
-- call them **credentials** in the Console and documentation
-- store them outside prompts and Agent instructions
-- treat the value as write-only after saving; the Console and public API return masked metadata rather than the stored value
-- do not paste secret values into prompts, Agent instructions, or test input
+## Changes and running Sessions
+
+Each Session freezes its Agent version, Skill versions, and MCP connection configuration when it starts.
+
+- After you replace a connection or rotate its Secret, new Sessions use the new configuration; existing Sessions keep their snapshot.
+- Disabling a connection or revoking its Secret blocks new Sessions and rejects further input to existing Sessions that use it. A tool call that is already running is not interrupted.
+
+## Keep tokens out of prompts
+
+Store tokens as Secrets. Do not put them in instructions, Session input, metadata, or Skill bundles: those are readable by anyone with access to the Agent or Session.
## Next steps
-- [APIs](/store/apis)
-- [Skills](/store/skills)
-- [API Credentials](/agents/api-credentials)
+- [Credentials for MCP tools](/agents/api-credentials)
+- [MCP Connections API](/api-reference/mcp-connections/)
+- [Skills API](/api-reference/skills/)
- [Define an Agent](/agents/agent-api)
-- [Sessions](/agents/sessions)
diff --git a/agents/openai-compatibility.md b/agents/openai-compatibility.md
new file mode 100644
index 00000000..9749edf4
--- /dev/null
+++ b/agents/openai-compatibility.md
@@ -0,0 +1,269 @@
+---
+title: OpenAI compatibility
+description: Use the official OpenAI SDK with SandBase Agents, Sessions, and Skills, and see where SandBase differs from the OpenAI Agents API.
+---
+
+# OpenAI compatibility
+
+SandBase Agents implement the [OpenAI Agents API](https://developers.openai.com/api/docs/guides/agents-api/overview) protocol for Agents, Sessions, session events, items, turns, and Skills. For those resources you can use the official OpenAI SDK directly: point `base_url` at `https://api.sandbase.ai/v1` and pass a SandBase API key.
+
+Services, Schedules, MCP connections, Secrets, and the catalogs are SandBase-only resources. The SDK has no methods for them, so call them over HTTP (or with the SDK's generic `client.post` / `client.get`).
+
+Verified SDK versions:
+
+| SDK | Versions | Notes |
+|---|---|---|
+| openai-python | 3.13.0, 3.24.0 | `sessions.update(agent=...)` and `sessions.traces` do not exist in 3.13.0 |
+| openai-node | 7.28.0 (Node.js 26) | Use an HTTP/1.1 dispatcher for streaming, see [below](#node-js-and-http-2) |
+
+## Set up the client
+
+::: code-group
+
+```python [Python]
+import os
+from openai import OpenAI
+
+client = OpenAI(
+ api_key=os.environ["SANDBASE_API_KEY"], # your SandBase API key
+ base_url="https://api.sandbase.ai/v1",
+)
+```
+
+```js [Node.js]
+// npm install openai@7.28.0 undici
+import OpenAI from 'openai'
+import { Agent } from 'undici'
+
+const client = new OpenAI({
+ apiKey: process.env.SANDBASE_API_KEY, // your SandBase API key
+ baseURL: 'https://api.sandbase.ai/v1',
+ // HTTP/1.1 keeps the live event stream and the input request on separate connections.
+ fetchOptions: { dispatcher: new Agent({ allowH2: false }) },
+})
+```
+
+:::
+
+The SDK sends `OpenAI-Beta: agents=v1` automatically. SandBase accepts that value, or no header at all; any other value returns `400`.
+
+## Run an Agent end to end
+
+Create an Agent, create a Session, open the live event stream, send one input, and read the history. The stream is live-only, so open it before you send input.
+
+::: code-group
+
+```python [Python]
+import os
+import threading
+from openai import OpenAI
+
+client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+
+agent = client.beta.agents.create(
+ model="deepseek/deepseek-v4-flash",
+ name="Research assistant",
+ instructions="Keep output brief.",
+ metadata={"team": "research"},
+)
+session = client.beta.agents.sessions.create(
+ agent_id=agent.id,
+ environment={"type": "openai_hosted"}, # Required by the OpenAI SDK; SandBase always runs in its hosted cloud.
+)
+
+def send_input():
+ client.beta.agents.sessions.events.create(
+ session.id,
+ events=[{
+ "type": "agent.session.input.message",
+ "input": [{"role": "user", "content": [{"type": "input_text", "text": "Reply with exactly: ok"}]}],
+ }],
+ idempotency_key="research-input-1",
+ )
+
+with client.beta.agents.sessions.events.stream(session.id) as stream:
+ threading.Thread(target=send_input).start()
+ for event in stream:
+ print(event.type)
+ if event.type in ("agent.session.turn.completed", "agent.session.turn.failed", "agent.session.turn.cancelled"):
+ break
+
+for item in client.beta.agents.sessions.items.list(session.id, order="asc", limit=100).data:
+ print(item.type, getattr(item, "role", None))
+
+# Clean up: both calls archive the resource.
+client.beta.agents.sessions.delete(session.id)
+client.beta.agents.delete(agent.id)
+```
+
+```js [Node.js]
+import OpenAI from 'openai'
+import { Agent } from 'undici'
+
+const client = new OpenAI({
+ apiKey: process.env.SANDBASE_API_KEY,
+ baseURL: 'https://api.sandbase.ai/v1',
+ fetchOptions: { dispatcher: new Agent({ allowH2: false }) },
+})
+
+const agent = await client.beta.agents.create({
+ model: 'deepseek/deepseek-v4-flash',
+ name: 'Research assistant',
+ instructions: 'Keep output brief.',
+ metadata: { team: 'research' },
+})
+const session = await client.beta.agents.sessions.create({
+ agent_id: agent.id,
+ environment: { type: 'openai_hosted' }, // Required by the SDK types; SandBase always runs in its hosted cloud.
+})
+
+const stream = await client.beta.agents.sessions.events.stream(session.id)
+const done = (async () => {
+ for await (const event of stream) {
+ console.log(event.type)
+ if (/^agent\.session\.turn\.(completed|failed|cancelled)$/.test(event.type)) break
+ }
+})()
+
+await client.beta.agents.sessions.events.create(session.id, {
+ events: [{ type: 'agent.session.input.message', input: 'Reply with exactly: ok' }],
+ 'Idempotency-Key': 'research-input-1',
+})
+await done
+
+const items = await client.beta.agents.sessions.items.list(session.id, { order: 'asc', limit: 100 })
+for (const item of items.data) console.log(item.type, item.role)
+
+// Clean up: both calls archive the resource.
+await client.beta.agents.sessions.delete(session.id)
+await client.beta.agents.delete(agent.id)
+```
+
+:::
+
+`events.create` returns `202` with no body. Each request carries exactly one event. Reusing the same `Idempotency-Key` with the same body does not start a second turn. To stop the running turn, send `{"type": "agent.session.input.cancel"}` as the single event.
+
+### Node.js and HTTP/2
+
+Node.js 26 `fetch` may negotiate HTTP/2 with the SandBase API. If it does, on a shared HTTP/2 connection `events.create` can stall while `events.stream` is open on the same connection, until the stream closes. The `undici` dispatcher with `allowH2: false` above avoids this. The Python SDK uses HTTP/1.1 by default and needs no change.
+
+## Skills with the SDK
+
+`client.skills` works as in OpenAI, except that deleting a single Skill version is not available. The ZIP must contain one top-level folder with `SKILL.md` inside it (`zip -r release-notes.zip release-notes/`). A `SKILL.md` at the ZIP root returns `400 invalid_request`.
+
+::: code-group
+
+```python [Python]
+with open("release-notes.zip", "rb") as bundle:
+ skill = client.skills.create(files=[("release-notes.zip", bundle.read(), "application/zip")])
+print(skill.id, skill.default_version)
+
+zip_bytes = client.skills.content.retrieve(skill.id).read()
+```
+
+```js [Node.js]
+import fs from 'node:fs'
+
+const skill = await client.skills.create({ files: fs.createReadStream('release-notes.zip') })
+console.log(skill.id, skill.default_version)
+
+const zip = Buffer.from(await (await client.skills.content.retrieve(skill.id)).arrayBuffer())
+```
+
+:::
+
+`skills.retrieve`, `skills.list`, `skills.update(default_version=...)`, `skills.versions.create / list / retrieve`, `skills.versions.content.retrieve`, and `skills.delete` work as well. A Skill referenced by any Agent version cannot be deleted (`409`); set `status` to `disabled` with Update Skill instead.
+
+## MCP connections with the SDK
+
+OpenAI declares MCP servers inline as `tools: [{"type": "mcp", ...}]`. SandBase returns `400` for that form. Register the server once with [`POST /v1/mcp-connections`](/api-reference/mcp-connections/) and reference it from the Agent's `mcp_connections`. The SDK's generic request methods and extra body fields cover both steps:
+
+::: code-group
+
+```python [Python]
+import uuid
+
+connection = client.post(
+ "/mcp-connections",
+ cast_to=object,
+ body={"name": "DeepWiki", "server_url": "https://mcp.deepwiki.com/mcp"},
+ options={"headers": {"Idempotency-Key": str(uuid.uuid4())}}, # Use a new key per create; reusing one returns the original (possibly disabled) connection.
+)
+agent = client.beta.agents.update(
+ agent.id,
+ extra_body={"mcp_connections": [
+ {"connection_id": connection["id"], "server_label": "deepwiki", "allowed_tools": None, "required": False},
+ ]},
+)
+```
+
+```js [Node.js]
+const connection = await client.post('/mcp-connections', {
+ body: { name: 'DeepWiki', server_url: 'https://mcp.deepwiki.com/mcp' },
+ headers: { 'Idempotency-Key': crypto.randomUUID() }, // Use a new key per create; reusing one returns the original (possibly disabled) connection.
+})
+await client.beta.agents.update(agent.id, {
+ mcp_connections: [
+ { connection_id: connection.id, server_label: 'deepwiki', allowed_tools: null, required: false },
+ ],
+})
+```
+
+:::
+
+The Node.js call passes `mcp_connections` in plain JavaScript; in TypeScript the field is not part of the SDK types. Bearer tokens for MCP servers are stored as [Secrets](/agents/api-credentials), not OpenAI vaults.
+
+## Execution environment
+
+SandBase runs every Session in its hosted cloud; there is no environment to choose or manage.
+
+- Direct HTTP calls omit `environment`.
+- The OpenAI SDK requires an `environment` argument on `sessions.create` (Python raises `TypeError` without it, and the Node.js types require it). Pass `{"type": "openai_hosted"}`; SandBase accepts it. Any other value, such as `self_hosted`, returns `400`.
+- Session responses include an `environment` object for SDK compatibility. You do not need to read or act on it.
+
+## Compatibility at a glance
+
+| Area | Status | Details |
+|---|---|---|
+| `beta.agents.create / retrieve / update / list / delete` | Compatible, with extensions | Responses add `status`, `version`, `row_version`, `projection_status`, `runtime_profile`, `skills`, `mcp_connections`, and more. `delete` archives the Agent and returns `agent.deleted`. |
+| `beta.agents.sessions.create / retrieve / update / list / delete` | Compatible, with limits | Requires a saved `agent_id`; `agent` only accepts `{"model": ...}`. `update` only accepts `metadata`. `delete` archives the Session. |
+| `sessions.events.stream` | Compatible | Live-only, like OpenAI. `Last-Event-ID` or `?after=` returns `400`. |
+| `sessions.events.create` | Compatible, with limits | Exactly one `agent.session.input.message` or `agent.session.input.cancel` event per request; the message has one user `input_text` part or a plain string. |
+| `sessions.items.list`, `sessions.turns.list / retrieve` | Compatible | Cursor pagination with `limit` 1-100, `after`, `order`. |
+| `client.skills` and `skills.versions` | Compatible, except version delete | `skills.versions.delete` returns `404`. |
+| Error envelope | Compatible | `{"error": {"type", "code", "param", "message"}}`; SDK exceptions expose it as `exc.body`. |
+| Agent versions, restore, archive, catalog, Session rename/archive/reconnect/files | SandBase only | See the [Agents](/api-reference/agents/) and [Sessions](/api-reference/sessions/) API reference. |
+| Services, Schedules, MCP connections, Secrets | SandBase only | Use HTTP or `client.post` / `client.get`. |
+
+### Not supported
+
+These OpenAI SDK calls fail against SandBase. The message is the actual `error.message` returned.
+
+| SDK call | Result |
+|---|---|
+| `sessions.create(...)` with `agent` but no `agent_id` (inline Agent) | `400`: `a saved SandBase agent_id and openai_hosted environment are required; standalone inline agents are unsupported` |
+| `sessions.create(..., stream=True)` (also `with_result_collection()`) | `400`: `creation streaming is not supported; create the session then subscribe to events before sending input` |
+| `sessions.create(..., environment={"type": "self_hosted"})` | `400`, same message as the inline Agent case |
+| `sessions.create(..., vault_ids=[...])` | `400 invalid_request` |
+| `sessions.update(..., agent={...})` | `400` (openai-python 3.13.0 has no `agent` parameter and raises `TypeError`) |
+| `agents.create(..., multi_agent={"enabled": True})` | `400`: `multi_agent is not supported` |
+| `agents.create(..., tools=[{"type": "mcp", ...}])`; `tool_search`, `programmatic_tool_calling`, and `computer_use` tools | `400 invalid_request`. Only `function` and one `web_search` tool are accepted |
+| `events.create` with a `tool_result` or computer-use approval event | `400 invalid_request`. Sessions that reach `requires_action` are not supported |
+| `events.create` with more than one event | `400`: `exactly one text input or cancel event is supported per request` |
+| `sessions.artifacts`, `sessions.traces`, `sessions.subagents`, `environments.templates` | `404 not_found` (openai-python 3.13.0 has no `traces`) |
+| `beta.agents.vaults` | `404` with a plain-text body (`404 page not found`), not the JSON error envelope |
+| `skills.versions.delete` | `404 not_found` |
+
+### Field differences
+
+- Sessions report failures in `failure` (`{code, message, action, retryable?}`), not OpenAI's `error`. With the SDK, `session.error` is `None`; read `session.failure` instead.
+- `required_actions` and `vault_ids` are always empty arrays.
+- Agent and Session timestamps are Unix seconds. Agent version `created_at`, MCP connections, Secrets, and Runs use ISO 8601 strings.
+- `402 spending_limit_exceeded` has no dedicated SDK exception class; it surfaces as a generic `APIStatusError`.
+- An unsupported method on a known path (for example `PUT` on `/v1/agents`) returns `404` with the JSON error envelope, not `405`.
+
+## Next steps
+
+- [Sessions](/agents/sessions)
+- [Skills and MCP tools](/agents/mcp-tools)
+- [Agents API reference](/api-reference/agents/)
diff --git a/agents/schedules.md b/agents/schedules.md
index 3725d0c9..a90d740a 100644
--- a/agents/schedules.md
+++ b/agents/schedules.md
@@ -1,45 +1,76 @@
---
title: Schedules
-description: Configure manual or cron-based Agent runs and inspect each Schedule trigger through SandBase Sessions.
+description: Run a pinned Agent version on a cron schedule or on demand, and review every Run and Session.
---
# Schedules
-A **Schedule** stores a pinned Agent version, starting events, and optionally a cron expression. The underlying public API resource keeps the compatibility name **Deployment**, so Schedule operations use `/v1/deployments` and `depl_` IDs.
+A **Schedule** runs a pinned Agent version with a saved input on a cron expression. Every firing and every manual trigger creates a **Run**, and each Run creates its own Session. Schedule IDs start with `sch_`.
-## Trigger lifecycle
+## Create a Schedule
-Every manual or cron trigger:
+`Idempotency-Key` is required, `input` must be non-empty, and `schedule` sets the `cron` expression and `timezone`.
-1. creates a new `drun_*` DeploymentRun record;
-2. attempts to create exactly one new `sess_*` Session;
-3. records either a `session_id` on success or an error when Session creation fails.
+```bash
+curl -X POST https://api.sandbase.ai/v1/schedules \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: daily-digest-schedule" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Daily digest",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "input": "Summarize yesterday'"'"'s AI agent news in five bullet points.",
+ "schedule": {"cron": "0 9 * * 1-5", "timezone": "Asia/Shanghai"}
+ }'
+```
+
+- `cron` uses five fields: minute, hour, day of month, month, day of week. Seconds are not supported. An expression with no match in the next 366 days returns 400.
+- `timezone` is an IANA name and defaults to `UTC`.
+- Omitting `agent_version` pins the Agent's current version at creation time.
+- `next_run_at` shows the next firing time in UTC.
+
+## Model credentials for scheduled Runs
-The public DeploymentRun object has no `status` field. List endpoints accept `status=pending|succeeded|failed` as trigger-outcome filters, but those filters do not describe the linked Agent Session's execution status. Inspect the linked Session for subsequent Agent events and errors.
+Scheduled Runs have no caller, so the Schedule stores a model credential, encrypted, when you create it: the `model_provider` you send, or your calling SandBase API key when you omit it. `PUT /v1/schedules/{schedule_id}/timing` keeps it unless you send a new `model_provider`. If that key is later revoked or runs out of balance, scheduled Runs fail with an `error_code` instead of running.
-## Trigger manually
+## Trigger a Run now
```bash
-curl -X POST https://api.sandbase.ai/v1/deployments/depl_01.../runs \
+curl -X POST https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17/runs \
-H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: daily-digest-manual-1" \
-H "Content-Type: application/json" \
- -d '{}'
+ -d '{"input": "Summarize this morning'"'"'s AI agent news."}'
```
-Manual triggers do not accept input overrides; the Schedule's `initial_events` are used.
+The response is **202 Accepted** with a pending Run (`trigger_source: "manual"`). `input`, `model`, and `model_provider` override the saved values for this Run only; send `{}` to use the saved input.
-## Inspect trigger records
+## Change timing or configuration
-```bash
-curl "https://api.sandbase.ai/v1/deployment_runs?deployment_id=depl_01...&trigger_type=schedule" \
- -H "Authorization: Bearer $SANDBASE_API_KEY"
-```
+- `PUT /v1/schedules/{schedule_id}/timing` with `{"row_version": …, "cron": "…", "timezone": "…"}` changes when it fires.
+- `PATCH /v1/schedules/{schedule_id}` changes `name`, `description`, `agent_id`, `agent_version`, or `input`.
+- `POST /v1/schedules/{schedule_id}/pause`, `/resume`, and `/archive` take `{"row_version": …}`. Paused Schedules neither fire nor accept manual Runs. Archived is permanent.
+
+A Schedule's `row_version` advances every time it fires, so read the Schedule right before you write to it.
+
+## Runs
+
+| Field | Values |
+|---|---|
+| `trigger_source` | `scheduled` (with `scheduled_at`) or `manual` |
+| `status` | `pending`, `succeeded`, `failed`, `cancelled`, or `skipped` |
+| `error_code` | Present on failures and skips, for example `execution_failed`, `schedule_configuration_invalid`, or `concurrency_limit` |
+| `session_id` | The Session that holds the conversation, once it exists |
+
+- Only one Run per Schedule can be pending. If a firing finds one still pending, it records a `skipped` Run with `error_code: "concurrency_limit"`; a manual trigger returns `429 concurrency_limit`.
+- If the scheduler was down when a firing was due, it creates one Run for the overdue time and continues from the next future time. Missed firings are not replayed one by one.
+- `GET /v1/schedules/{schedule_id}/runs` lists Runs; `GET /v1/schedules/{schedule_id}/runs/{run_id}` reads one. Reads do not change a Run; status advances in the background.
+- Cancel a pending Run with `POST /v1/schedules/{schedule_id}/runs/{run_id}/cancel` (body `{}`, `Idempotency-Key` required).
-Use `trigger_type=manual|schedule` and `status=pending|succeeded|failed` filters. Retrieve the returned `session_id` through the Sessions API after a successful trigger.
+Schedules support the same Feishu or Lark notifications as [Services](/api-reference/services/#notifications).
## Next steps
-- [Schedules API](/api-reference/deployments/)
-- [Trigger a Schedule](/api-reference/deployments/run)
-- [List Schedule runs](/api-reference/deployments/list-runs)
+- [Schedules API reference](/api-reference/schedules/)
+- [Services](/agents/services)
- [Sessions](/agents/sessions)
diff --git a/agents/services.md b/agents/services.md
index fab03e03..af98e003 100644
--- a/agents/services.md
+++ b/agents/services.md
@@ -1,40 +1,75 @@
---
title: Services
-description: Publish a tested Agent as a stable REST, MCP, or ACP Service.
+description: Deploy a tested Agent version as a Service, invoke it, track each Run, and call it from AI tools.
---
# Services
-A **Service** makes a tested Agent version callable from an application or another supported client. In the public API, the underlying resource keeps the compatibility name `endpoint`: Service management and invocation use `/v1/endpoints` and `ep_` IDs. Public invocation uses REST, MCP, or experimental ACP according to the Service's `protocols` setting.
+A **Service** makes a tested Agent version callable on demand. It stores a name, a pinned `agent_version`, and an optional default input. Each invocation creates a **Run**, and each Run creates its own Session with the full conversation and output.
-## When to create a Service
+Service IDs start with `svc_`; Run IDs start with `run_`.
-Create one when the Agent and its input contract are tested and an external caller needs a stable invocation URL. A Service is pinned to a selected Agent version. Publishing a newer Agent version does not silently change the Service; upgrade its binding deliberately.
+## Create a Service
-## Invoke a Service
+`Idempotency-Key` is required. Omitting `agent_version` pins the Agent's current version at creation time.
```bash
-curl -X POST https://api.sandbase.ai/v1/endpoints/ep_01.../run \
+curl -X POST https://api.sandbase.ai/v1/services \
-H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: weekly-brief-service" \
-H "Content-Type: application/json" \
- -d '{"input":"Prepare a sourced customer brief."}'
+ -d '{"name": "Weekly brief", "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60", "input": "Summarize this week in AI agents."}'
```
-Omit `session_id` to create a Session. To continue one, provide a Session previously created by the same Service and still bound to the same Agent version. A successful request returns `202 Accepted` with the persistent `session_id` and accepted event metadata; it does not create a DeploymentRun.
+The Service stays on that Agent version until you change it with `PATCH /v1/services/{service_id}` (`agent_version` plus the current `row_version`).
-## Connect through MCP
+## Invoke it and read the result
-For MCP clients, include `mcp` in the Service's `protocols` and send MCP JSON-RPC messages to `POST /v1/endpoints/{endpoint_id}/mcp`. Initialize the connection before calling tools. Use `DELETE /v1/endpoints/{endpoint_id}/mcp` to close transport state; this does not delete the Service. See [Invoke a Service through MCP](/api-reference/endpoints/mcp) for the request envelope and response behavior.
+```bash
+curl -X POST https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95/invoke \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: weekly-brief-2026-w41" \
+ -H "Content-Type: application/json" \
+ -d '{"input": "Summarize this week in AI agents, focusing on open-source releases."}'
+```
+
+The response is **202 Accepted** with a Run whose `status` is `pending`. Send `{}` to use the saved default input.
+
+1. Poll `GET /v1/services/{service_id}/runs/{run_id}`. Each read also advances the Run from its Session.
+2. When `session_id` is set, you can follow the conversation with the [Sessions API](/agents/sessions).
+3. When `status` is `succeeded`, read the output with `GET /v1/agents/sessions/{session_id}/items`.
+
+| Run `status` | Meaning |
+|---|---|
+| `pending` | Accepted; the Session is starting or running |
+| `succeeded` | The turn completed |
+| `failed` | See `error_code`, for example `session_creation_rejected`, `execution_failed`, or `spending_limit_exceeded` |
+| `cancelled` | The turn was cancelled |
+
+## Retries and concurrency
-## API compatibility names
+- Retrying an invocation with the same `Idempotency-Key` and body returns the original Run. The same key with a different body returns 409.
+- Only one Run per Service can be pending. Another invocation returns `429 concurrency_limit` until it finishes. Cancel a pending Run with `POST /v1/services/{service_id}/runs/{run_id}/cancel` (body `{}`, `Idempotency-Key` required).
+- Each invocation uses your calling API key for model calls unless the body sends `model_provider`.
+- `pause`, `resume`, and `archive` take the current `row_version`. Paused Services reject invocations; archived is permanent.
-| Product | API path name | API prefix |
-|---|---|---|
-| Service | `endpoint` | `/v1/endpoints` |
-| Schedule | Deployment | `/v1/deployments` |
+## Use a Service from AI tools
+
+When you [connect an AI tool to SandBase](/setup/), your Services become available through SandBase MCP tools:
+
+- `sandbase_discover` lists Services (type `service`).
+- `sandbase_inspect` with the `svc_` ID shows the input it expects.
+- `sandbase_run` with `name` set to the `svc_` ID and `arguments: {"idempotency_key": "…", "input": "…"}` starts a Run. Reuse the same key and input on retries.
+- `sandbase_run_get`, `sandbase_runs`, and `sandbase_run_cancel` follow, list, and cancel Runs.
+
+Acceptance is not completion: poll `sandbase_run_get` until the Run finishes.
+
+## Notifications
+
+A Service can post Run results to a Feishu or Lark bot. Save the webhook with `PUT /v1/services/{service_id}/notifications/feishu` and `{"webhook_url": "https://open.feishu.cn/open-apis/bot/v2/hook/…"}`. The URL is write-only. See [Notifications](/api-reference/services/#notifications) for delivery records.
## Next steps
-- [Services API](/api-reference/endpoints/)
-- [Sessions](/agents/sessions)
+- [Services API reference](/api-reference/services/)
- [Schedules](/agents/schedules)
+- [Sessions](/agents/sessions)
diff --git a/agents/sessions.md b/agents/sessions.md
index 79c8d3d3..692a11cd 100644
--- a/agents/sessions.md
+++ b/agents/sessions.md
@@ -1,55 +1,137 @@
---
title: Sessions
-description: Understand persistent Agent interactions, events, and the Sessions API.
+description: Run an Agent in a Session, stream its events, read history, attach files, and handle failures.
---
# Sessions
-A **Session** is one persistent interaction with an Agent. Its public ID begins with `sess_` and remains stable while accepted messages and subsequent Agent events are appended to its event history.
+A **Session** runs one saved Agent version in SandBase's hosted cloud; you do not choose an execution environment. Its ID starts with `ses_`, and all Session paths live under `/v1/agents/sessions`. The Session freezes the Agent version, Skill versions, and MCP connection configuration it started with.
## How Sessions start
-- Create one directly with `POST /v1/sessions`.
-- Invoke a [Service](/agents/services) without `session_id`.
-- Trigger a [Schedule](/agents/schedules); every successful trigger creates a new Session.
+- Create one directly with `POST /v1/agents/sessions`. The Session reports `source: "direct"`.
+- Invoke a [Service](/agents/services). Each Run creates its own Session with `source: "service"` and `source_id` set to the `svc_` ID.
+- Let a [Schedule](/agents/schedules) fire or trigger it manually. Each Run creates its own Session with `source: "schedule"`.
-A Service can continue only a Session created by that same Service and still matching its Agent-version binding. A Schedule never reuses a Session.
+## Run a Session
-## Sessions and Schedule runs
-
-A Schedule uses a compatibility resource behind the `/v1/deployments` path. Each manual or cron trigger first creates a separate `drun_*` **DeploymentRun** record. A successful trigger links the record to a newly created `session_id`; a failed trigger has no Session and includes an error. The public DeploymentRun object does not expose a `status` field; status filters on list endpoints select trigger outcomes, not the later Agent execution lifecycle.
-
-## Work with a Session
+The native event stream is **live-only**: events are not replayed after a disconnect. Open it before you send input, and read history from items or turns.
```bash
-# Inspect a Session
-curl https://api.sandbase.ai/v1/sessions/sess_01... \
+# 1. Create the Session
+curl -X POST https://api.sandbase.ai/v1/agents/sessions \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: market-scan-2026-10-06" \
+ -H "Content-Type: application/json" \
+ -d '{"agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60"}'
+
+# 2. In another terminal, follow live events (server-sent events)
+curl -N https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/events \
-H "Authorization: Bearer $SANDBASE_API_KEY"
-# Send another event
-curl -X POST https://api.sandbase.ai/v1/sessions/sess_01.../events \
+# 3. Send input (202 Accepted, no body)
+curl -X POST https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/events \
-H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: market-scan-input-1" \
-H "Content-Type: application/json" \
- -d '{"events":[{"type":"user.message","content":[{"type":"text","text":"Continue the analysis."}]}]}'
+ -d '{"events": [{"type": "agent.session.input.message", "input": "List three notable launches this week."}]}'
-# Replay persisted events over SSE
-curl -N https://api.sandbase.ai/v1/sessions/sess_01.../events/stream \
+# 4. Read the history afterwards
+curl "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/items?order=asc" \
-H "Authorization: Bearer $SANDBASE_API_KEY"
```
-## Identity summary
+The same flow with the official OpenAI Python SDK. Open the stream first, send input from another thread, and stop at the terminal turn event. See [OpenAI compatibility](/agents/openai-compatibility) for the Node.js version and the SDK differences.
+
+```python
+# Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+import os
+import threading
+from openai import OpenAI
+
+client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+session = client.beta.agents.sessions.create(
+ agent_id="agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ environment={"type": "openai_hosted"}, # Required by the OpenAI SDK; SandBase always runs in its hosted cloud.
+)
+
+def send_input():
+ client.beta.agents.sessions.events.create(
+ session.id,
+ events=[{
+ "type": "agent.session.input.message",
+ "input": [{"role": "user", "content": [{"type": "input_text", "text": "List three notable launches this week."}]}],
+ }],
+ idempotency_key="market-scan-input-1",
+ )
+
+with client.beta.agents.sessions.events.stream(session.id) as stream:
+ threading.Thread(target=send_input).start()
+ for event in stream:
+ print(event.type)
+ if event.type in ("agent.session.turn.completed", "agent.session.turn.failed", "agent.session.turn.cancelled"):
+ break
+```
+
+Each send carries exactly one event. `input` is a text string, or an array with exactly one user message that has exactly one `input_text` part: `[{"role": "user", "content": [{"type": "input_text", "text": "…"}]}]`. Any other shape returns `400 invalid_request`. To stop the running turn, send `{"type": "agent.session.input.cancel"}` or call `POST /v1/agents/sessions/{session_id}/cancel` with an `Idempotency-Key`. Cancelling an idle Session still returns `202 {"accepted": true}` and changes nothing. You can also pass a first `input` when you create the Session. `stream: true` on create is not supported.
+
+## Status and failures
-| Resource | ID | Meaning |
+| Field | Values | Meaning |
|---|---|---|
-| Agent | `agent_*` | Versioned workflow definition |
-| Service | `ep_*` | Stable callable surface |
-| Schedule (compatibility resource) | `depl_*` | Repeatable trigger configuration |
-| DeploymentRun | `drun_*` | One Schedule trigger and its Session-creation result |
-| Session | `sess_*` | Persistent Agent interaction and event history |
+| `status` | `idle`, `in_progress`, `failed` | Execution state of the Session |
+| `lifecycle_status` | `active`, `archived` | Whether the Session accepts input |
+| `failure` | `null` or `{code, message, action, retryable?}` | Public reason for a failure and the suggested next step |
+
+`failure.action` tells you what to do: `retry_later`, `inspect_history`, `create_new_session`, `check_model_account`, or `create_session_with_updated_model_provider`. See the [failure code table](/api-reference/sessions/#status-fields).
+
+Function tools that ask the client for a result (`requires_action`) are not supported yet; reading such a Session returns `502 executor_contract_error`.
+
+If a send returns `409 session_input_pending`, earlier input is still being delivered. Wait and read the Session; do not resend with a new `Idempotency-Key`, or the input could run twice.
+
+## Archive
+
+`DELETE /v1/agents/sessions/{session_id}` archives a Session; Sessions are not hard-deleted. `POST /v1/agents/sessions/{session_id}/archive` and `/unarchive` take `{"row_version": …}`. Archived Sessions keep their history but reject input, metadata updates, and streams.
+
+## Attachments
+
+Upload a file into the Session workspace with `POST /v1/agents/sessions/{session_id}/files` (multipart field `file`, up to 5 MiB, `Idempotency-Key` required). Supported types are `.txt`, `.md`, `.csv`, `.log`, `.json`, `.pdf`, `.png`, `.jpg`, `.jpeg`, and `.webp`. When the file's `status` is `ready`, send it with up to nine others:
+
+```bash
+curl -X POST https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/events \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: market-scan-input-2" \
+ -H "Content-Type: application/json" \
+ -d '{"input": "Summarize the attached report.", "file_ids": ["fil_6d8f0a2c-4e6b-5d8f-a1c3-5e7a9c1e3b72"]}'
+```
+
+## Reconnecting clients
+
+Browser-style clients that need a consistent view after a dropped connection can call `POST /v1/agents/sessions/{session_id}/reconnect`. It returns the Session and a `stream_url` for `GET /v1/agents/sessions/{session_id}/events/stream`, which sends a `session.snapshot` first and then live events and `heartbeat`. If that stream emits `stream.interrupted`, reconnect again instead of resending input.
+
+## Model credentials
+
+A Session calls models with the API key that created it unless you send `model_provider`. Use `{"type": "custom", "base_url": "https://…", "api_key": "…"}` to run against your own OpenAI-compatible HTTPS endpoint. `model_provider` is write-only.
+
+## IDs
+
+| Prefix | Resource |
+|---|---|
+| `agt_` | Agent |
+| `ses_` | Session |
+| `svc_` | Service |
+| `sch_` | Schedule |
+| `run_` | Service or Schedule Run |
+| `skl_` | Skill |
+| `mcp_` | MCP connection |
+| `sec_` | Secret |
+| `fil_` | Session attachment |
+
+Treat IDs as opaque strings.
## Next steps
+- [Sessions API reference](/api-reference/sessions/)
+- [OpenAI compatibility](/agents/openai-compatibility)
- [Services](/agents/services)
- [Schedules](/agents/schedules)
-- [Sessions API](/api-reference/sessions/)
-- [Schedule runs API](/api-reference/deployments/list-runs)
diff --git a/api-reference/agents/archive.md b/api-reference/agents/archive.md
index 6127d7ee..13afd014 100644
--- a/api-reference/agents/archive.md
+++ b/api-reference/agents/archive.md
@@ -1,6 +1,6 @@
---
title: Archive Agent
-description: Archive an agent so it cannot be used for new sessions.
+description: Archive Agent with row_version.
aside: false
outline: false
apiReference:
@@ -8,56 +8,114 @@ apiReference:
operation: Agents
method: POST
path: /v1/agents/{agent_id}/archive
- description: Archive an agent. Existing sessions keep their agent snapshot, while new sessions can no longer reference the archived agent.
+ description: Archive an Agent with compare-and-set. Archived Agents cannot start new Sessions, Services, or Schedules.
groups:
- title: Path parameters
fields:
- name: agent_id
type: string
required: true
- description: Unique agent identifier beginning with agent_.
+ description: Agent ID (agt_ prefix).
+ - title: Headers
+ fields:
+ - name: Idempotency-Key
+ type: string
+ required: false
+ description: Optional. When omitted the service generates one, so a retried request can create a duplicate. Send your own stable key for retries.
+ - title: Request body
+ description: JSON object.
+ fields:
+ - name: row_version
+ type: integer
+ required: true
+ description: The row_version you last read. A stale value returns 409.
examples:
- label: cURL
language: bash
- code: |
- curl -X POST \
- "https://api.sandbase.ai/v1/agents/agent_01HqR2k7.../archive" \
- -H "Authorization: Bearer $SANDBASE_API_KEY"
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60/archive \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "row_version": 3
+ }'
- label: Python
language: python
- code: |
- agent = client.beta.agents.archive(
- agent_id="agent_01HqR2k7..."
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60/archive",
+ headers=headers,
+ json={
+ "row_version": 3
+ },
)
- print(agent.archived_at)
+ response.raise_for_status()
+ print(response.json())
- label: TypeScript
language: typescript
- code: |
- const agent = await client.beta.agents.archive(
- 'agent_01HqR2k7...'
- );
- console.log(agent.archived_at);
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60/archive', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "row_version": 3
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
response:
status: 200 OK
- code: |
+ code: |-
{
- "id": "agent_01HqR2k7...",
- "type": "agent",
- "version": 1,
- "name": "Research Assistant",
- "archived_at": "2026-05-29T12:00:00Z",
- "updated_at": "2026-05-29T12:00:00Z"
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "object": "agent",
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
+ "tools": [],
+ "text": {
+ "format": {
+ "type": "text"
+ },
+ "verbosity": "medium"
+ },
+ "reasoning": {
+ "effort": null,
+ "summary": null
+ },
+ "service_tier": "auto",
+ "multi_agent": {
+ "enabled": false,
+ "max_concurrent_subagents": null
+ },
+ "visibility": "private",
+ "status": "archived",
+ "version": 3,
+ "row_version": 4,
+ "projection_status": "ready",
+ "runtime_profile": "codex",
+ "description": "",
+ "metadata": {
+ "team": "research"
+ },
+ "mcp_connections": [],
+ "skills": [],
+ "created_at": 1791273600,
+ "updated_at": 1791273600,
+ "archived_at": 1791360000
}
- notes:
- - title: Soft delete
- description: Archiving is designed as a soft delete. Existing sessions continue to run from their pinned agent snapshot.
- errors:
- - status: 401
- type: authentication_error
- description: The API key is missing or invalid.
- - status: 404
- type: not_found
- description: The agent does not exist.
---
diff --git a/api-reference/agents/catalog.md b/api-reference/agents/catalog.md
new file mode 100644
index 00000000..a0b3ee80
--- /dev/null
+++ b/api-reference/agents/catalog.md
@@ -0,0 +1,47 @@
+---
+title: Agent Catalog
+description: Browse public Agents and clone one into your organization.
+---
+# Agent Catalog
+
+The catalog lists Agents that SandBase publishes for reuse. Catalog entries are read-only. Clone one to get a private Agent in your organization that you can edit, run, and deploy.
+
+| Method | Path | Purpose |
+|---|---|---|
+| `GET` | `/v1/agents/catalog` | List public Agents |
+| `GET` | `/v1/agents/catalog/{agent_id}` | Get one public Agent, optionally at `?version=` |
+| `POST` | `/v1/agents/catalog/{agent_id}/clone` | Copy it into your organization |
+
+## List catalog Agents
+
+Cursor pagination with `limit` (1-100), `after`, and `order`.
+
+```bash
+curl "https://api.sandbase.ai/v1/agents/catalog?limit=20" \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+```
+
+Each entry has `id`, `object: "agent"`, `visibility: "public"`, `version`, `name`, `description`, `model`, `created_at`, and `updated_at`.
+
+## Get a catalog Agent
+
+```bash
+curl "https://api.sandbase.ai/v1/agents/catalog/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60?version=2" \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+```
+
+`version` is optional and must be a positive integer. The detail response adds a read-only `configuration` object (name, description, model, instructions, runtime profile, metadata, tools, text, reasoning, and Skills).
+
+## Clone a catalog Agent
+
+```bash
+curl -X POST https://api.sandbase.ai/v1/agents/catalog/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60/clone \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: clone-research-agent" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "My research assistant"}'
+```
+
+The body accepts optional `version` (positive integer, defaults to the current public version) and `name` (up to 128 characters). The response is `201 Created` with a full [Agent object](./create) that has a new `agt_` ID in your organization. `Idempotency-Key` is optional; send a stable key so a retry does not create a second copy.
+
+After cloning, [create a Session](/api-reference/sessions/create) to test it, then deploy it as a [Service](/api-reference/services/) or [Schedule](/api-reference/schedules/).
diff --git a/api-reference/agents/create.md b/api-reference/agents/create.md
index d651fb85..69d89aa8 100644
--- a/api-reference/agents/create.md
+++ b/api-reference/agents/create.md
@@ -1,6 +1,6 @@
---
title: Create Agent
-description: Create a reusable, versioned agent configuration.
+description: Create a saved, versioned Agent with POST /v1/agents.
aside: false
outline: false
apiReference:
@@ -8,127 +8,229 @@ apiReference:
operation: Agents
method: POST
path: /v1/agents
- description: Create a reusable Agent configuration that bundles a catalog model, instructions, capabilities, and metadata. Model IDs use vendor/model form. Unknown request fields are accepted and ignored.
+ description: "Create a saved Agent at version 1. The body is strict JSON: unknown fields return 400. multi_agent must be omitted or null."
groups:
+ - title: Headers
+ fields:
+ - name: Idempotency-Key
+ type: string
+ required: false
+ description: Optional. When omitted the service generates one, so a retried request can create a duplicate. Send your own stable key for retries.
- title: Request body
- description: Send a JSON object with the agent configuration.
+ description: JSON object.
fields:
- name: model
- type: string | object
+ type: string
required: true
- description: Available vendor/model identifier or {id, effort?, speed?}. Responses always use the structured object.
+ description: Model ID from the Models API, up to 200 characters.
+ default: deepseek/deepseek-v4-flash
- name: name
type: string
- required: true
- description: Human-readable Agent name.
- - name: description
+ required: false
+ description: Display name, up to 128 characters.
+ - name: instructions
type: string
- description: Short explanation of the Agent's purpose. Null is accepted as an empty string on creation.
- - name: system
+ required: false
+ description: System instructions, up to 256 KiB.
+ - name: runtime_profile
type: string
- description: System instructions that define the Agent's behavior. Null is accepted as an empty string on creation.
- - name: tools
- type: array
- description: Built-in toolset and custom tool configurations. Omission installs the default Agent toolset; explicit null does not.
- - name: mcp_servers
- type: array
- description: MCP server configurations available to the Agent runtime.
+ required: false
+ description: "Execution runtime: codex (default), claude, or mcode."
- name: skills
type: array
- description: Skills available to the Agent runtime.
- - name: handoffs
+ required: false
+ description: 'Skill references: {"skill_id": "skl_…", "version": null | ""}. null uses the Skill default version, resolved when a Session starts.'
+ - name: mcp_connections
+ type: array
+ required: false
+ description: "MCP references: {connection_id, server_label, allowed_tools, required}. Up to 32; server_label must be unique in the Agent."
+ - name: tools
type: array
- description: Agent handoff configurations.
+ required: false
+ description: Up to 64 function tools plus at most one web_search tool. Sessions currently require web_search mode disabled, and client-side function results are not supported yet.
+ - name: text
+ type: object
+ required: false
+ description: format.type text or json_schema (with an object schema); verbosity low, medium, or high.
+ - name: reasoning
+ type: object
+ required: false
+ description: effort none, minimal, low, medium, high, xhigh, or max; summary concise, detailed, or auto.
+ - name: service_tier
+ type: string
+ required: false
+ description: auto (default), default, flex, priority, or fast.
- name: metadata
type: object
- description: Application-defined JSON metadata. The platform-owned _sandbase namespace is stripped from public requests.
+ required: false
+ description: Up to 16 string pairs; keys up to 64 and values up to 512 characters.
+ - name: description
+ type: string
+ required: false
+ description: Longer description, up to 4096 characters.
+ - name: note
+ type: string
+ required: false
+ description: Change note recorded on version 1.
- title: Agent object
- description: Fields returned for an agent resource.
+ description: Returned by create, get, update, archive, unarchive, restore, and clone.
fields:
- name: id
type: string
required: true
- description: Stable identifier beginning with agent_.
+ description: Agent ID with the agt_ prefix.
+ - name: object
+ type: string
+ required: true
+ description: Always agent.
- name: version
type: integer
required: true
- description: Configuration version, beginning at 1 and incrementing after a successful change.
- - name: archived_at
- type: string · null
- description: RFC 3339 timestamp set after the agent is archived.
- - name: created_at
- type: string · RFC 3339
+ description: Current version. Every effective write creates a new version.
+ - name: row_version
+ type: integer
+ required: true
+ description: Compare-and-set token for PATCH, archive, unarchive, and restore.
+ - name: status
+ type: string
+ required: true
+ description: active or archived.
+ - name: projection_status
+ type: string
+ required: true
+ description: Execution readiness of the current version. Sessions, Services, and Schedules need ready; a new Agent can briefly report pending.
+ - name: runtime_profile
+ type: string
+ required: true
+ description: codex, claude, or mcode.
+ - name: visibility
+ type: string
required: true
- description: Time the agent was created.
- - name: updated_at
- type: string · RFC 3339
+ description: private for your Agents; public for catalog entries.
+ - name: created_at · updated_at · archived_at
+ type: integer · null
required: true
- description: Time the current version was last updated.
+ description: Unix seconds. archived_at is null while active.
examples:
- label: cURL
language: bash
- code: |
+ code: |-
curl -X POST https://api.sandbase.ai/v1/agents \
-H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: create-research-agent-1" \
-H "Content-Type: application/json" \
-d '{
- "model": "anthropic/claude-sonnet-5",
- "name": "Research Assistant",
- "system": "Research carefully and cite sources.",
- "tools": [{"type": "agent_toolset_20260401"}]
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
+ "metadata": {
+ "team": "research"
+ }
}'
- - label: Python
+ - label: Python (OpenAI SDK)
language: python
- code: |
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
agent = client.beta.agents.create(
- model="anthropic/claude-sonnet-5",
- name="Research Assistant",
- system="Research carefully and cite sources.",
- tools=[{"type": "agent_toolset_20260401"}],
+ model="deepseek/deepseek-v4-flash",
+ name="Research assistant",
+ instructions="Answer with cited sources.",
+ metadata={"team": "research"},
+ )
+ print(agent.id, agent.version)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ "Idempotency-Key": "create-research-agent-1",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/agents",
+ headers=headers,
+ json={
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
+ "metadata": {
+ "team": "research"
+ }
+ },
)
- print(agent.id)
+ response.raise_for_status()
+ print(response.json())
- label: TypeScript
language: typescript
- code: |
- const agent = await client.beta.agents.create({
- model: 'anthropic/claude-sonnet-5',
- name: 'Research Assistant',
- system: 'Research carefully and cite sources.',
- tools: [{ type: 'agent_toolset_20260401' }],
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Idempotency-Key': 'create-research-agent-1',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
+ "metadata": {
+ "team": "research"
+ }
+ }),
});
- console.log(agent.id);
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
response:
- status: 200 OK
- code: |
+ status: 201 Created
+ code: |-
{
- "id": "agent_01HqR2k7...",
- "type": "agent",
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "object": "agent",
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
+ "tools": [],
+ "text": {
+ "format": {
+ "type": "text"
+ },
+ "verbosity": "medium"
+ },
+ "reasoning": {
+ "effort": null,
+ "summary": null
+ },
+ "service_tier": "auto",
+ "multi_agent": {
+ "enabled": false,
+ "max_concurrent_subagents": null
+ },
+ "visibility": "private",
+ "status": "active",
"version": 1,
- "name": "Research Assistant",
+ "row_version": 1,
+ "projection_status": "ready",
+ "runtime_profile": "codex",
"description": "",
- "model": {"id": "anthropic/claude-sonnet-5"},
- "system": "Research carefully and cite sources.",
- "tools": [{ "type": "agent_toolset_20260401" }],
- "mcp_servers": null,
- "skills": null,
- "handoffs": null,
- "metadata": null,
- "archived_at": null,
- "created_at": "2026-05-29T10:00:00Z",
- "updated_at": "2026-05-29T10:00:00Z"
+ "metadata": {
+ "team": "research"
+ },
+ "mcp_connections": [],
+ "skills": [],
+ "created_at": 1791273600,
+ "updated_at": 1791273600,
+ "archived_at": null
}
- notes:
- - title: Model choice
- description: SandBase is multi-model. The model field accepts any available model identifier from the SandBase catalog.
- - title: Tool execution
- description: Built-in tools run inside the managed agent runtime. Custom tools are executed by your client after an agent.custom_tool_use event.
- errors:
- - status: 400
- type: invalid_request
- description: A required field is missing or a field has an invalid value.
- - status: 401
- type: authentication_error
- description: The API key is missing or invalid.
---
diff --git a/api-reference/agents/delete.md b/api-reference/agents/delete.md
new file mode 100644
index 00000000..98f46072
--- /dev/null
+++ b/api-reference/agents/delete.md
@@ -0,0 +1,74 @@
+---
+title: Delete Agent
+description: Delete (archive) an Agent.
+aside: false
+outline: false
+apiReference:
+ title: Delete Agent
+ operation: Agents
+ method: DELETE
+ path: /v1/agents/{agent_id}
+ description: Archives the Agent. Agents are never hard-deleted; existing Sessions keep running on their frozen configuration and you can unarchive the Agent later.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: agent_id
+ type: string
+ required: true
+ description: Agent ID (agt_ prefix).
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X DELETE https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ result = client.beta.agents.delete("agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60")
+ print(result.deleted)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "DELETE",
+ "https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60', {
+ method: 'DELETE',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "object": "agent.deleted",
+ "deleted": true
+ }
+---
+
+
diff --git a/api-reference/agents/get-version.md b/api-reference/agents/get-version.md
index 8f403add..8937a54f 100644
--- a/api-reference/agents/get-version.md
+++ b/api-reference/agents/get-version.md
@@ -1,6 +1,6 @@
---
title: Get Agent Version
-description: Retrieve one immutable Agent snapshot by version resource ID or version number.
+description: Read one immutable Agent version.
aside: false
outline: false
apiReference:
@@ -8,51 +8,66 @@ apiReference:
operation: Agents
method: GET
path: /v1/agents/{agent_id}/versions/{version}
- description: Retrieve an immutable historical Agent snapshot. The version path value accepts either a version resource ID or a positive numeric version.
+ description: Read one version snapshot. version must be a positive integer, otherwise 400. created_at is an ISO 8601 string here, while Agent objects use Unix seconds. note appears only when the change that created the version sent one.
groups:
- title: Path parameters
fields:
- name: agent_id
type: string
required: true
- description: Stable identifier of the parent Agent.
+ description: Agent ID (agt_ prefix).
- name: version
- type: string | integer
+ type: integer
required: true
- description: Immutable version resource ID or positive numeric version.
+ description: Version number, starting at 1.
examples:
- label: cURL
language: bash
- code: |
- curl "https://api.sandbase.ai/v1/agents/agent_01HqR2k7.../versions/2" \
+ code: |-
+ curl -X GET https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60/versions/1 \
-H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60/versions/1",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60/versions/1', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
response:
status: 200 OK
- code: |
+ code: |-
{
- "id": "agent_01HqR2k7...",
- "type": "agent",
- "version": 2,
- "name": "Research Assistant",
- "description": "A general-purpose research Agent.",
- "model": {"id": "anthropic/claude-sonnet-5", "effort": {"type": "high"}, "speed": "standard"},
- "system": "Always cite sources.",
- "tools": [],
- "mcp_servers": [],
- "skills": [],
- "handoffs": [],
- "metadata": {},
- "created_at": "2026-05-29T11:00:00Z",
- "updated_at": "2026-05-29T11:00:00Z",
- "archived_at": null
+ "version": 1,
+ "agent": {
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources."
+ },
+ "checksum": "d04d38eb890a1273fa7965a6a7cdc2426232326e43b901d1aad3ed669455acbf",
+ "created_at": "2026-10-05T08:00:00.000Z"
}
- errors:
- - status: 401
- type: authentication_error
- description: The API key is missing or invalid.
- - status: 404
- type: not_found
- description: The Agent or requested version does not exist.
---
diff --git a/api-reference/agents/get.md b/api-reference/agents/get.md
index 4ea80d56..64a98d8d 100644
--- a/api-reference/agents/get.md
+++ b/api-reference/agents/get.md
@@ -1,6 +1,6 @@
---
title: Get Agent
-description: Retrieve an agent by ID, optionally pinned to a version.
+description: Read one Agent by ID.
aside: false
outline: false
apiReference:
@@ -8,66 +8,139 @@ apiReference:
operation: Agents
method: GET
path: /v1/agents/{agent_id}
- description: Retrieve the latest version of an agent, or request a specific historical version.
+ description: Read an Agent, including archived Agents.
groups:
- title: Path parameters
fields:
- name: agent_id
type: string
required: true
- description: Unique agent identifier beginning with agent_.
- - title: Query parameters
+ description: Agent ID (agt_ prefix).
+ - title: Agent object
+ description: Returned by create, get, update, archive, unarchive, restore, and clone.
fields:
+ - name: id
+ type: string
+ required: true
+ description: Agent ID with the agt_ prefix.
+ - name: object
+ type: string
+ required: true
+ description: Always agent.
- name: version
type: integer
- description: Historical version to retrieve. Omit this field to return the latest version.
+ required: true
+ description: Current version. Every effective write creates a new version.
+ - name: row_version
+ type: integer
+ required: true
+ description: Compare-and-set token for PATCH, archive, unarchive, and restore.
+ - name: status
+ type: string
+ required: true
+ description: active or archived.
+ - name: projection_status
+ type: string
+ required: true
+ description: Execution readiness of the current version. Sessions, Services, and Schedules need ready; a new Agent can briefly report pending.
+ - name: runtime_profile
+ type: string
+ required: true
+ description: codex, claude, or mcode.
+ - name: visibility
+ type: string
+ required: true
+ description: private for your Agents; public for catalog entries.
+ - name: created_at · updated_at · archived_at
+ type: integer · null
+ required: true
+ description: Unix seconds. archived_at is null while active.
examples:
- label: cURL
language: bash
- code: |
- curl "https://api.sandbase.ai/v1/agents/agent_01HqR2k7..." \
+ code: |-
+ curl -X GET https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60 \
-H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ agent = client.beta.agents.retrieve("agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60")
+ print(agent.version, agent.status)
- label: Python
language: python
- code: |
- agent = client.beta.agents.retrieve(
- agent_id="agent_01HqR2k7..."
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ headers=headers,
)
- print(agent.name, agent.version)
+ response.raise_for_status()
+ print(response.json())
- label: TypeScript
language: typescript
- code: |
- const agent = await client.beta.agents.retrieve(
- 'agent_01HqR2k7...'
- );
- console.log(agent.name, agent.version);
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
response:
status: 200 OK
- code: |
+ code: |-
{
- "id": "agent_01HqR2k7...",
- "type": "agent",
- "version": 1,
- "name": "Research Assistant",
- "description": "A general-purpose research agent.",
- "model": {"id": "anthropic/claude-sonnet-5", "effort": {"type": "high"}, "speed": "standard"},
- "system": "You are a research assistant...",
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "object": "agent",
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
"tools": [],
- "mcp_servers": [],
+ "text": {
+ "format": {
+ "type": "text"
+ },
+ "verbosity": "medium"
+ },
+ "reasoning": {
+ "effort": null,
+ "summary": null
+ },
+ "service_tier": "auto",
+ "multi_agent": {
+ "enabled": false,
+ "max_concurrent_subagents": null
+ },
+ "visibility": "private",
+ "status": "active",
+ "version": 1,
+ "row_version": 1,
+ "projection_status": "ready",
+ "runtime_profile": "codex",
+ "description": "",
+ "metadata": {
+ "team": "research"
+ },
+ "mcp_connections": [],
"skills": [],
- "handoffs": [],
- "metadata": {},
- "archived_at": null,
- "created_at": "2026-05-29T10:00:00Z",
- "updated_at": "2026-05-29T10:00:00Z"
+ "created_at": 1791273600,
+ "updated_at": 1791273600,
+ "archived_at": null
}
- errors:
- - status: 401
- type: authentication_error
- description: The API key is missing or invalid.
- - status: 404
- type: not_found
- description: The agent or requested version does not exist.
---
diff --git a/api-reference/agents/index.md b/api-reference/agents/index.md
index 47682cd9..b9326c46 100644
--- a/api-reference/agents/index.md
+++ b/api-reference/agents/index.md
@@ -1,31 +1,57 @@
---
title: Agents API
-description: Create and manage versioned Agent definitions.
+description: Create, version, archive, and clone saved Agent configurations.
---
-
# Agents API
-An Agent is a reusable, versioned definition containing a model, instructions, tools, and Skills. Creating or updating an Agent does not run it. Use a [Session](/api-reference/sessions/) for an interactive execution, a [Service](/api-reference/endpoints/) for a stable callable surface, or a [Schedule](/api-reference/deployments/) for recurring work. Schedule resources retain the `/v1/deployments` path for API compatibility.
+An Agent is a saved, versioned configuration: a model, instructions, a runtime profile, Skills, MCP connections, and output settings. Creating or updating an Agent does not run it. Run it in a [Session](/api-reference/sessions/), deploy a pinned version as a [Service](/api-reference/services/) for on-demand Runs, or as a [Schedule](/api-reference/schedules/) for recurring Runs.
-Agent model IDs use the public `vendor/model` identity returned by the [Models API](/api-reference/models/). Effective updates create immutable versions; no-op updates keep the current version number.
+The Agents API follows the OpenAI Agents protocol (`client.beta.agents` in the OpenAI Python SDK). The `OpenAI-Beta` header is optional; when you send it, it must be `agents=v1`. SandBase adds versions, compare-and-set updates, archive and restore, and a public catalog.
## Agent operations
| Method | Path | Purpose |
|---|---|---|
-| `POST` | `/v1/agents` | [Create an Agent](./create) |
+| `POST` | `/v1/agents` | [Create an Agent](./create) at version 1 |
| `GET` | `/v1/agents` | [List Agents](./list) |
| `GET` | `/v1/agents/{agent_id}` | [Get an Agent](./get) |
-| `POST` | `/v1/agents/{agent_id}` | [Update an Agent](./update) and create a version |
+| `POST` | `/v1/agents/{agent_id}` | [Update an Agent](./update) (SDK-style partial update) |
+| `PATCH` | `/v1/agents/{agent_id}` | [Replace with compare-and-set](./replace) on `row_version` |
+| `DELETE` | `/v1/agents/{agent_id}` | [Delete](./delete), which archives the Agent |
| `POST` | `/v1/agents/{agent_id}/archive` | [Archive an Agent](./archive) |
+| `POST` | `/v1/agents/{agent_id}/unarchive` | [Unarchive an Agent](./unarchive) |
+| `POST` | `/v1/agents/{agent_id}/restore` | [Restore an earlier version](./restore) as a new version |
| `GET` | `/v1/agents/{agent_id}/versions` | [List versions](./versions) |
-| `GET` | `/v1/agents/{agent_id}/versions/{version}` | [Get one immutable version](./get-version) by resource ID or number. |
+| `GET` | `/v1/agents/{agent_id}/versions/{version}` | [Get one version](./get-version) |
+| `GET` | `/v1/agents/catalog` | [Browse the public catalog](./catalog) |
+| `POST` | `/v1/agents/catalog/{agent_id}/clone` | [Clone a catalog Agent](./catalog#clone-a-catalog-agent) into your organization |
+
+Agent IDs use the `agt_` prefix. Request bodies are strict JSON up to 1 MiB; unknown fields return 400.
+
+## Versions and updates
+
+- Every effective write (create, update, replace, restore) produces an immutable version. `version` is the current version number.
+- `POST /v1/agents/{agent_id}` is the OpenAI SDK update. It needs no `row_version`, so the last writer wins.
+- `PATCH /v1/agents/{agent_id}` requires the `row_version` you last read and returns 409 when someone else changed the Agent first. Use it from editors and automation that can run concurrently.
+- Services and Schedules pin an Agent version when they are created. New Agent versions do not change them until you update their `agent_version`.
+- Each Session freezes the Agent version, Skill versions, and MCP connection configuration it started with.
+
+## Readiness
+
+A newly written version reports `projection_status` while it is prepared for execution. Sessions, Services, and Schedules need an active Agent whose `projection_status` is `ready`; otherwise they return 409. Archived Agents also return 409 for new executions.
+
+## Current limits
+
+- `tools` accepts `function` tools and at most one `web_search` tool. Execution currently requires `web_search.mode` to be `disabled`.
+- Function tools can be saved, but Runs that ask the client to return a tool result (`requires_action`) are not supported yet and surface as `502 executor_contract_error`.
+- `multi_agent` must be omitted or `null`.
+
+## Pagination
+
+`GET /v1/agents` and the catalog use cursor pagination: `limit` (1-100, default 20), `after`, and `order` (`asc` or `desc`, default `desc`). Responses contain `object: "list"`, `data`, `has_more`, `first_id`, and `last_id`; pass `last_id` as `after` for the next page.
-All endpoints are organization-scoped and require a SandBase API key.
+`GET /v1/agents/{agent_id}/versions` uses offset pagination: `limit` and `offset`, with `data`, `total`, `limit`, and `offset` in the response.
-## Lifecycle
+## Errors
-1. Create an Agent and save its `id`.
-2. Update it with the current `version` to prevent concurrent overwrites.
-3. Pin a Session, Service, or Schedule to a version when reproducibility matters.
-4. Archive the Agent when it should no longer be used for new Sessions.
+Errors use the Agents envelope `{"error":{"type","code","param":null,"message"}}`. See [Errors](/api-reference/errors#agents-platform-errors) for the code list.
diff --git a/api-reference/agents/list.md b/api-reference/agents/list.md
index 19d33b52..2dfb398f 100644
--- a/api-reference/agents/list.md
+++ b/api-reference/agents/list.md
@@ -1,103 +1,130 @@
---
title: List Agents
-description: List agents for your organization with cursor pagination.
+description: List Agents with cursor pagination.
aside: false
outline: false
apiReference:
title: List Agents
operation: Agents
- signature: AgentListPage agents().list(params = AgentListParams.none())
method: GET
path: /v1/agents
- description: List agents owned by your organization, ordered from most recently created. The response includes an opaque cursor when another page is available.
+ description: "List Agents owned by your organization. Results use cursor pagination: pass last_id as after while has_more is true."
groups:
- title: Query parameters
- description: All parameters are optional.
- schema: AgentListParams params
fields:
+ - name: status
+ type: string
+ required: false
+ description: active, archived, or all. Other values return 400.
+ default: active
- name: limit
type: integer
- description: Number of agents to return. Accepts values from 1 to 100.
+ required: false
+ description: Page size from 1 to 100. Values outside the range return 400.
default: "20"
- - name: page
+ - name: after
+ type: string
+ required: false
+ description: Cursor. Pass last_id from the previous page.
+ - name: order
type: string
- description: Opaque cursor returned in a previous response's next_page field.
- - name: include_archived
- type: boolean
- description: Include agents that have been archived.
- default: "false"
- - name: created_at[gte]
- type: string · RFC 3339
- description: Return agents created at or after this timestamp.
- - name: created_at[lte]
- type: string · RFC 3339
- description: Return agents created at or before this timestamp.
+ required: false
+ description: asc or desc by creation time.
+ default: desc
examples:
- label: cURL
language: bash
- code: |
- curl "https://api.sandbase.ai/v1/agents?limit=20" \
+ code: |-
+ curl -X GET "https://api.sandbase.ai/v1/agents?status=active&limit=20" \
-H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ for agent in client.beta.agents.list(limit=20):
+ print(agent.id, agent.name)
- label: Python
language: python
- code: |
- from anthropic import Anthropic
+ code: |-
+ import os
+ import requests
- client = Anthropic(
- api_key="sk-YOUR_KEY",
- base_url="https://api.sandbase.ai",
- )
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
- page = client.beta.agents.list(limit=20)
- for agent in page.data:
- print(agent.id, agent.name)
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/agents?status=active&limit=20",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
- label: TypeScript
language: typescript
- code: |
- import Anthropic from '@anthropic-ai/sdk';
-
- const client = new Anthropic({
- apiKey: process.env.SANDBASE_API_KEY,
- baseURL: 'https://api.sandbase.ai',
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents?status=active&limit=20', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
});
- const page = await client.beta.agents.list({ limit: 20 });
- page.data.forEach((agent) => console.log(agent.id, agent.name));
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
response:
status: 200 OK
- code: |
+ code: |-
{
+ "object": "list",
"data": [
{
- "id": "agent_01HqR2k7...",
- "type": "agent",
- "version": 1,
- "name": "Research Assistant",
- "description": "A general-purpose research Agent.",
- "model": {"id": "anthropic/claude-sonnet-5", "effort": {"type": "high"}, "speed": "standard"},
- "system": "You are a research assistant...",
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "object": "agent",
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
"tools": [],
- "mcp_servers": [],
+ "text": {
+ "format": {
+ "type": "text"
+ },
+ "verbosity": "medium"
+ },
+ "reasoning": {
+ "effort": null,
+ "summary": null
+ },
+ "service_tier": "auto",
+ "multi_agent": {
+ "enabled": false,
+ "max_concurrent_subagents": null
+ },
+ "visibility": "private",
+ "status": "active",
+ "version": 1,
+ "row_version": 1,
+ "projection_status": "ready",
+ "runtime_profile": "codex",
+ "description": "",
+ "metadata": {
+ "team": "research"
+ },
+ "mcp_connections": [],
"skills": [],
- "handoffs": [],
- "metadata": {},
- "archived_at": null,
- "created_at": "2026-05-29T10:00:00Z",
- "updated_at": "2026-05-29T10:00:00Z"
+ "created_at": 1791273600,
+ "updated_at": 1791273600,
+ "archived_at": null
}
],
- "next_page": "page_eyJjIjoi..."
+ "has_more": false,
+ "first_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "last_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60"
}
- notes:
- - title: Pagination
- description: Results are ordered by created_at descending. Pass next_page back as page; when next_page is absent, you have reached the final page.
- errors:
- - status: 400
- type: invalid_request
- description: The page cursor is malformed.
- - status: 401
- type: authentication_error
- description: The API key is missing or invalid.
---
diff --git a/api-reference/agents/replace.md b/api-reference/agents/replace.md
new file mode 100644
index 00000000..ed902562
--- /dev/null
+++ b/api-reference/agents/replace.md
@@ -0,0 +1,179 @@
+---
+title: Replace Agent (CAS)
+description: Update an Agent with compare-and-set on row_version.
+aside: false
+outline: false
+apiReference:
+ title: Replace Agent (CAS)
+ operation: Agents
+ method: PATCH
+ path: /v1/agents/{agent_id}
+ description: Product update with compare-and-set. row_version is required and must equal the current value, otherwise 409. Supplied fields replace saved values; explicit null values return 400. Use this form when several editors can change the same Agent.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: agent_id
+ type: string
+ required: true
+ description: Agent ID (agt_ prefix).
+ - title: Headers
+ fields:
+ - name: Idempotency-Key
+ type: string
+ required: false
+ description: Optional. When omitted the service generates one, so a retried request can create a duplicate. Send your own stable key for retries.
+ - title: Request body
+ description: row_version plus any subset of the Agent fields.
+ fields:
+ - name: row_version
+ type: integer
+ required: true
+ description: The row_version you last read. A stale value returns 409.
+ - name: model
+ type: string
+ required: false
+ description: Model ID from the Models API, up to 200 characters.
+ - name: name
+ type: string
+ required: false
+ description: Display name, up to 128 characters.
+ - name: instructions
+ type: string
+ required: false
+ description: System instructions, up to 256 KiB.
+ - name: runtime_profile
+ type: string
+ required: false
+ description: "Execution runtime: codex (default), claude, or mcode."
+ - name: skills
+ type: array
+ required: false
+ description: 'Skill references: {"skill_id": "skl_…", "version": null | ""}. null uses the Skill default version, resolved when a Session starts.'
+ - name: mcp_connections
+ type: array
+ required: false
+ description: "MCP references: {connection_id, server_label, allowed_tools, required}. Up to 32; server_label must be unique in the Agent."
+ - name: tools
+ type: array
+ required: false
+ description: Up to 64 function tools plus at most one web_search tool. Sessions currently require web_search mode disabled, and client-side function results are not supported yet.
+ - name: text
+ type: object
+ required: false
+ description: format.type text or json_schema (with an object schema); verbosity low, medium, or high.
+ - name: reasoning
+ type: object
+ required: false
+ description: effort none, minimal, low, medium, high, xhigh, or max; summary concise, detailed, or auto.
+ - name: service_tier
+ type: string
+ required: false
+ description: auto (default), default, flex, priority, or fast.
+ - name: metadata
+ type: object
+ required: false
+ description: Up to 16 string pairs; keys up to 64 and values up to 512 characters.
+ - name: description
+ type: string
+ required: false
+ description: Up to 4096 characters.
+ - name: note
+ type: string
+ required: false
+ description: Change note recorded on the new version.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X PATCH https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "row_version": 2,
+ "description": "Research Agent used by the weekly brief.",
+ "note": "Add description"
+ }'
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "PATCH",
+ "https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ headers=headers,
+ json={
+ "row_version": 2,
+ "description": "Research Agent used by the weekly brief.",
+ "note": "Add description"
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60', {
+ method: 'PATCH',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "row_version": 2,
+ "description": "Research Agent used by the weekly brief.",
+ "note": "Add description"
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "object": "agent",
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
+ "tools": [],
+ "text": {
+ "format": {
+ "type": "text"
+ },
+ "verbosity": "medium"
+ },
+ "reasoning": {
+ "effort": null,
+ "summary": null
+ },
+ "service_tier": "auto",
+ "multi_agent": {
+ "enabled": false,
+ "max_concurrent_subagents": null
+ },
+ "visibility": "private",
+ "status": "active",
+ "version": 3,
+ "row_version": 3,
+ "projection_status": "ready",
+ "runtime_profile": "codex",
+ "description": "Research Agent used by the weekly brief.",
+ "metadata": {
+ "team": "research"
+ },
+ "mcp_connections": [],
+ "skills": [],
+ "created_at": 1791273600,
+ "updated_at": 1791273600,
+ "archived_at": null
+ }
+---
+
+
diff --git a/api-reference/agents/restore.md b/api-reference/agents/restore.md
new file mode 100644
index 00000000..d6cbf303
--- /dev/null
+++ b/api-reference/agents/restore.md
@@ -0,0 +1,128 @@
+---
+title: Restore Agent Version
+description: Restore an earlier Agent version as the new current version.
+aside: false
+outline: false
+apiReference:
+ title: Restore Agent Version
+ operation: Agents
+ method: POST
+ path: /v1/agents/{agent_id}/restore
+ description: Copy the configuration of source_version into a new current version. History is never rewritten. Services and Schedules stay pinned to the version they were created with until you update them.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: agent_id
+ type: string
+ required: true
+ description: Agent ID (agt_ prefix).
+ - title: Headers
+ fields:
+ - name: Idempotency-Key
+ type: string
+ required: false
+ description: Optional. When omitted the service generates one, so a retried request can create a duplicate. Send your own stable key for retries.
+ - title: Request body
+ description: JSON object.
+ fields:
+ - name: source_version
+ type: integer
+ required: true
+ description: Earlier version to restore.
+ - name: row_version
+ type: integer
+ required: true
+ description: The row_version you last read. A stale value returns 409.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60/restore \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "source_version": 1,
+ "row_version": 3
+ }'
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60/restore",
+ headers=headers,
+ json={
+ "source_version": 1,
+ "row_version": 3
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60/restore', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "source_version": 1,
+ "row_version": 3
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "object": "agent",
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
+ "tools": [],
+ "text": {
+ "format": {
+ "type": "text"
+ },
+ "verbosity": "medium"
+ },
+ "reasoning": {
+ "effort": null,
+ "summary": null
+ },
+ "service_tier": "auto",
+ "multi_agent": {
+ "enabled": false,
+ "max_concurrent_subagents": null
+ },
+ "visibility": "private",
+ "status": "active",
+ "version": 4,
+ "row_version": 4,
+ "projection_status": "ready",
+ "runtime_profile": "codex",
+ "description": "",
+ "metadata": {
+ "team": "research"
+ },
+ "mcp_connections": [],
+ "skills": [],
+ "created_at": 1791273600,
+ "updated_at": 1791273600,
+ "archived_at": null
+ }
+---
+
+
diff --git a/api-reference/agents/unarchive.md b/api-reference/agents/unarchive.md
new file mode 100644
index 00000000..96c6ded6
--- /dev/null
+++ b/api-reference/agents/unarchive.md
@@ -0,0 +1,121 @@
+---
+title: Unarchive Agent
+description: Unarchive Agent with row_version.
+aside: false
+outline: false
+apiReference:
+ title: Unarchive Agent
+ operation: Agents
+ method: POST
+ path: /v1/agents/{agent_id}/unarchive
+ description: Return an archived Agent to active with compare-and-set.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: agent_id
+ type: string
+ required: true
+ description: Agent ID (agt_ prefix).
+ - title: Headers
+ fields:
+ - name: Idempotency-Key
+ type: string
+ required: false
+ description: Optional. When omitted the service generates one, so a retried request can create a duplicate. Send your own stable key for retries.
+ - title: Request body
+ description: JSON object.
+ fields:
+ - name: row_version
+ type: integer
+ required: true
+ description: The row_version you last read. A stale value returns 409.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60/unarchive \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "row_version": 3
+ }'
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60/unarchive",
+ headers=headers,
+ json={
+ "row_version": 3
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60/unarchive', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "row_version": 3
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "object": "agent",
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
+ "tools": [],
+ "text": {
+ "format": {
+ "type": "text"
+ },
+ "verbosity": "medium"
+ },
+ "reasoning": {
+ "effort": null,
+ "summary": null
+ },
+ "service_tier": "auto",
+ "multi_agent": {
+ "enabled": false,
+ "max_concurrent_subagents": null
+ },
+ "visibility": "private",
+ "status": "active",
+ "version": 3,
+ "row_version": 4,
+ "projection_status": "ready",
+ "runtime_profile": "codex",
+ "description": "",
+ "metadata": {
+ "team": "research"
+ },
+ "mcp_connections": [],
+ "skills": [],
+ "created_at": 1791273600,
+ "updated_at": 1791273600,
+ "archived_at": null
+ }
+---
+
+
diff --git a/api-reference/agents/update.md b/api-reference/agents/update.md
index 8fb63cb3..7d537b73 100644
--- a/api-reference/agents/update.md
+++ b/api-reference/agents/update.md
@@ -1,6 +1,6 @@
---
title: Update Agent
-description: Update an agent and create a new version.
+description: Partially update an Agent with the OpenAI SDK-style POST update.
aside: false
outline: false
apiReference:
@@ -8,117 +8,167 @@ apiReference:
operation: Agents
method: POST
path: /v1/agents/{agent_id}
- description: Update an Agent configuration. Every effective change creates a new immutable version; a no-op returns the current version unchanged. Existing Sessions keep the version they started with. Unknown request fields are accepted and ignored.
+ description: SDK-style partial update. Supplied fields replace the saved values and create a new version; omitted fields keep their values. No row_version is needed. description and note are not accepted here; use Replace (PATCH) for them. Archived Agents return 404.
groups:
- title: Path parameters
fields:
- name: agent_id
type: string
required: true
- description: Unique agent identifier beginning with agent_.
- - title: Request body
+ description: Agent ID (agt_ prefix).
+ - title: Headers
fields:
- - name: version
- type: integer
+ - name: Idempotency-Key
+ type: string
required: false
- description: Optional current Agent version for optimistic locking. When omitted, the server applies the update to the version it just loaded.
+ description: Optional. When omitted the service generates one, so a retried request can create a duplicate. Send your own stable key for retries.
+ - title: Request body
+ description: Any subset of these fields. Other fields return 400.
+ fields:
- name: model
type: string
- description: Replacement model identifier. Omit to preserve the current value.
+ required: false
+ description: Model ID from the Models API, up to 200 characters.
- name: name
type: string
- description: Replacement name. Omit to preserve the current value.
- - name: description
+ required: false
+ description: Display name, up to 128 characters.
+ - name: instructions
type: string
- description: Replacement description. Send an empty string to clear it; null is treated as omitted and preserves the current value.
- - name: system
+ required: false
+ description: System instructions, up to 256 KiB.
+ - name: runtime_profile
type: string
- description: Replacement system instructions. Send an empty string to clear them; null is treated as omitted and preserves the current value.
- - name: tools
- type: array · null
- description: Full replacement tool list. Send an empty array or null to clear all tools.
- - name: mcp_servers
- type: array · null
- description: Full replacement MCP server configuration list.
+ required: false
+ description: "Execution runtime: codex (default), claude, or mcode."
- name: skills
- type: array · null
- description: Full replacement Skill list.
- - name: handoffs
- type: array · null
- description: Full replacement handoff configuration list.
+ type: array
+ required: false
+ description: 'Skill references: {"skill_id": "skl_…", "version": null | ""}. null uses the Skill default version, resolved when a Session starts.'
+ - name: mcp_connections
+ type: array
+ required: false
+ description: "MCP references: {connection_id, server_label, allowed_tools, required}. Up to 32; server_label must be unique in the Agent."
+ - name: tools
+ type: array
+ required: false
+ description: Up to 64 function tools plus at most one web_search tool. Sessions currently require web_search mode disabled, and client-side function results are not supported yet.
+ - name: text
+ type: object
+ required: false
+ description: format.type text or json_schema (with an object schema); verbosity low, medium, or high.
+ - name: reasoning
+ type: object
+ required: false
+ description: effort none, minimal, low, medium, high, xhigh, or max; summary concise, detailed, or auto.
+ - name: service_tier
+ type: string
+ required: false
+ description: auto (default), default, flex, priority, or fast.
- name: metadata
- type: object · null
- description: Full replacement metadata object. Omitted metadata is preserved; null clears caller-owned metadata. Supplied metadata replaces caller-owned metadata rather than merging individual keys, while the platform-owned _sandbase namespace remains unchanged.
+ type: object
+ required: false
+ description: Up to 16 string pairs; keys up to 64 and values up to 512 characters.
examples:
- label: cURL
language: bash
- code: |
- curl -X POST \
- "https://api.sandbase.ai/v1/agents/agent_01HqR2k7..." \
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60 \
-H "Authorization: Bearer $SANDBASE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
- "version": 1,
- "system": "Research carefully and always cite sources."
+ "instructions": "Answer in five bullet points with cited sources."
}'
- - label: Python
+ - label: Python (OpenAI SDK)
language: python
- code: |
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
agent = client.beta.agents.update(
- agent_id="agent_01HqR2k7...",
- version=1,
- system="Research carefully and always cite sources.",
+ "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ instructions="Answer in five bullet points with cited sources.",
)
print(agent.version)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ headers=headers,
+ json={
+ "instructions": "Answer in five bullet points with cited sources."
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
- label: TypeScript
language: typescript
- code: |
- const agent = await client.beta.agents.update(
- 'agent_01HqR2k7...',
- {
- version: 1,
- system: 'Research carefully and always cite sources.',
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Content-Type': 'application/json',
},
- );
- console.log(agent.version);
+ body: JSON.stringify({
+ "instructions": "Answer in five bullet points with cited sources."
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
response:
status: 200 OK
- code: |
+ code: |-
{
- "id": "agent_01HqR2k7...",
- "type": "agent",
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "object": "agent",
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer in five bullet points with cited sources.",
+ "tools": [],
+ "text": {
+ "format": {
+ "type": "text"
+ },
+ "verbosity": "medium"
+ },
+ "reasoning": {
+ "effort": null,
+ "summary": null
+ },
+ "service_tier": "auto",
+ "multi_agent": {
+ "enabled": false,
+ "max_concurrent_subagents": null
+ },
+ "visibility": "private",
+ "status": "active",
"version": 2,
- "name": "Research Assistant",
+ "row_version": 2,
+ "projection_status": "ready",
+ "runtime_profile": "codex",
"description": "",
- "model": {"id": "anthropic/claude-sonnet-5", "effort": {"type": "high"}, "speed": "standard"},
- "system": "Research carefully and always cite sources.",
- "tools": [{"type":"agent_toolset_20260401"}],
- "mcp_servers": null,
- "skills": null,
- "handoffs": null,
- "metadata": null,
- "created_at": "2026-05-29T10:00:00Z",
- "updated_at": "2026-05-29T11:00:00Z",
+ "metadata": {
+ "team": "research"
+ },
+ "mcp_connections": [],
+ "skills": [],
+ "created_at": 1791273600,
+ "updated_at": 1791273600,
"archived_at": null
}
- notes:
- - title: Optimistic locking
- description: When version is supplied, a stale value returns 409 conflict. Fetch the current Agent, reapply the change, and retry with the latest version.
- - title: Array replacement
- description: Array fields are not merged. Read the current agent and send the complete intended list when changing tools, MCP servers, Skills, or handoffs.
- errors:
- - status: 400
- type: invalid_request
- description: One or more fields have invalid values.
- - status: 401
- type: authentication_error
- description: The API key is missing or invalid.
- - status: 404
- type: not_found
- description: The agent does not exist.
- - status: 409
- type: conflict
- description: The supplied version does not match the current agent version.
---
diff --git a/api-reference/agents/versions.md b/api-reference/agents/versions.md
index 30d9fc07..269cf51e 100644
--- a/api-reference/agents/versions.md
+++ b/api-reference/agents/versions.md
@@ -1,6 +1,6 @@
---
title: List Agent Versions
-description: List immutable historical versions of an Agent, including version IDs and creation metadata for rollback and auditing.
+description: List immutable Agent version snapshots.
aside: false
outline: false
apiReference:
@@ -8,80 +8,81 @@ apiReference:
operation: Agents
method: GET
path: /v1/agents/{agent_id}/versions
- description: List immutable agent configuration snapshots, ordered from newest version to oldest.
+ description: List version snapshots with offset pagination. Each item contains the saved configuration document, checksum, optional note, and an RFC 3339 created_at.
groups:
- title: Path parameters
fields:
- name: agent_id
type: string
required: true
- description: Unique agent identifier beginning with agent_.
+ description: Agent ID (agt_ prefix).
- title: Query parameters
fields:
- name: limit
type: integer
- description: Number of versions to return. Accepts values from 1 to 100.
+ required: false
+ description: Page size. Values above 200 are capped at 200.
default: "20"
- - name: page
- type: string
- description: Opaque cursor returned in a previous response's next_page field.
+ - name: offset
+ type: integer
+ required: false
+ description: Number of records to skip.
+ default: "0"
examples:
- label: cURL
language: bash
- code: |
- curl "https://api.sandbase.ai/v1/agents/agent_01HqR2k7.../versions" \
+ code: |-
+ curl -X GET "https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60/versions?limit=20&offset=0" \
-H "Authorization: Bearer $SANDBASE_API_KEY"
- label: Python
language: python
- code: |
- page = client.beta.agents.versions.list(
- agent_id="agent_01HqR2k7..."
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60/versions?limit=20&offset=0",
+ headers=headers,
)
- for item in page.data:
- print(item.version, item.created_at)
+ response.raise_for_status()
+ print(response.json())
- label: TypeScript
language: typescript
- code: |
- const page = await client.beta.agents.versions.list(
- 'agent_01HqR2k7...'
- );
- page.data.forEach((item) => console.log(item.version));
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60/versions?limit=20&offset=0', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
response:
status: 200 OK
- code: |
+ code: |-
{
"data": [
{
- "id": "agent_01HqR2k7...",
- "type": "agent",
"version": 2,
- "name": "Research Assistant",
- "description": "A general-purpose research Agent.",
- "model": {"id": "anthropic/claude-sonnet-5", "effort": {"type": "high"}, "speed": "standard"},
- "system": "Always cite sources.",
- "tools": [],
- "mcp_servers": [],
- "skills": [],
- "handoffs": [],
- "metadata": {},
- "created_at": "2026-05-29T11:00:00Z",
- "updated_at": "2026-05-29T11:00:00Z",
- "archived_at": null
+ "agent": {
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer in five bullet points with cited sources."
+ },
+ "checksum": "d04d38eb890a1273fa7965a6a7cdc2426232326e43b901d1aad3ed669455acbf",
+ "created_at": "2026-10-06T08:00:00.000Z"
}
- ]
+ ],
+ "total": 2,
+ "limit": 20,
+ "offset": 0
}
- notes:
- - title: Immutable Agent projections
- description: Each item is the full Agent projection at that immutable version. No-op updates do not create a version.
- - title: Pagination
- description: When another page is available, the response includes next_page. The field is absent on the final page.
- errors:
- - status: 401
- type: authentication_error
- description: The API key is missing or invalid.
- - status: 404
- type: not_found
- description: The agent does not exist.
---
diff --git a/api-reference/authentication.md b/api-reference/authentication.md
index 1ea2c1c5..ee741a11 100644
--- a/api-reference/authentication.md
+++ b/api-reference/authentication.md
@@ -44,7 +44,24 @@ Gemini GenerateContent and Interactions operations accept `x-goog-api-key`, `Aut
### Extraction Logic
Standard endpoints read `Authorization: Bearer ` only. The Anthropic Messages middleware reads `x-api-key`
-first and then falls back to the Bearer header. A missing supported credential returns `401 Unauthorized`.
+first and then falls back to the Bearer header. Agents platform resources are the other exception: they accept
+`Authorization: Bearer` or `X-API-Key` (see below). A missing supported credential returns `401 Unauthorized`.
+
+### Agents platform resources
+
+`/v1/agents*` (including `/v1/agents/sessions*`), `/v1/services*`, `/v1/schedules*`, `/v1/mcp-connections*`,
+`/v1/secrets*`, and `/v1/skills*` accept the key in either header:
+
+```bash
+curl https://api.sandbase.ai/v1/agents \
+ -H "X-API-Key: $SANDBASE_API_KEY"
+```
+
+- If both `Authorization: Bearer` and `X-API-Key` are sent, they must carry the same key; different values return `401 unauthorized`. Repeating either header also returns 401.
+- `OpenAI-Beta` is optional. If present, it must be `agents=v1`.
+- Read requests need the `agents.read` permission and writes need `agents.write`. Standard Console-created keys have both. Scoped SandBase-issued keys, such as CLI Login keys, return `403 insufficient_scope`.
+- `402 spending_limit_exceeded` means the key's spending limit or the organization balance is exhausted.
+- Failures use the [Agents error envelope](/api-reference/errors#agents-platform-errors), not the flat `{"error": "…"}` form shown below.
## API key format
@@ -129,6 +146,7 @@ Keys can optionally have an expiration date. Expired keys return `401 Unauthoriz
| `POST /v1/messages` | API Key (Bearer or x-api-key) | Anthropic Messages |
| `/v1beta/models/*` | API Key (x-goog-api-key, Bearer, or query key) | Gemini GenerateContent |
| `/v1beta/interactions*` | API Key (x-goog-api-key, Bearer, or query key) | Gemini Interactions |
+| `/v1/agents*`, `/v1/services*`, `/v1/schedules*`, `/v1/mcp-connections*`, `/v1/secrets*`, `/v1/skills*` | API Key (Bearer or X-API-Key) | Agents platform resources |
The SandBase Console uses a separate browser authentication flow. Its internal requests are not supported public
endpoints and must not be used by integrations.
diff --git a/api-reference/credentials/index.md b/api-reference/credentials/index.md
index cb9055d8..ec05b39b 100644
--- a/api-reference/credentials/index.md
+++ b/api-reference/credentials/index.md
@@ -1,87 +1,11 @@
---
-title: Credentials API
-description: Create and manage private values injected into SandBase Agent runtimes.
+title: Credential resources (retired)
+description: Credential resources that injected runtime environment variables have been retired. Use Secrets with MCP connections.
+robots: noindex,follow
---
-# Credentials API
+# Credential resources (retired)
-Credentials store API keys, tokens, and other private values used by an Agent at runtime. Values are scoped to your organization and are masked after creation.
+The Credential resources, which injected private values into Agent runtimes as environment variables, have been retired. Existing credential records are not migrated.
-::: warning Server-side only
-Credential endpoints require a secret SandBase API key. Never call them from browser code or expose credential values in prompts, logs, or source control.
-:::
-
-## Credential operations
-
-| Method | Path | Purpose |
-|---|---|---|
-| `POST` | `/v1/credentials` | Create a credential. |
-| `GET` | `/v1/credentials` | List all credentials in the organization. |
-| `GET` | `/v1/credentials/{credential_id}` | Get masked credential metadata. |
-| `PATCH` | `/v1/credentials/{credential_id}` | Update `status`, `strategy`, or `weight`. |
-| `POST` | `/v1/credentials/{credential_id}/rotate` | Replace the stored secret value. |
-
-Credentials do not currently have a delete endpoint. Set `status` to `disabled` with `PATCH` when a value must no longer be selected. Disabled records remain visible to list and get operations.
-
-## Create a credential
-
-```bash
-curl -X POST https://api.sandbase.ai/v1/credentials \
- -H "Authorization: Bearer $SANDBASE_API_KEY" \
- -H "Content-Type: application/json" \
- -d '{
- "agent_id": "agent_01...",
- "scope": "environment",
- "scope_name": "GITHUB_TOKEN",
- "secret_key": "GITHUB_TOKEN",
- "value": "YOUR_SECRET_VALUE"
- }'
-```
-
-Credential IDs use the `sec_` prefix. The plaintext `value` is accepted only on creation or rotation. Retrieval and list responses return masked metadata, never the stored plaintext or encrypted payload. Treat `value_hint` as sensitive metadata because it retains a short prefix and suffix. Credential success responses use the standard `{ "code": 0, "data": ..., "message": "ok" }` envelope.
-
-The only supported selection strategy is `round_robin`, and `weight` must be a positive integer.
-
-`scope`, `scope_name`, and `secret_key` are caller-defined identifiers. SandBase trims their surrounding whitespace and rejects control characters. If `group_key` is omitted, it defaults to `scope_name:secret_key`.
-
-## List credentials
-
-```bash
-curl https://api.sandbase.ai/v1/credentials \
- -H "Authorization: Bearer $SANDBASE_API_KEY"
-```
-
-The response lists credentials in your organization. Secret values are never returned; each record contains masked metadata such as `value_hint` instead.
-
-## Get a credential
-
-```bash
-curl https://api.sandbase.ai/v1/credentials/sec_01... \
- -H "Authorization: Bearer $SANDBASE_API_KEY"
-```
-
-Use the `sec_` credential ID returned by create or list. The response contains the credential's masked metadata and current selection status, but not its stored value.
-
-## Update a credential
-
-```bash
-curl -X PATCH https://api.sandbase.ai/v1/credentials/sec_01... \
- -H "Authorization: Bearer $SANDBASE_API_KEY" \
- -H "Content-Type: application/json" \
- -d '{"status":"disabled"}'
-```
-
-Use update to change `status`, `strategy`, or `weight`. To stop a credential from being selected, set `status` to `disabled`; credentials do not currently have a delete endpoint.
-
-## Rotate a credential
-
-```bash
-curl -X POST https://api.sandbase.ai/v1/credentials/sec_01.../rotate \
- -H "Authorization: Bearer $SANDBASE_API_KEY" \
- -H "Content-Type: application/json" \
- -d '{"value":"YOUR_NEW_SECRET_VALUE"}'
-```
-
-Rotation replaces the encrypted value and `value_hint`, resets `failure_count` to `0`, and clears `cooldown_until`. Other metadata is preserved.
-
-For product-level guidance, see [API Credentials](/agents/api-credentials).
+To give an Agent authenticated access to an external system, register the system as an [MCP connection](/api-reference/mcp-connections/) and store its Bearer token as a [Secret](/api-reference/secrets/). See [Credentials for MCP tools](/agents/api-credentials) for the walkthrough.
diff --git a/api-reference/deployments/index.md b/api-reference/deployments/index.md
index cfe86a21..95326ca4 100644
--- a/api-reference/deployments/index.md
+++ b/api-reference/deployments/index.md
@@ -1,72 +1,11 @@
---
-title: Schedules API
-description: Create, manage, trigger, and inspect SandBase Schedules through the compatibility /v1/deployments resource.
+title: Deployment resources (retired)
+description: Deployment and DeploymentRun resources have been retired. Use Schedules and Services.
+robots: noindex,follow
---
-# Schedules API
+# Deployment resources (retired)
-A **Schedule** binds an Agent to repeatable `initial_events`, runtime settings, timeout policy, and an optional cron expression. For API compatibility, Schedule resources use the `/v1/deployments` path, `depl_*` IDs, and `Deployment` schema name.
+Deployment and DeploymentRun resources have been retired and are no longer served.
-::: info Schedule execution identity
-Every trigger creates a durable `drun_*` `DeploymentRun`. A successful trigger associates that record with exactly one newly created `sess_*` Session; a failed trigger has no Session. `DeploymentRun` and Session are separate resources and neither ID substitutes for the other.
-:::
-
-## Schedule operations
-
-| Method | Path | Purpose |
-|---|---|---|
-| `POST` | `/v1/deployments` | Create a Schedule (`Deployment`). |
-| `GET` | `/v1/deployments` | List Schedules. |
-| `GET` | `/v1/deployments/{deployment_id}` | Get a Schedule and resolved bindings. |
-| `PATCH` or `POST` | `/v1/deployments/{deployment_id}` | Update supported fields. |
-| `DELETE` | `/v1/deployments/{deployment_id}` | [Permanently delete](./delete) an eligible Schedule. |
-| `POST` | `/v1/deployments/{deployment_id}/pause` | Pause scheduled triggers. |
-| `POST` | `/v1/deployments/{deployment_id}/unpause` | Resume scheduled triggers. |
-| `POST` | `/v1/deployments/{deployment_id}/archive` | Archive a Schedule. |
-| `POST` | `/v1/deployments/{deployment_id}/notifications/feishu/test` | Test the saved Feishu notification target. |
-
-## Next steps
-
-- [Create a Schedule](./create)
-- [List Schedules](./list)
-- [Update a Schedule](./update)
-- [Trigger a Schedule](./run)
-- [List runs for one Schedule](./list-runs)
-- [List runs across all Schedules](./list-all-runs)
-
-## Create example
-
-`name`, `agent_id`, and valid `initial_events` are required. `initial_events` must contain at least one `user.message` event. Runtime binding is resolved by SandBase and is not a public request field.
-
-```bash
-curl -X POST https://api.sandbase.ai/v1/deployments \
- -H "Authorization: Bearer $SANDBASE_API_KEY" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "Daily customer research",
- "agent_id": "agent_01...",
- "initial_events": [{
- "type": "user.message",
- "content": [{"type":"text","text":"Prepare the daily customer brief."}]
- }],
- "schedule": {
- "type": "cron",
- "expression": "0 9 * * *",
- "timezone": "Asia/Shanghai"
- }
- }'
-```
-
-The schedule object requires `type: "cron"`, a valid cron `expression`, and an IANA `timezone`. Omit `schedule` only when the Schedule will be triggered manually.
-
-Alternatively, create a declarative Schedule with `name`, `runtime`, and `initial_events` in JSON or YAML. Declarative Schedules resolve their runtime binding from the runtime definition and use the public REST invocation transport. This release supports the `hermes` runtime. Do not combine `runtime` with `agent_id`.
-
-## Schedule run records
-
-Use `POST /v1/deployments/{deployment_id}/runs` to trigger manually. This is the preferred trigger path. `POST /v1/deployments/{deployment_id}/run` remains a compatibility alias for clients using the older singular path. The request body must be empty or `{}`; input overrides are rejected. The response is a DeploymentRun object with `id`, `type`, `deployment_id`, `agent`, `trigger_context`, nullable `session_id`, nullable `error`, and `created_at`.
-
-List one Schedule's records at `/v1/deployments/{deployment_id}/runs`, or query `/v1/deployment_runs` across all Schedules. Retrieve a known run directly with `/v1/deployment_runs/{drun_id}`. The global list accepts `trigger_type=manual|schedule`, status filters `pending|succeeded|failed`, `has_error`, Agent/Deployment IDs, time bounds, and cursor pagination. These filters select trigger outcomes; the current public `DeploymentRun` object does not include a `status` field. A nested get returns `409 deployment_trigger_in_progress` while the record is pending.
-
-Updates support `name`, `initial_events`, `schedule`, `timeout_policy`, `expected_version`, `agent_binding`, `agent_config`, and `notification_settings`. Changing the Agent binding requires a paused Deployment and the current `expected_version` when applicable.
-
-`notification_settings` accepts exactly one `feishu_webhook_url` on the official `open.feishu.cn` custom-bot path. The URL is encrypted and never returned; detail responses expose only `feishu_webhook_configured`. Send `notification_settings: null` or `feishu_webhook_url: null` to remove it. The test endpoint sends a fixed SandBase message to the saved target and accepts only an absent body or `{}`.
+Use the [Schedules API](/api-reference/schedules/) for cron-based and manually triggered Runs, and the [Services API](/api-reference/services/) for on-demand Runs.
diff --git a/api-reference/endpoints/index.md b/api-reference/endpoints/index.md
index 61daec30..df1b119f 100644
--- a/api-reference/endpoints/index.md
+++ b/api-reference/endpoints/index.md
@@ -1,160 +1,11 @@
---
-title: Services API
-description: Publish an Agent as a reusable REST, MCP, or experimental ACP Service.
+title: Endpoint resources (retired)
+description: Endpoint resources and their REST, MCP, and ACP transports have been retired. Use Services.
+robots: noindex,follow
---
-# Services API
+# Endpoint resources (retired)
-A **Service** publishes a tested Agent behind a stable callable interface. Service URLs and IDs retain the existing `/v1/endpoints` path and `ep_` prefix for API compatibility. Public invocation uses REST, MCP, or the experimental ACP protocol.
+Endpoint resources, including their REST, MCP, and ACP invocation transports, have been retired and are no longer served.
-::: info Session identity
-Service invocation accepts an optional `session_id`. When omitted, SandBase creates a persistent Session. When supplied, the message is appended to that Session. No public Run or Runtime Session identity is created.
-:::
-
-## Service operations
-
-| Method | Path | Purpose |
-|---|---|---|
-| `POST` | `/v1/endpoints` | Create a Service from an existing Agent or from a declarative runtime definition. |
-| `GET` | `/v1/endpoints` | List Services with cursor pagination. |
-| `GET` | `/v1/endpoints/{endpoint_id}` | Get a Service. |
-| `PATCH` or `POST` | `/v1/endpoints/{endpoint_id}` | Update an advanced Service. Declarative definitions are immutable. |
-| `DELETE` | `/v1/endpoints/{endpoint_id}` | Delete a Service. |
-
-For the advanced creation mode, `name`, `agent_id`, and an explicit non-empty `protocols` array are required by the public contract. A declarative definition similarly requires `name`, `runtime`, and `protocols`; it cannot be mixed with `agent_id`. Public protocol values are `rest`, `mcp`, and experimental `acp`. Always send the intended values instead of relying on server defaults. Optional advanced fields include `agent_version`, `slug`, and Session configuration. Runtime binding is resolved internally. `session_metadata` is copied to each newly created Session. `memory_config`, `resource_config`, and `vault_config` are reserved fields: SandBase stores and returns them, but does not currently apply them to Session execution.
-
-List requests support cursor pagination and `q` search across IDs, names, slugs, runtimes, and Agent IDs. `search` is retained as an alias for `q`.
-
-For advanced Services, update requests can change the name, slug, bound Agent, protocols, status, and Session configuration. When changing `agent_version`, include the currently bound version as `expected_agent_version` to protect against concurrent upgrades. Declarative Service definitions remain immutable.
-
-### Create a Service
-
-Create a Service with an existing Agent and an explicit public protocol list:
-
-```bash
-curl -X POST https://api.sandbase.ai/v1/endpoints \
- -H "Authorization: Bearer $SANDBASE_API_KEY" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "Research service",
- "agent_id": "agent_01...",
- "protocols": ["rest"]
- }'
-```
-
-The response is `201 Created`. Store its `id` (`ep_...`) for later management and invocation calls.
-
-### List Services
-
-```bash
-curl "https://api.sandbase.ai/v1/endpoints?limit=20" \
- -H "Authorization: Bearer $SANDBASE_API_KEY"
-```
-
-Pass the returned opaque `next_page` value as `page` to request the next page. Do not parse or construct cursors.
-
-### Get a Service
-
-```bash
-curl https://api.sandbase.ai/v1/endpoints/ep_01... \
- -H "Authorization: Bearer $SANDBASE_API_KEY"
-```
-
-Check `status` before invocation and `protocols` before choosing REST, MCP, or ACP. Returned URLs do not by themselves indicate that a protocol is enabled.
-
-### Update a Service
-
-```bash
-curl -X PATCH https://api.sandbase.ai/v1/endpoints/ep_01... \
- -H "Authorization: Bearer $SANDBASE_API_KEY" \
- -H "Content-Type: application/json" \
- -d '{"name":"Research service v2"}'
-```
-
-Use `PATCH` for new integrations. `POST /v1/endpoints/{endpoint_id}` is only a compatibility alias. An effective update returns `200 OK`.
-
-### Delete a Service
-
-```bash
-curl -X DELETE https://api.sandbase.ai/v1/endpoints/ep_01... \
- -H "Authorization: Bearer $SANDBASE_API_KEY"
-```
-
-A successful deletion returns `200 OK` with `{ "id": "ep_01...", "deleted": true }`. This operation removes the Service; it is not the same as setting `status` to `disabled`.
-
-## Invoke with REST
-
-`POST /v1/endpoints/{endpoint_id}/run`
-
-```bash
-curl -X POST https://api.sandbase.ai/v1/endpoints/ep_01.../run \
- -H "Authorization: Bearer $SANDBASE_API_KEY" \
- -H "Content-Type: application/json" \
- -d '{"input":"Research Acme and return a sourced brief."}'
-```
-
-The body accepts `input` or `content`, plus an optional `session_id`.
-
-Each invocation sends a standard Session event. If `session_id` is omitted, SandBase first creates a Session fixed to the Service's Agent-version runtime binding.
-
-The response is `202 Accepted`:
-
-```json
-{
- "session_id": "sess_01...",
- "events": [{
- "id": "sevt_01...",
- "type": "user.message",
- "processed_at": "2026-08-05T10:00:00Z"
- }]
-}
-```
-
-The Service must be active and include the `rest` protocol.
-
-Read history or stream results through the Session Events APIs using the returned `session_id`.
-
-## Read invocation results
-
-Service invocations are asynchronous Session events; there is no separate Service-result or
-`/v1/endpoints/{endpoint_id}/result` endpoint. Use the returned `session_id` to inspect the
-current Session and its persisted output:
-
-```bash
-# Check the Session lifecycle status
-curl https://api.sandbase.ai/v1/sessions/sess_01... \
- -H "Authorization: Bearer $SANDBASE_API_KEY"
-
-# Read the event history, including agent messages and tool activity
-curl "https://api.sandbase.ai/v1/sessions/sess_01.../events?order=asc" \
- -H "Authorization: Bearer $SANDBASE_API_KEY"
-```
-
-For live results, use [`GET /v1/sessions/{id}/events/stream`](/api-reference/sessions/stream)
-with Server-Sent Events (SSE). Follow `next_page` as an opaque cursor when listing events; do
-not infer completion from a fixed event count. See [Get Session](/api-reference/sessions/get),
-[List Events](/api-reference/sessions/list-events), and [Stream Events](/api-reference/sessions/stream)
-for response shapes and resume behavior.
-
-## Invoke with ACP
-
-`POST /v1/endpoints/{endpoint_id}/acp` handles the experimental ACP JSON-RPC transport for Services with the `acp` protocol.
-
-The implemented methods are `initialize`, `session/new`, `session/prompt`, and `session/cancel`. `session/new` returns a canonical `sess_*` Session ID. `session/prompt` returns `application/x-ndjson`: zero or more `session/update` notifications followed by the final JSON-RPC result. See [Invoke with ACP](./acp) for the request envelopes.
-
-REST, MCP, and ACP are invocation protocols on the same Service.
-
-## Connect through MCP
-
-`POST /v1/endpoints/{endpoint_id}/mcp` handles MCP JSON-RPC for a Service whose `protocols` includes `mcp`. Send standard MCP messages with `Content-Type: application/json`; initialize the connection before calling the tools exposed by the Service. A streaming response may use newline-delimited JSON.
-
-```bash
-curl -X POST https://api.sandbase.ai/v1/endpoints/ep_01.../mcp \
- -H "Authorization: Bearer $SANDBASE_API_KEY" \
- -H "Content-Type: application/json" \
- -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-app","version":"1.0.0"}}}'
-```
-
-Use `DELETE /v1/endpoints/{endpoint_id}/mcp` to close and clean up MCP transport state. This does not delete the Service itself.
-
-See the [Services guide](/agents/services) for product guidance, or use the management and invocation examples on this page.
+Use the [Services API](/api-reference/services/) instead. A Service pins an Agent version, each invocation returns a Run, and each Run creates a Session you can read with the [Sessions API](/api-reference/sessions/). To call Services from AI tools, see [Use a Service from AI tools](/api-reference/services/#use-a-service-from-ai-tools).
diff --git a/api-reference/errors.md b/api-reference/errors.md
index 3900a7d4..120b1bb2 100644
--- a/api-reference/errors.md
+++ b/api-reference/errors.md
@@ -15,10 +15,10 @@ Core middleware and several `/v1/*` endpoints return a flat error:
{"error":"API key rate limit exceeded"}
```
-Agent resource APIs can return a typed error object:
+Agents platform resources (`/v1/agents*`, `/v1/services*`, `/v1/schedules*`, `/v1/mcp-connections*`, `/v1/secrets*`, `/v1/skills*`) return an OpenAI-style object with a stable `code`. See [Agents platform errors](#agents-platform-errors):
```json
-{"error":{"type":"invalid_request","message":"invalid request body"}}
+{"error":{"type":"invalid_request_error","code":"invalid_request","param":null,"message":"invalid request"}}
```
OpenAI-compatible endpoints retain an OpenAI-compatible envelope. `POST /v1/messages` retains an Anthropic-compatible envelope:
@@ -89,6 +89,36 @@ A suitable delay is:
delay = min(base_delay * 2^attempt, max_delay) + random_jitter
```
+## Agents platform errors
+
+Agents, Sessions, Services, Schedules, Skills, MCP connections, and Secrets return one envelope. For most codes, `message` is a fixed, safe sentence; `invalid_request` may carry a short explanation, such as which input shape or option is not supported. Branch on `code`, not on `message`.
+
+```json
+{"error":{"type":"invalid_request_error","code":"conflict","param":null,"message":"conflicting request"}}
+```
+
+`type` is `authentication_error` for 401, `server_error` for 5xx, and `invalid_request_error` otherwise.
+
+| HTTP | `code` | Meaning | What to do |
+|---|---|---|---|
+| 400 | `invalid_request` | Malformed or unknown fields, failed validation, or an unsupported option | Fix the request; do not retry unchanged |
+| 400 | `skill_not_found` | A referenced Skill or version is not available | Fix the Agent's `skills` |
+| 401 | `unauthorized` | Missing, invalid, or conflicting API key headers | Check `Authorization` / `X-API-Key` |
+| 402 | `spending_limit_exceeded` | Key spending limit or organization balance exhausted | Add credits or raise the limit |
+| 403 | `insufficient_scope` | The key is scoped and cannot use Agents resources | Use a standard Console-created key |
+| 403 | `org_disabled` | The organization is not permitted to run Agents | Contact support |
+| 404 | `not_found` | The resource does not exist in your organization | Check the ID |
+| 409 | `conflict` | Stale `row_version`, a state that does not allow the change, or an `Idempotency-Key` reused with a different body | Read the resource again, then decide |
+| 409 | `session_input_pending` | Earlier input is still being delivered | Wait; do not resend with a new key |
+| 409 | `session_input_expired` | Input admission expired | Inspect history before sending again |
+| 409 | `session_environment_unavailable` | The execution environment is unavailable | Wait or create a new Session |
+| 429 | `concurrency_limit` | A Service or Schedule already has a pending Run | Wait for it to finish or cancel it |
+| 429 | `quota_exceeded` | Agents quota exceeded | Retry later |
+| 502 | `executor_rejected`, `executor_contract_error` | The execution layer rejected the request or answered unexpectedly | Check the Agent configuration, then retry |
+| 503 | `executor_unavailable`, `storage_unavailable`, `skill_unavailable`, `tenant_unavailable`, `tenant_provisioning_failed`, `authorization_unavailable`, `admission_unavailable`, `internal_error` | A dependency is temporarily unavailable | Retry with backoff and the same `Idempotency-Key` |
+
+`skill_runtime_incompatible` can also appear; it is returned as 503 with the message `internal error`.
+
## Streaming failures
After a stream starts, an error may arrive in that protocol's stream format instead of as a new HTTP response. Treat the stream as incomplete, retain any partial output your application needs, and retry only when regenerating the request is safe. Do not assume every provider emits the same terminal event or finish reason.
diff --git a/api-reference/index.md b/api-reference/index.md
index c8c5f040..dd387807 100644
--- a/api-reference/index.md
+++ b/api-reference/index.md
@@ -1,11 +1,11 @@
---
title: Platform API Reference
-description: Manage SandBase Agents, Sessions, Services, Schedules, Skills, credentials, and account resources.
+description: Manage SandBase Agents, Sessions, Services, Schedules, Skills, MCP connections, Secrets, and account resources.
---
# Platform API Reference
-Use the Platform API to define reusable Agents, run stateful work, publish callable services, and schedule repeatable execution.
+Use the Platform API to define reusable Agents, run them in Sessions, deploy them as Services or Schedules, and connect them to Skills and MCP tools.
::: tip Looking for model inference?
Chat, image, video, audio, and model-specific request schemas live in the [Model API Reference](/model-api-reference/). The Platform API covers managed Agent resources and operations.
@@ -16,26 +16,38 @@ Chat, image, video, audio, and model-specific request schemas live in the [Model
| Goal | Start with | What happens next |
|---|---|---|
| Define reusable behavior | [Agents](/api-reference/agents/) | Create and version an Agent configuration |
-| Run stateful work directly | [Sessions](/api-reference/sessions/) | Exchange persisted input, output, and tool events |
-| Expose an Agent to an application | [Services](/api-reference/endpoints/) | Invoke a stable REST, MCP, or ACP Service that creates or continues a Session |
-| Run work later or repeatedly | [Schedules](/api-reference/deployments/) | Trigger a Schedule run and inspect its linked Session |
-| Reuse instruction bundles | [Skills](/api-reference/skills/) | Create and attach versioned Skills to Agents |
-| Supply private integration values | [Credentials](/api-reference/credentials/) | Keep secrets out of Agent instructions and request bodies |
+| Run an Agent directly | [Sessions](/api-reference/sessions/) | Send input, stream events, and read history |
+| Invoke an Agent from an application | [Services](/api-reference/services/) | Each invocation returns a Run that creates a Session |
+| Run work on a schedule | [Schedules](/api-reference/schedules/) | Each firing or manual trigger creates a Run and a Session |
+| Reuse instruction bundles | [Skills](/api-reference/skills/) | Upload versioned ZIP bundles and attach them to Agents |
+| Call external tools | [MCP Connections](/api-reference/mcp-connections/) | Register remote HTTPS MCP servers for Agents |
+| Authenticate external tools | [Secrets](/api-reference/secrets/) | Store write-only Bearer tokens for MCP connections |
## Resource lifecycle
```text
-Agent + version
-├── direct execution ────────────────→ Session ─→ events
-├── Service (API path: endpoints) ───→ Session ─→ events
-└── Schedule (API resource: Deployment) → DeploymentRun ─→ Session ─→ events
+Agent + version ─┬─ direct ──────────────────────────────→ Session ─→ events, items, turns
+ ├─ Service ── invoke ──→ Run ────────────→ Session
+ └─ Schedule ─ cron or manual trigger → Run → Session
+Agent ─ skills[] → Skill version Agent ─ mcp_connections[] → MCP connection ─ secret_id → Secret
```
-- An **Agent** is reusable configuration; creating one does not execute it.
-- A **Session** is the durable execution and event history for one interaction.
-- A **Service** publishes an Agent behind a stable invocation surface. Calling it creates a new Session or continues an authorized existing Session. Its API path remains `/v1/endpoints` for compatibility.
-- A **Schedule** stores repeatable input and optional cron configuration. Its API resource remains `Deployment` at `/v1/deployments` for compatibility. Every trigger creates a distinct `DeploymentRun` and, after successful Session creation, links that run to a Session.
-- A **Skill** packages reusable instructions and supporting files. A **Credential** stores a private value required by an Agent capability.
+- An **Agent** (`agt_`) is a saved, versioned configuration; creating one does not execute it.
+- A **Session** (`ses_`) runs one Agent version and freezes its Skills and MCP connections. Session paths live under `/v1/agents/sessions`.
+- A **Service** (`svc_`) pins an Agent version for on-demand calls. Each invocation returns a **Run** (`run_`) that creates its own Session.
+- A **Schedule** (`sch_`) pins an Agent version and input and fires on a cron expression. Each firing or manual trigger creates a Run and a Session.
+- A **Skill** (`skl_`) is a versioned instruction bundle. An **MCP connection** (`mcp_`) registers a remote tool server, and a **Secret** (`sec_`) stores its Bearer token.
+
+## Conventions for Agents platform resources
+
+Agents, Sessions, Services, Schedules, Skills, MCP connections, and Secrets are served under `https://api.sandbase.ai/v1` and share these rules:
+
+- Authenticate with `Authorization: Bearer` or `X-API-Key`. See [Authentication](/api-reference/authentication#agents-platform-resources).
+- Request bodies are strict JSON up to 1 MiB; unknown fields return 400.
+- Writes that create Runs, Services, Schedules, MCP connections, or Secrets require an `Idempotency-Key`. Other writes accept one.
+- Optimistic concurrency uses `row_version`: send the value you last read, and handle 409 by reading again.
+- Agents, Sessions, items, turns, and Skills use cursor pagination (`limit`, `after`, `order`; responses have `has_more` and `last_id`). Agent versions, Services, Schedules, Runs, attachments, MCP connections, and Secrets use offset pagination (`limit`, `offset`; responses have `total`).
+- Errors use `{"error": {"type", "code", "param", "message"}}`. See [Agents platform errors](/api-reference/errors#agents-platform-errors).
## Authentication
@@ -60,6 +72,6 @@ Platform resources belong to the organization associated with the API key. Use t
## Next steps
- New to managed Agents: [Build Agent](/agents/)
-- Publish an application service: [Services guide](/agents/services)
+- Deploy an application service: [Services guide](/agents/services)
- Schedule repeatable work: [Schedules guide](/agents/schedules)
- Call a model directly: [Model API Reference](/model-api-reference/)
diff --git a/api-reference/mcp-connections/index.md b/api-reference/mcp-connections/index.md
new file mode 100644
index 00000000..fb58c12f
--- /dev/null
+++ b/api-reference/mcp-connections/index.md
@@ -0,0 +1,88 @@
+---
+title: MCP Connections API
+description: Register remote HTTPS MCP servers that Agents can call.
+---
+# MCP Connections API
+
+An MCP connection registers a remote MCP server reachable over HTTPS. Agents reference connections in `mcp_connections`; each Session freezes the connection configuration when it starts. Connection IDs use the `mcp_` prefix. To authenticate to the server with a Bearer token, first create a [Secret](/api-reference/secrets/) for the same URL.
+
+| Method | Path | Purpose |
+|---|---|---|
+| `POST` | `/v1/mcp-connections` | [Create a connection](#create-a-connection) |
+| `GET` | `/v1/mcp-connections` | [List connections](#list-connections) |
+| `GET` | `/v1/mcp-connections/{connection_id}` | [Get a connection](#get-a-connection) |
+| `PUT` | `/v1/mcp-connections/{connection_id}` | [Replace a connection](#replace-a-connection) |
+| `POST` | `/v1/mcp-connections/{connection_id}/enable` | [Enable a connection](#enable-a-connection) |
+| `DELETE` | `/v1/mcp-connections/{connection_id}` | [Disable a connection](#disable-a-connection) |
+
+Every write requires `Idempotency-Key` (1-128 characters). The connection object has `id`, `name`, `server_url`, `secret_id` (when set), `status` (`active` or `disabled`), `row_version`, `created_at`, and `updated_at`.
+
+## Create a connection
+
+```bash
+curl -X POST https://api.sandbase.ai/v1/mcp-connections \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: github-mcp" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "GitHub", "server_url": "https://mcp.example.com/github", "secret_id": "sec_9b1d3f5a-7c9e-5b2d-8f4a-6c8e0a2d4f61"}'
+```
+
+- `name` is required, up to 128 characters.
+- `server_url` must be HTTPS without user info, query, or fragment (up to 2048 characters).
+- `secret_id` is optional. Omit it for an anonymous server. When set, the Secret must be `active` and its `server_url` must equal this `server_url` exactly, otherwise the request fails (409 for an inactive Secret, 400 for a URL mismatch).
+
+Returns `201 Created`.
+
+## List connections
+
+```bash
+curl "https://api.sandbase.ai/v1/mcp-connections?limit=20&offset=0" \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+```
+
+Offset pagination with `data`, `total`, `limit`, and `offset`.
+
+## Get a connection
+
+```bash
+curl https://api.sandbase.ai/v1/mcp-connections/mcp_1f3d5b7a-9c2e-5f4a-8b6d-0e2c4a6f8b13 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+```
+
+## Replace a connection
+
+```bash
+curl -X PUT https://api.sandbase.ai/v1/mcp-connections/mcp_1f3d5b7a-9c2e-5f4a-8b6d-0e2c4a6f8b13 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: github-mcp-v2" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "GitHub", "server_url": "https://mcp.example.com/github", "row_version": 1}'
+```
+
+`PUT` replaces `name`, `server_url`, and `secret_id` together; omitting `secret_id` makes the connection anonymous. `row_version` must match the current value. A disabled connection stays disabled after replacement until you enable it.
+
+## Enable a connection
+
+```bash
+curl -X POST https://api.sandbase.ai/v1/mcp-connections/mcp_1f3d5b7a-9c2e-5f4a-8b6d-0e2c4a6f8b13/enable \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: github-mcp-enable" \
+ -H "Content-Type: application/json" \
+ -d '{"row_version": 3}'
+```
+
+Enabling keeps the same connection ID. It does not restore a revoked Secret; replace the connection with a new active Secret first.
+
+## Disable a connection
+
+```bash
+curl -X DELETE https://api.sandbase.ai/v1/mcp-connections/mcp_1f3d5b7a-9c2e-5f4a-8b6d-0e2c4a6f8b13 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: github-mcp-disable"
+```
+
+Returns `204 No Content` and sets `status` to `disabled`. New Sessions cannot use the connection, and existing Sessions that froze it reject further input. A tool call that is already running is not interrupted.
+
+## Changes and running Sessions
+
+Sessions freeze connection settings when they start. After you replace a connection or rotate its Secret, new Sessions use the new configuration and existing Sessions keep their snapshot. Disabling the connection or revoking its Secret blocks both new Sessions and further input to existing ones.
diff --git a/api-reference/schedules/create.md b/api-reference/schedules/create.md
new file mode 100644
index 00000000..ba77ff46
--- /dev/null
+++ b/api-reference/schedules/create.md
@@ -0,0 +1,144 @@
+---
+title: Create Schedule
+description: Create a Schedule with POST /v1/schedules.
+aside: false
+outline: false
+apiReference:
+ title: Create Schedule
+ operation: Schedules
+ method: POST
+ path: /v1/schedules
+ description: Create a Schedule that runs a pinned Agent version on a cron expression. Idempotency-Key is required. The model credential (model_provider, or your calling API key) is stored encrypted and used for every scheduled run.
+ groups:
+ - title: Headers
+ fields:
+ - name: Idempotency-Key
+ type: string
+ required: true
+ description: 1-128 characters, no leading or trailing whitespace. Reuse the same key and body when retrying; the same key with a different body returns 409.
+ - title: Request body
+ description: Strict JSON object.
+ fields:
+ - name: name
+ type: string
+ required: true
+ description: Up to 200 characters.
+ - name: agent_id
+ type: string
+ required: true
+ description: Saved Agent ID. The Agent must be active and ready.
+ - name: agent_version
+ type: integer
+ required: false
+ description: Agent version to pin. Omit to pin the current version at creation time.
+ - name: input
+ type: string
+ required: true
+ description: Input sent on every run. Required and non-empty, up to 256 KiB.
+ - name: description
+ type: string
+ required: false
+ description: Up to 16384 characters.
+ - name: model
+ type: string
+ required: false
+ description: Default model override.
+ - name: schedule
+ type: object
+ required: true
+ description: '{"cron": "<5-field cron>", "timezone": ""}. timezone defaults to UTC. Seconds are not supported; an expression with no match in the next 366 days returns 400.'
+ - name: model_provider
+ type: object
+ required: false
+ description: Model credential stored encrypted for scheduled runs. Omit to store your calling API key.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/schedules \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: create-schedule-1" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Daily digest",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "input": "Summarize the previous day of AI agent news.",
+ "schedule": {
+ "cron": "0 9 * * 1-5",
+ "timezone": "Asia/Shanghai"
+ }
+ }'
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ "Idempotency-Key": "create-schedule-1",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/schedules",
+ headers=headers,
+ json={
+ "name": "Daily digest",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "input": "Summarize the previous day of AI agent news.",
+ "schedule": {
+ "cron": "0 9 * * 1-5",
+ "timezone": "Asia/Shanghai"
+ }
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/schedules', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Idempotency-Key': 'create-schedule-1',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "name": "Daily digest",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "input": "Summarize the previous day of AI agent news.",
+ "schedule": {
+ "cron": "0 9 * * 1-5",
+ "timezone": "Asia/Shanghai"
+ }
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 201 Created
+ code: |-
+ {
+ "id": "sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17",
+ "kind": "schedule",
+ "name": "Daily digest",
+ "description": "",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 3,
+ "input": "Summarize the previous day of AI agent news.",
+ "status": "active",
+ "row_version": 1,
+ "schedule": {
+ "cron": "0 9 * * 1-5",
+ "timezone": "Asia/Shanghai"
+ },
+ "next_run_at": "2026-10-07T01:00:00Z",
+ "created_at": "2026-10-06T08:00:00Z",
+ "updated_at": "2026-10-06T08:00:00Z"
+ }
+---
+
+
diff --git a/api-reference/schedules/get.md b/api-reference/schedules/get.md
new file mode 100644
index 00000000..d1471426
--- /dev/null
+++ b/api-reference/schedules/get.md
@@ -0,0 +1,77 @@
+---
+title: Get Schedule
+description: Read one Schedule.
+aside: false
+outline: false
+apiReference:
+ title: Get Schedule
+ operation: Schedules
+ method: GET
+ path: /v1/schedules/{schedule_id}
+ description: Read a Schedule. row_version advances after every scheduled tick, so read it right before a write.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: schedule_id
+ type: string
+ required: true
+ description: Schedule ID (sch_ prefix).
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17",
+ "kind": "schedule",
+ "name": "Daily digest",
+ "description": "",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 3,
+ "input": "Summarize the previous day of AI agent news.",
+ "status": "active",
+ "row_version": 1,
+ "schedule": {
+ "cron": "0 9 * * 1-5",
+ "timezone": "Asia/Shanghai"
+ },
+ "next_run_at": "2026-10-07T01:00:00Z",
+ "created_at": "2026-10-06T08:00:00Z",
+ "updated_at": "2026-10-06T08:00:00Z"
+ }
+---
+
+
diff --git a/api-reference/schedules/index.md b/api-reference/schedules/index.md
new file mode 100644
index 00000000..610cec14
--- /dev/null
+++ b/api-reference/schedules/index.md
@@ -0,0 +1,65 @@
+---
+title: Schedules API
+description: Run a pinned Agent version on a cron schedule or on demand, and inspect its Runs.
+---
+# Schedules API
+
+A Schedule runs one pinned [Agent](/api-reference/agents/) version with a saved input on a cron expression. Every firing, and every manual trigger, creates a Run (`run_` IDs) that creates one [Session](/api-reference/sessions/). Schedule IDs use the `sch_` prefix.
+
+## Schedule operations
+
+| Method | Path | Purpose |
+|---|---|---|
+| `POST` | `/v1/schedules` | [Create a Schedule](./create) |
+| `GET` | `/v1/schedules` | [List Schedules](./list) |
+| `GET` | `/v1/schedules/{schedule_id}` | [Get a Schedule](./get) |
+| `PATCH` | `/v1/schedules/{schedule_id}` | [Update name, Agent, or input](./update) |
+| `PUT` | `/v1/schedules/{schedule_id}/timing` | [Change cron and timezone](./timing) |
+| `POST` | `/v1/schedules/{schedule_id}/pause` | [Pause](./lifecycle) |
+| `POST` | `/v1/schedules/{schedule_id}/resume` | [Resume](./lifecycle) |
+| `POST` | `/v1/schedules/{schedule_id}/archive` | [Archive](./lifecycle) (terminal) |
+| `POST` | `/v1/schedules/{schedule_id}/runs` | [Trigger a manual Run](./run) |
+| `GET` | `/v1/schedules/{schedule_id}/runs` | [List Runs](./runs) |
+| `GET` | `/v1/schedules/{schedule_id}/runs/{run_id}` | [Get a Run](./runs) |
+| `POST` | `/v1/schedules/{schedule_id}/runs/{run_id}/cancel` | [Cancel a Run](./runs) |
+| `GET` | `/v1/schedules/{schedule_id}/runs/{run_id}/notifications` | [Notification deliveries](#notifications) |
+| `GET` | `/v1/schedules/{schedule_id}/notifications` | [Notification targets](#notifications) |
+| `PUT` | `/v1/schedules/{schedule_id}/notifications/{channel}` | [Set a target](#notifications) |
+| `DELETE` | `/v1/schedules/{schedule_id}/notifications/{channel}` | [Remove a target](#notifications) |
+
+## Timing
+
+`schedule` is `{"cron": "…", "timezone": "…"}`.
+
+- `cron` has five fields: minute, hour, day of month, month, day of week (0-7, where 0 and 7 are Sunday). Lists, ranges, and steps such as `*/15` work. Seconds are not supported.
+- When both day of month and day of week are restricted, the Schedule fires when either matches.
+- `timezone` is an IANA name such as `Asia/Shanghai` and defaults to `UTC`. Unknown names return 400.
+- An expression with no match in the next 366 days returns 400.
+- `next_run_at` shows the next firing time in UTC while the Schedule is active.
+
+If the scheduler was unavailable when a firing was due, it creates one Run for the overdue time and then continues from the next future time; missed firings are not replayed one by one.
+
+## Runs
+
+- Scheduled Runs have `trigger_source: "scheduled"` and `scheduled_at`; manual Runs have `trigger_source: "manual"`.
+- Only one Run per Schedule can be pending. A manual trigger returns `429 concurrency_limit`; a scheduled firing is recorded as a `skipped` Run with `error_code: "concurrency_limit"`.
+- Run `status` values are `pending`, `succeeded`, `failed`, `cancelled`, and `skipped`. `error_code` explains failures, for example `session_creation_rejected`, `execution_failed`, `schedule_configuration_invalid`, or `spending_limit_exceeded`.
+- Reading a Schedule Run is read-only; Run status advances in the background.
+
+## Model credentials
+
+Scheduled Runs have no caller, so the Schedule stores a model credential, encrypted, when it is created: `model_provider` from the request, or your calling SandBase API key when you omit it. `PUT …/timing` keeps the stored credential unless you send a new `model_provider`. A manual trigger runs with your calling key unless its body sends `model_provider`.
+
+## Rules that apply to writes
+
+- Creating a Schedule, triggering a Run, and cancelling a Run require `Idempotency-Key`.
+- `PATCH`, `PUT …/timing`, `pause`, `resume`, and `archive` require the current `row_version`. A Schedule's `row_version` also advances every time it fires, so read it right before writing.
+- Paused Schedules neither fire nor accept manual Runs (409). `archived` is terminal.
+
+## Notifications
+
+Schedules support the same Feishu or Lark bot notifications as Services. See [Services notifications](/api-reference/services/#notifications); replace `/v1/services/{service_id}` with `/v1/schedules/{schedule_id}`.
+
+## Pagination
+
+Schedule and Run lists use offset pagination: `limit` (1-100, default 20) and `offset`.
diff --git a/api-reference/schedules/lifecycle.md b/api-reference/schedules/lifecycle.md
new file mode 100644
index 00000000..fe35eb08
--- /dev/null
+++ b/api-reference/schedules/lifecycle.md
@@ -0,0 +1,94 @@
+---
+title: Pause, Resume, or Archive Schedule
+description: Change the lifecycle of a Schedule.
+aside: false
+outline: false
+apiReference:
+ title: Pause, Resume, or Archive Schedule
+ operation: Schedules
+ method: POST
+ path: /v1/schedules/{schedule_id}/pause
+ description: "POST /v1/schedules/{schedule_id}/pause, POST /v1/schedules/{schedule_id}/resume, and POST /v1/schedules/{schedule_id}/archive share this body. archived is terminal: resume after archive returns 409. Paused Schedules neither fire nor accept manual runs (409); resume computes the next run time from now."
+ groups:
+ - title: Path parameters
+ fields:
+ - name: schedule_id
+ type: string
+ required: true
+ description: Schedule ID (sch_ prefix).
+ - title: Request body
+ description: JSON object.
+ fields:
+ - name: row_version
+ type: integer
+ required: true
+ description: The row_version you last read. A stale value returns 409.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17/pause \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "row_version": 2
+ }'
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17/pause",
+ headers=headers,
+ json={
+ "row_version": 2
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17/pause', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "row_version": 2
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17",
+ "kind": "schedule",
+ "name": "Daily digest",
+ "description": "",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 3,
+ "input": "Summarize the previous day of AI agent news.",
+ "status": "paused",
+ "row_version": 3,
+ "schedule": {
+ "cron": "0 9 * * 1-5",
+ "timezone": "Asia/Shanghai"
+ },
+ "created_at": "2026-10-06T08:00:00Z",
+ "updated_at": "2026-10-06T08:00:00Z"
+ }
+---
+
+
diff --git a/api-reference/schedules/list.md b/api-reference/schedules/list.md
new file mode 100644
index 00000000..a486a46d
--- /dev/null
+++ b/api-reference/schedules/list.md
@@ -0,0 +1,99 @@
+---
+title: List Schedules
+description: List Schedules with offset pagination.
+aside: false
+outline: false
+apiReference:
+ title: List Schedules
+ operation: Schedules
+ method: GET
+ path: /v1/schedules
+ description: List definitions with offset pagination.
+ groups:
+ - title: Query parameters
+ fields:
+ - name: status
+ type: string
+ required: false
+ description: active, paused, archived, or all. Other values return 400.
+ default: active
+ - name: agent_id
+ type: string
+ required: false
+ description: Only definitions bound to this Agent.
+ - name: limit
+ type: integer
+ required: false
+ description: Page size up to 100; values above 100 return 400, and 0 or non-numeric values fall back to 20.
+ default: "20"
+ - name: offset
+ type: integer
+ required: false
+ description: Number of records to skip.
+ default: "0"
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET "https://api.sandbase.ai/v1/schedules?status=active&limit=20" \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/schedules?status=active&limit=20",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/schedules?status=active&limit=20', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "data": [
+ {
+ "id": "sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17",
+ "kind": "schedule",
+ "name": "Daily digest",
+ "description": "",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 3,
+ "input": "Summarize the previous day of AI agent news.",
+ "status": "active",
+ "row_version": 1,
+ "schedule": {
+ "cron": "0 9 * * 1-5",
+ "timezone": "Asia/Shanghai"
+ },
+ "next_run_at": "2026-10-07T01:00:00Z",
+ "created_at": "2026-10-06T08:00:00Z",
+ "updated_at": "2026-10-06T08:00:00Z"
+ }
+ ],
+ "total": 1,
+ "limit": 20,
+ "offset": 0
+ }
+---
+
+
diff --git a/api-reference/schedules/run.md b/api-reference/schedules/run.md
new file mode 100644
index 00000000..1c577f59
--- /dev/null
+++ b/api-reference/schedules/run.md
@@ -0,0 +1,101 @@
+---
+title: Trigger Schedule Run
+description: Start a manual Schedule Run.
+aside: false
+outline: false
+apiReference:
+ title: Trigger Schedule Run
+ operation: Schedules
+ method: POST
+ path: /v1/schedules/{schedule_id}/runs
+ description: Start a manual Run now and return 202 Accepted with a pending Run (trigger_source manual). Idempotency-Key is required. input, model, and model_provider override the saved values for this Run only. Only one Run per Schedule can be pending; another trigger returns 429 concurrency_limit.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: schedule_id
+ type: string
+ required: true
+ description: Schedule ID (sch_ prefix).
+ - title: Headers
+ fields:
+ - name: Idempotency-Key
+ type: string
+ required: true
+ description: 1-128 characters, no leading or trailing whitespace. Reuse the same key and body when retrying; the same key with a different body returns 409.
+ - title: Request body
+ description: JSON object. Send {} to use the saved input.
+ fields:
+ - name: input
+ type: string
+ required: false
+ description: Overrides the saved input for this Run. The effective input must be non-empty.
+ - name: model
+ type: string
+ required: false
+ description: Overrides the saved model for this Run.
+ - name: model_provider
+ type: object
+ required: false
+ description: Model credential for this Run. Omit to use your calling API key.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17/runs \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: daily-digest-manual-1" \
+ -H "Content-Type: application/json" \
+ -d '{}'
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ "Idempotency-Key": "daily-digest-manual-1",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17/runs",
+ headers=headers,
+ json={},
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17/runs', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Idempotency-Key': 'daily-digest-manual-1',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({}),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 202 Accepted
+ code: |-
+ {
+ "id": "run_4a6c8e0b-1d3f-5a7c-9e1b-3d5f7a9c1e24",
+ "source_type": "schedule",
+ "source_id": "sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17",
+ "source_version": 1,
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 3,
+ "trigger_source": "manual",
+ "status": "pending",
+ "session_id": null,
+ "created_at": "2026-10-06T08:05:00Z",
+ "updated_at": "2026-10-06T08:05:00Z"
+ }
+---
+
+
diff --git a/api-reference/schedules/runs.md b/api-reference/schedules/runs.md
new file mode 100644
index 00000000..228e522b
--- /dev/null
+++ b/api-reference/schedules/runs.md
@@ -0,0 +1,111 @@
+---
+title: List Schedule Runs
+description: List and inspect Schedule Runs.
+aside: false
+outline: false
+apiReference:
+ title: List Schedule Runs
+ operation: Schedules
+ method: GET
+ path: /v1/schedules/{schedule_id}/runs
+ description: "List Runs with offset pagination (limit 1-100). Read one Run with GET /v1/schedules/{schedule_id}/runs/{run_id}; Schedule Run reads are read-only and status advances in the background. Request cancellation of a pending Run with POST /v1/schedules/{schedule_id}/runs/{run_id}/cancel (Idempotency-Key required, body {}). Cancel only reaches a Run whose Session has started: while session_id is null it returns 409 conflict, and while the Run input is still being delivered it returns 409 session_input_pending; in that state the Run may not be cancellable, so wait for it to finish. Cancelling a Run that has already finished returns 202 with the unchanged Run. Read notification deliveries with GET /v1/schedules/{schedule_id}/runs/{run_id}/notifications."
+ groups:
+ - title: Path parameters
+ fields:
+ - name: schedule_id
+ type: string
+ required: true
+ description: Schedule ID (sch_ prefix).
+ - title: Query parameters
+ fields:
+ - name: limit
+ type: integer
+ required: false
+ description: Page size up to 100; values above 100 return 400, and 0 or non-numeric values fall back to 20.
+ default: "20"
+ - name: offset
+ type: integer
+ required: false
+ description: Number of records to skip.
+ default: "0"
+ - title: Run object
+ description: Returned by every Run operation.
+ fields:
+ - name: status
+ type: string
+ required: true
+ description: pending, succeeded, failed, or cancelled; skipped when a scheduled tick finds a pending Run (error_code concurrency_limit).
+ - name: session_id
+ type: string · null
+ required: true
+ description: Session created for the Run; null until it exists.
+ - name: error_code
+ type: string
+ required: false
+ description: Set when the Run failed, for example session_creation_rejected, execution_failed, or spending_limit_exceeded.
+ - name: trigger_source
+ type: string
+ required: true
+ description: manual or scheduled.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET "https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17/runs?limit=20" \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17/runs?limit=20",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17/runs?limit=20', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "data": [
+ {
+ "id": "run_4a6c8e0b-1d3f-5a7c-9e1b-3d5f7a9c1e24",
+ "source_type": "schedule",
+ "source_id": "sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17",
+ "source_version": 1,
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 3,
+ "trigger_source": "scheduled",
+ "status": "succeeded",
+ "session_id": "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ "created_at": "2026-10-06T08:05:00Z",
+ "updated_at": "2026-10-06T08:05:00Z",
+ "scheduled_at": "2026-10-06T01:00:00Z"
+ }
+ ],
+ "total": 1,
+ "limit": 20,
+ "offset": 0
+ }
+---
+
+
diff --git a/api-reference/schedules/timing.md b/api-reference/schedules/timing.md
new file mode 100644
index 00000000..737bcec7
--- /dev/null
+++ b/api-reference/schedules/timing.md
@@ -0,0 +1,114 @@
+---
+title: Update Schedule Timing
+description: Change the cron expression and timezone of a Schedule.
+aside: false
+outline: false
+apiReference:
+ title: Update Schedule Timing
+ operation: Schedules
+ method: PUT
+ path: /v1/schedules/{schedule_id}/timing
+ description: Replace cron and timezone with compare-and-set. Omitting model_provider keeps the stored model credential; send it to replace the credential. Archived Schedules return 409.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: schedule_id
+ type: string
+ required: true
+ description: Schedule ID (sch_ prefix).
+ - title: Request body
+ description: JSON object.
+ fields:
+ - name: row_version
+ type: integer
+ required: true
+ description: The row_version you last read. A stale value returns 409.
+ - name: cron
+ type: string
+ required: true
+ description: Five-field cron expression.
+ - name: timezone
+ type: string
+ required: false
+ description: IANA timezone name.
+ default: UTC
+ - name: model_provider
+ type: object
+ required: false
+ description: Replacement model credential.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X PUT https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17/timing \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "row_version": 3,
+ "cron": "30 8 * * 1-5",
+ "timezone": "Asia/Shanghai"
+ }'
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "PUT",
+ "https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17/timing",
+ headers=headers,
+ json={
+ "row_version": 3,
+ "cron": "30 8 * * 1-5",
+ "timezone": "Asia/Shanghai"
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17/timing', {
+ method: 'PUT',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "row_version": 3,
+ "cron": "30 8 * * 1-5",
+ "timezone": "Asia/Shanghai"
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17",
+ "kind": "schedule",
+ "name": "Daily digest",
+ "description": "",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 3,
+ "input": "Summarize the previous day of AI agent news.",
+ "status": "active",
+ "row_version": 4,
+ "schedule": {
+ "cron": "30 8 * * 1-5",
+ "timezone": "Asia/Shanghai"
+ },
+ "next_run_at": "2026-10-07T01:00:00Z",
+ "created_at": "2026-10-06T08:00:00Z",
+ "updated_at": "2026-10-06T08:00:00Z"
+ }
+---
+
+
diff --git a/api-reference/schedules/update.md b/api-reference/schedules/update.md
new file mode 100644
index 00000000..62760ea1
--- /dev/null
+++ b/api-reference/schedules/update.md
@@ -0,0 +1,118 @@
+---
+title: Update Schedule
+description: Update a Schedule with compare-and-set.
+aside: false
+outline: false
+apiReference:
+ title: Update Schedule
+ operation: Schedules
+ method: PATCH
+ path: /v1/schedules/{schedule_id}
+ description: Compare-and-set update. Only name, description, agent_id, agent_version, input, and row_version are accepted; explicit null returns 400. Changing agent_id without agent_version pins that Agent's current version. Change cron and timezone with PUT /v1/schedules/{schedule_id}/timing.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: schedule_id
+ type: string
+ required: true
+ description: Schedule ID (sch_ prefix).
+ - title: Request body
+ description: row_version plus any subset.
+ fields:
+ - name: row_version
+ type: integer
+ required: true
+ description: The row_version you last read. A stale value returns 409.
+ - name: name
+ type: string
+ required: false
+ description: Up to 200 characters.
+ - name: description
+ type: string
+ required: false
+ description: Up to 16384 characters.
+ - name: agent_id
+ type: string
+ required: false
+ description: Rebind to another Agent.
+ - name: agent_version
+ type: integer
+ required: false
+ description: Pin a different version.
+ - name: input
+ type: string
+ required: false
+ description: New default input.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X PATCH https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "row_version": 1,
+ "agent_version": 4
+ }'
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "PATCH",
+ "https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17",
+ headers=headers,
+ json={
+ "row_version": 1,
+ "agent_version": 4
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/schedules/sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17', {
+ method: 'PATCH',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "row_version": 1,
+ "agent_version": 4
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "sch_7e3b1c58-2a9d-5e4f-b6c1-8d0a2f4e6b17",
+ "kind": "schedule",
+ "name": "Daily digest",
+ "description": "",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 4,
+ "input": "Summarize the previous day of AI agent news.",
+ "status": "active",
+ "row_version": 2,
+ "schedule": {
+ "cron": "0 9 * * 1-5",
+ "timezone": "Asia/Shanghai"
+ },
+ "next_run_at": "2026-10-07T01:00:00Z",
+ "created_at": "2026-10-06T08:00:00Z",
+ "updated_at": "2026-10-06T08:00:00Z"
+ }
+---
+
+
diff --git a/api-reference/secrets/index.md b/api-reference/secrets/index.md
new file mode 100644
index 00000000..a8b3a24f
--- /dev/null
+++ b/api-reference/secrets/index.md
@@ -0,0 +1,77 @@
+---
+title: Secrets API
+description: Store write-only Bearer tokens for MCP connections.
+---
+# Secrets API
+
+A Secret stores a Bearer token for one MCP server URL. [MCP connections](/api-reference/mcp-connections/) reference it with `secret_id`, and the execution environment sends the token to that server. Secret IDs use the `sec_` prefix. The token is write-only: no response ever contains it.
+
+Secrets are not environment variables. They are never injected into the Agent runtime and only authenticate MCP calls to the bound `server_url`.
+
+::: warning Server-side only
+Secret endpoints take a SandBase API key and a plaintext token. Call them from a trusted backend, never from browser code.
+:::
+
+| Method | Path | Purpose |
+|---|---|---|
+| `POST` | `/v1/secrets` | [Create a Secret](#create-a-secret) |
+| `GET` | `/v1/secrets` | [List Secrets](#list-secrets) |
+| `GET` | `/v1/secrets/{secret_id}` | [Get a Secret](#get-a-secret) |
+| `POST` | `/v1/secrets/{secret_id}/rotate` | [Rotate the token](#rotate-a-secret) |
+| `DELETE` | `/v1/secrets/{secret_id}` | [Revoke a Secret](#revoke-a-secret) |
+
+Every write requires `Idempotency-Key` (1-128 characters). The Secret object has `id`, `name`, `kind`, `server_url`, `status`, `row_version`, `created_at`, and `updated_at`. `status` is `pending` while the token is being stored, `active` when it can be used, and `disabled` after revocation.
+
+## Create a Secret
+
+```bash
+curl -X POST https://api.sandbase.ai/v1/secrets \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: github-mcp-token" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "GitHub MCP token", "kind": "mcp_bearer", "server_url": "https://mcp.example.com/github", "token": "'"$GITHUB_MCP_TOKEN"'"}'
+```
+
+- `kind` must be `mcp_bearer`.
+- `server_url` must be HTTPS without user info, query, or fragment. The Secret only works for this exact URL; to use another URL, create another Secret.
+- `name` is up to 128 characters; `token` is up to 16384 characters.
+
+Returns `201 Created` without the token.
+
+## List Secrets
+
+```bash
+curl "https://api.sandbase.ai/v1/secrets?limit=20&offset=0" \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+```
+
+Offset pagination with `data`, `total`, `limit`, and `offset`.
+
+## Get a Secret
+
+```bash
+curl https://api.sandbase.ai/v1/secrets/sec_9b1d3f5a-7c9e-5b2d-8f4a-6c8e0a2d4f61 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+```
+
+## Rotate a Secret
+
+```bash
+curl -X POST https://api.sandbase.ai/v1/secrets/sec_9b1d3f5a-7c9e-5b2d-8f4a-6c8e0a2d4f61/rotate \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: github-mcp-token-rotate-1" \
+ -H "Content-Type: application/json" \
+ -d '{"token": "'"$GITHUB_MCP_TOKEN"'", "row_version": 2}'
+```
+
+Rotation replaces the token for the same `server_url`. The Secret must be `active` and `row_version` must match. Returns `201 Created`. New Sessions use the new token; existing Sessions keep the credential they froze.
+
+## Revoke a Secret
+
+```bash
+curl -X DELETE https://api.sandbase.ai/v1/secrets/sec_9b1d3f5a-7c9e-5b2d-8f4a-6c8e0a2d4f61 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: github-mcp-token-revoke"
+```
+
+Returns `204 No Content` and sets `status` to `disabled`. Connections that use the Secret stop working for new Sessions and for further input to existing Sessions. Revocation cannot be undone: create a new Secret and replace the connection.
diff --git a/api-reference/services/create.md b/api-reference/services/create.md
new file mode 100644
index 00000000..35368f54
--- /dev/null
+++ b/api-reference/services/create.md
@@ -0,0 +1,119 @@
+---
+title: Create Service
+description: Create a Service with POST /v1/services.
+aside: false
+outline: false
+apiReference:
+ title: Create Service
+ operation: Services
+ method: POST
+ path: /v1/services
+ description: "Create a Service that pins a saved Agent version. Idempotency-Key is required: the same key and body return the original Service, and a different body returns 409. schedule and model_provider are not accepted on Services."
+ groups:
+ - title: Headers
+ fields:
+ - name: Idempotency-Key
+ type: string
+ required: true
+ description: 1-128 characters, no leading or trailing whitespace. Reuse the same key and body when retrying; the same key with a different body returns 409.
+ - title: Request body
+ description: Strict JSON object.
+ fields:
+ - name: name
+ type: string
+ required: true
+ description: Up to 200 characters.
+ - name: agent_id
+ type: string
+ required: true
+ description: Saved Agent ID. The Agent must be active and ready.
+ - name: agent_version
+ type: integer
+ required: false
+ description: Agent version to pin. Omit to pin the current version at creation time.
+ - name: input
+ type: string
+ required: false
+ description: Default input, up to 256 KiB. Invocations without input use it; the effective input must be non-empty.
+ - name: description
+ type: string
+ required: false
+ description: Up to 16384 characters.
+ - name: model
+ type: string
+ required: false
+ description: Default model override.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/services \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: create-service-1" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Weekly brief",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "input": "Summarize this week in AI agents."
+ }'
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ "Idempotency-Key": "create-service-1",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/services",
+ headers=headers,
+ json={
+ "name": "Weekly brief",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "input": "Summarize this week in AI agents."
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/services', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Idempotency-Key': 'create-service-1',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "name": "Weekly brief",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "input": "Summarize this week in AI agents."
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 201 Created
+ code: |-
+ {
+ "id": "svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95",
+ "kind": "service",
+ "name": "Weekly brief",
+ "description": "",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 3,
+ "input": "Summarize this week in AI agents.",
+ "status": "active",
+ "row_version": 1,
+ "created_at": "2026-10-06T08:00:00Z",
+ "updated_at": "2026-10-06T08:00:00Z"
+ }
+---
+
+
diff --git a/api-reference/services/get.md b/api-reference/services/get.md
new file mode 100644
index 00000000..14e57c79
--- /dev/null
+++ b/api-reference/services/get.md
@@ -0,0 +1,72 @@
+---
+title: Get Service
+description: Read one Service.
+aside: false
+outline: false
+apiReference:
+ title: Get Service
+ operation: Services
+ method: GET
+ path: /v1/services/{service_id}
+ description: Read a Service.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: service_id
+ type: string
+ required: true
+ description: Service ID (svc_ prefix).
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95",
+ "kind": "service",
+ "name": "Weekly brief",
+ "description": "",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 3,
+ "input": "Summarize this week in AI agents.",
+ "status": "active",
+ "row_version": 1,
+ "created_at": "2026-10-06T08:00:00Z",
+ "updated_at": "2026-10-06T08:00:00Z"
+ }
+---
+
+
diff --git a/api-reference/services/index.md b/api-reference/services/index.md
new file mode 100644
index 00000000..1889c04d
--- /dev/null
+++ b/api-reference/services/index.md
@@ -0,0 +1,67 @@
+---
+title: Services API
+description: Invoke a pinned Agent version on demand and track each invocation as a Run.
+---
+# Services API
+
+A Service binds a name and a default input to one pinned [Agent](/api-reference/agents/) version. Each invocation creates a Run (`run_` IDs), and each Run creates one [Session](/api-reference/sessions/) that holds the conversation and output. Service IDs use the `svc_` prefix.
+
+## Service operations
+
+| Method | Path | Purpose |
+|---|---|---|
+| `POST` | `/v1/services` | [Create a Service](./create) |
+| `GET` | `/v1/services` | [List Services](./list) |
+| `GET` | `/v1/services/{service_id}` | [Get a Service](./get) |
+| `PATCH` | `/v1/services/{service_id}` | [Update a Service](./update) |
+| `POST` | `/v1/services/{service_id}/pause` | [Pause](./lifecycle) |
+| `POST` | `/v1/services/{service_id}/resume` | [Resume](./lifecycle) |
+| `POST` | `/v1/services/{service_id}/archive` | [Archive](./lifecycle) (terminal) |
+| `POST` | `/v1/services/{service_id}/invoke` | [Invoke](./invoke) and get a Run |
+| `GET` | `/v1/services/{service_id}/runs` | [List Runs](./runs) |
+| `GET` | `/v1/services/{service_id}/runs/{run_id}` | [Get a Run](./runs) |
+| `POST` | `/v1/services/{service_id}/runs/{run_id}/cancel` | [Cancel a Run](./runs) |
+| `GET` | `/v1/services/{service_id}/runs/{run_id}/notifications` | [Notification deliveries](#notifications) |
+| `GET` | `/v1/services/{service_id}/notifications` | [Notification targets](#notifications) |
+| `PUT` | `/v1/services/{service_id}/notifications/{channel}` | [Set a target](#notifications) |
+| `DELETE` | `/v1/services/{service_id}/notifications/{channel}` | [Remove a target](#notifications) |
+
+## Invoke and read the result
+
+1. `POST /v1/services/{service_id}/invoke` with an `Idempotency-Key` returns **202 Accepted** and a Run with `status: "pending"`.
+2. Poll `GET /v1/services/{service_id}/runs/{run_id}`. Each read also advances the Run from its Session, so `session_id` appears once the Session exists.
+3. When `status` is `succeeded`, read the output with `GET /v1/agents/sessions/{session_id}/items` or `GET /v1/agents/sessions/{session_id}/turns`.
+
+Run `status` values are `pending`, `succeeded`, `failed`, and `cancelled`. A failed Run carries `error_code`, for example `session_creation_rejected`, `execution_failed`, or an admission code such as `spending_limit_exceeded`.
+
+## Rules that apply to writes
+
+- Creating a Service, invoking it, and cancelling a Run require `Idempotency-Key` (1-128 characters, no surrounding whitespace). The same key and body return the original resource; the same key with a different body returns 409.
+- Only one Run per Service can be pending. Another invocation returns `429 concurrency_limit` until the current Run finishes or is cancelled.
+- `PATCH`, `pause`, `resume`, and `archive` require the current `row_version`. `archived` is terminal.
+- Paused Services reject invocations with 409.
+- Each invocation runs with your calling API key unless the body sends `model_provider`.
+
+## Use a Service from AI tools
+
+When you connect SandBase to an MCP client through [Connect AI tools](/setup/), your Services appear as tools: `sandbase_discover` (with type `service`) lists them, `sandbase_inspect` shows the input, `sandbase_run` with `name` set to the `svc_` ID and `arguments: {"idempotency_key": "…", "input": "…"}` starts a Run, and `sandbase_run_get`, `sandbase_runs`, and `sandbase_run_cancel` follow it.
+
+## Notifications
+
+A Service can post Run results to a Feishu or Lark bot. Only the `feishu` channel exists.
+
+```bash
+curl -X PUT https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95/notifications/feishu \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"webhook_url": "https://open.feishu.cn/open-apis/bot/v2/hook/YOUR_BOT_TOKEN"}'
+```
+
+- `webhook_url` must be `https://open.feishu.cn/open-apis/bot/v2/hook/` or the `open.larksuite.com` equivalent. It is write-only.
+- `GET …/notifications` returns `{"data": [{"channel", "version", "updated_at"}]}` without the URL. `PUT` returns the same list.
+- `DELETE …/notifications/feishu` returns `{"deleted": true}`.
+- `GET …/runs/{run_id}/notifications` lists deliveries with `status` (`pending`, `processing`, `succeeded`, `failed`, `canceled`), `attempts`, `error_code`, `delivered_at`, and `next_attempt_at`.
+
+## Pagination
+
+Service and Run lists use offset pagination: `limit` (1-100, default 20) and `offset`, with `data`, `total`, `limit`, and `offset` in the response.
diff --git a/api-reference/services/invoke.md b/api-reference/services/invoke.md
new file mode 100644
index 00000000..76f46359
--- /dev/null
+++ b/api-reference/services/invoke.md
@@ -0,0 +1,107 @@
+---
+title: Invoke Service
+description: Start a Service Run.
+aside: false
+outline: false
+apiReference:
+ title: Invoke Service
+ operation: Services
+ method: POST
+ path: /v1/services/{service_id}/invoke
+ description: "Start a Run and return 202 Accepted with the Run. Idempotency-Key is required: the same key and body return the original Run, a different body returns 409. Only one Run per Service can be pending; another invoke returns 429 concurrency_limit. Poll GET /v1/services/{service_id}/runs/{run_id} until status leaves pending, then read the result from the Session in session_id."
+ groups:
+ - title: Path parameters
+ fields:
+ - name: service_id
+ type: string
+ required: true
+ description: Service ID (svc_ prefix).
+ - title: Headers
+ fields:
+ - name: Idempotency-Key
+ type: string
+ required: true
+ description: 1-128 characters, no leading or trailing whitespace. Reuse the same key and body when retrying; the same key with a different body returns 409.
+ - title: Request body
+ description: JSON object. Send {} to use the saved input.
+ fields:
+ - name: input
+ type: string
+ required: false
+ description: Overrides the saved input for this Run. The effective input must be non-empty.
+ - name: model
+ type: string
+ required: false
+ description: Overrides the saved model for this Run.
+ - name: model_provider
+ type: object
+ required: false
+ description: Model credential for this Run. Omit to use your calling API key.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95/invoke \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: weekly-brief-2026-w41" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "input": "Summarize this week in AI agents, focusing on open-source releases."
+ }'
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ "Idempotency-Key": "weekly-brief-2026-w41",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95/invoke",
+ headers=headers,
+ json={
+ "input": "Summarize this week in AI agents, focusing on open-source releases."
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95/invoke', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Idempotency-Key': 'weekly-brief-2026-w41',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "input": "Summarize this week in AI agents, focusing on open-source releases."
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 202 Accepted
+ code: |-
+ {
+ "id": "run_4a6c8e0b-1d3f-5a7c-9e1b-3d5f7a9c1e24",
+ "source_type": "service",
+ "source_id": "svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95",
+ "source_version": 1,
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 3,
+ "trigger_source": "manual",
+ "status": "pending",
+ "session_id": null,
+ "created_at": "2026-10-06T08:05:00Z",
+ "updated_at": "2026-10-06T08:05:00Z"
+ }
+---
+
+
diff --git a/api-reference/services/lifecycle.md b/api-reference/services/lifecycle.md
new file mode 100644
index 00000000..ef719904
--- /dev/null
+++ b/api-reference/services/lifecycle.md
@@ -0,0 +1,90 @@
+---
+title: Pause, Resume, or Archive Service
+description: Change the lifecycle of a Service.
+aside: false
+outline: false
+apiReference:
+ title: Pause, Resume, or Archive Service
+ operation: Services
+ method: POST
+ path: /v1/services/{service_id}/pause
+ description: "POST /v1/services/{service_id}/pause, POST /v1/services/{service_id}/resume, and POST /v1/services/{service_id}/archive share this body. archived is terminal: resume after archive returns 409. Paused Services reject invocations with 409."
+ groups:
+ - title: Path parameters
+ fields:
+ - name: service_id
+ type: string
+ required: true
+ description: Service ID (svc_ prefix).
+ - title: Request body
+ description: JSON object.
+ fields:
+ - name: row_version
+ type: integer
+ required: true
+ description: The row_version you last read. A stale value returns 409.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95/pause \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "row_version": 2
+ }'
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95/pause",
+ headers=headers,
+ json={
+ "row_version": 2
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95/pause', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "row_version": 2
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95",
+ "kind": "service",
+ "name": "Weekly brief",
+ "description": "",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 3,
+ "input": "Summarize this week in AI agents.",
+ "status": "paused",
+ "row_version": 3,
+ "created_at": "2026-10-06T08:00:00Z",
+ "updated_at": "2026-10-06T08:00:00Z"
+ }
+---
+
+
diff --git a/api-reference/services/list.md b/api-reference/services/list.md
new file mode 100644
index 00000000..56704a26
--- /dev/null
+++ b/api-reference/services/list.md
@@ -0,0 +1,94 @@
+---
+title: List Services
+description: List Services with offset pagination.
+aside: false
+outline: false
+apiReference:
+ title: List Services
+ operation: Services
+ method: GET
+ path: /v1/services
+ description: List definitions with offset pagination.
+ groups:
+ - title: Query parameters
+ fields:
+ - name: status
+ type: string
+ required: false
+ description: active, paused, archived, or all. Other values return 400.
+ default: active
+ - name: agent_id
+ type: string
+ required: false
+ description: Only definitions bound to this Agent.
+ - name: limit
+ type: integer
+ required: false
+ description: Page size up to 100; values above 100 return 400, and 0 or non-numeric values fall back to 20.
+ default: "20"
+ - name: offset
+ type: integer
+ required: false
+ description: Number of records to skip.
+ default: "0"
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET "https://api.sandbase.ai/v1/services?status=active&limit=20" \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/services?status=active&limit=20",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/services?status=active&limit=20', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "data": [
+ {
+ "id": "svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95",
+ "kind": "service",
+ "name": "Weekly brief",
+ "description": "",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 3,
+ "input": "Summarize this week in AI agents.",
+ "status": "active",
+ "row_version": 1,
+ "created_at": "2026-10-06T08:00:00Z",
+ "updated_at": "2026-10-06T08:00:00Z"
+ }
+ ],
+ "total": 1,
+ "limit": 20,
+ "offset": 0
+ }
+---
+
+
diff --git a/api-reference/services/runs.md b/api-reference/services/runs.md
new file mode 100644
index 00000000..bbdc8a43
--- /dev/null
+++ b/api-reference/services/runs.md
@@ -0,0 +1,110 @@
+---
+title: List Service Runs
+description: List and inspect Service Runs.
+aside: false
+outline: false
+apiReference:
+ title: List Service Runs
+ operation: Services
+ method: GET
+ path: /v1/services/{service_id}/runs
+ description: "List Runs with offset pagination (limit 1-100). Read one Run with GET /v1/services/{service_id}/runs/{run_id}, which also advances its status from the linked Session. Request cancellation of a pending Run with POST /v1/services/{service_id}/runs/{run_id}/cancel (Idempotency-Key required, body {}). Cancel only reaches a Run whose Session has started: while session_id is null it returns 409 conflict, and while the Run input is still being delivered it returns 409 session_input_pending; in that state the Run may not be cancellable, so wait for it to finish. Cancelling a Run that has already finished returns 202 with the unchanged Run. Read notification deliveries with GET /v1/services/{service_id}/runs/{run_id}/notifications."
+ groups:
+ - title: Path parameters
+ fields:
+ - name: service_id
+ type: string
+ required: true
+ description: Service ID (svc_ prefix).
+ - title: Query parameters
+ fields:
+ - name: limit
+ type: integer
+ required: false
+ description: Page size up to 100; values above 100 return 400, and 0 or non-numeric values fall back to 20.
+ default: "20"
+ - name: offset
+ type: integer
+ required: false
+ description: Number of records to skip.
+ default: "0"
+ - title: Run object
+ description: Returned by every Run operation.
+ fields:
+ - name: status
+ type: string
+ required: true
+ description: pending, succeeded, failed, or cancelled.
+ - name: session_id
+ type: string · null
+ required: true
+ description: Session created for the Run; null until it exists.
+ - name: error_code
+ type: string
+ required: false
+ description: Set when the Run failed, for example session_creation_rejected, execution_failed, or spending_limit_exceeded.
+ - name: trigger_source
+ type: string
+ required: true
+ description: manual or scheduled.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET "https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95/runs?limit=20" \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95/runs?limit=20",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95/runs?limit=20', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "data": [
+ {
+ "id": "run_4a6c8e0b-1d3f-5a7c-9e1b-3d5f7a9c1e24",
+ "source_type": "service",
+ "source_id": "svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95",
+ "source_version": 1,
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 3,
+ "trigger_source": "manual",
+ "status": "succeeded",
+ "session_id": "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ "created_at": "2026-10-06T08:05:00Z",
+ "updated_at": "2026-10-06T08:05:00Z"
+ }
+ ],
+ "total": 1,
+ "limit": 20,
+ "offset": 0
+ }
+---
+
+
diff --git a/api-reference/services/update.md b/api-reference/services/update.md
new file mode 100644
index 00000000..c553c859
--- /dev/null
+++ b/api-reference/services/update.md
@@ -0,0 +1,113 @@
+---
+title: Update Service
+description: Update a Service with compare-and-set.
+aside: false
+outline: false
+apiReference:
+ title: Update Service
+ operation: Services
+ method: PATCH
+ path: /v1/services/{service_id}
+ description: Compare-and-set update. Only name, description, agent_id, agent_version, input, and row_version are accepted; explicit null returns 400. Changing agent_id without agent_version pins that Agent's current version.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: service_id
+ type: string
+ required: true
+ description: Service ID (svc_ prefix).
+ - title: Request body
+ description: row_version plus any subset.
+ fields:
+ - name: row_version
+ type: integer
+ required: true
+ description: The row_version you last read. A stale value returns 409.
+ - name: name
+ type: string
+ required: false
+ description: Up to 200 characters.
+ - name: description
+ type: string
+ required: false
+ description: Up to 16384 characters.
+ - name: agent_id
+ type: string
+ required: false
+ description: Rebind to another Agent.
+ - name: agent_version
+ type: integer
+ required: false
+ description: Pin a different version.
+ - name: input
+ type: string
+ required: false
+ description: New default input.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X PATCH https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "row_version": 1,
+ "agent_version": 4
+ }'
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "PATCH",
+ "https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95",
+ headers=headers,
+ json={
+ "row_version": 1,
+ "agent_version": 4
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/services/svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95', {
+ method: 'PATCH',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "row_version": 1,
+ "agent_version": 4
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "svc_2d7a9e14-6b3c-5f8d-a1e2-4c6b8d0f3a95",
+ "kind": "service",
+ "name": "Weekly brief",
+ "description": "",
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 4,
+ "input": "Summarize this week in AI agents.",
+ "status": "active",
+ "row_version": 2,
+ "created_at": "2026-10-06T08:00:00Z",
+ "updated_at": "2026-10-06T08:00:00Z"
+ }
+---
+
+
diff --git a/api-reference/sessions/archive.md b/api-reference/sessions/archive.md
index 05c71eb6..b0ecf1f4 100644
--- a/api-reference/sessions/archive.md
+++ b/api-reference/sessions/archive.md
@@ -1,9 +1,122 @@
---
-title: "Archive Session"
-description: "Archive a Session while preserving its record."
+title: Archive Session
+description: Archive Session with row_version.
aside: false
outline: false
-apiReferenceKey: "sessions/archive"
+apiReference:
+ title: Archive Session
+ operation: Sessions
+ method: POST
+ path: /v1/agents/sessions/{session_id}/archive
+ description: Archive a Session with compare-and-set. Archived Sessions reject input, metadata updates, and streams. DELETE is the SDK form of archive.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
+ - title: Request body
+ description: JSON object.
+ fields:
+ - name: row_version
+ type: integer
+ required: true
+ description: The row_version you last read. A stale value returns 409.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/archive \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "row_version": 2
+ }'
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/archive",
+ headers=headers,
+ json={
+ "row_version": 2
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/archive', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "row_version": 2
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ "object": "agent.session",
+ "agent": {
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
+ "multi_agent": {"enabled": false, "max_concurrent_subagents": null},
+ "reasoning": {"effort": null, "summary": null},
+ "service_tier": "auto",
+ "text": {"format": {"type": "text"}, "verbosity": "medium"},
+ "tools": []
+ },
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 1,
+ "title": "Market scan",
+ "model": "deepseek/deepseek-v4-flash",
+ "environment": {
+ "id": "env_…",
+ "type": "openai_hosted",
+ "capability_directories": [],
+ "files": [],
+ "network": {"access": "enabled", "allowed_domains": []},
+ "packages": {"npm": [], "python": [], "system": []},
+ "plugins": [],
+ "skills": []
+ },
+ "metadata": {},
+ "status": "idle",
+ "lifecycle_status": "archived",
+ "sync_status": "ready",
+ "execution_status": "idle",
+ "failure": null,
+ "row_version": 3,
+ "required_actions": [],
+ "vault_ids": [],
+ "usage": null,
+ "created_at": 1791273600,
+ "last_active_at": 1791273600,
+ "updated_at": 1791273600,
+ "archived_at": null,
+ "source": "direct",
+ "source_id": null
+ }
---
diff --git a/api-reference/sessions/cancel.md b/api-reference/sessions/cancel.md
new file mode 100644
index 00000000..afad7adf
--- /dev/null
+++ b/api-reference/sessions/cancel.md
@@ -0,0 +1,77 @@
+---
+title: Cancel Session Turn
+description: Cancel the in-progress Turn of a Session.
+aside: false
+outline: false
+apiReference:
+ title: Cancel Session Turn
+ operation: Sessions
+ method: POST
+ path: /v1/agents/sessions/{session_id}/cancel
+ description: "SandBase extension. Request cancellation of the in-progress Turn. The body may be empty or {}. Idempotency-Key is required. If the Session is idle, the call still returns 202 with accepted true and changes nothing. The standard alternative is sending an agent.session.input.cancel event."
+ groups:
+ - title: Path parameters
+ fields:
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
+ - title: Headers
+ fields:
+ - name: Idempotency-Key
+ type: string
+ required: true
+ description: 1-128 characters, no leading or trailing whitespace. Reuse the same key and body when retrying; the same key with a different body returns 409.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/cancel \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: market-scan-cancel-1" \
+ -H "Content-Type: application/json" \
+ -d '{}'
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ "Idempotency-Key": "market-scan-cancel-1",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/cancel",
+ headers=headers,
+ json={},
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/cancel', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Idempotency-Key': 'market-scan-cancel-1',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({}),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 202 Accepted
+ code: |-
+ {
+ "session_id": "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ "accepted": true
+ }
+---
+
+
diff --git a/api-reference/sessions/create.md b/api-reference/sessions/create.md
index 7175fee6..e5a1686c 100644
--- a/api-reference/sessions/create.md
+++ b/api-reference/sessions/create.md
@@ -1,9 +1,200 @@
---
-title: "Create Session"
-description: "Create a stateful execution of an Agent. SandBase resolves runtime binding internally."
+title: Create Session
+description: Create a Session for a saved Agent with POST /v1/agents/sessions.
aside: false
outline: false
-apiReferenceKey: "sessions/create"
+apiReference:
+ title: Create Session
+ operation: Sessions
+ method: POST
+ path: /v1/agents/sessions
+ description: "Create a Session for a saved Agent version. The Agent configuration, Skill versions, and MCP connections are frozen for the life of the Session. Sessions always run in SandBase's hosted cloud, so there is no execution environment to choose. Inline Agents and stream=true return 400: create the Session, open the event stream, then send input. Execution admission (balance and organization status) is checked here. The OpenAI SDK requires an environment argument; see OpenAI compatibility (/agents/openai-compatibility)."
+ groups:
+ - title: Headers
+ fields:
+ - name: Idempotency-Key
+ type: string
+ required: false
+ description: Optional. When omitted the service generates one, so a retried request can create a duplicate. Send your own stable key for retries.
+ - title: Request body
+ description: Strict JSON object.
+ fields:
+ - name: agent_id
+ type: string
+ required: true
+ description: Saved Agent ID (agt_ prefix). The Agent must be active with projection_status ready.
+ - name: version
+ type: integer
+ required: false
+ description: Agent version. Defaults to the current version.
+ - name: title
+ type: string
+ required: false
+ description: Session title.
+ - name: input
+ type: string | array
+ required: false
+ description: 'Optional first input: a string, or an array with exactly one message, e.g. [{"role":"user","content":[{"type":"input_text","text":"…"}]}]. A bare object returns 400.'
+ - name: model
+ type: string
+ required: false
+ description: Model override. agent.model is the OpenAI SDK form; when both are sent they must match.
+ - name: metadata
+ type: object
+ required: false
+ description: Up to 16 string pairs.
+ - name: model_provider
+ type: object
+ required: false
+ description: Model credential. Omit to use your calling API key. {"type":"custom","base_url":"https://…","api_key":"…"} calls your own OpenAI-compatible endpoint. Write-only.
+ - title: Session object
+ description: Returned by create, get, update, rename, archive, and unarchive.
+ fields:
+ - name: id
+ type: string
+ required: true
+ description: Session ID with the ses_ prefix.
+ - name: status
+ type: string
+ required: true
+ description: "Execution status: idle, in_progress, or failed. requires_action is not supported yet and surfaces as 502 executor_contract_error."
+ - name: lifecycle_status
+ type: string
+ required: true
+ description: active or archived. Archived Sessions reject input.
+ - name: failure
+ type: object · null
+ required: true
+ description: Public failure with code, message, action, and optional retryable.
+ - name: agent_id · agent_version · model
+ type: string · integer · string
+ required: true
+ description: Frozen Agent version and effective model.
+ - name: agent · usage
+ type: object · object | null
+ required: true
+ description: OpenAI-shaped snapshot of the effective Agent configuration, and token usage (null until a turn reports usage).
+ - name: source · source_id
+ type: string · null
+ required: false
+ description: direct, service, or schedule, plus the Service or Schedule ID.
+ - name: row_version
+ type: integer
+ required: true
+ description: Compare-and-set token for rename, archive, and unarchive.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/agents/sessions \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: market-scan-2026-10-06" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "title": "Market scan"
+ }'
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ session = client.beta.agents.sessions.create(
+ agent_id="agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ environment={"type": "openai_hosted"}, # Required by the OpenAI SDK; SandBase always runs in its hosted cloud.
+ )
+ print(session.id, session.status)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ "Idempotency-Key": "market-scan-2026-10-06",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/agents/sessions",
+ headers=headers,
+ json={
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "title": "Market scan",
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Idempotency-Key': 'market-scan-2026-10-06',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "title": "Market scan"
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 201 Created
+ code: |-
+ {
+ "id": "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ "object": "agent.session",
+ "agent": {
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
+ "multi_agent": {"enabled": false, "max_concurrent_subagents": null},
+ "reasoning": {"effort": null, "summary": null},
+ "service_tier": "auto",
+ "text": {"format": {"type": "text"}, "verbosity": "medium"},
+ "tools": []
+ },
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 1,
+ "title": "Market scan",
+ "model": "deepseek/deepseek-v4-flash",
+ "environment": {
+ "id": "env_…",
+ "type": "openai_hosted",
+ "capability_directories": [],
+ "files": [],
+ "network": {"access": "enabled", "allowed_domains": []},
+ "packages": {"npm": [], "python": [], "system": []},
+ "plugins": [],
+ "skills": []
+ },
+ "metadata": {},
+ "status": "idle",
+ "lifecycle_status": "active",
+ "sync_status": "ready",
+ "execution_status": "idle",
+ "failure": null,
+ "row_version": 1,
+ "required_actions": [],
+ "vault_ids": [],
+ "usage": null,
+ "created_at": 1791273600,
+ "last_active_at": 1791273600,
+ "updated_at": 1791273600,
+ "archived_at": null,
+ "source": "direct",
+ "source_id": null
+ }
---
diff --git a/api-reference/sessions/delete-file.md b/api-reference/sessions/delete-file.md
new file mode 100644
index 00000000..36c9da16
--- /dev/null
+++ b/api-reference/sessions/delete-file.md
@@ -0,0 +1,67 @@
+---
+title: Delete Session File
+description: Delete a Session attachment.
+aside: false
+outline: false
+apiReference:
+ title: Delete Session File
+ operation: Sessions
+ method: DELETE
+ path: /v1/agents/sessions/{session_id}/files/{file_id}
+ description: Delete an attachment. A pending upload or an archived Session returns 409.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
+ - name: file_id
+ type: string
+ required: true
+ description: Attachment ID (fil_ prefix).
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X DELETE https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/files/fil_6d8f0a2c-4e6b-5d8f-a1c3-5e7a9c1e3b72 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "DELETE",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/files/fil_6d8f0a2c-4e6b-5d8f-a1c3-5e7a9c1e3b72",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/files/fil_6d8f0a2c-4e6b-5d8f-a1c3-5e7a9c1e3b72', {
+ method: 'DELETE',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "deleted": true,
+ "id": "fil_6d8f0a2c-4e6b-5d8f-a1c3-5e7a9c1e3b72"
+ }
+---
+
+
diff --git a/api-reference/sessions/delete.md b/api-reference/sessions/delete.md
index 5a30baf3..907f1fa4 100644
--- a/api-reference/sessions/delete.md
+++ b/api-reference/sessions/delete.md
@@ -1,30 +1,73 @@
---
-title: "Delete Session"
-description: "Permanently delete a Session that is not running or rescheduling."
+title: Delete Session
+description: Delete (archive) a Session.
aside: false
outline: false
apiReference:
title: Delete Session
operation: Sessions
method: DELETE
- path: /v1/sessions/{id}
- description: Permanently delete a Session. Running and rescheduling Sessions return 409; archive instead when history must be retained.
+ path: /v1/agents/sessions/{session_id}
+ description: Archives the Session. Sessions are not hard-deleted; history stays readable and you can unarchive later.
groups:
- title: Path parameters
fields:
- - { name: id, type: string, required: true, description: Session ID. }
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
examples:
- label: cURL
language: bash
code: |-
- curl -X DELETE https://api.sandbase.ai/v1/sessions/sess_01... \
+ curl -X DELETE https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83 \
-H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ result = client.beta.agents.sessions.delete("ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83")
+ print(result.deleted)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "DELETE",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83', {
+ method: 'DELETE',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
response:
status: 200 OK
code: |-
{
- "id": "sess_01...",
- "type": "session_deleted"
+ "id": "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ "object": "agent.session.deleted",
+ "deleted": true
}
---
diff --git a/api-reference/sessions/download-file.md b/api-reference/sessions/download-file.md
new file mode 100644
index 00000000..85230433
--- /dev/null
+++ b/api-reference/sessions/download-file.md
@@ -0,0 +1,62 @@
+---
+title: Download Session File
+description: Download a Session attachment.
+aside: false
+outline: false
+apiReference:
+ title: Download Session File
+ operation: Sessions
+ method: GET
+ path: /v1/agents/sessions/{session_id}/files/{file_id}/content
+ description: Download the original bytes of a ready attachment. Other statuses return 404.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
+ - name: file_id
+ type: string
+ required: true
+ description: Attachment ID (fil_ prefix).
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/files/fil_6d8f0a2c-4e6b-5d8f-a1c3-5e7a9c1e3b72/content \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/files/fil_6d8f0a2c-4e6b-5d8f-a1c3-5e7a9c1e3b72/content",
+ headers=headers,
+ )
+ response.raise_for_status()
+ with open("q3-report.pdf", "wb") as f:
+ f.write(response.content)
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/files/fil_6d8f0a2c-4e6b-5d8f-a1c3-5e7a9c1e3b72/content', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ const bytes = new Uint8Array(await response.arrayBuffer());
+ console.log(bytes.length, response.headers.get('content-type'));
+---
+
+
diff --git a/api-reference/sessions/get-turn.md b/api-reference/sessions/get-turn.md
new file mode 100644
index 00000000..db538b12
--- /dev/null
+++ b/api-reference/sessions/get-turn.md
@@ -0,0 +1,70 @@
+---
+title: Get Session Turn
+description: Read one Session Turn.
+aside: false
+outline: false
+apiReference:
+ title: Get Session Turn
+ operation: Sessions
+ method: GET
+ path: /v1/agents/sessions/{session_id}/turns/{turn_id}
+ description: Read one Turn by ID. Unknown IDs return 404.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
+ - name: turn_id
+ type: string
+ required: true
+ description: Turn ID from the turns list.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/turns/turn_… \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ turn = client.beta.agents.sessions.turns.retrieve("turn_…", session_id="ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83")
+ print(turn.status)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/turns/turn_…",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/turns/turn_…', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+---
+
+
diff --git a/api-reference/sessions/get.md b/api-reference/sessions/get.md
index 42091a9c..9ffd9d71 100644
--- a/api-reference/sessions/get.md
+++ b/api-reference/sessions/get.md
@@ -1,9 +1,149 @@
---
-title: "Get Session"
-description: "Retrieve a session by ID, including current status and agent reference."
+title: Get Session
+description: Read one Session.
aside: false
outline: false
-apiReferenceKey: "sessions/get"
+apiReference:
+ title: Get Session
+ operation: Sessions
+ method: GET
+ path: /v1/agents/sessions/{session_id}
+ description: Read a Session. Execution status is read live, so poll this endpoint or use the event stream to follow progress.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
+ - title: Session object
+ description: Returned by create, get, update, rename, archive, and unarchive.
+ fields:
+ - name: id
+ type: string
+ required: true
+ description: Session ID with the ses_ prefix.
+ - name: status
+ type: string
+ required: true
+ description: "Execution status: idle, in_progress, or failed. requires_action is not supported yet and surfaces as 502 executor_contract_error."
+ - name: lifecycle_status
+ type: string
+ required: true
+ description: active or archived. Archived Sessions reject input.
+ - name: failure
+ type: object · null
+ required: true
+ description: Public failure with code, message, action, and optional retryable.
+ - name: agent_id · agent_version · model
+ type: string · integer · string
+ required: true
+ description: Frozen Agent version and effective model.
+ - name: agent · usage
+ type: object · object | null
+ required: true
+ description: OpenAI-shaped snapshot of the effective Agent configuration, and token usage (null until a turn reports usage).
+ - name: source · source_id
+ type: string · null
+ required: false
+ description: direct, service, or schedule, plus the Service or Schedule ID.
+ - name: row_version
+ type: integer
+ required: true
+ description: Compare-and-set token for rename, archive, and unarchive.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ session = client.beta.agents.sessions.retrieve("ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83")
+ print(session.status, session.failure)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ "object": "agent.session",
+ "agent": {
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
+ "multi_agent": {"enabled": false, "max_concurrent_subagents": null},
+ "reasoning": {"effort": null, "summary": null},
+ "service_tier": "auto",
+ "text": {"format": {"type": "text"}, "verbosity": "medium"},
+ "tools": []
+ },
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 1,
+ "title": "Market scan",
+ "model": "deepseek/deepseek-v4-flash",
+ "environment": {
+ "id": "env_…",
+ "type": "openai_hosted",
+ "capability_directories": [],
+ "files": [],
+ "network": {"access": "enabled", "allowed_domains": []},
+ "packages": {"npm": [], "python": [], "system": []},
+ "plugins": [],
+ "skills": []
+ },
+ "metadata": {},
+ "status": "in_progress",
+ "lifecycle_status": "active",
+ "sync_status": "ready",
+ "execution_status": "in_progress",
+ "failure": null,
+ "row_version": 1,
+ "required_actions": [],
+ "vault_ids": [],
+ "usage": null,
+ "created_at": 1791273600,
+ "last_active_at": 1791273600,
+ "updated_at": 1791273600,
+ "archived_at": null,
+ "source": "direct",
+ "source_id": null
+ }
---
diff --git a/api-reference/sessions/index.md b/api-reference/sessions/index.md
index a3cf4042..ee1ea054 100644
--- a/api-reference/sessions/index.md
+++ b/api-reference/sessions/index.md
@@ -1,49 +1,85 @@
---
title: Sessions API
-description: Create an Agent Session, send input, and inspect its event history.
+description: Run a saved Agent version, send input, stream events, and read Session history.
---
-
# Sessions API
-A Session is a stateful execution of an [Agent](/api-reference/agents/). You can include the first event in Create Session, then send later events to continue the Session.
+A Session runs one saved [Agent](/api-reference/agents/) version in SandBase's hosted cloud; there is no execution environment to choose. Session IDs use the `ses_` prefix. Sessions are a sub-resource of Agents: every path starts with `/v1/agents/sessions`.
+
+The standard operations follow the OpenAI Agents protocol (`client.beta.agents.sessions`); see [OpenAI compatibility](/agents/openai-compatibility) for what the official SDK can and cannot do. SandBase extensions add renaming, archive state, cancellation, attachments, and a reconnectable product stream.
-::: info Session identity contract
-The public `session_id` identifies one persistent Session across Direct Session and Service entry points. Every Schedule (Deployment) trigger creates a separate `drun_*` DeploymentRun and, when Session creation succeeds, links it to one new Session. Runtime replacement and migration are internal and never change the public Session identity.
+::: info Where Sessions come from
+You create a Session directly with `POST /v1/agents/sessions`, or a [Service](/api-reference/services/) invocation or [Schedule](/api-reference/schedules/) Run creates one for you. The Session reports `source` (`direct`, `service`, or `schedule`) and `source_id`, and the Run reports the Session in `session_id`.
:::
+## Recommended flow
+
+1. [Create a Session](./create) for a saved Agent. The Agent version, Skill versions, and MCP connections are frozen at this point.
+2. [Open the event stream](./stream) with `GET /v1/agents/sessions/{session_id}/events`. The stream is live-only, so open it before you send input.
+3. [Send input](./send-events) with `POST /v1/agents/sessions/{session_id}/events` and an `agent.session.input.message` event. The call returns 202.
+4. Read results from the stream, or afterwards from [items](./items) and [turns](./turns).
+
## Session operations
| Method | Path | Purpose |
|---|---|---|
-| `POST` | `/v1/sessions` | [Create a Session](./create) |
-| `GET` | `/v1/sessions` | [List Sessions](./list) |
-| `GET` | `/v1/sessions/{session_id}` | [Get a Session](./get) |
-| `POST` | `/v1/sessions/{session_id}` | [Update a Session](./update) |
-| `DELETE` | `/v1/sessions/{session_id}` | [Delete a Session](./delete) |
-| `POST` | `/v1/sessions/{session_id}/events` | [Send events](./send-events) |
-| `GET` | `/v1/sessions/{session_id}/events` | [List persisted events](./list-events) |
-| `GET` | `/v1/sessions/{session_id}/events/stream` | [Replay events as SSE](./stream) |
-| `POST` | `/v1/sessions/{session_id}/archive` | [Archive a Session](./archive) |
-
-## Basic flow
-
-1. Create a Session with an Agent; SandBase resolves the runtime binding internally.
-2. Include the first `user.message` in `initial_events`, or send it later through the events endpoint.
-3. Poll the event list, or replay persisted events through the SSE endpoint.
-4. Archive the Session when it should no longer accept new events. History remains readable.
-
-Session updates shallow-merge `metadata`: a null property value removes that key. Agent `tools` and `mcp_servers` overrides are full-array replacements, while the pinned Agent version and runtime binding remain immutable.
-
-::: warning Recover an uncertain initial delivery
-If native runtime delivery returns `502` with a top-level `session_id`, the delivery outcome is unknown. Open that Session and inspect or stream its Events before taking another action. Do not blindly create a new Session and resend the message.
-:::
+| `POST` | `/v1/agents/sessions` | [Create a Session](./create) |
+| `GET` | `/v1/agents/sessions` | [List Sessions](./list) |
+| `GET` | `/v1/agents/sessions/{session_id}` | [Get a Session](./get) |
+| `POST` | `/v1/agents/sessions/{session_id}` | [Replace metadata](./update) |
+| `PATCH` | `/v1/agents/sessions/{session_id}` | [Rename](./rename) with `row_version` |
+| `DELETE` | `/v1/agents/sessions/{session_id}` | [Delete](./delete), which archives the Session |
+| `POST` | `/v1/agents/sessions/{session_id}/events` | [Send one input or cancel event](./send-events) |
+| `GET` | `/v1/agents/sessions/{session_id}/events` | [Stream live events](./stream) (SSE) |
+| `GET` | `/v1/agents/sessions/{session_id}/items` | [List items](./items) |
+| `GET` | `/v1/agents/sessions/{session_id}/turns` | [List turns](./turns) |
+| `GET` | `/v1/agents/sessions/{session_id}/turns/{turn_id}` | [Get one turn](./get-turn) |
+| `POST` | `/v1/agents/sessions/{session_id}/cancel` | [Cancel the running turn](./cancel) |
+| `POST` | `/v1/agents/sessions/{session_id}/archive` | [Archive](./archive) |
+| `POST` | `/v1/agents/sessions/{session_id}/unarchive` | [Unarchive](./unarchive) |
+| `POST` | `/v1/agents/sessions/{session_id}/reconnect` | [Reconnect](./reconnect) after a dropped stream |
+| `GET` | `/v1/agents/sessions/{session_id}/events/stream` | [Snapshot-then-live stream](./stream-snapshot) (SSE) |
+| `POST` | `/v1/agents/sessions/{session_id}/files` | [Upload an attachment](./upload-file) |
+| `GET` | `/v1/agents/sessions/{session_id}/files` | [List attachments](./list-files) |
+| `GET` | `/v1/agents/sessions/{session_id}/files/{file_id}/content` | [Download an attachment](./download-file) |
+| `DELETE` | `/v1/agents/sessions/{session_id}/files/{file_id}` | [Delete an attachment](./delete-file) |
+
+## Status fields
+
+- `status` is the execution status: `idle`, `in_progress`, or `failed`. Function tools that require a client result (`requires_action`) are not supported yet; such a Session read returns `502 executor_contract_error`.
+- `lifecycle_status` is `active` or `archived`. Archived Sessions reject input, metadata updates, and streams. `DELETE` archives; `unarchive` restores.
+- `failure` is `null` or an object with `code`, `message`, `action`, and sometimes `retryable`.
+
+| `failure.code` | Meaning | Suggested `action` |
+|---|---|---|
+| `session_creation_rejected` | The execution layer rejected the Session | `create_new_session` |
+| `model_authentication_failed` | The model provider rejected the key | `create_session_with_updated_model_provider` |
+| `model_rate_limited` | The model provider is rate limiting (retryable) | `retry_later` |
+| `model_quota_exceeded` | The model account has no quota | `check_model_account` |
+| `context_limit_exceeded` | The conversation exceeds the model context | `create_new_session` |
+| `environment_connection_timeout` | The environment did not connect in time (retryable) | `retry_later` |
+| `runtime_unavailable` | The execution environment is unavailable (retryable) | `retry_later` |
+| `model_request_failed` | The model request did not complete | `inspect_history` |
+| `model_configuration_rejected` | The model or execution layer rejected the configuration | `create_session_with_updated_model_provider` |
+| `execution_failed` | Failure without a more specific public cause | `inspect_history` |
+
+List results can also contain placeholder rows with `session_not_ready`, `session_execution_unavailable`, `session_configuration_unavailable`, or `session_snapshot_unavailable` when a Session cannot be read live.
+
+## Model credentials
+
+By default a Session calls models with the same SandBase API key that created it. Send `model_provider` to choose explicitly:
+
+- `{"type": "sandbase"}` uses your calling key. `base_url` is not allowed.
+- `{"type": "custom", "base_url": "https://…", "api_key": "…"}` calls your own OpenAI-compatible HTTPS endpoint. The URL cannot contain credentials, a query, or a fragment, and SandBase never falls back to your SandBase key.
+
+`model_provider` is write-only and never appears in responses.
-Archive keeps Session history readable and rejects new events with `409 session_archived`. Delete permanently removes the Session and its persisted events.
+## Admission and errors
-## Filtering and updates
+Creating a Session and sending input check execution admission: `402 spending_limit_exceeded` when the key limit or organization balance is exhausted, `403 org_disabled` when the organization cannot run Agents, and `503 admission_unavailable` when the check cannot complete. Reads, cancellation, and archive are not blocked by balance.
-List Sessions with repeated `statuses` parameters to select one or more of `idle`, `running`, `rescheduling`, or `terminated`. Cursor responses include `next_page` only when another page exists.
+Sending input can also return `409 session_input_pending` (earlier input is still being delivered; wait, and do not resend with a new key), `409 session_input_expired` (inspect history before sending again), or `409 session_environment_unavailable` (wait or create a new Session). See [Errors](/api-reference/errors#agents-platform-errors).
-The `source` field records how the Session was created: `direct`, `endpoint`, `deployment`, or `store_trial` for a Service store trial.
+## Pagination
-`POST /v1/sessions/{session_id}` can update the title, metadata, and Session-local `agent.tools` or `agent.mcp_servers` overrides. It never changes the pinned Agent version. Successful deletion returns the deleted Session ID with `type: "session_deleted"`.
+Sessions, items, and turns use cursor pagination (`limit` 1-100, `after`, `order`). Attachments use offset pagination (`limit` 1-100, `offset`).
diff --git a/api-reference/sessions/items.md b/api-reference/sessions/items.md
new file mode 100644
index 00000000..d027a57a
--- /dev/null
+++ b/api-reference/sessions/items.md
@@ -0,0 +1,83 @@
+---
+title: List Session Items
+description: Read Session items history.
+aside: false
+outline: false
+apiReference:
+ title: List Session Items
+ operation: Sessions
+ method: GET
+ path: /v1/agents/sessions/{session_id}/items
+ description: Read conversation items (messages, tool calls, and outputs) with cursor pagination. Use this to recover output that was produced while no stream was open.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
+ - title: Query parameters
+ fields:
+ - name: limit
+ type: integer
+ required: false
+ description: Page size from 1 to 100. Values outside the range return 400.
+ default: "20"
+ - name: after
+ type: string
+ required: false
+ description: Cursor. Pass last_id from the previous page.
+ - name: order
+ type: string
+ required: false
+ description: asc or desc by creation time.
+ default: desc
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/items?order=asc&limit=50" \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ page = client.beta.agents.sessions.items.list("ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83", order="asc", limit=50)
+ for row in page.data:
+ print(row.id)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/items?order=asc&limit=50",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/items?order=asc&limit=50', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+---
+
+
diff --git a/api-reference/sessions/list-events.md b/api-reference/sessions/list-events.md
deleted file mode 100644
index 186c8fc4..00000000
--- a/api-reference/sessions/list-events.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-title: "List Events"
-description: "List persisted Agent, user, and tool events for a Session with ordering, filtering, and cursor pagination."
-aside: false
-outline: false
-apiReferenceKey: "sessions/list-events"
----
-
-
diff --git a/api-reference/sessions/list-files.md b/api-reference/sessions/list-files.md
new file mode 100644
index 00000000..9fb69b0d
--- /dev/null
+++ b/api-reference/sessions/list-files.md
@@ -0,0 +1,88 @@
+---
+title: List Session Files
+description: List Session attachments.
+aside: false
+outline: false
+apiReference:
+ title: List Session Files
+ operation: Sessions
+ method: GET
+ path: /v1/agents/sessions/{session_id}/files
+ description: List attachments with offset pagination. limit must be 1-100.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
+ - title: Query parameters
+ fields:
+ - name: limit
+ type: integer
+ required: false
+ description: Page size up to 100; values above 100 return 400, and 0 or non-numeric values fall back to 20.
+ default: "20"
+ - name: offset
+ type: integer
+ required: false
+ description: Number of records to skip.
+ default: "0"
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/files?limit=20" \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/files?limit=20",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/files?limit=20', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "data": [
+ {
+ "id": "fil_6d8f0a2c-4e6b-5d8f-a1c3-5e7a9c1e3b72",
+ "session_id": "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ "name": "q3-report.pdf",
+ "mime_type": "application/pdf",
+ "size_bytes": 482113,
+ "status": "ready",
+ "created_at": "2026-10-06T08:10:00Z",
+ "updated_at": "2026-10-06T08:10:02Z"
+ }
+ ],
+ "total": 1,
+ "limit": 20,
+ "offset": 0
+ }
+---
+
+
diff --git a/api-reference/sessions/list.md b/api-reference/sessions/list.md
index 5f963e56..4c8fc3c7 100644
--- a/api-reference/sessions/list.md
+++ b/api-reference/sessions/list.md
@@ -1,9 +1,141 @@
---
-title: "List Sessions"
-description: "List Agent Sessions for your organization with filtering and cursor pagination."
+title: List Sessions
+description: List Sessions with cursor pagination.
aside: false
outline: false
-apiReferenceKey: "sessions/list"
+apiReference:
+ title: List Sessions
+ operation: Sessions
+ method: GET
+ path: /v1/agents/sessions
+ description: List Sessions in your organization with cursor pagination. Rows that cannot be read live still appear, with a failure such as session_not_ready or session_snapshot_unavailable.
+ groups:
+ - title: Query parameters
+ fields:
+ - name: lifecycle_status
+ type: string
+ required: false
+ description: active, archived, or all.
+ default: active
+ - name: agent_id
+ type: string
+ required: false
+ description: Only Sessions of this Agent.
+ - name: limit
+ type: integer
+ required: false
+ description: Page size from 1 to 100. Values outside the range return 400.
+ default: "20"
+ - name: after
+ type: string
+ required: false
+ description: Cursor. Pass last_id from the previous page.
+ - name: order
+ type: string
+ required: false
+ description: asc or desc by creation time.
+ default: desc
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET "https://api.sandbase.ai/v1/agents/sessions?agent_id=agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60&limit=20" \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ for session in client.beta.agents.sessions.list(limit=20):
+ print(session.id, session.status)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/agents/sessions?agent_id=agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60&limit=20",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions?agent_id=agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60&limit=20', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "object": "list",
+ "data": [
+ {
+ "id": "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ "object": "agent.session",
+ "agent": {
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
+ "multi_agent": {"enabled": false, "max_concurrent_subagents": null},
+ "reasoning": {"effort": null, "summary": null},
+ "service_tier": "auto",
+ "text": {"format": {"type": "text"}, "verbosity": "medium"},
+ "tools": []
+ },
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 1,
+ "title": "Market scan",
+ "model": "deepseek/deepseek-v4-flash",
+ "environment": {
+ "id": "env_…",
+ "type": "openai_hosted",
+ "capability_directories": [],
+ "files": [],
+ "network": {"access": "enabled", "allowed_domains": []},
+ "packages": {"npm": [], "python": [], "system": []},
+ "plugins": [],
+ "skills": []
+ },
+ "metadata": {},
+ "status": "idle",
+ "lifecycle_status": "active",
+ "sync_status": "ready",
+ "execution_status": "idle",
+ "failure": null,
+ "row_version": 1,
+ "required_actions": [],
+ "vault_ids": [],
+ "usage": null,
+ "created_at": 1791273600,
+ "last_active_at": 1791273600,
+ "updated_at": 1791273600,
+ "archived_at": null,
+ "source": "direct",
+ "source_id": null
+ }
+ ],
+ "has_more": false,
+ "first_id": "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ "last_id": "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83"
+ }
---
diff --git a/api-reference/sessions/reconnect.md b/api-reference/sessions/reconnect.md
new file mode 100644
index 00000000..593b3740
--- /dev/null
+++ b/api-reference/sessions/reconnect.md
@@ -0,0 +1,109 @@
+---
+title: Reconnect Session
+description: Get the current Session and the product stream URL after a disconnect.
+aside: false
+outline: false
+apiReference:
+ title: Reconnect Session
+ operation: Sessions
+ method: POST
+ path: /v1/agents/sessions/{session_id}/reconnect
+ description: "SandBase extension for browser-style clients. Returns the current Session plus stream_url. Opening stream_url (GET /v1/agents/sessions/{session_id}/events/stream) sends session.snapshot with the Session, recent items, and turns, then live events and heartbeat. If the upstream breaks, the stream emits stream.interrupted with resend_input false: reconnect instead of resending input. This call needs only the agents.read scope."
+ groups:
+ - title: Path parameters
+ fields:
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/reconnect \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/reconnect",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/reconnect', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "session": {
+ "id": "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ "object": "agent.session",
+ "agent": {
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
+ "multi_agent": {"enabled": false, "max_concurrent_subagents": null},
+ "reasoning": {"effort": null, "summary": null},
+ "service_tier": "auto",
+ "text": {"format": {"type": "text"}, "verbosity": "medium"},
+ "tools": []
+ },
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 1,
+ "title": "Market scan",
+ "model": "deepseek/deepseek-v4-flash",
+ "environment": {
+ "id": "env_…",
+ "type": "openai_hosted",
+ "capability_directories": [],
+ "files": [],
+ "network": {"access": "enabled", "allowed_domains": []},
+ "packages": {"npm": [], "python": [], "system": []},
+ "plugins": [],
+ "skills": []
+ },
+ "metadata": {},
+ "status": "in_progress",
+ "lifecycle_status": "active",
+ "sync_status": "ready",
+ "execution_status": "in_progress",
+ "failure": null,
+ "row_version": 1,
+ "required_actions": [],
+ "vault_ids": [],
+ "usage": null,
+ "created_at": 1791273600,
+ "last_active_at": 1791273600,
+ "updated_at": 1791273600,
+ "archived_at": null,
+ "source": "direct",
+ "source_id": null
+ },
+ "stream_url": "/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/events/stream",
+ "recovery": "snapshot_then_live",
+ "requires_authorization_header": true
+ }
+---
+
+
diff --git a/api-reference/sessions/rename.md b/api-reference/sessions/rename.md
new file mode 100644
index 00000000..18aa26f4
--- /dev/null
+++ b/api-reference/sessions/rename.md
@@ -0,0 +1,129 @@
+---
+title: Rename Session
+description: Change a Session title with compare-and-set.
+aside: false
+outline: false
+apiReference:
+ title: Rename Session
+ operation: Sessions
+ method: PATCH
+ path: /v1/agents/sessions/{session_id}
+ description: SandBase extension. Change the title with compare-and-set on row_version.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
+ - title: Request body
+ description: JSON object.
+ fields:
+ - name: title
+ type: string
+ required: true
+ description: New title, up to 512 characters.
+ - name: row_version
+ type: integer
+ required: true
+ description: The row_version you last read. A stale value returns 409.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X PATCH https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "title": "Market scan (final)",
+ "row_version": 1
+ }'
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "PATCH",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ headers=headers,
+ json={
+ "title": "Market scan (final)",
+ "row_version": 1
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83', {
+ method: 'PATCH',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "title": "Market scan (final)",
+ "row_version": 1
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ "object": "agent.session",
+ "agent": {
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
+ "multi_agent": {"enabled": false, "max_concurrent_subagents": null},
+ "reasoning": {"effort": null, "summary": null},
+ "service_tier": "auto",
+ "text": {"format": {"type": "text"}, "verbosity": "medium"},
+ "tools": []
+ },
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 1,
+ "title": "Market scan (final)",
+ "model": "deepseek/deepseek-v4-flash",
+ "environment": {
+ "id": "env_…",
+ "type": "openai_hosted",
+ "capability_directories": [],
+ "files": [],
+ "network": {"access": "enabled", "allowed_domains": []},
+ "packages": {"npm": [], "python": [], "system": []},
+ "plugins": [],
+ "skills": []
+ },
+ "metadata": {},
+ "status": "idle",
+ "lifecycle_status": "active",
+ "sync_status": "ready",
+ "execution_status": "idle",
+ "failure": null,
+ "row_version": 2,
+ "required_actions": [],
+ "vault_ids": [],
+ "usage": null,
+ "created_at": 1791273600,
+ "last_active_at": 1791273600,
+ "updated_at": 1791273600,
+ "archived_at": null,
+ "source": "direct",
+ "source_id": null
+ }
+---
+
+
diff --git a/api-reference/sessions/send-events.md b/api-reference/sessions/send-events.md
index ec183ee9..6a727a4d 100644
--- a/api-reference/sessions/send-events.md
+++ b/api-reference/sessions/send-events.md
@@ -1,9 +1,151 @@
---
-title: "Send Session Events"
-description: "Send a message or interrupt event to a Session."
+title: Send Session Input
+description: Send one input or cancel event to a Session.
aside: false
outline: false
-apiReferenceKey: "sessions/send-events"
+apiReference:
+ title: Send Session Input
+ operation: Sessions
+ method: POST
+ path: /v1/agents/sessions/{session_id}/events
+ description: 'Send exactly one event per request: agent.session.input.message or agent.session.input.cancel. Returns 202 with no body; follow progress on the event stream. The SandBase extension form {"input": "…", "file_ids": [...]} sends text with up to 10 ready attachments and cannot be combined with events. A 409 session_input_pending means earlier input is still being delivered: wait, and do not resend with a new key.'
+ groups:
+ - title: Path parameters
+ fields:
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
+ - title: Headers
+ fields:
+ - name: Idempotency-Key
+ type: string
+ required: false
+ description: Optional but recommended. Reuse the same key when retrying the same input.
+ - title: Request body
+ description: Either events or input/file_ids.
+ fields:
+ - name: events
+ type: array
+ required: false
+ description: Exactly one event. input.message carries input as text or one user input_text message; input.cancel carries no input.
+ - name: input
+ type: string
+ required: false
+ description: SandBase extension. Non-empty text up to 256 KiB.
+ - name: file_ids
+ type: array
+ required: false
+ description: SandBase extension. Up to 10 distinct attachment IDs with status ready.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/events \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: market-scan-input-1" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "events": [
+ {
+ "type": "agent.session.input.message",
+ "input": [
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "input_text",
+ "text": "List three notable launches this week."
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }'
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ client.beta.agents.sessions.events.create(
+ "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ events=[{
+ "type": "agent.session.input.message",
+ "input": [{"role": "user", "content": [{"type": "input_text", "text": "List three notable launches this week."}]}],
+ }],
+ )
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ "Idempotency-Key": "market-scan-input-1",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/events",
+ headers=headers,
+ json={
+ "events": [
+ {
+ "type": "agent.session.input.message",
+ "input": [
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "input_text",
+ "text": "List three notable launches this week."
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ )
+ response.raise_for_status()
+ print(response.status_code) # 202 Accepted, no body
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/events', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Idempotency-Key': 'market-scan-input-1',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "events": [
+ {
+ "type": "agent.session.input.message",
+ "input": [
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "input_text",
+ "text": "List three notable launches this week."
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(response.status); // 202 Accepted, no body
---
diff --git a/api-reference/sessions/stream-snapshot.md b/api-reference/sessions/stream-snapshot.md
new file mode 100644
index 00000000..4d898cd4
--- /dev/null
+++ b/api-reference/sessions/stream-snapshot.md
@@ -0,0 +1,68 @@
+---
+title: Stream Session Snapshot
+description: Product stream that starts with a Session snapshot.
+aside: false
+outline: false
+apiReference:
+ title: Stream Session Snapshot
+ operation: Sessions
+ method: GET
+ path: /v1/agents/sessions/{session_id}/events/stream
+ description: "SandBase extension stream. Emits session.snapshot first ({session, items, turns, live_only: true}), then live Session events and periodic heartbeat. On upstream failure it emits stream.interrupted ({session_id, reconnect: true, resend_input: false}). cursor or Last-Event-ID returns 400. Send the API key in the Authorization or X-API-Key header."
+ groups:
+ - title: Path parameters
+ fields:
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/events/stream \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Accept: text/event-stream"
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ "Accept": "text/event-stream",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/events/stream",
+ headers=headers,
+ stream=True,
+ )
+ response.raise_for_status()
+ for line in response.iter_lines(decode_unicode=True):
+ if line:
+ print(line)
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/events/stream', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Accept': 'text/event-stream',
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ for (;;) {
+ const { value, done } = await reader.read();
+ if (done) break;
+ process.stdout.write(decoder.decode(value));
+ }
+---
+
+
diff --git a/api-reference/sessions/stream.md b/api-reference/sessions/stream.md
index de9eaf4b..caee51b4 100644
--- a/api-reference/sessions/stream.md
+++ b/api-reference/sessions/stream.md
@@ -1,9 +1,82 @@
---
-title: "Stream Events"
-description: "Replay persisted session events and follow new events via Server-Sent Events (SSE)."
+title: Stream Session Events
+description: Subscribe to live Session events over server-sent events.
aside: false
outline: false
-apiReferenceKey: "sessions/stream"
+apiReference:
+ title: Stream Session Events
+ operation: Sessions
+ method: GET
+ path: /v1/agents/sessions/{session_id}/events
+ description: 'Open the native live event stream (text/event-stream). Open it before sending input. The stream is live-only: Last-Event-ID or an after query returns 400, and events emitted while you were disconnected are not replayed; read them from items or turns. The server first writes a ": connected" comment. Event types start with agent.session.'
+ groups:
+ - title: Path parameters
+ fields:
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/events \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Accept: text/event-stream"
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ with client.beta.agents.sessions.events.stream("ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83") as stream:
+ for event in stream:
+ print(event.type)
+ # The stream is live-only and stays open; stop at a terminal turn event.
+ if event.type in ("agent.session.turn.completed", "agent.session.turn.failed", "agent.session.turn.cancelled"):
+ break
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ "Accept": "text/event-stream",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/events",
+ headers=headers,
+ stream=True,
+ )
+ response.raise_for_status()
+ for line in response.iter_lines(decode_unicode=True):
+ if line:
+ print(line)
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/events', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Accept': 'text/event-stream',
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ for (;;) {
+ const { value, done } = await reader.read();
+ if (done) break;
+ process.stdout.write(decoder.decode(value));
+ }
---
diff --git a/api-reference/sessions/turns.md b/api-reference/sessions/turns.md
new file mode 100644
index 00000000..5b646d06
--- /dev/null
+++ b/api-reference/sessions/turns.md
@@ -0,0 +1,83 @@
+---
+title: List Session Turns
+description: Read Session turns history.
+aside: false
+outline: false
+apiReference:
+ title: List Session Turns
+ operation: Sessions
+ method: GET
+ path: /v1/agents/sessions/{session_id}/turns
+ description: Read Turns with cursor pagination. A Turn groups one input and the work it triggered, including its status. Read one Turn with GET /v1/agents/sessions/{session_id}/turns/{turn_id}.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
+ - title: Query parameters
+ fields:
+ - name: limit
+ type: integer
+ required: false
+ description: Page size from 1 to 100. Values outside the range return 400.
+ default: "20"
+ - name: after
+ type: string
+ required: false
+ description: Cursor. Pass last_id from the previous page.
+ - name: order
+ type: string
+ required: false
+ description: asc or desc by creation time.
+ default: desc
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/turns?order=asc&limit=50" \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ page = client.beta.agents.sessions.turns.list("ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83", order="asc", limit=50)
+ for row in page.data:
+ print(row.id)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/turns?order=asc&limit=50",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/turns?order=asc&limit=50', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+---
+
+
diff --git a/api-reference/sessions/unarchive.md b/api-reference/sessions/unarchive.md
new file mode 100644
index 00000000..6c18d3e5
--- /dev/null
+++ b/api-reference/sessions/unarchive.md
@@ -0,0 +1,122 @@
+---
+title: Unarchive Session
+description: Unarchive Session with row_version.
+aside: false
+outline: false
+apiReference:
+ title: Unarchive Session
+ operation: Sessions
+ method: POST
+ path: /v1/agents/sessions/{session_id}/unarchive
+ description: Return an archived Session to active with compare-and-set.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
+ - title: Request body
+ description: JSON object.
+ fields:
+ - name: row_version
+ type: integer
+ required: true
+ description: The row_version you last read. A stale value returns 409.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/unarchive \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "row_version": 2
+ }'
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/unarchive",
+ headers=headers,
+ json={
+ "row_version": 2
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/unarchive', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "row_version": 2
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ "object": "agent.session",
+ "agent": {
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
+ "multi_agent": {"enabled": false, "max_concurrent_subagents": null},
+ "reasoning": {"effort": null, "summary": null},
+ "service_tier": "auto",
+ "text": {"format": {"type": "text"}, "verbosity": "medium"},
+ "tools": []
+ },
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 1,
+ "title": "Market scan",
+ "model": "deepseek/deepseek-v4-flash",
+ "environment": {
+ "id": "env_…",
+ "type": "openai_hosted",
+ "capability_directories": [],
+ "files": [],
+ "network": {"access": "enabled", "allowed_domains": []},
+ "packages": {"npm": [], "python": [], "system": []},
+ "plugins": [],
+ "skills": []
+ },
+ "metadata": {},
+ "status": "idle",
+ "lifecycle_status": "active",
+ "sync_status": "ready",
+ "execution_status": "idle",
+ "failure": null,
+ "row_version": 3,
+ "required_actions": [],
+ "vault_ids": [],
+ "usage": null,
+ "created_at": 1791273600,
+ "last_active_at": 1791273600,
+ "updated_at": 1791273600,
+ "archived_at": null,
+ "source": "direct",
+ "source_id": null
+ }
+---
+
+
diff --git a/api-reference/sessions/update.md b/api-reference/sessions/update.md
index 3d57963b..0b273205 100644
--- a/api-reference/sessions/update.md
+++ b/api-reference/sessions/update.md
@@ -1,32 +1,140 @@
---
-title: "Update Session"
-description: "Update mutable Session metadata and Session-local Agent overrides."
+title: Update Session Metadata
+description: Replace Session metadata.
aside: false
outline: false
apiReference:
- title: Update Session
+ title: Update Session Metadata
operation: Sessions
method: POST
- path: /v1/sessions/{id}
- description: Shallow-merge metadata or fully replace Session-local tool and MCP server overrides. Null metadata values delete keys; a null title preserves the current title.
+ path: /v1/agents/sessions/{session_id}
+ description: Replace the Session metadata map. metadata is the only accepted field; anything else returns 400. Archived Sessions return 404.
groups:
- title: Path parameters
fields:
- - { name: id, type: string, required: true, description: Session ID. }
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
- title: Request body
+ description: JSON object.
fields:
- - { name: title, type: string | null, required: false, description: New title; null or omission preserves the current title. }
- - { name: metadata, type: object, required: false, description: Shallow merge patch; null property values delete keys. }
- - { name: agent.tools, type: array, required: false, description: Full replacement for the Session-local tool override. }
- - { name: agent.mcp_servers, type: array, required: false, description: Full replacement for the Session-local MCP server override. }
+ - name: metadata
+ type: object
+ required: true
+ description: Complete new metadata map, up to 16 string pairs.
examples:
- label: cURL
language: bash
code: |-
- curl -X POST https://api.sandbase.ai/v1/sessions/sess_01... \
+ curl -X POST https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83 \
-H "Authorization: Bearer $SANDBASE_API_KEY" \
-H "Content-Type: application/json" \
- -d '{"title":"Research follow-up","metadata":{"phase":2,"obsolete_key":null}}'
+ -d '{
+ "metadata": {
+ "ticket": "OPS-142"
+ }
+ }'
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ session = client.beta.agents.sessions.update("ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83", metadata={"ticket": "OPS-142"})
+ print(session.metadata)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ headers=headers,
+ json={
+ "metadata": {
+ "ticket": "OPS-142"
+ }
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "metadata": {
+ "ticket": "OPS-142"
+ }
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ "object": "agent.session",
+ "agent": {
+ "id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Answer with cited sources.",
+ "multi_agent": {"enabled": false, "max_concurrent_subagents": null},
+ "reasoning": {"effort": null, "summary": null},
+ "service_tier": "auto",
+ "text": {"format": {"type": "text"}, "verbosity": "medium"},
+ "tools": []
+ },
+ "agent_id": "agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60",
+ "agent_version": 1,
+ "title": "Market scan",
+ "model": "deepseek/deepseek-v4-flash",
+ "environment": {
+ "id": "env_…",
+ "type": "openai_hosted",
+ "capability_directories": [],
+ "files": [],
+ "network": {"access": "enabled", "allowed_domains": []},
+ "packages": {"npm": [], "python": [], "system": []},
+ "plugins": [],
+ "skills": []
+ },
+ "metadata": {
+ "ticket": "OPS-142"
+ },
+ "status": "idle",
+ "lifecycle_status": "active",
+ "sync_status": "ready",
+ "execution_status": "idle",
+ "failure": null,
+ "row_version": 1,
+ "required_actions": [],
+ "vault_ids": [],
+ "usage": null,
+ "created_at": 1791273600,
+ "last_active_at": 1791273600,
+ "updated_at": 1791273600,
+ "archived_at": null,
+ "source": "direct",
+ "source_id": null
+ }
---
diff --git a/api-reference/sessions/upload-file.md b/api-reference/sessions/upload-file.md
new file mode 100644
index 00000000..c140fc99
--- /dev/null
+++ b/api-reference/sessions/upload-file.md
@@ -0,0 +1,93 @@
+---
+title: Upload Session File
+description: Upload an attachment into a Session workspace.
+aside: false
+outline: false
+apiReference:
+ title: Upload Session File
+ operation: Sessions
+ method: POST
+ path: /v1/agents/sessions/{session_id}/files
+ description: "SandBase extension. Upload one file (multipart field file, up to 5 MiB) into the Session workspace. Idempotency-Key is required. Supported extensions: .txt, .md, .csv, .log, .json, .pdf, .png, .jpg, .jpeg, .webp; content must match the extension. File names use letters, digits, spaces, dots, underscores, and hyphens. If the transfer fails the response is still 201 with status failed and error_code attachment_unavailable. Send a ready file with POST /v1/agents/sessions/{session_id}/events using file_ids."
+ groups:
+ - title: Path parameters
+ fields:
+ - name: session_id
+ type: string
+ required: true
+ description: Session ID (ses_ prefix).
+ - title: Headers
+ fields:
+ - name: Idempotency-Key
+ type: string
+ required: true
+ description: 1-128 characters, no leading or trailing whitespace. Reuse the same key and body when retrying; the same key with a different body returns 409.
+ - title: Request body
+ description: multipart/form-data.
+ fields:
+ - name: file
+ type: file
+ required: true
+ description: Exactly one file part.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/files \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: upload-q3-report" \
+ -F "file=@q3-report.pdf;type=application/pdf"
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ "Idempotency-Key": "upload-q3-report",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/files",
+ headers=headers,
+ files={"file": open("q3-report.pdf", "rb")},
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ import { readFile } from 'node:fs/promises';
+
+ const form = new FormData();
+ form.append('file', new Blob([await readFile('q3-report.pdf')], { type: 'application/pdf' }), 'q3-report.pdf');
+
+ const response = await fetch('https://api.sandbase.ai/v1/agents/sessions/ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83/files', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Idempotency-Key': 'upload-q3-report',
+ },
+ body: form,
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 201 Created
+ code: |-
+ {
+ "id": "fil_6d8f0a2c-4e6b-5d8f-a1c3-5e7a9c1e3b72",
+ "session_id": "ses_5b0e7c3a-91d2-5f4e-8a6b-2c9d4e1f7a83",
+ "name": "q3-report.pdf",
+ "mime_type": "application/pdf",
+ "size_bytes": 482113,
+ "status": "ready",
+ "created_at": "2026-10-06T08:10:00Z",
+ "updated_at": "2026-10-06T08:10:02Z"
+ }
+---
+
+
diff --git a/api-reference/skills/catalog.md b/api-reference/skills/catalog.md
new file mode 100644
index 00000000..fc7e7c50
--- /dev/null
+++ b/api-reference/skills/catalog.md
@@ -0,0 +1,91 @@
+---
+title: Skill Catalog
+description: Browse public Skills.
+aside: false
+outline: false
+apiReference:
+ title: Skill Catalog
+ operation: Skills
+ method: GET
+ path: /v1/skills/catalog
+ description: List public Skills with cursor pagination. Read one with GET /v1/skills/catalog/{skill_id}, list its versions with GET /v1/skills/catalog/{skill_id}/versions, read a version with GET /v1/skills/catalog/{skill_id}/versions/{version}, and download it with GET /v1/skills/catalog/{skill_id}/versions/{version}/content.
+ groups:
+ - title: Query parameters
+ fields:
+ - name: limit
+ type: integer
+ required: false
+ description: Page size from 0 to 100. Values outside the range return 400.
+ default: "20"
+ - name: after
+ type: string
+ required: false
+ description: Cursor. Pass last_id from the previous page.
+ - name: order
+ type: string
+ required: false
+ description: asc or desc.
+ default: desc
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET "https://api.sandbase.ai/v1/skills/catalog?limit=20" \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/skills/catalog?limit=20",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/skills/catalog?limit=20', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "object": "list",
+ "data": [
+ {
+ "id": "skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57",
+ "object": "skill",
+ "name": "release-notes",
+ "description": "Draft release notes from merged changes.",
+ "default_version": "1",
+ "latest_version": "1",
+ "status": "active",
+ "visibility": "public",
+ "row_version": 1,
+ "metadata": {},
+ "created_at": 1791273600,
+ "updated_at": 1791273600
+ }
+ ],
+ "has_more": false,
+ "first_id": "skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57",
+ "last_id": "skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57"
+ }
+---
+
+
diff --git a/api-reference/skills/create.md b/api-reference/skills/create.md
index 82434989..fea16793 100644
--- a/api-reference/skills/create.md
+++ b/api-reference/skills/create.md
@@ -1,9 +1,102 @@
---
-title: "Create Skill"
-description: "Create a reusable Skill resource from an uploaded bundle or Git source."
+title: Create Skill
+description: Upload a Skill ZIP bundle with POST /v1/skills.
aside: false
outline: false
-apiReferenceKey: "skills/create"
+apiReference:
+ title: Create Skill
+ operation: Skills
+ method: POST
+ path: /v1/skills
+ description: Upload a ZIP bundle and create the Skill at version 1. The multipart body must contain exactly one file part named files (or files[]), up to 5 MiB. The ZIP must contain one top-level folder with SKILL.md inside it (for example zip -r release-notes.zip release-notes/), and the SKILL.md frontmatter must set name and description. A SKILL.md at the ZIP root returns 400 invalid_request.
+ groups:
+ - title: Headers
+ fields:
+ - name: Idempotency-Key
+ type: string
+ required: false
+ description: Optional. When omitted the service generates one, so a retried request can create a duplicate. Send your own stable key for retries.
+ - title: Request body
+ description: multipart/form-data.
+ fields:
+ - name: files
+ type: file
+ required: true
+ description: ZIP archive whose single top-level folder contains SKILL.md.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/skills \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: release-notes-skill-1" \
+ -F "files=@release-notes.zip;type=application/zip"
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ with open("release-notes.zip", "rb") as bundle:
+ skill = client.skills.create(files=[("release-notes.zip", bundle.read(), "application/zip")])
+ print(skill.id, skill.default_version)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ "Idempotency-Key": "release-notes-skill-1",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/skills",
+ headers=headers,
+ files={"files": open("release-notes.zip", "rb")},
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ import { readFile } from 'node:fs/promises';
+
+ const form = new FormData();
+ form.append('files', new Blob([await readFile('release-notes.zip')], { type: 'application/zip' }), 'release-notes.zip');
+
+ const response = await fetch('https://api.sandbase.ai/v1/skills', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Idempotency-Key': 'release-notes-skill-1',
+ },
+ body: form,
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 201 Created
+ code: |-
+ {
+ "id": "skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57",
+ "object": "skill",
+ "name": "release-notes",
+ "description": "Draft release notes from merged changes.",
+ "default_version": "1",
+ "latest_version": "1",
+ "status": "active",
+ "visibility": "private",
+ "row_version": 1,
+ "metadata": {},
+ "created_at": 1791273600,
+ "updated_at": 1791273600
+ }
---
diff --git a/api-reference/skills/delete.md b/api-reference/skills/delete.md
index 7b5d4b46..fe8390b4 100644
--- a/api-reference/skills/delete.md
+++ b/api-reference/skills/delete.md
@@ -1,9 +1,73 @@
---
-title: "Delete Skill"
-description: "Soft-delete a Skill owned by the current organization."
+title: Delete Skill
+description: Delete a Skill.
aside: false
outline: false
-apiReferenceKey: "skills/delete"
+apiReference:
+ title: Delete Skill
+ operation: Skills
+ method: DELETE
+ path: /v1/skills/{skill_id}
+ description: Delete a Skill. A Skill referenced by any Agent version returns 409, even after that Agent is archived; disable it with Update Skill (status disabled) instead.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: skill_id
+ type: string
+ required: true
+ description: Skill ID (skl_ prefix).
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X DELETE https://api.sandbase.ai/v1/skills/skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ print(client.skills.delete("skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57").deleted)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "DELETE",
+ "https://api.sandbase.ai/v1/skills/skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/skills/skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57', {
+ method: 'DELETE',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57",
+ "object": "skill.deleted",
+ "deleted": true
+ }
---
diff --git a/api-reference/skills/get.md b/api-reference/skills/get.md
index 9d131d6c..35edadc0 100644
--- a/api-reference/skills/get.md
+++ b/api-reference/skills/get.md
@@ -1,9 +1,83 @@
---
-title: "Get Skill"
-description: "Retrieve one Skill and its source metadata."
+title: Get Skill
+description: Read one Skill.
aside: false
outline: false
-apiReferenceKey: "skills/get"
+apiReference:
+ title: Get Skill
+ operation: Skills
+ method: GET
+ path: /v1/skills/{skill_id}
+ description: Read a Skill. Download the default version ZIP with GET /v1/skills/{skill_id}/content.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: skill_id
+ type: string
+ required: true
+ description: Skill ID (skl_ prefix).
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET https://api.sandbase.ai/v1/skills/skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ skill = client.skills.retrieve("skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57")
+ print(skill.default_version, skill.latest_version)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/skills/skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/skills/skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57",
+ "object": "skill",
+ "name": "release-notes",
+ "description": "Draft release notes from merged changes.",
+ "default_version": "1",
+ "latest_version": "1",
+ "status": "active",
+ "visibility": "private",
+ "row_version": 1,
+ "metadata": {},
+ "created_at": 1791273600,
+ "updated_at": 1791273600
+ }
---
diff --git a/api-reference/skills/index.md b/api-reference/skills/index.md
index 72ea4a55..81c7ab95 100644
--- a/api-reference/skills/index.md
+++ b/api-reference/skills/index.md
@@ -1,211 +1,66 @@
---
title: Skills API
-description: Upload, create, and manage reusable Skills for SandBase Agents.
+description: Upload versioned Skill bundles and attach them to Agents.
---
-
# Skills API
-Skills are reusable instruction and resource bundles for Agents. Create a Skill once, then use its full `vendor_slug/plugin_slug` reference in an Agent or Service configuration.
-
-## Create a Skill in 5 minutes
-
-This example requires `curl`, `jq`, and `zip`. Keep API keys in environment variables rather than source files or Skill bundles.
-
-### 1. Build the bundle
-
-A Hermes Skill ZIP must contain exactly one `SKILL.md`. Its YAML frontmatter must include a normalized runtime `name`.
-
-```bash
-export SANDBASE_API_KEY='sk-YOUR_KEY'
-export SANDBASE_API_BASE='https://api.sandbase.ai'
-
-: "${SANDBASE_API_KEY:?SANDBASE_API_KEY is required}"
-
-mkdir -p hello-sandbase
-
-cat > hello-sandbase/SKILL.md <<'SKILL_EOF'
----
-name: hello-sandbase
-description: Give a short SandBase greeting when the user asks for one.
----
-
-# Hello SandBase
-
-When the user asks for a SandBase greeting, reply with one concise sentence that starts with "Hello from SandBase".
-SKILL_EOF
-
-(
- cd hello-sandbase
- zip -q -X ../hello-sandbase.zip SKILL.md
-)
-```
-
-The runtime `name` must match `^[a-z0-9][a-z0-9._-]{0,63}$`, cannot end in `.`, and cannot be a reserved system filename. The registered plugin slug can differ from this runtime name.
-
-### 2. Upload and register it
-
-Upload the ZIP to persistent Skill storage, then register the returned private object URL:
-
-```bash
-UPLOAD_RESPONSE="$(
- curl -fsS -X POST "$SANDBASE_API_BASE/v1/skills/files" \
- -H "Authorization: Bearer $SANDBASE_API_KEY" \
- -F 'file=@hello-sandbase.zip'
-)"
-
-SKILL_FILE_URL="$(jq -er '.url' <<<"$UPLOAD_RESPONSE")"
-
-SKILL_PAYLOAD="$(
- jq -nc \
- --arg url "$SKILL_FILE_URL" \
- '{
- name: "hello-sandbase",
- description: "A minimal Skill created through the SandBase API",
- categories: ["quickstart"],
- skill_file_url: $url
- }'
-)"
-
-SKILL_RESPONSE="$(
- curl -fsS -X POST "$SANDBASE_API_BASE/v1/skills" \
- -H "Authorization: Bearer $SANDBASE_API_KEY" \
- -H 'Content-Type: application/json' \
- --data "$SKILL_PAYLOAD"
-)"
-
-SKILL_ID="$(jq -er '.id' <<<"$SKILL_RESPONSE")"
-SKILL_REF="$(jq -er '.name' <<<"$SKILL_RESPONSE")"
-
-jq '{id, name, display_name, vendor_slug, plugin_slug}' <<<"$SKILL_RESPONSE"
-```
-
-`SKILL_REF` is the full reference required by Agent definitions. Do not substitute the Skill ID, a bare plugin slug, or the file URL.
-
-### 3. Attach it to a Service
-
-```bash
-ENDPOINT_PAYLOAD="$(
- jq -nc \
- --arg skill "$SKILL_REF" \
- '{
- name: "hello-skill-endpoint",
- runtime: "hermes",
- skills: [$skill],
- protocols: ["rest"]
- }'
-)"
-
-curl -fsS -X POST "$SANDBASE_API_BASE/v1/endpoints" \
- -H "Authorization: Bearer $SANDBASE_API_KEY" \
- -H 'Content-Type: application/json' \
- --data "$ENDPOINT_PAYLOAD" \
- | jq .
-```
-
-SandBase validates and installs the bundle before the Service invokes its Agent. Missing or duplicate `SKILL.md` files, invalid frontmatter names, unsafe archive paths, and unreadable bundles fail materialization instead of starting the Agent without the Skill.
-
-## Upload a Skill file
-
-
- POST
- /v1/skills/files
-
-
-Send `multipart/form-data` with a required `file` field. The same endpoint accepts Skill archives and preview images. The response is `201 Created`:
-
-```json
-{
- "url": "https://media.sandbase.ai/_private/your-org/skills/file-id/my-skill.zip",
- "key": "_private/your-org/skills/file-id/my-skill.zip",
- "filename": "my-skill.zip",
- "size": 1234,
- "content_type": "application/zip"
-}
-```
-
-Skill archives can be up to 50 MB. JPEG, PNG, WebP, and GIF preview images can be up to 10 MB. Although the upload API accepts several archive formats, use ZIP for Agent runtime usage. A bundle can contain up to 1,024 files and 60 MB of uncompressed data at runtime. Files outside the directory containing the unique `SKILL.md` are not installed.
-
-## Create a Skill resource
-
-
- POST
- /v1/skills
-
-
-```json
-{
- "name": "my-skill",
- "description": "A reusable Skill",
- "categories": ["productivity"],
- "skill_file_url": "https://media.sandbase.ai/_private/.../my-skill.zip",
- "preview_image_urls": []
-}
-```
+A Skill is a versioned ZIP bundle of instructions and resources that an Agent can mount. Skill IDs use the `skl_` prefix, and version identifiers are strings such as `"1"`.
-| Field | Type | Required | Description |
-|-------|------|----------|-------------|
-| `name` | string | ✅ | Display name, up to 100 characters. SandBase derives a unique plugin slug from it. |
-| `description` | string | ❌ | Description, up to 1,000 characters. |
-| `categories` | string[] | ❌ | Category labels. |
-| `skill_file_url` | string | Conditional | URL returned by `/v1/skills/files`. Required unless `git_url` is provided. |
-| `git_url` | string | Conditional | Git source URL. Required unless `skill_file_url` is provided. Use an uploaded ZIP when attaching the Skill to an Agent runtime. |
-| `preview_image_urls` | string[] | ❌ | Preview image URLs. |
+## Skill operations
-A successful response is `201 Created`. Its `name` is the generated full Skill reference:
+| Method | Path | Purpose |
+|---|---|---|
+| `POST` | `/v1/skills` | [Create a Skill](./create) from a ZIP upload |
+| `GET` | `/v1/skills` | [List Skills](./list) |
+| `GET` | `/v1/skills/{skill_id}` | [Get a Skill](./get) |
+| `POST` | `/v1/skills/{skill_id}` | [Update](./update) default version, status, or metadata |
+| `DELETE` | `/v1/skills/{skill_id}` | [Delete a Skill](./delete) |
+| `GET` | `/v1/skills/{skill_id}/content` | Download the default version ZIP |
+| `GET` | `/v1/skills/{skill_id}/versions` | [List versions](./versions) |
+| `POST` | `/v1/skills/{skill_id}/versions` | [Upload a version](./versions) |
+| `GET` | `/v1/skills/{skill_id}/versions/{version}` | [Get a version](./versions) |
+| `GET` | `/v1/skills/{skill_id}/versions/{version}/content` | Download a version ZIP |
+| `GET` | `/v1/skills/catalog` | [Browse public Skills](./catalog) |
-```json
-{
- "id": "550e8400-e29b-41d4-a716-446655440000",
- "name": "vendor-slug/my-skill",
- "display_name": "my-skill",
- "vendor_slug": "vendor-slug",
- "plugin_slug": "my-skill",
- "icon_url": "",
- "preview_urls": [],
- "created_at": "2026-07-17T00:00:00Z"
-}
-```
+## Five-minute setup
-## Manage Skills
+1. Create a folder with a `SKILL.md` whose frontmatter sets `name` and `description`:
-All operations are scoped to the organization associated with the API key.
+ ```markdown
+ ---
+ name: release-notes
+ description: Draft release notes from merged changes.
+ ---
+ Group changes by feature, fix, and breaking change. Keep each entry to one line.
+ ```
-Skill resource IDs are UUIDs. Agent and Service definitions use the separate `vendor_slug/plugin_slug` reference returned in `name`.
+2. Zip it (`zip -r release-notes.zip release-notes/`). The archive can be at most 5 MiB.
+3. Upload it as the single multipart part `files`:
-| Method | Path | Description |
-|--------|------|-------------|
-| `POST` | `/v1/skills` | [Create a Skill](./create) from an uploaded bundle or Git source. |
-| `GET` | `/v1/skills?page=1&page_size=20` | [List Skills](./list). `page_size` is capped at 1,000. |
-| `GET` | `/v1/skills/{skill_id}` | [Get a Skill](./get) and its source metadata. |
-| `PUT` | `/v1/skills/{skill_id}` | [Update Skill metadata](./update) or replace its uploaded bundle URL. |
-| `DELETE` | `/v1/skills/{skill_id}` | [Soft-delete a Skill](./delete). |
+ ```bash
+ curl -X POST https://api.sandbase.ai/v1/skills \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -F "files=@release-notes.zip;type=application/zip"
+ ```
-`PUT` has mixed replacement semantics. Always send the display fields you want to keep: omitted `name`, `description`, and `categories` are cleared. By contrast, omitted `skill_file_url`, `preview_image_urls`, `metadata`, and `unified_schema` preserve their stored values. A blank bundle URL also preserves the current bundle; an explicit empty preview array clears previews. The generated full Skill reference in the response's `name` field never changes.
+4. Reference it from an Agent with [Update Agent](/api-reference/agents/update):
-Delete the Quickstart Skill when you no longer need it:
+ ```bash
+ curl -X POST https://api.sandbase.ai/v1/agents/agt_8c1f2b9e-3d4a-4f6b-9c2e-1a7d5e3b4c60 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"skills": [{"skill_id": "skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57", "version": null}]}'
+ ```
-```bash
-curl -fsS -X DELETE "$SANDBASE_API_BASE/v1/skills/$SKILL_ID" \
- -H "Authorization: Bearer $SANDBASE_API_KEY" \
- | jq .
-```
+`"version": null` follows the Skill's `default_version`; a string pins one version. The version is resolved and frozen when each Session starts, and the Session reports it in `environment.skills`.
-## Errors
+## Versions
-```json
-{
- "error": {
- "type": "invalid_request",
- "message": "name is required"
- }
-}
-```
+- Uploading to `POST /v1/skills/{skill_id}/versions` adds the next version. Send the form field `default=true` to make it the default.
+- `POST /v1/skills/{skill_id}` with `default_version` switches the default without uploading.
+- `status` is `active` or `disabled`. New Sessions of an Agent that references a disabled Skill fail with `400 skill_not_found`; Sessions that already froze a version keep it.
+- A Skill that any Agent version references cannot be deleted (409). Disable it instead, or remove it from your Agents first.
-| Status | Type | Description |
-|--------|------|-------------|
-| 400 | `invalid_request` | Invalid request body, missing name/source, or invalid file upload. |
-| 401 | `authentication_error` | API key is missing or invalid. |
-| 404 | `not_found` | Skill does not exist or is not visible to the caller. |
-| 500 | `internal_error` | Skill creation or storage failed. |
+## Pagination and errors
-For Service validation and runtime errors, see [Service errors](/api-reference/endpoints/#errors).
+Lists use cursor pagination with `limit` 0-100, `after`, and `order`. Errors use the [Agents error envelope](/api-reference/errors#agents-platform-errors).
diff --git a/api-reference/skills/list.md b/api-reference/skills/list.md
index eafb0b19..3b5b62f2 100644
--- a/api-reference/skills/list.md
+++ b/api-reference/skills/list.md
@@ -1,9 +1,101 @@
---
-title: "List Skills"
-description: "List Skills owned by the current organization."
+title: List Skills
+description: List organization Skills.
aside: false
outline: false
-apiReferenceKey: "skills/list"
+apiReference:
+ title: List Skills
+ operation: Skills
+ method: GET
+ path: /v1/skills
+ description: List Skills owned by your organization with cursor pagination.
+ groups:
+ - title: Query parameters
+ fields:
+ - name: limit
+ type: integer
+ required: false
+ description: Page size from 0 to 100. Values outside the range return 400.
+ default: "20"
+ - name: after
+ type: string
+ required: false
+ description: Cursor. Pass last_id from the previous page.
+ - name: order
+ type: string
+ required: false
+ description: asc or desc.
+ default: desc
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X GET "https://api.sandbase.ai/v1/skills?limit=20" \
+ -H "Authorization: Bearer $SANDBASE_API_KEY"
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ for skill in client.skills.list(limit=20):
+ print(skill.id, skill.name)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "GET",
+ "https://api.sandbase.ai/v1/skills?limit=20",
+ headers=headers,
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/skills?limit=20', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "object": "list",
+ "data": [
+ {
+ "id": "skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57",
+ "object": "skill",
+ "name": "release-notes",
+ "description": "Draft release notes from merged changes.",
+ "default_version": "1",
+ "latest_version": "1",
+ "status": "active",
+ "visibility": "private",
+ "row_version": 1,
+ "metadata": {},
+ "created_at": 1791273600,
+ "updated_at": 1791273600
+ }
+ ],
+ "has_more": false,
+ "first_id": "skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57",
+ "last_id": "skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57"
+ }
---
diff --git a/api-reference/skills/update.md b/api-reference/skills/update.md
index 9e1b00ba..0b39bf11 100644
--- a/api-reference/skills/update.md
+++ b/api-reference/skills/update.md
@@ -1,9 +1,113 @@
---
-title: "Update Skill"
-description: "Update Skill metadata or replace its uploaded bundle."
+title: Update Skill
+description: Change the default version, status, or metadata of a Skill.
aside: false
outline: false
-apiReferenceKey: "skills/update"
+apiReference:
+ title: Update Skill
+ operation: Skills
+ method: POST
+ path: /v1/skills/{skill_id}
+ description: Change default_version, status, or market metadata. row_version is required when metadata is sent; otherwise the current value is used.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: skill_id
+ type: string
+ required: true
+ description: Skill ID (skl_ prefix).
+ - title: Request body
+ description: JSON object.
+ fields:
+ - name: default_version
+ type: string
+ required: false
+ description: Version used by Agents that reference the Skill with version null.
+ - name: status
+ type: string
+ required: false
+ description: active or disabled.
+ - name: metadata
+ type: object
+ required: false
+ description: Market metadata. Requires row_version.
+ - name: row_version
+ type: integer
+ required: false
+ description: Compare-and-set token.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/skills/skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57 \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "default_version": "2"
+ }'
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ skill = client.skills.update("skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57", default_version="2")
+ print(skill.default_version)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/skills/skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57",
+ headers=headers,
+ json={
+ "default_version": "2"
+ },
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ const response = await fetch('https://api.sandbase.ai/v1/skills/skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "default_version": "2"
+ }),
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "id": "skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57",
+ "object": "skill",
+ "name": "release-notes",
+ "description": "Draft release notes from merged changes.",
+ "default_version": "2",
+ "latest_version": "2",
+ "status": "active",
+ "visibility": "private",
+ "row_version": 3,
+ "metadata": {},
+ "created_at": 1791273600,
+ "updated_at": 1791273600
+ }
---
diff --git a/api-reference/skills/versions.md b/api-reference/skills/versions.md
new file mode 100644
index 00000000..3a66eb46
--- /dev/null
+++ b/api-reference/skills/versions.md
@@ -0,0 +1,114 @@
+---
+title: Upload Skill Version
+description: Upload a new Skill version.
+aside: false
+outline: false
+apiReference:
+ title: Upload Skill Version
+ operation: Skills
+ method: POST
+ path: /v1/skills/{skill_id}/versions
+ description: Upload a new ZIP bundle as the next version (200 OK). The ZIP layout is the same as Create Skill (one top-level folder containing SKILL.md). Send default=true to make it the default version. List versions with GET /v1/skills/{skill_id}/versions, read one with GET /v1/skills/{skill_id}/versions/{version}, and download one with GET /v1/skills/{skill_id}/versions/{version}/content. Version identifiers are strings.
+ groups:
+ - title: Path parameters
+ fields:
+ - name: skill_id
+ type: string
+ required: true
+ description: Skill ID (skl_ prefix).
+ - title: Headers
+ fields:
+ - name: Idempotency-Key
+ type: string
+ required: false
+ description: Optional. When omitted the service generates one, so a retried request can create a duplicate. Send your own stable key for retries.
+ - title: Request body
+ description: multipart/form-data.
+ fields:
+ - name: files
+ type: file
+ required: true
+ description: ZIP archive, up to 5 MiB.
+ - name: default
+ type: string
+ required: false
+ description: true or false.
+ examples:
+ - label: cURL
+ language: bash
+ code: |-
+ curl -X POST https://api.sandbase.ai/v1/skills/skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57/versions \
+ -H "Authorization: Bearer $SANDBASE_API_KEY" \
+ -H "Idempotency-Key: release-notes-v2" \
+ -F "files=@release-notes-v2.zip;type=application/zip" \
+ -F "default=true"
+ - label: Python (OpenAI SDK)
+ language: python
+ code: |-
+ # Verified with openai-python 3.13.0 and 3.24.0. See /agents/openai-compatibility.
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(api_key=os.environ["SANDBASE_API_KEY"], base_url="https://api.sandbase.ai/v1")
+ with open("release-notes-v2.zip", "rb") as bundle:
+ version = client.skills.versions.create(
+ "skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57",
+ files=[("release-notes-v2.zip", bundle.read(), "application/zip")],
+ default=True,
+ )
+ print(version.version)
+ - label: Python
+ language: python
+ code: |-
+ import os
+ import requests
+
+ headers = {
+ "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
+ "Idempotency-Key": "release-notes-v2",
+ }
+
+ response = requests.request(
+ "POST",
+ "https://api.sandbase.ai/v1/skills/skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57/versions",
+ headers=headers,
+ files={"files": open("release-notes-v2.zip", "rb")},
+ data={"default": "true"},
+ )
+ response.raise_for_status()
+ print(response.json())
+ - label: TypeScript
+ language: typescript
+ code: |-
+ import { readFile } from 'node:fs/promises';
+
+ const form = new FormData();
+ form.append('files', new Blob([await readFile('release-notes-v2.zip')], { type: 'application/zip' }), 'release-notes-v2.zip');
+ form.append('default', 'true');
+
+ const response = await fetch('https://api.sandbase.ai/v1/skills/skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57/versions', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.SANDBASE_API_KEY}`,
+ 'Idempotency-Key': 'release-notes-v2',
+ },
+ body: form,
+ });
+
+ if (!response.ok) throw new Error(await response.text());
+ console.log(await response.json());
+ response:
+ status: 200 OK
+ code: |-
+ {
+ "object": "skill.version",
+ "id": "skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57:2",
+ "skill_id": "skl_3c5e7a9b-1d4f-5a6c-8e2b-4d6f8a0c2e57",
+ "version": "2",
+ "name": "release-notes",
+ "description": "Draft release notes from merged changes.",
+ "created_at": 1791360000
+ }
+---
+
+
diff --git a/api-reference/webhooks.md b/api-reference/webhooks.md
index 385752c8..edc2bb1f 100644
--- a/api-reference/webhooks.md
+++ b/api-reference/webhooks.md
@@ -293,4 +293,4 @@ X-Signature = hex(HMAC-SHA256(signatureSecret, rawRequestBody))
- Return a 2xx response after durable acceptance, then perform slow work asynchronously. The examples use in-memory deduplication only to stay runnable; production receivers must replace it with a database unique key or durable queue. Do not assume exactly-once delivery or event ordering.
-For session event delivery and lifecycle semantics, see [Session events](/api-reference/sessions/list-events).
+For session event delivery and lifecycle semantics, see [Sessions](/api-reference/sessions/).
diff --git a/faq.md b/faq.md
index a07420f6..bb003d8a 100644
--- a/faq.md
+++ b/faq.md
@@ -9,7 +9,7 @@ description: Frequently asked questions about SandBase Setup, Store, Models, API
SandBase helps Agents connect to the real world.
-You can use ready-made Models and APIs, install them into AI tools with Setup, or build reusable Agents and publish or schedule them.
+You can use ready-made Models and APIs, install them into AI tools with Setup, or build reusable Agents and deploy them as Services or Schedules.
## Who is SandBase for?
@@ -40,17 +40,17 @@ It manages Models, APIs, and Services. Skills are used inside Build Agent, not u
Build Agent is where you define and test an Agent.
-An Agent combines instructions, a Model, APIs, Skills, and credentials into one reusable workflow.
+An Agent combines instructions, a Model, Skills, and MCP tools into one reusable, versioned workflow.
## Is an Agent different from an API?
-An Agent can be used like an API after you publish it.
+An Agent can be used like an API after you deploy it as a Service.
-Before publishing, it is a configuration you can edit and test. After publishing, it becomes callable by your app or Setup.
+Before that, it is a configuration you can edit and test in Sessions. A Service pins one tested version, so your app or Setup can call it while you keep editing the Agent.
## What are Services?
-Services are tested Agents made available outside Build Agent through a stable callable surface. The compatibility API path remains `/v1/endpoints`.
+Services make a tested Agent version callable outside Build Agent through `/v1/services`. Each invocation returns a Run.
Use them when your app, your users, or your AI tools need to call an Agent.
@@ -60,13 +60,13 @@ Schedules run an existing Agent later or repeatedly.
They do not create Agents from scratch. Build and test the Agent first, then schedule it.
-## What are Sessions and DeploymentRuns?
+## What are Sessions and Runs?
-A Session is a persistent Agent interaction and its event history. A Service can create or continue a Session. Each Schedule trigger creates a distinct DeploymentRun (`drun_*`) which links to a new Session when Session creation succeeds.
+A Session is one Agent execution with its conversation, tool activity, and output. A Run (`run_*`) is one Service invocation or Schedule firing; each Run creates its own Session and reports it in `session_id`.
## How do I give an Agent access to external APIs?
-Add the required values under **Developer → Credentials**, then select those credentials when configuring the Agent's tools. SandBase injects selected values only at runtime, so secrets do not need to appear in prompts or source code.
+Connect the external system as an MCP server and add it to the Agent's MCP connections. If the server needs a Bearer token, store it with the [Secrets API](/api-reference/secrets/) as a write-only Secret bound to that server URL. Tokens are sent only to that server and never appear in prompts or the Agent runtime. See [Credentials for MCP tools](/agents/api-credentials).
## Where do Models live?
diff --git a/for-agents/errors.md b/for-agents/errors.md
index aaa410e5..afd20e93 100644
--- a/for-agents/errors.md
+++ b/for-agents/errors.md
@@ -17,10 +17,11 @@ Core `/v1/*` middleware and several model or resource endpoints return a flat er
{"error":"API key rate limit exceeded"}
```
-Agent resource APIs can return a typed error object:
+Agents platform resources (`/v1/agents*`, `/v1/services*`, `/v1/schedules*`, `/v1/mcp-connections*`, `/v1/secrets*`,
+`/v1/skills*`) return an OpenAI-style object with a stable `code`. See [Agents platform errors](#agents-platform-errors):
```json
-{"error":{"type":"invalid_request","message":"invalid request body"}}
+{"error":{"type":"invalid_request_error","code":"invalid_request","param":null,"message":"invalid request"}}
```
OpenAI-compatible gateways retain an OpenAI-compatible envelope. `POST /v1/messages` retains the
@@ -86,6 +87,36 @@ Apply these rules:
- retain partial output when a stream fails and decide whether continuing or restarting is safe;
- surface the final HTTP status and sanitized endpoint-specific error to the caller.
+## Agents platform errors
+
+Agents, Sessions, Services, Schedules, Skills, MCP connections, and Secrets return one envelope. For most codes, `message` is a fixed, safe sentence; `invalid_request` may carry a short explanation, such as which input shape or option is not supported. Branch on `code`, not on `message`.
+
+```json
+{"error":{"type":"invalid_request_error","code":"conflict","param":null,"message":"conflicting request"}}
+```
+
+`type` is `authentication_error` for 401, `server_error` for 5xx, and `invalid_request_error` otherwise.
+
+| HTTP | `code` | Meaning | What to do |
+|---|---|---|---|
+| 400 | `invalid_request` | Malformed or unknown fields, failed validation, or an unsupported option | Fix the request; do not retry unchanged |
+| 400 | `skill_not_found` | A referenced Skill or version is not available | Fix the Agent's `skills` |
+| 401 | `unauthorized` | Missing, invalid, or conflicting API key headers | Check `Authorization` / `X-API-Key` |
+| 402 | `spending_limit_exceeded` | Key spending limit or organization balance exhausted | Add credits or raise the limit |
+| 403 | `insufficient_scope` | The key is scoped and cannot use Agents resources | Use a standard Console-created key |
+| 403 | `org_disabled` | The organization is not permitted to run Agents | Contact support |
+| 404 | `not_found` | The resource does not exist in your organization | Check the ID |
+| 409 | `conflict` | Stale `row_version`, a state that does not allow the change, or an `Idempotency-Key` reused with a different body | Read the resource again, then decide |
+| 409 | `session_input_pending` | Earlier input is still being delivered | Wait; do not resend with a new key |
+| 409 | `session_input_expired` | Input admission expired | Inspect history before sending again |
+| 409 | `session_environment_unavailable` | The execution environment is unavailable | Wait or create a new Session |
+| 429 | `concurrency_limit` | A Service or Schedule already has a pending Run | Wait for it to finish or cancel it |
+| 429 | `quota_exceeded` | Agents quota exceeded | Retry later |
+| 502 | `executor_rejected`, `executor_contract_error` | The execution layer rejected the request or answered unexpectedly | Check the Agent configuration, then retry |
+| 503 | `executor_unavailable`, `storage_unavailable`, `skill_unavailable`, `tenant_unavailable`, `tenant_provisioning_failed`, `authorization_unavailable`, `admission_unavailable`, `internal_error` | A dependency is temporarily unavailable | Retry with backoff and the same `Idempotency-Key` |
+
+`skill_runtime_incompatible` can also appear; it is returned as 503 with the message `internal error`.
+
## Streaming failures
After a streaming response has started, a failure may arrive in the protocol's stream format instead of as a new
diff --git a/for-agents/full.md b/for-agents/full.md
index 1248e75a..7118aa6c 100644
--- a/for-agents/full.md
+++ b/for-agents/full.md
@@ -5,8 +5,8 @@ description: Expanded SandBase API guide for AI agents, with core workflows, req
# AI API Guide
-::: info Session contract
-`session_id` is the persistent public identity for Agent interaction. Service invocation creates or continues a Session. Every Schedule (Deployment) trigger creates a public `drun_*` DeploymentRun and attempts to create one new Session. Internal Runtime Session IDs are not exposed.
+::: info Session and Run contract
+A Session (`ses_*`) runs one saved Agent version and is created with `POST /v1/agents/sessions`. Invoking a Service or firing a Schedule creates a Run (`run_*`) that creates its own Session, reported in `session_id`. Internal Runtime Session IDs are not exposed.
:::
> A single-page guide to the most common API workflows. For the complete machine-readable contract, use the [OpenAPI specification](https://www.sandbase.ai/docs/openapi.yaml). Plain-text version: [`llms-full.txt`](https://www.sandbase.ai/docs/llms-full.txt).
@@ -29,9 +29,10 @@ Base URL: `https://api.sandbase.ai/v1`
|------|---------|
| **Task** | A billable execution record for operations such as model inference. Not every API operation creates one. |
| **Run** | A media generation request via `POST /v1/run`. May be sync or async. Poll with `GET /v1/run/{id}`. |
-| **Session** | An agent execution via `POST /v1/sessions`. A sequence of steps (tool calls, LLM reasoning). Query with `GET /v1/sessions/{id}`. |
+| **Session** | An Agent execution via `POST /v1/agents/sessions`, holding the conversation, tool activity, and output. Query with `GET /v1/agents/sessions/{session_id}`. |
+| **Service or Schedule Run** | One invocation of a Service or firing of a Schedule (`run_*`). Query with `GET /v1/services/{service_id}/runs/{run_id}` or `GET /v1/schedules/{schedule_id}/runs/{run_id}`; it links to one Session. |
-> **run vs session**: "Run" (`/v1/run`) is for model generation tasks. "Session" (`/v1/sessions`) is for persistent Agent interactions. Run IDs are opaque and must not be parsed. A Schedule trigger additionally creates a `drun_*` DeploymentRun.
+> **run vs session**: A model "run" (`/v1/run`) is a model generation task. A "Session" (`/v1/agents/sessions`) is an Agent execution. A Service or Schedule Run (`run_*`) is a separate resource that creates one Session. All IDs are opaque and must not be parsed.
>
> **task vs run**: A "run" or "session" is the request you make; a "task" is a billable execution record. Use
> `GET /v1/tasks/{task_id}/cost` when an operation returns a task ID. Not every API operation creates a task.
@@ -318,316 +319,186 @@ curl https://api.sandbase.ai/v1/tasks/f3d2e8a1-7c4b-4a12-9d2e-123456789abc/cost
-## Agents
+## Agents platform conventions
-### POST /v1/agents — Create Agent
+- Base URL `https://api.sandbase.ai/v1`. Send `Authorization: Bearer sk-YOUR_KEY` or `X-API-Key: sk-YOUR_KEY` (same value if both). `OpenAI-Beta` is optional; if sent it must be `agents=v1`. The official OpenAI SDK (`client.beta.agents`, `client.skills`) works with `base_url="https://api.sandbase.ai/v1"`; differences are listed in [OpenAI compatibility](/agents/openai-compatibility).
-```bash
-curl -X POST https://api.sandbase.ai/v1/agents \
- -H "Authorization: Bearer sk-YOUR_KEY" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "Research Assistant",
- "model": "openai/gpt-5.6-luna",
- "system": "You are a research assistant. Use tools to find and summarize information.",
- "tools": [{"type": "agent_toolset_20260401"}]
- }'
-```
+- Bodies are strict JSON up to 1 MiB; unknown fields return 400.
-**Response:**
+- Errors: `{"error":{"type":"invalid_request_error","code":"conflict","param":null,"message":"conflicting request"}}`. Branch on `code`; see the [Error Guide](./errors#agents-platform-errors).
-```json
-{
- "id": "agent_abc123",
- "name": "Research Assistant",
- "model": "openai/gpt-5.6-luna",
- "system": "You are a research assistant...",
- "tools": [
- { "type": "agent_toolset_20260401" }
- ],
- "created_at": "2026-08-02T12:00:00Z",
- "version": 1
-}
-```
+- `row_version` is the compare-and-set token for PATCH, archive, restore, pause, timing, and rotate. On 409, read again.
-### GET /v1/agents — List Agents
+- Cursor lists (Agents, Sessions, items, turns, Skills): `limit` 1-100, `after`, `order`; response `{object:"list", data, has_more, first_id, last_id}`.
-```bash
-curl https://api.sandbase.ai/v1/agents \
- -H "Authorization: Bearer sk-YOUR_KEY"
-```
+- Offset lists (Agent versions, Services, Schedules, Runs, attachments, MCP connections, Secrets): `limit`, `offset`; response `{data, total, limit, offset}`.
-### GET /v1/agents/{id} — Get Agent
+- IDs: Agent `agt_`, Session `ses_`, Service `svc_`, Schedule `sch_`, Run `run_`, Skill `skl_`, MCP connection `mcp_`, Secret `sec_`, attachment `fil_`. Treat them as opaque.
-```bash
-curl https://api.sandbase.ai/v1/agents/agent_abc123 \
- -H "Authorization: Bearer sk-YOUR_KEY"
-```
+---
+
+## Agents
-### POST /v1/agents/{id} — Update Agent
+### POST /v1/agents — Create Agent
```bash
-curl -X POST https://api.sandbase.ai/v1/agents/agent_abc123 \
+curl -X POST https://api.sandbase.ai/v1/agents \
-H "Authorization: Bearer sk-YOUR_KEY" \
+ -H "Idempotency-Key: create-research-agent-1" \
-H "Content-Type: application/json" \
- -d '{"model": "anthropic/claude-sonnet-5"}'
+ -d '{
+ "name": "Research assistant",
+ "model": "deepseek/deepseek-v4-flash",
+ "instructions": "Research the question and cite sources."
+ }'
```
-### POST /v1/agents/{id}/archive — Archive Agent
+Required: `model`. Optional: `name`, `instructions`, `description`, `runtime_profile` (`codex` default, `claude`, `mcode`), `skills` (`[{"skill_id","version"}]`), `mcp_connections` (`[{"connection_id","server_label","allowed_tools","required"}]`), `tools`, `text`, `reasoning`, `service_tier`, `metadata`, `note`. Returns `201` with `id` (`agt_`), `version`, `row_version`, `status`, and `projection_status`; start Sessions when `projection_status` is `ready`. `web_search` tools must use `mode: "disabled"`, and client-side function results (`requires_action`) are not supported.
-```bash
-curl -X POST https://api.sandbase.ai/v1/agents/agent_abc123/archive \
- -H "Authorization: Bearer sk-YOUR_KEY"
-```
+### GET /v1/agents — List Agents
-### GET /v1/agents/{id}/versions — List Versions
+Query `status` (`active` default, `archived`, `all`) plus cursor pagination.
-```bash
-curl https://api.sandbase.ai/v1/agents/agent_abc123/versions \
- -H "Authorization: Bearer sk-YOUR_KEY"
-```
+### GET /v1/agents/{agent_id} — Get Agent
----
+### POST /v1/agents/{agent_id} — Update Agent (SDK style)
-## Services
+Partial update of `name`, `instructions`, `model`, `metadata`, `service_tier`, `tools`, `text`, `reasoning`, `runtime_profile`, `mcp_connections`, `skills`. No `row_version`; creates a new version.
-### POST /v1/endpoints/{id}/run — Invoke Service
+### PATCH /v1/agents/{agent_id} — Update Agent (compare-and-set)
-Creates or continues a Session and sends one message. Omit `session_id` to create a Session. A supplied Session must have been created by the same Service and still match its Agent-version binding.
+Same fields plus `description` and `note`; `row_version` is required. Explicit `null` returns 400.
-```bash
-curl -X POST https://api.sandbase.ai/v1/endpoints/ep_abc/run \
- -H "Authorization: Bearer sk-YOUR_KEY" \
- -H "Content-Type: application/json" \
- -d '{"input": "Summarize the latest news about AI agents"}'
-```
+### Versions, archive, restore, catalog
-**Response (`202 Accepted`):**
+- `GET /v1/agents/{agent_id}/versions` and `GET /v1/agents/{agent_id}/versions/{version}` read immutable snapshots.
-```json
-{
- "session_id": "sess_abc123",
- "events": [{
- "id": "sevt_abc123",
- "type": "user.message",
- "processed_at": "2026-08-03T12:00:01Z"
- }]
-}
-```
+- `POST /v1/agents/{agent_id}/archive`, `POST /v1/agents/{agent_id}/unarchive` take `{"row_version"}`. `DELETE /v1/agents/{agent_id}` also archives.
+
+- `POST /v1/agents/{agent_id}/restore` takes `{"source_version","row_version"}`.
+
+- `GET /v1/agents/catalog` lists public Agents; `POST /v1/agents/catalog/{agent_id}/clone` copies one into your organization (`201`, new `agt_`).
---
## Sessions
-### POST /v1/sessions — Create Session
+A Session runs one saved Agent version. Paths are under `/v1/agents/sessions`. The Agent version, Skill versions, and MCP connections are frozen at creation.
-Create a version-pinned Agent Session. SandBase resolves the Agent's runtime binding internally. Use singular `initial_events` to submit the first message with creation.
+### POST /v1/agents/sessions — Create Session
```bash
-curl -X POST https://api.sandbase.ai/v1/sessions \
+curl -X POST https://api.sandbase.ai/v1/agents/sessions \
-H "Authorization: Bearer sk-YOUR_KEY" \
-H "Content-Type: application/json" \
- -d '{
- "agent": "agent_abc123",
- "title": "GitHub research",
- "initial_events": [{
- "type": "user.message",
- "content": [{"type": "text", "text": "Research AI coding assistants"}]
- }]
- }'
+ -d '{"agent_id": "agt_..."}'
```
-**Response:**
-
-```json
-{
- "id": "sess_xyz789",
- "type": "session",
- "agent": {
- "id": "agent_abc123",
- "type": "agent",
- "version": 3
- },
- "status": "idle",
- "created_at": "2026-08-03T12:00:00Z"
-}
-```
+Required: `agent_id`. Optional: `version`, `title`, `input` (a string, or an array with exactly one user message containing one `input_text` part), `model` or `agent.model`, `metadata`, `model_provider` (`{"type":"custom","base_url":"https://...","api_key":"..."}`; omitted means your calling key). `stream: true` and inline Agents return 400. Sessions always run in SandBase's hosted cloud, so there is no environment to choose; the OpenAI SDK still requires `environment={"type": "openai_hosted"}`. Returns `201` with `status` (`idle`, `in_progress`, `failed`), `lifecycle_status` (`active`, `archived`), `failure`, `source` (`direct`, `service`, `schedule`), and `row_version`.
-If native runtime delivery returns `502` with a top-level `session_id`, its outcome is unknown. Retrieve that Session and inspect or stream its Events before continuing. Do not blindly create another Session and resend the message.
+### GET /v1/agents/sessions/{session_id}/events — Stream Events (SSE)
-### POST /v1/sessions/{id}/events — Send Events
+Live-only. Open it before sending input. `Last-Event-ID` or `after` returns 400; events are not replayed. Event `type` values start with `agent.session.`.
-Send `user.message` to start or continue an idle Session, or `user.interrupt` to interrupt a running turn.
+### POST /v1/agents/sessions/{session_id}/events — Send Input
```bash
-curl -X POST https://api.sandbase.ai/v1/sessions/sess_xyz789/events \
+curl -X POST https://api.sandbase.ai/v1/agents/sessions/ses_.../events \
-H "Authorization: Bearer sk-YOUR_KEY" \
+ -H "Idempotency-Key: input-1" \
-H "Content-Type: application/json" \
- -d '{"events":[{"type":"user.message","content":[{"type":"text","text":"Focus on AI coding assistants specifically"}]}]}'
+ -d '{"events": [{"type": "agent.session.input.message", "input": "Summarize the findings."}]}'
```
-**Response:**
-
-```json
-{
- "data": [{
- "id": "sevt_001",
- "type": "user.message",
- "processed_at": "2026-08-03T12:00:01Z"
- }]
-}
-```
+Exactly one event per request: `agent.session.input.message` or `agent.session.input.cancel`. Returns `202` with no body. Extension form: `{"input": "...", "file_ids": ["fil_..."]}` (up to 10 ready attachments). `409 session_input_pending` means earlier input is still being delivered; wait and do not resend with a new key.
-### GET /v1/sessions — List Sessions
+### History and other Session operations
-```bash
-curl https://api.sandbase.ai/v1/sessions \
- -H "Authorization: Bearer sk-YOUR_KEY"
-```
+- `GET /v1/agents/sessions` (query `lifecycle_status`, `agent_id`), `GET /v1/agents/sessions/{session_id}`.
-**Response:**
+- `GET /v1/agents/sessions/{session_id}/items`, `GET /v1/agents/sessions/{session_id}/turns`, `GET /v1/agents/sessions/{session_id}/turns/{turn_id}` read history.
-```json
-{
- "data": [
- {
- "id": "sess_xyz789",
- "agent_id": "agent_abc123",
- "status": "idle",
- "created_at": "2026-08-03T12:00:00Z"
- }
- ]
-}
-```
+- `POST /v1/agents/sessions/{session_id}` replaces `metadata`; `PATCH /v1/agents/sessions/{session_id}` renames with `{"title","row_version"}`.
-### GET /v1/sessions/{id} — Get Session
+- `POST /v1/agents/sessions/{session_id}/cancel` (requires `Idempotency-Key`) cancels the running turn.
-```bash
-curl https://api.sandbase.ai/v1/sessions/sess_xyz789 \
- -H "Authorization: Bearer sk-YOUR_KEY"
-```
+- `DELETE /v1/agents/sessions/{session_id}` archives; `POST /v1/agents/sessions/{session_id}/unarchive` restores.
-### GET /v1/sessions/{id}/events — List Events
+- `POST /v1/agents/sessions/{session_id}/files` uploads one attachment (multipart `file`, 5 MiB, `Idempotency-Key` required).
-```bash
-curl https://api.sandbase.ai/v1/sessions/sess_xyz789/events \
- -H "Authorization: Bearer sk-YOUR_KEY"
-```
+- `POST /v1/agents/sessions/{session_id}/reconnect` returns `stream_url` for `GET /v1/agents/sessions/{session_id}/events/stream`, which sends `session.snapshot` and then live events.
-**Response:**
+---
-```json
-{
- "data": [
- {
- "id": "sevt_001",
- "type": "agent.message",
- "content": [{"type":"text","text":"I'll search GitHub for trending repositories..."}],
- "processed_at": "2026-08-02T12:00:05Z"
- },
- {
- "id": "sevt_002",
- "type": "agent.tool_use",
- "content": {"name":"github.search_repositories","input":{"query":"trending today"}},
- "processed_at": "2026-08-02T12:00:06Z"
- }
- ]
-}
-```
+## Services
-### GET /v1/sessions/{id}/events/stream — Stream Events (SSE)
+A Service pins an Agent version. Each invocation creates a Run (`run_`) that creates its own Session.
-Replays persisted events, then continues streaming newly persisted events. While idle, the server may send `: heartbeat` comment frames. The connection remains open until the client disconnects, the request context is cancelled, or a write fails; Session terminal state does not produce a separate close event.
+### POST /v1/services — Create Service
```bash
-curl -N https://api.sandbase.ai/v1/sessions/sess_xyz789/events/stream \
- -H "Authorization: Bearer sk-YOUR_KEY"
+curl -X POST https://api.sandbase.ai/v1/services \
+ -H "Authorization: Bearer sk-YOUR_KEY" \
+ -H "Idempotency-Key: weekly-brief-service" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "Weekly brief", "agent_id": "agt_...", "input": "Summarize this week in AI agents."}'
```
-**Event format:**
+`Idempotency-Key` is required. Omitted `agent_version` pins the current version. `schedule` and `model_provider` are rejected.
+### POST /v1/services/{service_id}/invoke — Invoke Service
+
+```bash
+curl -X POST https://api.sandbase.ai/v1/services/svc_.../invoke \
+ -H "Authorization: Bearer sk-YOUR_KEY" \
+ -H "Idempotency-Key: weekly-brief-2026-w41" \
+ -H "Content-Type: application/json" \
+ -d '{}'
```
-data: {"id":"sevt_001","type":"agent.message","content":[{"type":"text","text":"I'll search GitHub for trending repositories..."}],"processed_at":"2026-08-03T12:00:02Z"}
-: heartbeat
-data: {"id":"sevt_002","type":"session.status_idle","stop_reason":{"type":"end_turn"},"processed_at":"2026-08-03T12:00:03Z"}
-```
+
+Returns `202` with a Run: `id`, `status` (`pending`, `succeeded`, `failed`, `cancelled`), `session_id` (null until the Session exists), `error_code`, `trigger_source`. Optional body fields `input`, `model`, `model_provider` apply to this Run. Same key and body return the same Run; a different body returns 409. A second pending Run returns `429 concurrency_limit`. Poll `GET /v1/services/{service_id}/runs/{run_id}`, then read `GET /v1/agents/sessions/{session_id}/items`. Cancel with `POST /v1/services/{service_id}/runs/{run_id}/cancel`.
---
## Schedules
-### POST /v1/deployments — Create Schedule
-
-Create a Schedule through the compatibility `/v1/deployments` resource. Every manual or cron trigger creates a distinct `drun_*` DeploymentRun and attempts to create one new Session.
+### POST /v1/schedules — Create Schedule
```bash
-curl -X POST https://api.sandbase.ai/v1/deployments \
+curl -X POST https://api.sandbase.ai/v1/schedules \
-H "Authorization: Bearer sk-YOUR_KEY" \
+ -H "Idempotency-Key: daily-digest-schedule" \
-H "Content-Type: application/json" \
- -d '{
- "name": "Daily team summary",
- "agent_id": "agent_abc123",
- "initial_events": [{
- "type": "user.message",
- "content": [{"type":"text","text":"Generate daily summary of team activity"}]
- }],
- "schedule": {
- "type": "cron",
- "expression": "0 9 * * *",
- "timezone": "America/New_York"
- }
- }'
-```
-
-**Response:**
-
-```json
-{
- "id": "depl_abc123",
- "agent_id": "agent_abc123",
- "schedule": {"type":"cron","expression":"0 9 * * *","timezone":"America/New_York"},
- "status": "active",
- "next_run_at": "2026-08-03T09:00:00-04:00"
-}
+ -d '{"name": "Daily digest", "agent_id": "agt_...", "input": "Summarize yesterday in AI agents.", "schedule": {"cron": "0 9 * * 1-5", "timezone": "Asia/Shanghai"}}'
```
-### GET /v1/deployments/{id}/runs — List DeploymentRuns
+`Idempotency-Key` and non-empty `input` are required. `cron` has five fields; `timezone` is IANA and defaults to `UTC`. The model credential (`model_provider` or your calling key) is stored encrypted for scheduled Runs.
-```bash
-curl https://api.sandbase.ai/v1/deployments/depl_abc123/runs \
- -H "Authorization: Bearer sk-YOUR_KEY"
-```
+### POST /v1/schedules/{schedule_id}/runs — Trigger a Run
-Each returned object has `id` (`drun_*`), `type`, `deployment_id`, `agent`, `trigger_context`, nullable `session_id`, nullable `error`, and `created_at`. Use the linked Session for subsequent Agent events. A pending nested record returns `409 deployment_trigger_in_progress` from the get endpoint.
+Returns `202` with a manual Run. Scheduled firings create Runs with `trigger_source: "scheduled"`; a firing that finds a pending Run records `status: "skipped"` with `error_code: "concurrency_limit"`. List with `GET /v1/schedules/{schedule_id}/runs`. Change timing with `PUT /v1/schedules/{schedule_id}/timing`.
---
-## Skills
+## Skills, MCP Connections, and Secrets
-### GET /v1/skills — List Skills
+### POST /v1/skills — Create Skill
```bash
-curl https://api.sandbase.ai/v1/skills \
- -H "Authorization: Bearer sk-YOUR_KEY"
+curl -X POST https://api.sandbase.ai/v1/skills \
+ -H "Authorization: Bearer sk-YOUR_KEY" \
+ -F "files=@release-notes.zip;type=application/zip"
```
-**Response:**
+One ZIP part `files` (5 MiB). The ZIP must contain one top-level folder with `SKILL.md` (`name` and `description` frontmatter) inside it, for example `zip -r release-notes.zip release-notes/`; a `SKILL.md` at the ZIP root returns `400 invalid_request`. A Skill referenced by any Agent version (including archived Agents) cannot be deleted (`409`); disable it with `POST /v1/skills/{skill_id}` and `{"status": "disabled"}` instead. Returns `201` with `id` (`skl_`) and `default_version`. Add versions with `POST /v1/skills/{skill_id}/versions`; list with `GET /v1/skills`. Reference from an Agent as `{"skill_id": "skl_...", "version": null}`.
-```json
-{
- "data": [
- {
- "id": "skill_web_scrape",
- "name": "Web Scraper",
- "description": "Extract content from URLs",
- "input_schema": {
- "type": "object",
- "properties": { "url": { "type": "string" } }
- }
- }
- ]
-}
-```
+### POST /v1/mcp-connections — Register an MCP server
+
+Body `{"name", "server_url" (HTTPS), "secret_id"?}`; `Idempotency-Key` required. Reference from an Agent's `mcp_connections`. Disabling it blocks new Sessions and further input.
+
+### POST /v1/secrets — Store an MCP Bearer token
+
+Body `{"name", "kind": "mcp_bearer", "server_url", "token"}`; `Idempotency-Key` required. The token is write-only and bound to that exact URL. Secrets are not environment variables. Rotate with `POST /v1/secrets/{secret_id}/rotate`; revoke with `DELETE /v1/secrets/{secret_id}` (204).
---
diff --git a/for-agents/index.md b/for-agents/index.md
index f51bcc78..e8d0fc49 100644
--- a/for-agents/index.md
+++ b/for-agents/index.md
@@ -5,8 +5,8 @@ description: Everything an AI agent needs to discover and use SandBase API — o
# AI-Friendly API Overview
-::: info Session contract
-`session_id` is the public identity for persistent Agent interaction. Direct Session creation and Service invocation create or continue that Session. Every Schedule (Deployment) trigger creates a separate public `drun_*` DeploymentRun and attempts to create one new Session. Runtime instances remain internal and are never returned.
+::: info Session and Run contract
+A Session (`ses_*`) runs one saved Agent version; create it with `POST /v1/agents/sessions`. Invoking a Service or firing a Schedule creates a Run (`run_*`), and each Run creates its own Session reported in `session_id`. Runtime instances remain internal and are never returned.
:::
> A concise reference for both humans and AI agents. For plain-text versions optimized for LLM ingestion, see [`llms.txt`](https://www.sandbase.ai/docs/llms.txt).
@@ -61,11 +61,14 @@ Beyond the core generation APIs, SandBase provides APIs for agent lifecycle mana
| Group | Key routes | Purpose |
|-------|---------------|---------|
-| **Agents** | `POST /v1/agents`, `GET /v1/agents`, `POST /v1/agents/{id}` | Create and manage agent definitions |
-| **Services** | `POST /v1/endpoints/{id}/run` | Invoke a Service and create or continue a Session |
-| **Sessions** | `POST /v1/sessions`, `POST /v1/sessions/{id}/events` | Create persistent Agent Sessions and send messages |
-| **Schedules** | `POST /v1/deployments` | Define manual or cron triggers; each creates a DeploymentRun compatibility record |
-| **Skills** | `GET /v1/skills` | Extend agent capabilities with reusable instruction and resource bundles |
+| **Agents** | `POST /v1/agents`, `GET /v1/agents`, `POST /v1/agents/{agent_id}` | Create and version saved Agent definitions |
+| **Sessions** | `POST /v1/agents/sessions`, `POST /v1/agents/sessions/{session_id}/events`, `GET /v1/agents/sessions/{session_id}/events` | Run an Agent, send input, and stream live events |
+| **Services** | `POST /v1/services`, `POST /v1/services/{service_id}/invoke` | Invoke a pinned Agent version; each call returns a Run |
+| **Schedules** | `POST /v1/schedules`, `POST /v1/schedules/{schedule_id}/runs` | Run a pinned Agent version on cron or on demand |
+| **Skills** | `POST /v1/skills`, `GET /v1/skills` | Versioned instruction bundles that Agents mount |
+| **MCP Connections and Secrets** | `POST /v1/mcp-connections`, `POST /v1/secrets` | Remote HTTPS MCP tools and their write-only Bearer tokens |
+
+Agents platform resources accept `Authorization: Bearer` or `X-API-Key` and return `{"error":{"type","code","param","message"}}` on failure.
## Pricing Model
diff --git a/getting-started/api-keys.md b/getting-started/api-keys.md
index 461bb05c..dead0980 100644
--- a/getting-started/api-keys.md
+++ b/getting-started/api-keys.md
@@ -59,8 +59,9 @@ curl https://api.sandbase.ai/v1/messages \
```
::: tip
-Only `POST /v1/messages` reads `x-api-key`, and it takes priority when both supported headers are present. Use the
-Bearer header for every other public endpoint.
+Only `POST /v1/messages` reads `x-api-key` among the model gateways, and it takes priority when both supported headers
+are present. [Agents platform resources](/api-reference/authentication#agents-platform-resources) also accept
+`X-API-Key`. Use the Bearer header for every other public endpoint.
:::
## Key permissions
diff --git a/getting-started/index.md b/getting-started/index.md
index fa07a49b..10aa87eb 100644
--- a/getting-started/index.md
+++ b/getting-started/index.md
@@ -16,7 +16,7 @@ After that, you can use SandBase in four ways:
| **Call Models and APIs** | Developers and builders | A working request you can call from your application |
| **Use on the website** | Trying a capability in the browser | A Store detail page you can run without writing code |
| **Setup** | People using Codex, Claude, Cursor, or Kiro | Useful Models, APIs, and Services inside your AI app |
-| **Build Agent** | FDEs and teams building reusable workflows | An Agent you can test, publish, or schedule |
+| **Build Agent** | FDEs and teams building reusable workflows | An Agent you can test, deploy as a Service, or schedule |
## Core ideas
@@ -39,13 +39,13 @@ Use it when you want tools available inside Codex, Claude, Cursor, or Kiro witho
Build Agent is where you define and test your own Agent.
-An Agent combines a Model, instructions, APIs, Skills, and credentials into one reusable unit.
+An Agent combines a Model, instructions, Skills, and MCP tools into one reusable, versioned unit.
After it works, you can:
-- publish it as a callable Service
+- deploy a tested version as a callable Service
- schedule it to run later or on a recurring cadence
-- review Sessions and Schedule trigger records in the Console
+- review each Run and its Session in the Console
## Where should I start?
diff --git a/getting-started/quickstart-cn.md b/getting-started/quickstart-cn.md
index a6c42fc4..6ac1abc6 100644
--- a/getting-started/quickstart-cn.md
+++ b/getting-started/quickstart-cn.md
@@ -113,9 +113,9 @@ curl https://api.sandbase.ai/v1/chat/completions \
1. 打开 [Build Agent](https://www.sandbase.ai/console/agents){target="_blank"}。
2. 编写 Agent 指令。
3. 选择 Model。
-4. 添加 APIs 和 Skills。
-5. 测试一次运行。
-6. 把选定版本发布为 Service,或创建 Schedule。
+4. 添加 Skills 和 MCP 工具。
+5. 在 Session 中测试。
+6. 把测试通过的版本部署为 Service 或 Schedule。
了解更多:[Build Agent](/agents/)。
diff --git a/getting-started/quickstart.md b/getting-started/quickstart.md
index cb42893b..aea07cab 100644
--- a/getting-started/quickstart.md
+++ b/getting-started/quickstart.md
@@ -111,11 +111,11 @@ Choose this if the work has multiple steps or should be reused.
1. Open [Build Agent](https://www.sandbase.ai/console/agents){target="_blank"}.
2. Define the Agent instructions.
3. Pick a Model.
-4. Add APIs and Skills.
-5. Test a run.
-6. Publish a selected version as a Service, or create a Schedule.
+4. Add Skills and MCP tools.
+5. Test it in a Session.
+6. Deploy the tested version as a Service or Schedule.
-Learn more: [Build Agent](/agents/).
+Learn more: [Build Agent](/agents/). To do the same from code with the official OpenAI SDK, see [OpenAI compatibility](/agents/openai-compatibility).
## 4. Confirm it works {#confirm-it-works}
diff --git a/index.md b/index.md
index 24fdc390..b8f4739e 100644
--- a/index.md
+++ b/index.md
@@ -35,26 +35,26 @@ Use Models for language, image, video, and audio workloads. Use APIs when the ap
### Agents
-An Agent is a reusable, versioned configuration containing a Model, instructions, APIs, Skills, and credentials. Define it once, test it, and then choose how it should run.
+An Agent is a reusable, versioned configuration containing a Model, instructions, Skills, and MCP connections. Define it once, test it in a Session, and then choose how it should run.
- [Understand Agents](/agents/)
- [Define an Agent](/agents/agent-api)
-- [Add APIs, Skills, and credentials](/agents/mcp-tools)
+- [Add Skills and MCP tools](/agents/mcp-tools)
### Services and Sessions
-A Service exposes a tested Agent to applications. Its compatibility API path is `/v1/endpoints`; invoking it creates or continues a Session whose `session_id` identifies the durable interaction.
+A Service makes a pinned, tested Agent version callable by applications and AI tools. Each invocation through `/v1/services` creates a Run, and each Run creates its own Session with the full conversation.
- [Services guide](/agents/services)
-- [Services API](/api-reference/endpoints/)
+- [Services API](/api-reference/services/)
- [Sessions API](/api-reference/sessions/)
### Schedules and run history
-Schedules execute an Agent later or repeatedly. Their API resource is a Deployment. Every trigger creates a distinct `drun_*` DeploymentRun and, on successful Session creation, links it to a new `sess_*` Session.
+Schedules run a pinned Agent version on a cron expression or on demand through `/v1/schedules`. Every firing or manual trigger creates a Run with its own `ses_*` Session, so you can review each result.
- [Schedules guide](/agents/schedules)
-- [Schedules API](/api-reference/deployments/)
+- [Schedules API](/api-reference/schedules/)
- [Sessions guide](/agents/sessions)
## First steps
@@ -62,7 +62,7 @@ Schedules execute an Agent later or repeatedly. Their API resource is a Deployme
1. Create an organization and [API key](/getting-started/api-keys).
2. Choose a Model, API, or Agent from the [Store](/store/).
3. Make a direct request or define and test an Agent.
-4. Publish the Agent as a Service when an application needs to invoke it.
+4. Deploy the tested Agent version as a Service when an application needs to invoke it.
5. Add a Schedule when the work should run automatically.
6. Review Sessions, errors, latency, and cost in the Console.
diff --git a/models/index.md b/models/index.md
index ad68bd17..f7e8d631 100644
--- a/models/index.md
+++ b/models/index.md
@@ -72,7 +72,7 @@ For model-specific parameters, response fields, limits, and working examples, op
In Build Agent, the Model is one part of the Agent.
-The Agent can also include APIs, Skills, instructions, and credentials.
+The Agent also includes instructions, Skills, and [MCP connections](/agents/mcp-tools#mcp-connections).
## Model capabilities
diff --git a/public/llms-full.txt b/public/llms-full.txt
index 7fa7f451..7f75f0fb 100644
--- a/public/llms-full.txt
+++ b/public/llms-full.txt
@@ -10,7 +10,8 @@ All requests require: Authorization: Bearer sk-YOUR_KEY
- Task: A billable execution record. Not every API operation creates one.
- Run: A model generation request via POST /v1/run. May be synchronous or asynchronous.
-- Session: An agent execution via POST /v1/sessions. Stateful multi-turn workflow.
+- Session: An Agent execution via POST /v1/agents/sessions. Stateful multi-turn workflow.
+- Service or Schedule Run: One invocation of a Service or firing of a Schedule (run_*). Each Run creates one Session.
Some billable operations return an x-task-id response header. Not every API operation creates a task.
@@ -212,123 +213,80 @@ Response:
---
-## POST /v1/agents
+## Agents platform conventions
-Create an agent.
+Agents, Sessions, Services, Schedules, Skills, MCP connections, and Secrets live under https://api.sandbase.ai/v1. Send Authorization: Bearer sk-YOUR_KEY or X-API-Key: sk-YOUR_KEY (same value if both). OpenAI-Beta is optional; if sent it must be agents=v1. Bodies are strict JSON up to 1 MiB. Errors use {"error":{"type":"invalid_request_error","code":"conflict","param":null,"message":"conflicting request"}}; branch on code. row_version is the compare-and-set token; on 409, read again. IDs: agt_ (Agent), ses_ (Session), svc_ (Service), sch_ (Schedule), run_ (Run), skl_ (Skill), mcp_ (MCP connection), sec_ (Secret), fil_ (attachment).
-Request:
-```
+## POST /v1/agents
+
+```bash
curl -X POST https://api.sandbase.ai/v1/agents \
-H "Authorization: Bearer sk-YOUR_KEY" \
+ -H "Idempotency-Key: create-research-agent-1" \
-H "Content-Type: application/json" \
- -d '{
- "name": "Research Assistant",
- "model": "openai/gpt-5.4",
- "system": "You are a research assistant.",
- "tools": [{"type": "agent_toolset_20260401"}]
- }'
+ -d '{"name": "Research assistant", "model": "deepseek/deepseek-v4-flash", "instructions": "Research the question and cite sources."}'
```
-Response:
-```
-{"id": "agent_abc123", "type": "agent", "name": "Research Assistant", "version": 1}
-```
+Required: model. Optional: name, instructions, description, runtime_profile (codex, claude, mcode), skills, mcp_connections, tools, text, reasoning, service_tier, metadata, note. Returns 201 with id, version, row_version, status, projection_status. Start Sessions when projection_status is ready. web_search must use mode disabled; client-side function results (requires_action) are not supported.
-## GET /v1/agents - List agents
-## GET /v1/agents/{id} - Get agent
-## POST /v1/agents/{id} - Update agent
-## POST /v1/agents/{id}/archive - Archive agent
-## GET /v1/agents/{id}/versions - List versions
+## GET /v1/agents - List agents (status=active|archived|all, cursor pagination)
+## GET /v1/agents/{agent_id} - Get agent
+## POST /v1/agents/{agent_id} - Partial update without row_version; creates a version
+## PATCH /v1/agents/{agent_id} - Update with row_version (compare-and-set)
+## POST /v1/agents/{agent_id}/archive - Archive agent ({"row_version"}); DELETE /v1/agents/{agent_id} also archives
+## GET /v1/agents/{agent_id}/versions - List versions
+## POST /v1/agents/catalog/{agent_id}/clone - Copy a public catalog Agent into your organization
---
-## POST /v1/endpoints/{id}/run
+## POST /v1/agents/sessions
-Invoke a Service by creating or continuing a Session and sending one message.
-
-Service invocation accepts an optional session_id. When omitted, SandBase creates a new persistent Session. A supplied Session must come from the same Service and match its current Agent-version binding. Invocation does not create a separate public Run identity.
-
-Request:
-```
-curl -X POST https://api.sandbase.ai/v1/endpoints/ep_abc/run \
+```bash
+curl -X POST https://api.sandbase.ai/v1/agents/sessions \
-H "Authorization: Bearer sk-YOUR_KEY" \
-H "Content-Type: application/json" \
- -d '{"input": "Summarize the latest AI news"}'
-```
-
-Default response (`202 Accepted`):
-```
-{"session_id":"sess_abc123","events":[{"id":"sevt_abc123","type":"user.message","processed_at":"2026-08-03T10:00:00Z"}]}
+ -d '{"agent_id": "agt_..."}'
```
----
-
-## POST /v1/sessions
-
-Create an Agent Session. `agent` is required; runtime binding is resolved internally from the Agent. Use `initial_events` to send the first message during creation.
+Creates a Session for a saved Agent and freezes its version, Skills, and MCP connections. Optional: version, title, input, model, metadata, model_provider (omit to use your calling key). Sessions always run in the SandBase hosted cloud; the OpenAI SDK requires environment={"type": "openai_hosted"}, which SandBase accepts. Returns 201 with status (idle, in_progress, failed), lifecycle_status (active, archived), failure, source (direct, service, schedule).
-POST /v1/sessions creates the persistent public Session and returns session_id. Events, SSE, archive, and delete all address that same Session. Archived history remains readable but rejects new events; deleted Sessions return 404.
+## GET /v1/agents/sessions/{session_id}/events - Live-only SSE; open before sending input; Last-Event-ID returns 400
+## POST /v1/agents/sessions/{session_id}/events - Send exactly one event; returns 202 with no body
-Request:
-```
-curl -X POST https://api.sandbase.ai/v1/sessions \
- -H "Authorization: Bearer sk-YOUR_KEY" \
- -H "Content-Type: application/json" \
- -d '{"agent": "agent_abc123", "title": "GitHub research", "initial_events":[{"type":"user.message","content":[{"type":"text","text":"Research AI coding tools"}]}]}'
-```
-
-Response:
-```
-{"id":"sess_xyz789","type":"session","agent_id":"agent_abc123","status":"idle"}
+```json
+{"events": [{"type": "agent.session.input.message", "input": "Summarize the findings."}]}
```
-## POST /v1/sessions/{id}/events
+409 session_input_pending means earlier input is still being delivered: wait, and do not resend with a new Idempotency-Key.
-Send a message to start or continue an idle Session, or send `user.interrupt` to interrupt a running turn.
+## GET /v1/agents/sessions/{session_id}/items - Read conversation history
+## GET /v1/agents/sessions/{session_id}/turns - Read turns
+## POST /v1/agents/sessions/{session_id}/cancel - Cancel the running turn (Idempotency-Key required)
-Request: {"events":[{"type":"user.message","content":[{"type":"text","text":"Focus on AI coding tools"}]}]}
-Response: {"data":[{"id":"sevt_001","type":"user.message","processed_at":"2026-08-03T10:00:00Z"}]}
+---
-## GET /v1/sessions - List sessions
-## GET /v1/sessions/{id} - Get session details
-## GET /v1/sessions/{id}/events - List all events
+## POST /v1/services
-## GET /v1/sessions/{id}/events/stream
+Create a Service pinned to an Agent version. Idempotency-Key is required. Body: name, agent_id, agent_version (optional; defaults to the current version), input, description, model.
-Replay persisted events and then stream newly persisted events. While idle, the server may send `: heartbeat` comments. The connection remains open until the client disconnects, the request context is cancelled, or a write fails; terminal Session state does not emit a separate close event.
+## POST /v1/services/{service_id}/invoke
-Event format:
-```
-data: {"id":"sevt_001","type":"agent.message","content":[{"type":"text","text":"I'll search GitHub..."}],"processed_at":"2026-08-03T10:00:01Z"}
-: heartbeat
-data: {"id":"sevt_002","type":"session.status_idle","stop_reason":{"type":"end_turn"},"processed_at":"2026-08-03T10:00:02Z"}
-```
+Returns 202 with a Run: id, status (pending, succeeded, failed, cancelled), session_id (null until the Session exists), error_code. Idempotency-Key is required; the same key and body return the same Run. Only one Run can be pending (429 concurrency_limit). Poll GET /v1/services/{service_id}/runs/{run_id}, then read GET /v1/agents/sessions/{session_id}/items.
---
-## POST /v1/deployments
-
-Create a Schedule through the `/v1/deployments` compatibility resource. Each trigger creates a distinct drun_* DeploymentRun and attempts to create exactly one new sess_* Session.
-
-Request:
-```
-{"name":"Daily summary","agent_id":"agent_abc123","initial_events":[{"type":"user.message","content":[{"type":"text","text":"Generate the daily summary"}]}],"schedule":{"type":"cron","expression":"0 9 * * *","timezone":"America/New_York"}}
-```
-
-Response:
-```
-{"id": "depl_abc123", "status": "active", "next_run_at": "2026-08-03T09:00:00-04:00"}
-```
+## POST /v1/schedules
-## POST /v1/deployments/{id}/runs - Create a manual DeploymentRun
-## GET /v1/deployments/{id}/runs - List DeploymentRuns for one Deployment
-## GET /v1/deployment_runs - List DeploymentRuns; supports trigger_type=manual|schedule and status=pending|succeeded|failed filters
+Create a cron Schedule. Idempotency-Key and non-empty input are required. schedule is {"cron": "0 9 * * 1-5", "timezone": "Asia/Shanghai"}; timezone defaults to UTC. The model credential is stored encrypted for scheduled Runs.
-The public DeploymentRun object contains id, type, deployment_id, agent, trigger_context, nullable session_id, nullable error, and created_at. Its current response shape does not include a status field.
+## POST /v1/schedules/{schedule_id}/runs - Trigger a manual Run (202)
+## GET /v1/schedules/{schedule_id}/runs - List Runs; trigger_source is manual or scheduled; status can also be skipped when a firing finds a pending Run
---
-## GET /v1/skills - List available skills
+## POST /v1/skills - Upload one ZIP part named files (5 MiB) whose single top-level folder contains SKILL.md with name and description (a SKILL.md at the ZIP root returns 400)
+## POST /v1/mcp-connections - Register a remote HTTPS MCP server; Idempotency-Key required
+## POST /v1/secrets - Store a write-only mcp_bearer token bound to one server_url; Idempotency-Key required. Secrets are not environment variables.
---
diff --git a/public/llms.txt b/public/llms.txt
index 74b5317d..50e2894f 100644
--- a/public/llms.txt
+++ b/public/llms.txt
@@ -41,26 +41,27 @@ Open-source CLI: https://github.com/sandbaseai/cli
- GET /v1/tasks/{id}/cost - Get task cost and usage
## Agent Lifecycle
-
-- POST /v1/agents - Create an agent
-- POST /v1/agents/{id} - Update an agent and create a version
-- POST /v1/agents/{id}/archive - Archive an agent
-- POST /v1/endpoints/{id}/run - Invoke a Service through its compatibility route and create or continue a Session
-- POST /v1/sessions - Create agent session directly
-- POST /v1/sessions/{id}/events - Send messages to running session
-- GET /v1/sessions/{id}/events/stream - Replay persisted session events (SSE)
-- POST /v1/deployments - Create a Schedule (Deployment)
-- POST /v1/deployments/{id}/runs - Create a manual DeploymentRun
-- GET /v1/deployment_runs - List DeploymentRuns across Schedules
-
-## Session Contract
-
-- session_id is the only public identity for persistent Agent interaction.
-- Service invocation accepts an optional session_id. Omit it to create a Session; a supplied Session must come from the same Service and match its current Agent-version binding.
-- Every manual or scheduled Deployment trigger creates a distinct drun_* DeploymentRun and attempts to create exactly one new Session.
-- A DeploymentRun links to session_id when Session creation succeeds; failed trigger records have no Session.
-- Service acceptance returns session_id and an events array. It does not create a DeploymentRun or expose an internal Runtime Session identity.
-
+Agents platform resources accept `Authorization: Bearer` or `X-API-Key` and return {"error":{"type","code","param","message"}}.
+- POST /v1/agents - Create a saved, versioned Agent (agt_)
+- POST /v1/agents/{agent_id} - Update an Agent (SDK style); PATCH /v1/agents/{agent_id} updates with row_version
+- POST /v1/agents/{agent_id}/archive - Archive an Agent; DELETE /v1/agents/{agent_id} also archives
+- POST /v1/agents/sessions - Create a Session (ses_) for a saved Agent
+- GET /v1/agents/sessions/{session_id}/events - Live-only event stream (SSE); open it before sending input
+- POST /v1/agents/sessions/{session_id}/events - Send one agent.session.input.message or agent.session.input.cancel event (202)
+- GET /v1/agents/sessions/{session_id}/items - Read Session history
+- POST /v1/services - Create a Service (svc_) pinned to an Agent version; Idempotency-Key required
+- POST /v1/services/{service_id}/invoke - Start a Run (run_); returns 202 with status pending
+- POST /v1/schedules - Create a cron Schedule (sch_); Idempotency-Key required
+- POST /v1/schedules/{schedule_id}/runs - Trigger a manual Schedule Run
+- POST /v1/skills - Upload a Skill ZIP (skl_)
+- POST /v1/mcp-connections - Register a remote HTTPS MCP server (mcp_)
+- POST /v1/secrets - Store a write-only MCP Bearer token (sec_)
+## Session and Run Contract
+- A Session runs one saved Agent version and freezes its Skills and MCP connections when it starts.
+- Each Service invocation and each Schedule firing or manual trigger creates one Run, and each Run creates its own Session reported in session_id.
+- Run status is pending, succeeded, failed, cancelled, or (Schedules only) skipped. Failed and skipped Runs carry error_code.
+- Only one Run per Service or Schedule can be pending; another invocation returns 429 concurrency_limit.
+- Internal Runtime Session identities are never exposed.
## Pricing
Pricing formulas and billing units differ by model. Use GET /v1/models for discovery and GET /v1/models/{id_or_name} for the current
diff --git a/public/openapi.yaml b/public/openapi.yaml
index 6a1e41b3..c2034540 100644
--- a/public/openapi.yaml
+++ b/public/openapi.yaml
@@ -15,16 +15,16 @@ info:
- **Images** (`/v1/images/generations`, `/v1/images/edits`): Use OpenAI SDK with `base_url="https://api.sandbase.ai/v1"`
- **Anthropic Messages** (`/v1/messages`): Use Anthropic SDK with `base_url="https://api.sandbase.ai"`
- **Google Gemini GenerateContent** (`/v1beta/models/{model}:generateContent`): Configure the Google Gen AI SDK to use `https://api.sandbase.ai`
- - **Agents and Sessions** (`/v1/agents`, `/v1/sessions`): Use the documented SandBase HTTP resource APIs
+ - **Agents and Sessions** (`/v1/agents`, `/v1/agents/sessions`): Follow the OpenAI Agents protocol (`OpenAI-Beta: agents=v1` is optional) plus the documented SandBase extensions
+ - **Agents platform resources** (`/v1/agents*`, `/v1/services*`, `/v1/schedules*`, `/v1/mcp-connections*`, `/v1/secrets*`, `/v1/skills*`): Accept `Authorization: Bearer` or `X-API-Key` and return the `AgentsError` envelope
version: 1.0.0
x-sandbase-session-contract:
status: active
- feature: persistent public Session identity
+ feature: Agents platform Sessions and Runs
description: >-
- Public session_id identifies persistent Agent interaction across Direct Session, Service, and Schedule
- (Deployment) entry points. Service invocation creates or continues a Session and sends one event. Every manual or
- scheduled Deployment trigger creates a public drun_* DeploymentRun and attempts to create one new Session.
- Internal Runtime Session identities are never exposed.
+ A Session (ses_*) runs one saved Agent version in a SandBase-hosted environment. Sessions are created directly
+ with POST /v1/agents/sessions, or by a Run (run_*) when a Service is invoked or a Schedule fires. Each Run
+ creates at most one Session and reports it in session_id. Internal runtime identities are never exposed.
contact:
name: SandBase
url: https://www.sandbase.ai
@@ -54,15 +54,13 @@ tags:
- name: Official Native API
description: Provider-native model protocols, currently ByteDance Seedance content-generation tasks
- name: Agents
- description: Build and manage Agent definitions
+ description: Saved, versioned Agent configurations and the public Agent catalog
- name: Sessions
- description: Create stateful Agent executions and exchange events
+ description: Run an Agent version, send input, stream events, and read history
- name: Services
- description: Publish and invoke tested Agents as Services
+ description: Invoke a pinned Agent version on demand; each invocation returns a Run
- name: Schedules
- description: Create and manage Schedules through Deployment resources
- - name: Events
- description: Run event streaming
+ description: Run a pinned Agent version on a cron schedule or on demand
- name: Models
description: Model catalog
- name: Tasks
@@ -70,9 +68,11 @@ tags:
- name: Account
description: Organization balance and API execution history
- name: Skills
- description: Reusable Agent instruction and resource bundles
- - name: Credentials
- description: Private values injected into Agent runtimes
+ description: Versioned ZIP instruction bundles that Agents can mount
+ - name: MCP Connections
+ description: Remote HTTPS MCP servers that Agents can call
+ - name: Secrets
+ description: Write-only Bearer tokens for MCP connections
paths:
# --- LLM Gateway ------------------------------------------------
@@ -1155,399 +1155,119 @@ paths:
application/json:
schema: { $ref: '#/components/schemas/TaskCostError' }
- # --- Services (Endpoint resources) -----------------------------
- /v1/endpoints:
- post:
- operationId: createEndpoint
- tags: [Services]
- summary: Create a Service
- description: |
- Creates a Service from either a declarative runtime definition or an existing Agent.
- The two creation modes are mutually exclusive. Declarative YAML uses the same schema as JSON.
- The Agent runtime binding must use an enabled provider for the organization.
- requestBody:
- required: true
- content:
- application/json:
- schema:
- oneOf:
- - $ref: '#/components/schemas/CreateDeclarativeEndpointRequest'
- - $ref: '#/components/schemas/CreateAdvancedEndpointRequest'
- application/yaml:
- schema:
- $ref: '#/components/schemas/CreateDeclarativeEndpointRequest'
- application/x-yaml:
- schema:
- $ref: '#/components/schemas/CreateDeclarativeEndpointRequest'
- responses:
- '201':
- description: Service created
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Endpoint'
- '400':
- description: Invalid definition or creation mode
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/APIError'
- '401':
- description: Missing or invalid API key
- content:
- application/json:
- schema: { $ref: '#/components/schemas/TaskCostError' }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404':
- description: Referenced Skill or Agent version was not found
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
- '409':
- description: Referenced Agent is archived
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
- '415':
- description: Content-Type is not application/json, application/yaml, or application/x-yaml
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
- '422':
- description: Runtime or Skill is incompatible
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/APIError'
- '500':
- description: Service creation or declarative compilation failed
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
+ # --- Models ---------------------------------------------------
+ /v1/models:
get:
- operationId: listEndpoints
- tags: [Services]
- summary: List Services
+ operationId: listModels
+ tags: [Models]
+ summary: List models
+ description: List enabled logical models in the OpenAI-compatible model-list shape. Use the detail endpoint for capabilities, schemas, and pricing.
parameters:
- name: q
in: query
- schema: { type: string }
- description: Search by ID, name, slug, runtime, or Agent ID. `search` is accepted as a compatibility alias.
- - name: limit
+ schema:
+ type: string
+ description: Fuzzy search over model name, display name, vendor, and description.
+ - name: vendor
in: query
- schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
- - name: page
+ schema:
+ type: string
+ description: Exact, case-sensitive vendor filter.
+ - name: type
in: query
- schema: { type: string }
- description: Opaque cursor from a previous response's next_page field.
+ schema:
+ type: string
+ default: llm
+ description: Filter by model type. Omission defaults to llm; an explicitly empty value removes the type filter.
+ - name: order
+ in: query
+ schema:
+ type: string
+ enum: [sort_order, most-popular, newest, name]
+ default: sort_order
+ description: Sort order.
responses:
'200':
- description: Service list
+ description: Model list
content:
application/json:
schema:
- type: object
- required: [data]
- properties:
- data:
- type: array
- items:
- $ref: '#/components/schemas/Endpoint'
- next_page:
- type: string
- description: Opaque cursor included only when another page is available.
- '400':
- description: Invalid page cursor
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
- '401':
- description: Missing or invalid API key
- content:
- application/json:
- schema: { $ref: '#/components/schemas/TaskCostError' }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
+ $ref: '#/components/schemas/ModelListResponse'
'500':
- description: Service list could not be loaded
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
-
- /v1/endpoints/{id}:
- get:
- operationId: getEndpoint
- tags: [Services]
- summary: Get a Service
- parameters:
- - $ref: '#/components/parameters/EndpointId'
- responses:
- '200':
- description: Service details
+ description: Model catalog lookup failed
content:
application/json:
schema:
- $ref: '#/components/schemas/Endpoint'
- '404':
- description: Service not found
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
+ $ref: '#/components/schemas/TaskCostError'
'401':
description: Missing or invalid API key
- content:
- application/json:
- schema: { $ref: '#/components/schemas/TaskCostError' }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- patch:
- operationId: updateEndpoint
- tags: [Services]
- summary: Update a Service
- description: Declarative definition fields are immutable in this release; operational fields such as name, status, and protocols may be updated.
- parameters:
- - $ref: '#/components/parameters/EndpointId'
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/UpdateEndpointRequest'
- responses:
- '200':
- description: Service updated
content:
application/json:
schema:
- $ref: '#/components/schemas/Endpoint'
- '409':
- description: Agent version conflict, archived Agent, or immutable declarative definition
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
- '400': { description: 'Invalid request, status, protocols, Agent binding, or required expected version', content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: 'Service or Agent version not found', content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '422': { description: Runtime provider is unsupported, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '500': { description: Service update failed, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- post:
- operationId: updateEndpointCompatibility
- tags: [Services]
- summary: Update a Service (compatibility alias)
- description: Compatibility alias for PATCH. New integrations should use PATCH.
- parameters:
- - $ref: '#/components/parameters/EndpointId'
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/UpdateEndpointRequest'
- responses:
- '200':
- description: Service updated
+ $ref: '#/components/schemas/TaskCostError'
+ '402':
+ description: API key spending limit exceeded
content:
application/json:
schema:
- $ref: '#/components/schemas/Endpoint'
- '409':
- description: Agent version conflict, archived Agent, or immutable declarative definition
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
- '400': { description: 'Invalid request, status, protocols, Agent binding, or required expected version', content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: 'Service or Agent version not found', content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '422': { description: Runtime provider is unsupported, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '500': { description: Service update failed, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- delete:
- operationId: deleteEndpoint
- tags: [Services]
- summary: Delete a Service
- parameters:
- - $ref: '#/components/parameters/EndpointId'
- responses:
- '200':
- description: Service deleted
+ $ref: '#/components/schemas/TaskCostError'
+ '403':
+ description: API key scope does not authorize this endpoint
content:
application/json:
schema:
- type: object
- required: [id, deleted]
- properties:
- id: { type: string }
- deleted: { type: boolean, const: true }
- '404': { description: Service not found, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: Service deletion failed, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
+ $ref: '#/components/schemas/APIKeyScopeError'
- /v1/endpoints/{id}/run:
- post:
- operationId: runEndpoint
- tags: [Services]
- summary: Invoke a Service over REST
- description: |
- Creates or continues a persistent Session and sends one standard event. Omit `session_id` to create a
- Session fixed to the Service's Agent version and runtime binding. Provide `session_id` to
- continue that Session. Returns the persistent Session ID and accepted events; no public execution Run or
- internal Runtime Session identity is exposed. When both input and content are supplied, content takes precedence.
+ /v1/models/{id_or_name}:
+ get:
+ operationId: getModel
+ tags: [Models]
+ summary: Get model details
+ description: Get model details by UUID or full logical model name. Model names containing slashes are supported as path segments, for example /v1/models/openai/gpt-5.6-luna. Name lookup returns enabled catalog models; UUID lookup can return a disabled catalog record with enabled set to false. Protocol-ingress-only models are never returned.
parameters:
- - $ref: '#/components/parameters/EndpointId'
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/EndpointInvokeRequest'
+ - name: id_or_name
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Model ID or full model name.
responses:
- '202':
- description: Session event accepted
+ '200':
+ description: Model details
content:
application/json:
schema:
- $ref: '#/components/schemas/EndpointInvokeAccepted'
+ $ref: '#/components/schemas/ModelDetail'
'400':
- description: Invalid JSON or missing usable input/content
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/APIError'
- '401':
- description: Missing or invalid API key
- content:
- application/json:
- schema: { $ref: '#/components/schemas/TaskCostError' }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403':
- description: Service is disabled, REST is not enabled, or the API key scope does not authorize this endpoint
+ description: Missing model ID or name
content:
application/json:
schema:
- oneOf:
- - $ref: '#/components/schemas/APIError'
- - $ref: '#/components/schemas/APIKeyScopeError'
+ $ref: '#/components/schemas/TaskCostError'
'404':
- description: Service or supplied Session not found
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
- '409':
- description: Session is busy, archived, terminal, or bound to another Service, Agent, or version
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/APIError'
- '500':
- description: Session creation or turn acceptance failed
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
- '503':
- description: Stateful persistence or recovery is unavailable
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/APIError'
-
- /v1/endpoints/{id}/acp:
- post:
- operationId: callEndpointAcp
- tags: [Services]
- summary: Invoke a Service over experimental ACP
- description: |
- Experimental ACP-over-HTTP JSON-RPC transport supporting initialize, session/new, session/prompt, and
- session/cancel. Prompt output is newline-delimited JSON. Runtime failures use redacted public error messages;
- the remote transport may change while ACP remains experimental.
- parameters:
- - $ref: '#/components/parameters/EndpointId'
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ACPRequest'
- responses:
- '200':
- description: ACP JSON-RPC response or newline-delimited prompt stream
+ description: Model not found or not publicly discoverable
content:
application/json:
schema:
- $ref: '#/components/schemas/ACPResponse'
- application/x-ndjson:
- schema:
- type: string
- description: ACP session/update notifications followed by the final JSON-RPC result.
- '400':
- description: Invalid JSON-RPC request
- content:
- application/json:
- schema: { $ref: '#/components/schemas/ACPResponse' }
+ $ref: '#/components/schemas/TaskCostError'
'401':
description: Missing or invalid API key
content:
application/json:
- schema: { $ref: '#/components/schemas/TaskCostError' }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404':
- description: Service or ACP session not found
- content:
- application/json:
- schema: { $ref: '#/components/schemas/ACPResponse' }
- '500':
- description: ACP Session could not be created
+ schema:
+ $ref: '#/components/schemas/TaskCostError'
+ '402':
+ description: API key spending limit exceeded
content:
application/json:
- schema: { $ref: '#/components/schemas/ACPResponse' }
-
- /v1/endpoints/{id}/mcp:
- post:
- operationId: callEndpointMcp
- tags: [Services]
- summary: Invoke a Service over MCP
- description: Handle MCP JSON-RPC for a Service whose protocols include mcp. Responses may be newline-delimited when the transport emits a stream.
- parameters:
- - $ref: '#/components/parameters/EndpointId'
- requestBody:
- required: true
- content:
- application/json:
- schema: { $ref: '#/components/schemas/MCPRequest' }
- responses:
- '200':
- description: MCP JSON-RPC response or newline-delimited stream
+ schema:
+ $ref: '#/components/schemas/TaskCostError'
+ '403':
+ description: API key scope does not authorize this endpoint
content:
application/json:
- schema: { $ref: '#/components/schemas/MCPResponse' }
- application/x-ndjson:
- schema: { type: string, description: Newline-delimited MCP JSON-RPC messages. }
- '400': { description: Invalid MCP JSON-RPC request, content: { application/json: { schema: { $ref: '#/components/schemas/MCPResponse' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: 'Service is disabled, MCP is not enabled, or the API key scope does not authorize this endpoint', content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Service not found, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '500': { description: MCP transport failed, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- delete:
- operationId: closeEndpointMcp
- tags: [Services]
- summary: Close a Service MCP transport
- description: Close and clean up MCP transport state without deleting or disabling the Service.
- parameters:
- - $ref: '#/components/parameters/EndpointId'
- responses:
- '204': { description: MCP transport state closed }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Service not found, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '500': { description: MCP transport cleanup failed, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
+ schema:
+ $ref: '#/components/schemas/APIKeyScopeError'
# --- Agents -----------------------------------------------------
/v1/agents:
@@ -1555,1635 +1275,2575 @@ paths:
operationId: createAgent
tags: [Agents]
summary: Create agent
- description: Create a reusable, versioned Agent configuration. Model IDs must use the public vendor/model identity. Unknown request fields are accepted and ignored. Omitted tools default to the built-in Agent toolset; an explicit null leaves tools null.
+ description: 'Create a saved, versioned Agent (version 1). The request is strict JSON (1 MiB maximum; unknown fields are rejected). Idempotency-Key is optional; the service generates one when omitted, so send your own key when you retry.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/IdempotencyKeyOptional'
+ - $ref: '#/components/parameters/OpenAIBeta'
requestBody:
required: true
content:
application/json:
- schema:
- $ref: '#/components/schemas/CreateAgentRequest'
+ schema: { $ref: '#/components/schemas/AgentCreateRequest' }
responses:
- '200':
- description: Agent created
+ '201':
+ description: 'Agent created'
content:
application/json:
- schema:
- $ref: '#/components/schemas/Agent'
- '400': { description: 'Missing or invalid name, model, visibility, multiagent configuration, or MCP headers', content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '422': { description: Model identity is invalid or unavailable, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '500': { description: Agent could not be created or validated, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
+ schema: { $ref: '#/components/schemas/Agent' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
get:
operationId: listAgents
tags: [Agents]
summary: List agents
+ description: 'List Agents owned by the API key organization with cursor pagination.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - { name: 'created_at[gte]', in: query, schema: { type: string, format: date-time } }
- - { name: 'created_at[lte]', in: query, schema: { type: string, format: date-time } }
- - { name: include_archived, in: query, schema: { type: boolean, default: false } }
- - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
- - { name: page, in: query, schema: { type: string } }
+ - $ref: '#/components/parameters/AgentStatusFilter'
+ - $ref: '#/components/parameters/AgentsLimit'
+ - $ref: '#/components/parameters/AgentsAfter'
+ - $ref: '#/components/parameters/AgentsOrder'
+ - $ref: '#/components/parameters/OpenAIBeta'
responses:
'200':
- description: Agent list
+ description: 'Agent page'
content:
application/json:
- schema: { $ref: '#/components/schemas/AgentListPage' }
- '400': { description: Invalid page cursor, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: Agents could not be listed, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
-
- /v1/agents/{id}:
+ schema: { $ref: '#/components/schemas/AgentList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/catalog:
get:
- operationId: getAgent
+ operationId: listAgentCatalog
tags: [Agents]
- summary: Get agent
+ summary: List public agent catalog
+ description: 'List public Agents published to the SandBase catalog. Catalog entries are read-only; clone one to customize it.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/AgentId'
- - name: version
- in: query
- schema: { type: integer, minimum: 1 }
- description: Return the immutable Agent snapshot for this numeric version. Omit for the current version.
+ - $ref: '#/components/parameters/AgentsLimit'
+ - $ref: '#/components/parameters/AgentsAfter'
+ - $ref: '#/components/parameters/AgentsOrder'
responses:
'200':
- description: Agent details
+ description: 'Catalog page'
content:
application/json:
- schema:
- $ref: '#/components/schemas/Agent'
- '400': { description: Invalid numeric version selector, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Agent or requested version not found, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '500': { description: Agent or snapshot could not be loaded, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- post:
- operationId: updateAgent
+ schema: { $ref: '#/components/schemas/AgentCatalogList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/catalog/{agent_id}:
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ get:
+ operationId: getAgentCatalogEntry
tags: [Agents]
- summary: Update agent
- description: Update Agent configuration. Effective changes create a new version; a no-op returns the current Agent without incrementing it. Omitted fields and null scalar fields preserve their current values. Array null values clear those arrays. Unknown fields are accepted and ignored.
+ summary: Get public catalog agent
+ description: 'Read one public catalog Agent, optionally at a specific version.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - { name: version, in: query, required: false, schema: { type: integer, minimum: 1 }, description: Catalog version. Defaults to the current public version. }
+ responses:
+ '200':
+ description: 'Catalog Agent'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/AgentCatalogEntry' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/catalog/{agent_id}/clone:
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ post:
+ operationId: cloneAgentCatalogEntry
+ tags: [Agents]
+ summary: Clone public catalog agent
+ description: 'Copy a public catalog Agent into your organization as a new private Agent.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/AgentId'
+ - $ref: '#/components/parameters/IdempotencyKeyOptional'
requestBody:
required: true
content:
application/json:
- schema:
- $ref: '#/components/schemas/UpdateAgentRequest'
+ schema: { $ref: '#/components/schemas/AgentCloneRequest' }
+ responses:
+ '201':
+ description: 'Private Agent created from the catalog entry'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/Agent' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/{agent_id}:
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ get:
+ operationId: getAgent
+ tags: [Agents]
+ summary: Get agent
+ description: 'Read an Agent, including archived Agents.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/OpenAIBeta'
responses:
'200':
- description: Agent updated
+ description: 'Agent'
content:
application/json:
- schema:
- $ref: '#/components/schemas/Agent'
- '400': { description: 'Invalid request, model, multiagent configuration, or MCP headers', content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403':
- description: Agent is archived or the API key scope does not authorize this endpoint
+ schema: { $ref: '#/components/schemas/Agent' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ post:
+ operationId: updateAgent
+ tags: [Agents]
+ summary: Update agent
+ description: 'OpenAI SDK-style partial update. Only the listed fields are accepted; supplied fields replace the saved values and create a new version. No row_version is needed. Archived Agents return 404.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/IdempotencyKeyOptional'
+ - $ref: '#/components/parameters/OpenAIBeta'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/AgentUpdateRequest' }
+ responses:
+ '200':
+ description: 'Updated Agent'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/Agent' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ patch:
+ operationId: replaceAgent
+ tags: [Agents]
+ summary: Update agent with row_version
+ description: 'Product compare-and-set update. row_version is required and must match the current value. Explicit null values are rejected. A successful write creates a new version.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/IdempotencyKeyOptional'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/AgentReplaceRequest' }
+ responses:
+ '200':
+ description: 'Updated Agent'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/Agent' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ delete:
+ operationId: deleteAgent
+ tags: [Agents]
+ summary: Delete agent
+ description: 'Archives the Agent. Agents are never hard-deleted; use unarchive to restore access.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/IdempotencyKeyOptional'
+ - $ref: '#/components/parameters/OpenAIBeta'
+ responses:
+ '200':
+ description: 'Agent archived'
content:
application/json:
- schema:
- oneOf:
- - $ref: '#/components/schemas/SessionAPIError'
- - $ref: '#/components/schemas/APIKeyScopeError'
- '404': { description: Agent not found, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '409': { description: Supplied version does not match the current Agent version, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '422': { description: Model identity is invalid or unavailable, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '500': { description: Agent could not be updated or validated, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
-
- /v1/agents/{id}/archive:
+ schema: { $ref: '#/components/schemas/AgentDeleted' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/{agent_id}/archive:
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
post:
operationId: archiveAgent
tags: [Agents]
summary: Archive agent
- description: Archive an agent, making it read-only.
+ description: 'Archive an Agent. row_version must match the current value.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/AgentId'
+ - $ref: '#/components/parameters/IdempotencyKeyOptional'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/AgentRowVersionRequest' }
responses:
'200':
- description: Agent archived
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Agent'
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Agent not found, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '409': { description: Agent is already archived, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '500': { description: Agent could not be archived, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
-
- /v1/agents/{id}/versions:
+ description: 'Agent'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/Agent' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/{agent_id}/unarchive:
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ post:
+ operationId: unarchiveAgent
+ tags: [Agents]
+ summary: Unarchive agent
+ description: 'Return an archived Agent to active. row_version must match the current value.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/IdempotencyKeyOptional'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/AgentRowVersionRequest' }
+ responses:
+ '200':
+ description: 'Agent'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/Agent' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/{agent_id}/restore:
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ post:
+ operationId: restoreAgentVersion
+ tags: [Agents]
+ summary: Restore agent version
+ description: 'Create a new current version from the configuration of an earlier version.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/IdempotencyKeyOptional'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/AgentRestoreRequest' }
+ responses:
+ '200':
+ description: 'Agent with the restored configuration as a new version'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/Agent' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/{agent_id}/versions:
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
get:
operationId: listAgentVersions
tags: [Agents]
summary: List agent versions
+ description: 'List immutable version snapshots with offset pagination.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/AgentId'
- - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
- - { name: page, in: query, schema: { type: string } }
+ - $ref: '#/components/parameters/OffsetLimit'
+ - $ref: '#/components/parameters/Offset'
responses:
'200':
- description: Version history
+ description: 'Version page'
content:
application/json:
- schema:
- type: object
- required: [data]
- properties:
- data:
- type: array
- items:
- $ref: '#/components/schemas/Agent'
- next_page: { type: string, description: Opaque cursor included only when another page is available. }
- '400': { description: Invalid page cursor, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Agent not found, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '500': { description: Agent versions or a stored snapshot could not be loaded, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
-
- /v1/agents/{id}/versions/{version}:
+ schema: { $ref: '#/components/schemas/AgentVersionList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/{agent_id}/versions/{version}:
+ parameters:
+ - $ref: '#/components/parameters/AgentIdPath'
+ - $ref: '#/components/parameters/AgentVersionPath'
get:
operationId: getAgentVersion
tags: [Agents]
summary: Get agent version
- description: Return one immutable Agent snapshot by version resource ID or positive numeric version.
- parameters:
- - $ref: '#/components/parameters/AgentId'
- - name: version
- in: path
- required: true
- schema:
- oneOf:
- - type: string
- - type: integer
- minimum: 1
+ description: 'Read one immutable version snapshot. version must be a positive integer.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
responses:
'200':
- description: Agent version
+ description: 'Version snapshot'
content:
application/json:
- schema:
- $ref: '#/components/schemas/Agent'
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Agent or version not found, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '500': { description: Agent version or stored snapshot could not be loaded, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
-
+ schema: { $ref: '#/components/schemas/AgentVersion' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
# --- Sessions ---------------------------------------------------
- /v1/sessions:
+ /v1/agents/sessions:
post:
operationId: createSession
tags: [Sessions]
summary: Create session
- description: Unknown top-level fields are rejected. resources and vault_ids are reserved and any non-null value is rejected.
+ description: 'Create a Session for a saved Agent. The Agent configuration, Skills, and MCP connections are frozen for the lifetime of the Session. stream=true is rejected: create the Session, open the event stream, then send input. Execution admission (balance and organization status) is checked here.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/IdempotencyKeyOptional'
+ - $ref: '#/components/parameters/OpenAIBeta'
requestBody:
required: true
content:
application/json:
- schema:
- $ref: '#/components/schemas/CreateSessionRequest'
+ schema: { $ref: '#/components/schemas/SessionCreateRequest' }
responses:
- '200':
- description: Session created
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Session'
- '400': { description: 'Invalid body, Agent reference, initial Event, or initial Event batch', content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403':
- description: The Agent is archived or the API key scope does not authorize this endpoint
- content:
- application/json:
- schema:
- oneOf:
- - $ref: '#/components/schemas/SessionAPIError'
- - $ref: '#/components/schemas/APIKeyScopeError'
- '404': { description: 'Agent or Agent version not found', content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '422':
- description: A reserved resources or vault_ids value was supplied, or initial_events used unsupported multi-Agent or runtime-resume behavior. When rejection occurs after Session creation, Events are not persisted and Session cleanup follows the documented best-effort compensation path.
- content:
- application/json:
- schema:
- type: object
- required: [error]
- properties:
- error:
- type: object
- required: [type, message]
- properties:
- type:
- type: string
- enum: [unsupported_event_type, unsupported_multiagent, unsupported_feature, agent_runtime_environment_mismatch]
- message: { type: string }
- '500': { description: Session could not be created, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '503': { description: Initial Event delivery is not configured, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '502':
- description: Native runtime delivery outcome is unknown. Recover the returned Session and inspect Events before retrying.
- content:
- application/json:
- schema:
- type: object
- required: [error, session_id]
- properties:
- error:
- type: object
- required: [type, message]
- properties:
- type: { type: string, enum: [delivery_outcome_unknown] }
- message: { type: string }
- session_id:
- type: string
- pattern: '^sess_'
- delivery_status:
- type: string
- enum: [unknown]
- recovery:
- type: object
- required: [recoverable, retry_create, action]
- properties:
- recoverable: { type: boolean, enum: [true] }
- retry_create: { type: boolean, enum: [false] }
- action: { type: string, enum: [get_session_and_list_events] }
+ '201':
+ description: 'Session created'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/Session' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: the Agent is archived or not ready, an MCP connection or Secret is not active, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
get:
operationId: listSessions
tags: [Sessions]
summary: List sessions
+ description: 'List Sessions with cursor pagination.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - name: agent_id
- in: query
- schema:
- type: string
- description: Filter by agent ID
- - name: agent_version
- in: query
- schema: { type: integer, minimum: 1 }
- - name: statuses
- in: query
- style: form
- explode: true
- schema:
- type: array
- items: { type: string, enum: [idle, running, rescheduling, terminated] }
- description: Repeat the parameter to filter by one or more Session statuses.
- - name: include_archived
- in: query
- schema: { type: boolean, default: false }
- - name: 'created_at[gt]'
- in: query
- schema: { type: string, format: date-time }
- - name: 'created_at[gte]'
- in: query
- schema: { type: string, format: date-time }
- - name: 'created_at[lt]'
- in: query
- schema: { type: string, format: date-time }
- - name: 'created_at[lte]'
- in: query
- schema: { type: string, format: date-time }
- - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
- - { name: page, in: query, schema: { type: string } }
+ - $ref: '#/components/parameters/SessionLifecycleFilter'
+ - { name: agent_id, in: query, required: false, schema: { type: string }, description: Only return Sessions of this Agent. }
+ - $ref: '#/components/parameters/AgentsLimit'
+ - $ref: '#/components/parameters/AgentsAfter'
+ - $ref: '#/components/parameters/AgentsOrder'
+ - $ref: '#/components/parameters/OpenAIBeta'
responses:
'200':
- description: Session list
+ description: 'Session page'
content:
application/json:
- schema:
- type: object
- required: [data]
- properties:
- data:
- type: array
- items:
- $ref: '#/components/schemas/Session'
- next_page: { type: string, description: Opaque cursor included only when another page is available. }
- '400': { description: Invalid created-at filter or page cursor, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: Sessions could not be listed, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
-
- /v1/sessions/{id}:
+ schema: { $ref: '#/components/schemas/SessionList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/sessions/{session_id}:
+ parameters:
+ - $ref: '#/components/parameters/SessionIdPath'
get:
operationId: getSession
tags: [Sessions]
summary: Get session
+ description: 'Read a Session. Execution status is read live from the execution layer.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/SessionId'
+ - $ref: '#/components/parameters/OpenAIBeta'
responses:
'200':
- description: Session details
+ description: 'Session'
content:
application/json:
- schema:
- $ref: '#/components/schemas/Session'
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Session not found, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '500': { description: Session could not be loaded, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
+ schema: { $ref: '#/components/schemas/Session' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
post:
operationId: updateSession
tags: [Sessions]
- summary: Update session metadata or Agent overrides
- description: Update title, shallow-merge metadata, or fully replace Session-local tool and MCP server overrides. In metadata, null removes a key. Omitted fields and a null title preserve the current value. The pinned Agent version remains immutable. Unknown fields are accepted and ignored.
+ summary: Update session metadata
+ description: 'Replace the Session metadata map. metadata is the only accepted field. Archived Sessions return 404.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/SessionId'
+ - $ref: '#/components/parameters/OpenAIBeta'
requestBody:
required: true
content:
application/json:
- schema:
- type: object
- additionalProperties: true
- properties:
- title: { type: [string, 'null'] }
- metadata:
- type: object
- additionalProperties: true
- description: Shallow merge patch. Null property values delete existing keys; the metadata field itself cannot be null.
- agent:
- $ref: '#/components/schemas/SessionAgentUpdate'
- vault_ids:
- type: array
- items: { type: string, pattern: '^vlt_' }
- description: Reserved for Claude compatibility; rejected until vault attachment is supported.
+ schema: { $ref: '#/components/schemas/SessionUpdateRequest' }
responses:
'200':
- description: Session updated
+ description: 'Session'
content:
application/json:
- schema:
- $ref: '#/components/schemas/Session'
- '400': { description: 'Invalid JSON, metadata, Agent override, or reserved vault_ids', content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Session not found, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '500': { description: Session could not be updated, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- delete:
- operationId: deleteSession
+ schema: { $ref: '#/components/schemas/Session' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ patch:
+ operationId: renameSession
tags: [Sessions]
- summary: Permanently delete a non-running session
- description: Allowed only when the Session is neither running nor rescheduling. Deletion is permanent; use archive to retain history.
- parameters:
- - $ref: '#/components/parameters/SessionId'
+ summary: Rename session
+ description: 'Change the Session title with compare-and-set on row_version.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SessionRenameRequest' }
responses:
'200':
- description: Session deleted
- content:
- application/json:
- schema:
- type: object
- required: [id, type]
- properties:
- id: { type: string, pattern: '^sess_' }
- type: { type: string, const: session_deleted }
- '404': { description: Session not found, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '409': { description: Running or rescheduling Session cannot be deleted, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: Session could not be deleted, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- /v1/sessions/{id}/archive:
- post:
- operationId: archiveSession
+ description: 'Session'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/Session' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ delete:
+ operationId: deleteSession
tags: [Sessions]
- summary: Archive session
+ summary: Delete session
+ description: 'Archives the Session. Sessions are not hard-deleted.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/SessionId'
+ - $ref: '#/components/parameters/OpenAIBeta'
responses:
'200':
- description: Session archived
+ description: 'Session archived'
content:
application/json:
- schema:
- $ref: '#/components/schemas/Session'
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Session not found or already archived, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '500': { description: Session could not be archived, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
-
- /v1/sessions/{id}/events:
+ schema: { $ref: '#/components/schemas/SessionDeleted' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/sessions/{session_id}/events:
+ parameters:
+ - $ref: '#/components/parameters/SessionIdPath'
post:
- operationId: sendEvents
- tags: [Events]
- summary: Send events to session
+ operationId: sendSessionEvents
+ tags: [Sessions]
+ summary: Send session input
+ description: 'Send exactly one event: agent.session.input.message or agent.session.input.cancel. The SandBase extension form {input, file_ids} sends text with ready attachments and cannot be combined with events. Idempotency-Key is optional but recommended; reuse the same key when retrying the same input.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/SessionId'
+ - $ref: '#/components/parameters/IdempotencyKeyOptional'
+ - $ref: '#/components/parameters/OpenAIBeta'
requestBody:
required: true
content:
application/json:
- schema:
- $ref: '#/components/schemas/SendEventsRequest'
+ schema: { $ref: '#/components/schemas/SessionEventsRequest' }
responses:
- '200':
- description: Implemented event types accepted. Schema-recognized event types without runtime behavior are never returned as accepted.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AcceptedSessionEvents'
- '400': { description: 'Invalid body, Event type, payload, or server-owned field', content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Session not found, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '409': { description: Event conflicts with Session state or the Session is archived or terminated, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '422':
- description: The batch was rejected without persisting any event. Tool confirmation, custom tool result, tool result, and define outcome currently return `unsupported_event_type`; any `session_thread_id` returns `unsupported_multiagent`.
- content:
- application/json:
- schema:
- type: object
- required: [error]
- properties:
- error:
- type: object
- required: [type, message]
- properties:
- type:
- type: string
- enum: [unsupported_event_type, unsupported_multiagent]
- message: { type: string }
- '500': { description: Events could not be accepted, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
+ '202': { description: 'Input accepted. The response has no body.', x-sandbase-empty-body: true }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict (Session archived or attachment not ready), session_input_pending, session_input_expired, or session_environment_unavailable', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
get:
- operationId: listEvents
- tags: [Events]
- summary: List session events
+ operationId: streamSessionEvents
+ tags: [Sessions]
+ summary: Stream session events
+ description: 'Open the live server-sent event stream. The stream is live-only: Last-Event-ID or after returns 400; read history with items or turns. The server writes a ": connected" comment first.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/SessionId'
- - name: types
- in: query
- schema: { type: array, items: { type: string } }
- style: form
- explode: true
- description: Repeat the parameter or provide a comma-separated list of event types.
- - name: order
- in: query
- schema: { type: string, enum: [asc, desc], default: asc }
- - name: limit
- in: query
- schema: { type: integer, minimum: 1, maximum: 100 }
- - name: page
- in: query
- schema: { type: string }
- - name: 'created_at[gt]'
- in: query
- schema: { type: string, format: date-time }
- - name: 'created_at[gte]'
- in: query
- schema: { type: string, format: date-time }
- - name: 'created_at[lt]'
- in: query
- schema: { type: string, format: date-time }
- - name: 'created_at[lte]'
- in: query
- schema: { type: string, format: date-time }
+ - $ref: '#/components/parameters/OpenAIBeta'
responses:
'200':
- description: Event list
+ description: 'Server-sent events whose type starts with agent.session.'
content:
- application/json:
- schema:
- type: object
- required: [data]
- properties:
- data:
- type: array
- items:
- $ref: '#/components/schemas/SessionEvent'
- next_page:
- type: string
- description: Opaque cursor present when another page is available.
- '400': { description: 'Invalid order, limit, page cursor, or created-at filter', content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Session not found or no longer stream-accessible, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '500': { description: Events could not be listed, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
-
- /v1/sessions/{id}/events/stream:
+ text/event-stream:
+ schema: { type: string }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/sessions/{session_id}/events/stream:
+ parameters:
+ - $ref: '#/components/parameters/SessionIdPath'
get:
- operationId: streamEvents
- tags: [Events]
- summary: Stream session events (SSE)
- parameters:
- - $ref: '#/components/parameters/SessionId'
- - name: cursor
- in: query
- schema: { type: string }
- description: Resume after the last delivered event ID. Last-Event-ID is also accepted.
- - name: event_delta
- in: query
- style: form
- explode: true
- schema:
- type: array
- items: { type: string, enum: [agent.message, agent.thinking] }
- description: Compatibility parameter. Accepted values are validated, but the current stream emits persisted canonical Events only and does not synthesize preview deltas.
+ operationId: streamSessionSnapshot
+ tags: [Sessions]
+ summary: Stream session snapshot and events
+ description: 'SandBase product stream. Emits session.snapshot first, then live events and heartbeat. If the upstream stream breaks, the server emits stream.interrupted; call reconnect. cursor or Last-Event-ID returns 400.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
responses:
'200':
- description: SSE event stream
+ description: 'session.snapshot, live Session events, heartbeat, and stream.interrupted'
content:
text/event-stream:
- schema:
- type: string
- '400': { description: Invalid event_delta or stream cursor, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Session not found, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
- '500': { description: Event history could not be loaded before streaming, content: { application/json: { schema: { $ref: '#/components/schemas/SessionAPIError' } } } }
-
-
- # --- Models -----------------------------------------------------
- /v1/models:
+ schema: { type: string }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/sessions/{session_id}/items:
+ parameters:
+ - $ref: '#/components/parameters/SessionIdPath'
get:
- operationId: listModels
- tags: [Models]
- summary: List models
- description: List enabled logical models in the OpenAI-compatible model-list shape. Use the detail endpoint for capabilities, schemas, and pricing.
+ operationId: listSessionItems
+ tags: [Sessions]
+ summary: List session items
+ description: 'Read Session items history with cursor pagination.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - name: q
- in: query
- schema:
- type: string
- description: Fuzzy search over model name, display name, vendor, and description.
- - name: vendor
- in: query
- schema:
- type: string
- description: Exact, case-sensitive vendor filter.
- - name: type
- in: query
- schema:
- type: string
- default: llm
- description: Filter by model type. Omission defaults to llm; an explicitly empty value removes the type filter.
- - name: order
- in: query
- schema:
- type: string
- enum: [sort_order, most-popular, newest, name]
- default: sort_order
- description: Sort order.
+ - $ref: '#/components/parameters/AgentsLimit'
+ - $ref: '#/components/parameters/AgentsAfter'
+ - $ref: '#/components/parameters/AgentsOrder'
+ - $ref: '#/components/parameters/OpenAIBeta'
responses:
'200':
- description: Model list
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ModelListResponse'
- '500':
- description: Model catalog lookup failed
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TaskCostError'
- '401':
- description: Missing or invalid API key
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TaskCostError'
- '402':
- description: API key spending limit exceeded
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TaskCostError'
- '403':
- description: API key scope does not authorize this endpoint
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/APIKeyScopeError'
-
- /v1/models/{id_or_name}:
+ description: 'items page'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/AgentsCursorList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/sessions/{session_id}/turns:
+ parameters:
+ - $ref: '#/components/parameters/SessionIdPath'
get:
- operationId: getModel
- tags: [Models]
- summary: Get model details
- description: Get model details by UUID or full logical model name. Model names containing slashes are supported as path segments, for example /v1/models/openai/gpt-5.6-luna. Name lookup returns enabled catalog models; UUID lookup can return a disabled catalog record with enabled set to false. Protocol-ingress-only models are never returned.
+ operationId: listSessionTurns
+ tags: [Sessions]
+ summary: List session turns
+ description: 'Read Session turns history with cursor pagination.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - name: id_or_name
- in: path
- required: true
- schema:
- type: string
- description: Model ID or full model name.
- responses:
- '200':
- description: Model details
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ModelDetail'
- '400':
- description: Missing model ID or name
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TaskCostError'
- '404':
- description: Model not found or not publicly discoverable
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TaskCostError'
- '401':
- description: Missing or invalid API key
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TaskCostError'
- '402':
- description: API key spending limit exceeded
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TaskCostError'
- '403':
- description: API key scope does not authorize this endpoint
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/APIKeyScopeError'
-
- /v1/deployments:
- post:
- operationId: createDeployment
- tags: [Schedules]
- summary: Create a Schedule
- description: Create either a declarative REST Deployment from a runtime definition or an advanced Deployment bound to an existing Agent.
- requestBody:
- required: true
- content:
- application/json:
- schema:
- oneOf:
- - $ref: '#/components/schemas/CreateDeclarativeDeploymentRequest'
- - $ref: '#/components/schemas/CreateDeploymentRequest'
- application/yaml:
- schema:
- oneOf:
- - $ref: '#/components/schemas/CreateDeclarativeDeploymentRequest'
- - $ref: '#/components/schemas/CreateDeploymentRequest'
- application/x-yaml:
- schema:
- oneOf:
- - $ref: '#/components/schemas/CreateDeclarativeDeploymentRequest'
- - $ref: '#/components/schemas/CreateDeploymentRequest'
+ - $ref: '#/components/parameters/AgentsLimit'
+ - $ref: '#/components/parameters/AgentsAfter'
+ - $ref: '#/components/parameters/AgentsOrder'
+ - $ref: '#/components/parameters/OpenAIBeta'
responses:
'200':
- description: Deployment created
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Deployment'
- '400':
- description: Invalid definition, creation mode, initial events, schedule, timeout policy, Agent, or reserved fields
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
- '401':
- description: Missing or invalid API key
- content:
- application/json:
- schema: { $ref: '#/components/schemas/TaskCostError' }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404':
- description: Referenced Skill not found
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
- '415':
- description: Content-Type is not application/json, application/yaml, or application/x-yaml
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
- '422':
- description: Runtime or Skill is incompatible
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
- '500':
- description: Deployment creation or declarative compilation failed
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
+ description: 'turns page'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/AgentsCursorList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/sessions/{session_id}/turns/{turn_id}:
+ parameters:
+ - $ref: '#/components/parameters/SessionIdPath'
+ - $ref: '#/components/parameters/TurnIdPath'
get:
- operationId: listDeployments
- tags: [Schedules]
- summary: List Schedules
+ operationId: getSessionTurn
+ tags: [Sessions]
+ summary: Get session turn
+ description: 'Read one Turn of the Session.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - { name: agent_id, in: query, schema: { type: string } }
- - { name: 'created_at[gte]', in: query, schema: { type: string, format: date-time } }
- - { name: 'created_at[lte]', in: query, schema: { type: string, format: date-time } }
- - { name: include_archived, in: query, schema: { type: boolean, default: false } }
- - name: status
- in: query
- style: form
- explode: true
- schema:
- type: array
- items: { type: string, enum: [active, paused] }
- description: Repeat the parameter to filter by one or more Deployment statuses.
- - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
- - { name: page, in: query, schema: { type: string } }
+ - $ref: '#/components/parameters/OpenAIBeta'
responses:
'200':
- description: Deployment list
- content:
- application/json:
- schema:
- type: object
- required: [data]
- properties:
- data:
- type: array
- items:
- $ref: '#/components/schemas/Deployment'
- next_page: { type: string, description: Opaque cursor included only when another page is available. }
- '400': { description: Invalid status filter or page cursor, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: Deployment list could not be loaded, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
-
- /v1/deployments/{id}:
- get:
- operationId: getDeployment
- tags: [Schedules]
- summary: Get a Schedule
+ description: 'Turn'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SessionTurn' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/sessions/{session_id}/cancel:
+ parameters:
+ - $ref: '#/components/parameters/SessionIdPath'
+ post:
+ operationId: cancelSession
+ tags: [Sessions]
+ summary: Cancel session turn
+ description: 'Request cancellation of the in-progress Turn. The body may be empty or {}. Idempotency-Key is required. An idle Session also returns 202 with accepted true and nothing changes.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/DeploymentId'
+ - $ref: '#/components/parameters/IdempotencyKeyRequired'
+ requestBody:
+ required: false
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/EmptyObject' }
+ responses:
+ '202':
+ description: 'Cancellation accepted'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SessionCancelAccepted' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/sessions/{session_id}/archive:
+ parameters:
+ - $ref: '#/components/parameters/SessionIdPath'
+ post:
+ operationId: archiveSession
+ tags: [Sessions]
+ summary: Archive session
+ description: 'Archive session with compare-and-set on row_version.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SessionRowVersionRequest' }
responses:
'200':
- description: Deployment details
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Deployment'
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Deployment not found, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '500': { description: Deployment detail could not be loaded, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- patch:
- operationId: updateDeployment
- tags: [Schedules]
- summary: Update a Schedule
- parameters:
- - $ref: '#/components/parameters/DeploymentId'
+ description: 'Session'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/Session' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/sessions/{session_id}/unarchive:
+ parameters:
+ - $ref: '#/components/parameters/SessionIdPath'
+ post:
+ operationId: unarchiveSession
+ tags: [Sessions]
+ summary: Unarchive session
+ description: 'Unarchive session with compare-and-set on row_version.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
requestBody:
required: true
content:
application/json:
- schema:
- $ref: '#/components/schemas/UpdateDeploymentRequest'
+ schema: { $ref: '#/components/schemas/SessionRowVersionRequest' }
+ responses:
+ '200':
+ description: 'Session'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/Session' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/sessions/{session_id}/reconnect:
+ parameters:
+ - $ref: '#/components/parameters/SessionIdPath'
+ post:
+ operationId: reconnectSession
+ tags: [Sessions]
+ summary: Reconnect session
+ description: 'Return the current Session and the product stream URL to reopen. Uses the agents.read scope.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
responses:
'200':
- description: Deployment updated
+ description: 'Reconnect instructions'
content:
application/json:
- schema:
- $ref: '#/components/schemas/Deployment'
- '400': { description: 'Invalid field, events, schedule, timeout, binding, Agent config, or notification target', content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: 'Deployment or Skill not found', content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '409': { description: 'Archived, active, running, or version-conflicted Deployment', content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '422': { description: Reserved field supplied or Runtime/Skill is incompatible, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '500': { description: Deployment update or detail reload failed, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
+ schema: { $ref: '#/components/schemas/SessionReconnect' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/sessions/{session_id}/files:
+ parameters:
+ - $ref: '#/components/parameters/SessionIdPath'
post:
- operationId: updateDeploymentClaude
- tags: [Schedules]
- summary: Update a Schedule (compatibility alias)
+ operationId: uploadSessionFile
+ tags: [Sessions]
+ summary: Upload session file
+ description: 'Upload one attachment (multipart field file, maximum 5 MiB) into the Session workspace. Idempotency-Key is required. Supported extensions: .txt, .md, .csv, .log, .json, .pdf, .png, .jpg, .jpeg, .webp. A failed transfer still returns 201 with status failed.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/DeploymentId'
+ - $ref: '#/components/parameters/IdempotencyKeyRequired'
requestBody:
required: true
content:
- application/json:
- schema:
- $ref: '#/components/schemas/UpdateDeploymentRequest'
+ multipart/form-data:
+ schema: { $ref: '#/components/schemas/SessionFileUpload' }
responses:
- '200':
- description: Deployment updated
+ '201':
+ description: 'Attachment record'
content:
application/json:
- schema:
- $ref: '#/components/schemas/Deployment'
- '400': { description: 'Invalid field, events, schedule, timeout, binding, Agent config, or notification target', content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: 'Deployment or Skill not found', content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '409': { description: 'Archived, active, running, or version-conflicted Deployment', content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '422': { description: Reserved field supplied or Runtime/Skill is incompatible, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '500': { description: Deployment update or detail reload failed, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- delete:
- operationId: deleteDeployment
- tags: [Schedules]
- summary: Delete a Schedule
- description: Permanently delete an eligible Deployment.
+ schema: { $ref: '#/components/schemas/SessionFile' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ get:
+ operationId: listSessionFiles
+ tags: [Sessions]
+ summary: List session files
+ description: 'List attachments with offset pagination (limit 1-100).'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/DeploymentId'
+ - $ref: '#/components/parameters/OffsetLimit'
+ - $ref: '#/components/parameters/Offset'
responses:
'200':
- description: Deployment deleted
+ description: 'Attachment page'
content:
application/json:
- schema:
- type: object
- required: [id, deleted]
- properties:
- id: { type: string }
- deleted: { type: boolean, const: true }
- '404': { description: Deployment not found, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '409': { description: Deployment cannot be deleted from its current state, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: Deployment deletion failed, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
-
- /v1/deployments/{id}/run:
+ schema: { $ref: '#/components/schemas/SessionFileList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/sessions/{session_id}/files/{file_id}:
+ parameters:
+ - $ref: '#/components/parameters/SessionIdPath'
+ - $ref: '#/components/parameters/FileIdPath'
+ delete:
+ operationId: deleteSessionFile
+ tags: [Sessions]
+ summary: Delete session file
+ description: 'Delete an attachment. Pending uploads and archived Sessions return 409.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ responses:
+ '200':
+ description: 'Attachment deleted'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SessionFileDeleted' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/agents/sessions/{session_id}/files/{file_id}/content:
+ parameters:
+ - $ref: '#/components/parameters/SessionIdPath'
+ - $ref: '#/components/parameters/FileIdPath'
+ get:
+ operationId: downloadSessionFile
+ tags: [Sessions]
+ summary: Download session file
+ description: 'Download the original bytes of a ready attachment.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ responses:
+ '200':
+ description: 'Attachment bytes with the stored MIME type'
+ content:
+ application/octet-stream:
+ schema: { type: string, format: binary }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ # --- Services -----------------------------------------------
+ /v1/services:
post:
- operationId: runDeployment
- tags: [Schedules]
- summary: Trigger a Schedule (compatibility alias)
+ operationId: createService
+ tags: [Services]
+ summary: Create service
+ description: 'Create a Service bound to a saved Agent version. Idempotency-Key is required. Omitting agent_version pins the current Agent version. schedule and model_provider are rejected.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/DeploymentId'
- - name: Idempotency-Key
- in: header
- required: false
- schema: { type: string }
+ - $ref: '#/components/parameters/IdempotencyKeyRequired'
requestBody:
- required: false
+ required: true
content:
application/json:
- schema: { $ref: '#/components/schemas/EmptyObjectRequest' }
+ schema: { $ref: '#/components/schemas/ServiceCreateRequest' }
responses:
- '200':
- description: Existing or newly created DeploymentRun result
+ '201':
+ description: 'Service created'
content:
application/json:
- schema:
- $ref: '#/components/schemas/DeploymentRun'
- '400': { description: 'Request body is not absent or an empty JSON object, or stored initial events are invalid', content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Deployment or a required resolved binding was not found, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '409': { description: Deployment is archived or Idempotency-Key conflicts with a different trigger, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '500': { description: 'Trigger, Session, or execution binding could not be created', content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '503': { description: DeploymentRun or execution runtime service is unavailable, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
-
- /v1/deployments/{id}/pause:
- post:
- operationId: pauseDeployment
- tags: [Schedules]
- summary: Pause scheduled triggers
+ schema: { $ref: '#/components/schemas/RunSourceDefinition' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ get:
+ operationId: listServices
+ tags: [Services]
+ summary: List services
+ description: 'List definitions with offset pagination (limit 1-100).'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/DeploymentId'
+ - $ref: '#/components/parameters/RunSourceStatusFilter'
+ - { name: agent_id, in: query, required: false, schema: { type: string }, description: Only return definitions bound to this Agent. }
+ - $ref: '#/components/parameters/OffsetLimit'
+ - $ref: '#/components/parameters/Offset'
responses:
'200':
- description: Deployment paused
+ description: 'Service page'
content:
application/json:
- schema: { $ref: '#/components/schemas/Deployment' }
- '404': { description: Deployment not found, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '409': { description: Deployment cannot be paused from its current state, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: Deployment pause failed, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
-
- /v1/deployments/{id}/unpause:
- post:
- operationId: unpauseDeployment
- tags: [Schedules]
- summary: Resume scheduled triggers
- parameters:
- - $ref: '#/components/parameters/DeploymentId'
+ schema: { $ref: '#/components/schemas/RunSourceDefinitionList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/services/{service_id}:
+ parameters:
+ - $ref: '#/components/parameters/ServiceIdPath'
+ get:
+ operationId: getService
+ tags: [Services]
+ summary: Get service
+ description: 'Read a Service.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
responses:
'200':
- description: Deployment resumed
+ description: 'Service'
content:
application/json:
- schema: { $ref: '#/components/schemas/Deployment' }
- '404': { description: Deployment not found, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '409': { description: Deployment cannot be resumed from its current state, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: Deployment resume failed, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
-
- /v1/deployments/{id}/archive:
+ schema: { $ref: '#/components/schemas/RunSourceDefinition' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ patch:
+ operationId: updateService
+ tags: [Services]
+ summary: Update service
+ description: 'Compare-and-set update of name, description, agent_id, agent_version, or input. Explicit null values are rejected. Changing agent_id without agent_version pins that Agent''s current version.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/RunSourcePatch' }
+ responses:
+ '200':
+ description: 'Service'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/RunSourceDefinition' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/services/{service_id}/pause:
+ parameters:
+ - $ref: '#/components/parameters/ServiceIdPath'
post:
- operationId: archiveDeployment
- tags: [Schedules]
- summary: Archive a Schedule
- parameters:
- - $ref: '#/components/parameters/DeploymentId'
+ operationId: pauseService
+ tags: [Services]
+ summary: Pause service
+ description: 'Pause the Service. row_version must match the current value.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/RowVersionRequest' }
responses:
'200':
- description: Deployment archived
- content:
- application/json:
- schema: { $ref: '#/components/schemas/Deployment' }
- '404': { description: Deployment not found, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '409': { description: Deployment cannot be archived from its current state, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: Deployment archive or reload failed, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
-
- /v1/deployments/{id}/notifications/feishu/test:
+ description: 'Service'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/RunSourceDefinition' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/services/{service_id}/resume:
+ parameters:
+ - $ref: '#/components/parameters/ServiceIdPath'
post:
- operationId: testDeploymentFeishuNotification
- tags: [Schedules]
- summary: Test the saved Feishu notification target
- description: |
- Sends a fixed SandBase test notification to the Feishu webhook already saved on this Deployment.
- The request body must be absent or an empty JSON object. This endpoint never accepts a webhook URL or custom message.
- parameters:
- - $ref: '#/components/parameters/DeploymentId'
+ operationId: resumeService
+ tags: [Services]
+ summary: Resume service
+ description: 'Resume a paused Service. row_version must match the current value.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
requestBody:
- required: false
+ required: true
content:
application/json:
- schema:
- type: object
- maxProperties: 0
- additionalProperties: false
+ schema: { $ref: '#/components/schemas/RowVersionRequest' }
responses:
'200':
- description: Test notification delivered
- content:
- application/json:
- schema:
- type: object
- required: [delivered]
- properties:
- delivered: { type: boolean, const: true }
- '400':
- description: Request body is not absent or an empty JSON object
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
- '401':
- description: Missing or invalid API key
- content:
- application/json:
- schema: { $ref: '#/components/schemas/TaskCostError' }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404':
- description: Deployment not found for the current organization
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
- '409':
- description: Deployment is archived or no Feishu webhook is configured
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
- '502':
- description: Feishu rejected the test notification or delivery failed
- content:
- application/json:
- schema: { $ref: '#/components/schemas/APIError' }
-
- /v1/deployments/{id}/runs:
+ description: 'Service'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/RunSourceDefinition' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/services/{service_id}/archive:
+ parameters:
+ - $ref: '#/components/parameters/ServiceIdPath'
post:
- operationId: triggerDeploymentRun
- tags: [Schedules]
- summary: Trigger a Schedule
- description: Creates one durable trigger record and attempts to create exactly one new Session.
- parameters:
- - $ref: '#/components/parameters/DeploymentId'
- - name: Idempotency-Key
- in: header
- required: false
- schema: { type: string }
+ operationId: archiveService
+ tags: [Services]
+ summary: Archive service
+ description: 'Archive the Service. Archived is terminal. row_version must match the current value.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
requestBody:
- required: false
+ required: true
content:
application/json:
- schema: { $ref: '#/components/schemas/EmptyObjectRequest' }
+ schema: { $ref: '#/components/schemas/RowVersionRequest' }
responses:
'200':
- description: Existing or newly created DeploymentRun result
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/DeploymentRun'
- '409':
- description: The same idempotency key was used with a different request
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/APIError'
- '400': { description: 'Request body is not absent or an empty JSON object, or stored initial events are invalid', content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Deployment or a required resolved binding was not found, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '500': { description: 'Trigger, Session, or execution binding could not be created', content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '503': { description: DeploymentRun or execution runtime service is unavailable, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- get:
- operationId: listDeploymentRuns
- tags: [Schedules]
- summary: List runs for a Schedule
+ description: 'Service'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/RunSourceDefinition' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/services/{service_id}/invoke:
+ parameters:
+ - $ref: '#/components/parameters/ServiceIdPath'
+ post:
+ operationId: invokeService
+ tags: [Services]
+ summary: Invoke service
+ description: 'Start a Run. Idempotency-Key is required; the same key and body return the original Run, the same key with a different body returns 409. Only one pending Run is allowed per Service.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/DeploymentId'
- - name: limit
- in: query
- schema: { type: integer, minimum: 1, maximum: 1000, default: 20 }
- description: Page size. Pass this parameter explicitly when following nested-list cursors.
- - name: page
- in: query
- schema: { type: string }
- description: Opaque cursor returned in a previous response's next_page field.
+ - $ref: '#/components/parameters/IdempotencyKeyRequired'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/AgentRunRequest' }
responses:
- '200':
- description: DeploymentRun list
- content:
- application/json:
- schema:
- type: object
- required: [data]
- properties:
- data:
- type: array
- items:
- $ref: '#/components/schemas/DeploymentRun'
- next_page:
- type: string
- description: Opaque cursor included when the current page reaches the requested limit.
- '400': { description: Invalid page cursor, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: DeploymentRun list could not be loaded, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '503': { description: DeploymentRun service is unavailable, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
-
- /v1/deployments/{id}/runs/{drun_id}:
+ '202':
+ description: 'Run accepted'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/AgentRun' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '429': { description: 'concurrency_limit: this resource already has a pending Run', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/services/{service_id}/runs:
+ parameters:
+ - $ref: '#/components/parameters/ServiceIdPath'
get:
- operationId: getDeploymentRun
- tags: [Schedules]
- summary: Get a run for a Schedule
+ operationId: listServiceRuns
+ tags: [Services]
+ summary: List service runs
+ description: 'List Runs with offset pagination (limit 1-100).'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/DeploymentId'
- - name: drun_id
- in: path
- required: true
- schema: { type: string, pattern: '^drun_' }
+ - $ref: '#/components/parameters/OffsetLimit'
+ - $ref: '#/components/parameters/Offset'
responses:
'200':
- description: DeploymentRun details
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/DeploymentRun'
- '409':
- description: The DeploymentRun is still pending and has not resolved to a Session creation result
+ description: 'Run page'
content:
application/json:
- schema:
- $ref: '#/components/schemas/APIError'
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: DeploymentRun not found or belongs to another Deployment, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '503': { description: DeploymentRun service is unavailable, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
-
- /v1/deployment_runs:
+ schema: { $ref: '#/components/schemas/AgentRunList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/services/{service_id}/runs/{run_id}:
+ parameters:
+ - $ref: '#/components/parameters/ServiceIdPath'
+ - $ref: '#/components/parameters/RunIdPath'
get:
- operationId: listAllDeploymentRuns
- tags: [Schedules]
- summary: List runs across all Schedules
+ operationId: getServiceRun
+ tags: [Services]
+ summary: Get service run
+ description: 'Read a Run. Reading a Service Run also advances its status from the linked Session.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ responses:
+ '200':
+ description: 'Run'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/AgentRun' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/services/{service_id}/runs/{run_id}/cancel:
+ parameters:
+ - $ref: '#/components/parameters/ServiceIdPath'
+ - $ref: '#/components/parameters/RunIdPath'
+ post:
+ operationId: cancelServiceRun
+ tags: [Services]
+ summary: Cancel service run
+ description: 'Request cancellation of a pending Run. Idempotency-Key is required and the body must be {}. A finished Run is returned unchanged (202). While the Run has no Session yet (session_id null) the call returns 409 conflict; while the Run input is still being delivered it returns 409 session_input_pending. Wait and retry with the same Idempotency-Key.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - name: deployment_id
- in: query
- schema: { type: string }
- - name: agent_id
- in: query
- schema: { type: string }
- - { name: 'created_at[gt]', in: query, schema: { type: string, format: date-time } }
- - { name: 'created_at[gte]', in: query, schema: { type: string, format: date-time } }
- - { name: 'created_at[lt]', in: query, schema: { type: string, format: date-time } }
- - { name: 'created_at[lte]', in: query, schema: { type: string, format: date-time } }
- - { name: created_at_gt, in: query, deprecated: true, description: 'Compatibility alias for created_at[gt].', schema: { type: string, format: date-time } }
- - { name: created_at_gte, in: query, deprecated: true, description: 'Compatibility alias for created_at[gte].', schema: { type: string, format: date-time } }
- - { name: created_at_lt, in: query, deprecated: true, description: 'Compatibility alias for created_at[lt].', schema: { type: string, format: date-time } }
- - { name: created_at_lte, in: query, deprecated: true, description: 'Compatibility alias for created_at[lte].', schema: { type: string, format: date-time } }
- - { name: has_error, in: query, schema: { type: boolean } }
- - name: trigger_type
- in: query
- style: form
- explode: true
- schema:
- type: array
- items: { type: string, enum: [schedule, manual] }
- description: Repeat the parameter, or use comma-separated values, to filter by trigger type.
- - name: status
- in: query
- description: Repeat to select one or more DeploymentRun creation outcomes. The current response object does not expose this internal status field.
- style: form
- explode: true
- schema:
- type: array
- items: { type: string, enum: [pending, succeeded, failed] }
- - name: limit
- in: query
- schema: { type: integer, minimum: 1, maximum: 1000, default: 20 }
- - name: page
- in: query
- schema: { type: string }
+ - $ref: '#/components/parameters/IdempotencyKeyRequired'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/EmptyObject' }
+ responses:
+ '202':
+ description: 'Cancellation accepted'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/AgentRun' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict (the Run has no Session yet, or Idempotency-Key reused with a different body) or session_input_pending (Run input still being delivered)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/services/{service_id}/runs/{run_id}/notifications:
+ parameters:
+ - $ref: '#/components/parameters/ServiceIdPath'
+ - $ref: '#/components/parameters/RunIdPath'
+ get:
+ operationId: listServiceRunNotifications
+ tags: [Services]
+ summary: List service run notifications
+ description: 'List notification deliveries for one Run.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
responses:
'200':
- description: DeploymentRun list
+ description: 'Deliveries'
content:
application/json:
- schema:
- type: object
- required: [data]
- properties:
- data:
- type: array
- items: { $ref: '#/components/schemas/DeploymentRun' }
- next_page: { type: string, description: Opaque cursor included when the current page reaches the requested limit. }
- '400': { description: 'Invalid filter, limit, timestamp, boolean, status, trigger type, or page cursor', content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: DeploymentRun list could not be loaded, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '503': { description: DeploymentRun service is unavailable, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
-
- /v1/deployment_runs/{id}:
+ schema: { $ref: '#/components/schemas/NotificationDeliveryList' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/services/{service_id}/notifications:
+ parameters:
+ - $ref: '#/components/parameters/ServiceIdPath'
get:
- operationId: getGlobalDeploymentRun
- tags: [Schedules]
- summary: Get a Schedule run by ID
- parameters:
- - name: id
- in: path
- required: true
- schema: { type: string, pattern: '^drun_' }
+ operationId: listServiceNotifications
+ tags: [Services]
+ summary: List service notification targets
+ description: 'List configured notification channels. Webhook URLs are never returned.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
responses:
'200':
- description: DeploymentRun details
+ description: 'Targets'
content:
application/json:
- schema:
- $ref: '#/components/schemas/DeploymentRun'
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: DeploymentRun not found, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '503': { description: DeploymentRun service is unavailable, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
-
- # --- Skills -----------------------------------------------------
- /v1/skills/files:
- post:
- operationId: uploadSkillFile
- tags: [Skills]
- summary: Upload a Skill bundle or preview image
- description: Archives are limited to 50 MB. JPEG, PNG, WebP, and GIF preview images are limited to 10 MB.
+ schema: { $ref: '#/components/schemas/NotificationTargetList' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/services/{service_id}/notifications/{channel}:
+ parameters:
+ - $ref: '#/components/parameters/ServiceIdPath'
+ - $ref: '#/components/parameters/NotificationChannelPath'
+ put:
+ operationId: putServiceNotification
+ tags: [Services]
+ summary: Set service notification target
+ description: 'Save a Feishu or Lark bot webhook. Archived definitions return 409.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
requestBody:
required: true
content:
- multipart/form-data:
- schema:
- type: object
- required: [file]
- properties:
- file: { type: string, format: binary }
- skill_id:
- type: string
- format: uuid
- description: Optional Skill ID used to organize the uploaded file path.
+ application/json:
+ schema: { $ref: '#/components/schemas/NotificationTargetRequest' }
responses:
- '201':
- description: Skill file uploaded
+ '200':
+ description: 'Targets'
content:
application/json:
- schema: { $ref: '#/components/schemas/SkillFile' }
- '400': { description: "Missing file, unsupported file type, or file over its category limit", content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '401': { description: Missing API identity or organization context, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: File could not be read or stored, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '503': { description: Persistent file storage is unavailable, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
-
- /v1/skills:
+ schema: { $ref: '#/components/schemas/NotificationTargetList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ delete:
+ operationId: deleteServiceNotification
+ tags: [Services]
+ summary: Delete service notification target
+ description: 'Remove the notification target for the channel.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ responses:
+ '200':
+ description: 'Target removed'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/DeletedFlag' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ # --- Schedules ----------------------------------------------
+ /v1/schedules:
post:
- operationId: createSkill
- tags: [Skills]
- summary: Create a Skill
+ operationId: createSchedule
+ tags: [Schedules]
+ summary: Create schedule
+ description: 'Create a cron Schedule bound to a saved Agent version. Idempotency-Key is required. input must be non-empty. The model credential (model_provider, or your API key when omitted) is stored encrypted and used for scheduled runs.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/IdempotencyKeyRequired'
requestBody:
required: true
content:
application/json:
- schema: { $ref: '#/components/schemas/CreateSkillRequest' }
+ schema: { $ref: '#/components/schemas/ScheduleCreateRequest' }
responses:
'201':
- description: Skill created
+ description: 'Schedule created'
content:
application/json:
- schema: { $ref: '#/components/schemas/SkillCreated' }
- '400': { description: Invalid Skill source or metadata, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing API identity or organization context, content: { application/json: { schema: { oneOf: [{ $ref: '#/components/schemas/TaskCostError' }, { $ref: '#/components/schemas/APIError' }] } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: Skill could not be created, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
+ schema: { $ref: '#/components/schemas/RunSourceDefinition' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
get:
- operationId: listSkills
- tags: [Skills]
- summary: List Skills
+ operationId: listSchedules
+ tags: [Schedules]
+ summary: List schedules
+ description: 'List definitions with offset pagination (limit 1-100).'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
- - { name: page_size, in: query, schema: { type: integer, minimum: 1, maximum: 1000, default: 20 } }
+ - $ref: '#/components/parameters/RunSourceStatusFilter'
+ - { name: agent_id, in: query, required: false, schema: { type: string }, description: Only return definitions bound to this Agent. }
+ - $ref: '#/components/parameters/OffsetLimit'
+ - $ref: '#/components/parameters/Offset'
responses:
'200':
- description: Skill list
+ description: 'Schedule page'
content:
application/json:
- schema:
- type: object
- required: [data, total, page, page_size]
- properties:
- data: { type: array, items: { $ref: '#/components/schemas/SkillListItem' } }
- total: { type: integer }
- page: { type: integer }
- page_size: { type: integer }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: Skill list could not be loaded, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
-
- /v1/skills/{id}:
+ schema: { $ref: '#/components/schemas/RunSourceDefinitionList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/schedules/{schedule_id}:
+ parameters:
+ - $ref: '#/components/parameters/ScheduleIdPath'
get:
- operationId: getSkill
- tags: [Skills]
- summary: Get a Skill
- parameters:
- - $ref: '#/components/parameters/SkillId'
+ operationId: getSchedule
+ tags: [Schedules]
+ summary: Get schedule
+ description: 'Read a Schedule.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
responses:
'200':
- description: Skill details
+ description: 'Schedule'
content:
application/json:
- schema: { $ref: '#/components/schemas/Skill' }
- '404': { description: Skill not found, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: Skill could not be loaded, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
+ schema: { $ref: '#/components/schemas/RunSourceDefinition' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ patch:
+ operationId: updateSchedule
+ tags: [Schedules]
+ summary: Update schedule
+ description: 'Compare-and-set update of name, description, agent_id, agent_version, or input. Explicit null values are rejected. Changing agent_id without agent_version pins that Agent''s current version.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/RunSourcePatch' }
+ responses:
+ '200':
+ description: 'Schedule'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/RunSourceDefinition' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/schedules/{schedule_id}/pause:
+ parameters:
+ - $ref: '#/components/parameters/ScheduleIdPath'
+ post:
+ operationId: pauseSchedule
+ tags: [Schedules]
+ summary: Pause schedule
+ description: 'Pause the Schedule. row_version must match the current value.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/RowVersionRequest' }
+ responses:
+ '200':
+ description: 'Schedule'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/RunSourceDefinition' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/schedules/{schedule_id}/resume:
+ parameters:
+ - $ref: '#/components/parameters/ScheduleIdPath'
+ post:
+ operationId: resumeSchedule
+ tags: [Schedules]
+ summary: Resume schedule
+ description: 'Resume a paused Schedule. row_version must match the current value.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/RowVersionRequest' }
+ responses:
+ '200':
+ description: 'Schedule'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/RunSourceDefinition' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/schedules/{schedule_id}/archive:
+ parameters:
+ - $ref: '#/components/parameters/ScheduleIdPath'
+ post:
+ operationId: archiveSchedule
+ tags: [Schedules]
+ summary: Archive schedule
+ description: 'Archive the Schedule. Archived is terminal. row_version must match the current value.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/RowVersionRequest' }
+ responses:
+ '200':
+ description: 'Schedule'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/RunSourceDefinition' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/schedules/{schedule_id}/timing:
+ parameters:
+ - $ref: '#/components/parameters/ScheduleIdPath'
put:
- operationId: updateSkill
- tags: [Skills]
- summary: Update a Skill
- description: Replacement-style update for display fields. Omitted name, description, and categories are cleared; omitted source URL, preview URLs, metadata, and unified schema are preserved.
+ operationId: updateScheduleTiming
+ tags: [Schedules]
+ summary: Update schedule timing
+ description: 'Replace cron and timezone. Omitting model_provider keeps the stored model credential.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/ScheduleTimingRequest' }
+ responses:
+ '200':
+ description: 'Schedule'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/RunSourceDefinition' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/schedules/{schedule_id}/runs:
+ parameters:
+ - $ref: '#/components/parameters/ScheduleIdPath'
+ post:
+ operationId: triggerSchedule
+ tags: [Schedules]
+ summary: Trigger schedule run
+ description: 'Start a manual Run now. Idempotency-Key is required. input, model, and model_provider override the saved values for this Run only. Only one pending Run is allowed per Schedule.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/SkillId'
+ - $ref: '#/components/parameters/IdempotencyKeyRequired'
requestBody:
required: true
content:
application/json:
- schema: { $ref: '#/components/schemas/UpdateSkillRequest' }
+ schema: { $ref: '#/components/schemas/AgentRunRequest' }
+ responses:
+ '202':
+ description: 'Run accepted'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/AgentRun' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '429': { description: 'concurrency_limit: this resource already has a pending Run', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ get:
+ operationId: listScheduleRuns
+ tags: [Schedules]
+ summary: List schedule runs
+ description: 'List Runs with offset pagination (limit 1-100).'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/OffsetLimit'
+ - $ref: '#/components/parameters/Offset'
responses:
'200':
- description: Skill updated
+ description: 'Run page'
content:
application/json:
- schema: { $ref: '#/components/schemas/Skill' }
- '404': { description: Skill not found, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '400': { description: 'Invalid JSON body, field length, or unified schema', content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: Skill could not be updated, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- delete:
- operationId: deleteSkill
- tags: [Skills]
- summary: Soft-delete a Skill
+ schema: { $ref: '#/components/schemas/AgentRunList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/schedules/{schedule_id}/runs/{run_id}:
+ parameters:
+ - $ref: '#/components/parameters/ScheduleIdPath'
+ - $ref: '#/components/parameters/RunIdPath'
+ get:
+ operationId: getScheduleRun
+ tags: [Schedules]
+ summary: Get schedule run
+ description: 'Read a Run. Schedule Run reads are read-only; status advances in the background.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ responses:
+ '200':
+ description: 'Run'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/AgentRun' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/schedules/{schedule_id}/runs/{run_id}/cancel:
+ parameters:
+ - $ref: '#/components/parameters/ScheduleIdPath'
+ - $ref: '#/components/parameters/RunIdPath'
+ post:
+ operationId: cancelScheduleRun
+ tags: [Schedules]
+ summary: Cancel schedule run
+ description: 'Request cancellation of a pending Run. Idempotency-Key is required and the body must be {}. A finished Run is returned unchanged (202). While the Run has no Session yet (session_id null) the call returns 409 conflict; while the Run input is still being delivered it returns 409 session_input_pending. Wait and retry with the same Idempotency-Key.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - $ref: '#/components/parameters/SkillId'
+ - $ref: '#/components/parameters/IdempotencyKeyRequired'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/EmptyObject' }
+ responses:
+ '202':
+ description: 'Cancellation accepted'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/AgentRun' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict (the Run has no Session yet, or Idempotency-Key reused with a different body) or session_input_pending (Run input still being delivered)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/schedules/{schedule_id}/runs/{run_id}/notifications:
+ parameters:
+ - $ref: '#/components/parameters/ScheduleIdPath'
+ - $ref: '#/components/parameters/RunIdPath'
+ get:
+ operationId: listScheduleRunNotifications
+ tags: [Schedules]
+ summary: List schedule run notifications
+ description: 'List notification deliveries for one Run.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
responses:
'200':
- description: Skill deleted
+ description: 'Deliveries'
content:
application/json:
- schema:
- type: object
- required: [id, deleted]
- properties:
- id: { type: string, format: uuid }
- deleted: { type: boolean, const: true }
- '404': { description: Skill not found, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: Skill could not be deleted, content: { application/json: { schema: { $ref: '#/components/schemas/APIError' } } } }
-
- # --- Credentials -----------------------------------------------
- /v1/credentials:
+ schema: { $ref: '#/components/schemas/NotificationDeliveryList' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/schedules/{schedule_id}/notifications:
+ parameters:
+ - $ref: '#/components/parameters/ScheduleIdPath'
+ get:
+ operationId: listScheduleNotifications
+ tags: [Schedules]
+ summary: List schedule notification targets
+ description: 'List configured notification channels. Webhook URLs are never returned.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ responses:
+ '200':
+ description: 'Targets'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/NotificationTargetList' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/schedules/{schedule_id}/notifications/{channel}:
+ parameters:
+ - $ref: '#/components/parameters/ScheduleIdPath'
+ - $ref: '#/components/parameters/NotificationChannelPath'
+ put:
+ operationId: putScheduleNotification
+ tags: [Schedules]
+ summary: Set schedule notification target
+ description: 'Save a Feishu or Lark bot webhook. Archived definitions return 409.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/NotificationTargetRequest' }
+ responses:
+ '200':
+ description: 'Targets'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/NotificationTargetList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ delete:
+ operationId: deleteScheduleNotification
+ tags: [Schedules]
+ summary: Delete schedule notification target
+ description: 'Remove the notification target for the channel.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ responses:
+ '200':
+ description: 'Target removed'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/DeletedFlag' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ # --- MCP Connections --------------------------------------------
+ /v1/mcp-connections:
post:
- operationId: createCredential
- tags: [Credentials]
- summary: Create credential
+ operationId: createMCPConnection
+ tags: [MCP Connections]
+ summary: Create MCP connection
+ description: 'Register a remote HTTPS MCP server, optionally authenticated with an active Secret whose server_url matches exactly. Idempotency-Key is required.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/IdempotencyKeyRequired'
requestBody:
required: true
content:
application/json:
- schema:
- $ref: '#/components/schemas/CreateCredentialRequest'
+ schema: { $ref: '#/components/schemas/MCPConnectionCreateRequest' }
responses:
'201':
- description: Credential created
+ description: 'Connection created'
content:
application/json:
- schema:
- $ref: '#/components/schemas/CredentialResponse'
- '400': { description: 'Missing, blank, oversized, unsupported, or otherwise invalid credential field', content: { application/json: { schema: { $ref: '#/components/schemas/LegacyResourceError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: Credential could not be encrypted or created, content: { application/json: { schema: { $ref: '#/components/schemas/LegacyResourceError' } } } }
+ schema: { $ref: '#/components/schemas/MCPConnection' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
get:
- operationId: listCredentials
- tags: [Credentials]
- summary: List credentials
+ operationId: listMCPConnections
+ tags: [MCP Connections]
+ summary: List MCP connections
+ description: 'List connections with offset pagination.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/OffsetLimit'
+ - $ref: '#/components/parameters/Offset'
responses:
'200':
- description: Credential list
+ description: 'Connection page'
content:
application/json:
- schema:
- $ref: '#/components/schemas/CredentialListResponse'
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '500': { description: Credential list could not be loaded, content: { application/json: { schema: { $ref: '#/components/schemas/LegacyResourceError' } } } }
- /v1/credentials/{id}:
+ schema: { $ref: '#/components/schemas/MCPConnectionList' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/mcp-connections/{connection_id}:
+ parameters:
+ - $ref: '#/components/parameters/MCPConnectionIdPath'
get:
- operationId: getCredential
- tags: [Credentials]
- summary: Get credential (masked value)
- parameters:
- - name: id
- in: path
- required: true
- schema: { type: string, pattern: '^sec_' }
+ operationId: getMCPConnection
+ tags: [MCP Connections]
+ summary: Get MCP connection
+ description: 'Read a connection.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
responses:
'200':
- description: Credential details (secret masked)
+ description: 'Connection'
content:
application/json:
- schema:
- $ref: '#/components/schemas/CredentialResponse'
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Credential not found or belongs to another organization, content: { application/json: { schema: { $ref: '#/components/schemas/LegacyResourceError' } } } }
- '500': { description: Credential could not be loaded, content: { application/json: { schema: { $ref: '#/components/schemas/LegacyResourceError' } } } }
- patch:
- operationId: updateCredential
- tags: [Credentials]
- summary: Update credential status or selection settings
+ schema: { $ref: '#/components/schemas/MCPConnection' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ put:
+ operationId: replaceMCPConnection
+ tags: [MCP Connections]
+ summary: Replace MCP connection
+ description: 'Replace name, server_url, and secret_id with compare-and-set on row_version. Omitting secret_id makes the connection anonymous. A disabled connection stays disabled. Idempotency-Key is required.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - name: id
- in: path
- required: true
- schema: { type: string, pattern: '^sec_' }
+ - $ref: '#/components/parameters/IdempotencyKeyRequired'
requestBody:
required: true
content:
application/json:
- schema:
- $ref: '#/components/schemas/UpdateCredentialRequest'
+ schema: { $ref: '#/components/schemas/MCPConnectionReplaceRequest' }
responses:
'200':
- description: Updated credential
+ description: 'Connection'
content:
application/json:
- schema:
- $ref: '#/components/schemas/CredentialResponse'
- '400': { description: 'Invalid JSON body, unsupported status or strategy, non-positive weight, or no mutable fields', content: { application/json: { schema: { $ref: '#/components/schemas/LegacyResourceError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Credential not found or belongs to another organization, content: { application/json: { schema: { $ref: '#/components/schemas/LegacyResourceError' } } } }
- '500': { description: Credential could not be updated, content: { application/json: { schema: { $ref: '#/components/schemas/LegacyResourceError' } } } }
- /v1/credentials/{id}/rotate:
+ schema: { $ref: '#/components/schemas/MCPConnection' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ delete:
+ operationId: deleteMCPConnection
+ tags: [MCP Connections]
+ summary: Disable MCP connection
+ description: 'Disable the connection. Idempotency-Key is required. New Sessions cannot use it and existing Sessions that froze it reject further input.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/IdempotencyKeyRequired'
+ responses:
+ '204': { description: 'Connection disabled' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/mcp-connections/{connection_id}/enable:
+ parameters:
+ - $ref: '#/components/parameters/MCPConnectionIdPath'
post:
- operationId: rotateCredential
- tags: [Credentials]
- summary: Rotate credential value
- description: Replace the encrypted value and masked hint, then reset failure_count to 0 and clear cooldown_until.
+ operationId: enableMCPConnection
+ tags: [MCP Connections]
+ summary: Enable MCP connection
+ description: 'Re-enable a disabled connection with compare-and-set on row_version. Idempotency-Key is required. A revoked Secret is never restored.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
parameters:
- - name: id
- in: path
- required: true
- schema: { type: string, pattern: '^sec_' }
+ - $ref: '#/components/parameters/IdempotencyKeyRequired'
requestBody:
required: true
content:
application/json:
- schema:
- type: object
- required: [value]
- properties:
- value:
- type: string
- minLength: 1
- maxLength: 8192
- writeOnly: true
- description: New plaintext value. Blank or whitespace-only values are rejected.
+ schema: { $ref: '#/components/schemas/RowVersionRequest' }
responses:
'200':
- description: Rotated credential
+ description: 'Connection'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/MCPConnection' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ # --- Secrets ----------------------------------------------------
+ /v1/secrets:
+ post:
+ operationId: createSecret
+ tags: [Secrets]
+ summary: Create secret
+ description: 'Store a write-only Bearer token for one MCP server URL. Idempotency-Key is required. The token is never returned.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/IdempotencyKeyRequired'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SecretCreateRequest' }
+ responses:
+ '201':
+ description: 'Secret created'
content:
application/json:
- schema:
- $ref: '#/components/schemas/CredentialResponse'
- '400': { description: 'Missing, blank, oversized, or invalid replacement value', content: { application/json: { schema: { $ref: '#/components/schemas/LegacyResourceError' } } } }
- '401': { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '402': { description: API key spending limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/TaskCostError' } } } }
- '403': { description: API key scope does not authorize this endpoint, content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyScopeError' } } } }
- '404': { description: Credential not found or belongs to another organization, content: { application/json: { schema: { $ref: '#/components/schemas/LegacyResourceError' } } } }
- '500': { description: Credential value could not be encrypted or stored, content: { application/json: { schema: { $ref: '#/components/schemas/LegacyResourceError' } } } }
-components:
- securitySchemes:
- BearerAuth:
- type: http
- scheme: bearer
- description: SandBase API key. Treat the complete credential as an opaque secret.
- AnthropicApiKey:
- type: apiKey
- in: header
- name: x-api-key
- description: "Supported only by `POST /v1/messages` for Anthropic SDK compatibility"
- GoogleApiKey:
- type: apiKey
- in: header
- name: x-goog-api-key
- description: Supported by Google Gemini-compatible endpoints. Takes precedence over the other key locations.
- GoogleQueryApiKey:
- type: apiKey
- in: query
- name: key
- description: Google SDK compatibility fallback. Prefer x-goog-api-key or Authorization because URLs can be logged.
-
- parameters:
- GeminiModel:
- name: model
- in: path
- required: true
- schema: { type: string, minLength: 1 }
- description: Bare Gemini model name, such as gemini-3.5-flash. SandBase adds the google/ catalog prefix.
- EndpointId:
- name: id
- in: path
- required: true
- schema:
- type: string
- description: Service ID (`ep_` prefix)
- AgentId:
- name: id
- in: path
- required: true
- schema:
- type: string
- pattern: '^agent_'
- description: Agent ID
- SessionId:
- name: id
- in: path
- required: true
- schema:
- type: string
- description: Session ID
- DeploymentId:
- name: id
- in: path
- required: true
- schema:
- type: string
- pattern: '^depl_'
- description: Deployment ID
- SkillId:
- name: id
- in: path
- required: true
- schema:
- type: string
- format: uuid
- description: Skill UUID
- schemas:
- # --- Skills ---------------------------------------------------
- SkillFile:
- type: object
- required: [url, key, filename, size, content_type]
- properties:
- url: { type: string, format: uri }
- key: { type: string }
- filename: { type: string }
- size: { type: integer }
- content_type: { type: string }
-
- CreateSkillRequest:
- type: object
- required: [name]
- properties:
- name: { type: string, minLength: 1, maxLength: 100, pattern: '.*\S.*' }
- description: { type: [string, 'null'], maxLength: 1000 }
- categories: { type: [array, 'null'], items: { type: string } }
- skill_file_url: { type: string, minLength: 1, pattern: '.*\S.*' }
- git_url: { type: string, minLength: 1, pattern: '.*\S.*' }
- preview_image_urls: { type: [array, 'null'], items: { type: string } }
- metadata: { type: [object, 'null'], additionalProperties: true }
- unified_schema: { type: [object, 'null'], additionalProperties: true }
- anyOf:
- - required: [skill_file_url]
- - required: [git_url]
-
- UpdateSkillRequest:
- type: object
- description: Omitted name, description, and categories are cleared. Omitted preview_image_urls, skill_file_url, metadata, and unified_schema preserve their stored values.
- properties:
- name: { type: [string, 'null'], maxLength: 100, description: 'New display name. Omitted, null, or blank clears display_name; the immutable full Skill ref in name is unchanged.' }
- description: { type: [string, 'null'], maxLength: 1000, description: Omitted or null clears the description. }
- categories: { type: [array, 'null'], items: { type: string }, description: 'Omitted or null clears categories; the detail response normalizes the result to an empty array.' }
- skill_file_url: { type: [string, 'null'], description: 'A non-blank value replaces the bundle URL; omitted, null, or blank preserves it.' }
- preview_image_urls: { type: [array, 'null'], items: { type: string }, description: 'Omitted or null preserves previews; an empty array clears them.' }
- metadata: { type: [object, 'null'], additionalProperties: true, description: 'Supplied non-reserved keys merge into stored metadata; omitted or null preserves them.' }
- unified_schema: { type: [object, 'null'], additionalProperties: true, description: 'Supplied value replaces the stored schema after validation; omitted or null preserves it.' }
-
- SkillCreated:
- type: object
- required: [id, name, display_name, vendor_slug, plugin_slug, icon_url, preview_urls, created_at]
- properties:
- id: { type: string, format: uuid }
- name: { type: string, description: Full vendor_slug/plugin_slug reference. }
- display_name: { type: string }
- vendor_slug: { type: string }
- plugin_slug: { type: string }
- icon_url: { type: string }
- preview_urls: { type: array, items: { type: string } }
- created_at: { type: string, format: date-time }
-
- Skill:
- type: object
- required: [id, name, display_name, vendor_slug, plugin_slug, description, categories, icon_url, preview_urls, skill_file_url, git_url, created_at, updated_at]
- properties:
- id: { type: string, format: uuid }
- name: { type: string, description: Full vendor_slug/plugin_slug reference. }
- display_name: { type: string }
- vendor_slug: { type: string }
- plugin_slug: { type: string }
- description: { type: string }
- categories: { type: array, items: { type: string } }
- icon_url: { type: string }
- preview_urls: { type: array, items: { type: string } }
- skill_file_url: { type: string }
- git_url: { type: string }
- created_at: { type: string, format: date-time }
- updated_at: { type: string, format: date-time }
-
- SkillListItem:
- type: object
- required: [id, name, display_name, vendor_slug, plugin_slug, description, icon_url, preview_urls, created_at, updated_at]
- properties:
- id: { type: string, format: uuid }
- name: { type: string, description: Full vendor_slug/plugin_slug reference. }
- display_name: { type: string }
- vendor_slug: { type: string }
- plugin_slug: { type: string }
- description: { type: string }
- icon_url: { type: string }
- preview_urls: { type: array, items: { type: string } }
- created_at: { type: string, format: date-time }
- updated_at: { type: string, format: date-time }
+ schema: { $ref: '#/components/schemas/Secret' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ get:
+ operationId: listSecrets
+ tags: [Secrets]
+ summary: List secrets
+ description: 'List Secret metadata with offset pagination.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/OffsetLimit'
+ - $ref: '#/components/parameters/Offset'
+ responses:
+ '200':
+ description: 'Secret page'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SecretList' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/secrets/{secret_id}:
+ parameters:
+ - $ref: '#/components/parameters/SecretIdPath'
+ get:
+ operationId: getSecret
+ tags: [Secrets]
+ summary: Get secret
+ description: 'Read Secret metadata. The token is never returned.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ responses:
+ '200':
+ description: 'Secret'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/Secret' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ delete:
+ operationId: deleteSecret
+ tags: [Secrets]
+ summary: Revoke secret
+ description: 'Revoke the Secret. Idempotency-Key is required. Connections that use it stop working for new Sessions and further input.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/IdempotencyKeyRequired'
+ responses:
+ '204': { description: 'Secret revoked' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/secrets/{secret_id}/rotate:
+ parameters:
+ - $ref: '#/components/parameters/SecretIdPath'
+ post:
+ operationId: rotateSecret
+ tags: [Secrets]
+ summary: Rotate secret
+ description: 'Replace the token for the same server_url with compare-and-set on row_version. Idempotency-Key is required. Only new Sessions use the new token.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/IdempotencyKeyRequired'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SecretRotateRequest' }
+ responses:
+ '201':
+ description: 'Secret'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/Secret' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ # --- Skills -----------------------------------------------------
+ /v1/skills:
+ post:
+ operationId: createSkill
+ tags: [Skills]
+ summary: Create skill
+ description: 'Upload a ZIP bundle (one files part, maximum 5 MiB) containing a SKILL.md with name and description frontmatter. Creates version 1. Idempotency-Key is optional.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/IdempotencyKeyOptional'
+ requestBody:
+ required: true
+ content:
+ multipart/form-data:
+ schema: { $ref: '#/components/schemas/SkillUploadForm' }
+ responses:
+ '201':
+ description: 'Skill created'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/Skill' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ get:
+ operationId: listSkills
+ tags: [Skills]
+ summary: List skills
+ description: 'List organization Skills with cursor pagination (limit 0-100).'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/SkillLimit'
+ - $ref: '#/components/parameters/AgentsAfter'
+ - $ref: '#/components/parameters/AgentsOrder'
+ responses:
+ '200':
+ description: 'Skill page'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SkillList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/skills/catalog:
+ get:
+ operationId: listSkillCatalog
+ tags: [Skills]
+ summary: List public skill catalog
+ description: 'List public Skills.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/SkillLimit'
+ - $ref: '#/components/parameters/AgentsAfter'
+ - $ref: '#/components/parameters/AgentsOrder'
+ responses:
+ '200':
+ description: 'Skill page'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SkillList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/skills/catalog/{skill_id}:
+ parameters:
+ - $ref: '#/components/parameters/SkillIdPath'
+ get:
+ operationId: getSkillCatalogEntry
+ tags: [Skills]
+ summary: Get public skill
+ description: 'Read one public Skill.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ responses:
+ '200':
+ description: 'Skill'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/Skill' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/skills/catalog/{skill_id}/versions:
+ parameters:
+ - $ref: '#/components/parameters/SkillIdPath'
+ get:
+ operationId: listSkillCatalogVersions
+ tags: [Skills]
+ summary: List public skill versions
+ description: 'List versions of a public Skill.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/SkillLimit'
+ - $ref: '#/components/parameters/AgentsAfter'
+ - $ref: '#/components/parameters/AgentsOrder'
+ responses:
+ '200':
+ description: 'Version page'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SkillVersionList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/skills/catalog/{skill_id}/versions/{version}:
+ parameters:
+ - $ref: '#/components/parameters/SkillIdPath'
+ - $ref: '#/components/parameters/SkillVersionPath'
+ get:
+ operationId: getSkillCatalogVersion
+ tags: [Skills]
+ summary: Get public skill version
+ description: 'Read one version of a public Skill.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ responses:
+ '200':
+ description: 'Version'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SkillVersion' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/skills/catalog/{skill_id}/versions/{version}/content:
+ parameters:
+ - $ref: '#/components/parameters/SkillIdPath'
+ - $ref: '#/components/parameters/SkillVersionPath'
+ get:
+ operationId: downloadSkillCatalogVersion
+ tags: [Skills]
+ summary: Download public skill version
+ description: 'Download the ZIP bundle of a public Skill version.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ responses:
+ '200':
+ description: 'Skill ZIP bundle'
+ content:
+ application/zip:
+ schema: { type: string, format: binary }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/skills/{skill_id}:
+ parameters:
+ - $ref: '#/components/parameters/SkillIdPath'
+ get:
+ operationId: getSkill
+ tags: [Skills]
+ summary: Get skill
+ description: 'Read a Skill.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ responses:
+ '200':
+ description: 'Skill'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/Skill' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ post:
+ operationId: updateSkill
+ tags: [Skills]
+ summary: Update skill
+ description: 'Change default_version, status, or metadata. row_version is required when metadata is supplied; otherwise the current value is used.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SkillUpdateRequest' }
+ responses:
+ '200':
+ description: 'Skill'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/Skill' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ delete:
+ operationId: deleteSkill
+ tags: [Skills]
+ summary: Delete skill
+ description: 'Delete a Skill. A Skill referenced by any Agent version returns 409.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ responses:
+ '200':
+ description: 'Skill deleted'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SkillDeleted' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/skills/{skill_id}/content:
+ parameters:
+ - $ref: '#/components/parameters/SkillIdPath'
+ get:
+ operationId: downloadSkill
+ tags: [Skills]
+ summary: Download skill
+ description: 'Download the ZIP bundle of the default version.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ responses:
+ '200':
+ description: 'Skill ZIP bundle'
+ content:
+ application/zip:
+ schema: { type: string, format: binary }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/skills/{skill_id}/versions:
+ parameters:
+ - $ref: '#/components/parameters/SkillIdPath'
+ get:
+ operationId: listSkillVersions
+ tags: [Skills]
+ summary: List skill versions
+ description: 'List Skill versions with cursor pagination (limit 0-100).'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/SkillLimit'
+ - $ref: '#/components/parameters/AgentsAfter'
+ - $ref: '#/components/parameters/AgentsOrder'
+ responses:
+ '200':
+ description: 'Version page'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SkillVersionList' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ post:
+ operationId: createSkillVersion
+ tags: [Skills]
+ summary: Upload skill version
+ description: 'Upload a new ZIP bundle as the next version. Set default=true to make it the default version. Idempotency-Key is optional.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ parameters:
+ - $ref: '#/components/parameters/IdempotencyKeyOptional'
+ requestBody:
+ required: true
+ content:
+ multipart/form-data:
+ schema: { $ref: '#/components/schemas/SkillVersionUploadForm' }
+ responses:
+ '200':
+ description: 'Version created'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SkillVersion' }
+ '400': { description: 'invalid_request (malformed JSON, unknown fields, failed validation, or an unsupported option)', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '409': { description: 'conflict: stale row_version, lifecycle state, or Idempotency-Key reused with a different body', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '502': { description: 'executor_rejected or executor_contract_error from the execution layer', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/skills/{skill_id}/versions/{version}:
+ parameters:
+ - $ref: '#/components/parameters/SkillIdPath'
+ - $ref: '#/components/parameters/SkillVersionPath'
+ get:
+ operationId: getSkillVersion
+ tags: [Skills]
+ summary: Get skill version
+ description: 'Read one Skill version. Version identifiers are strings.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ responses:
+ '200':
+ description: 'Version'
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SkillVersion' }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ /v1/skills/{skill_id}/versions/{version}/content:
+ parameters:
+ - $ref: '#/components/parameters/SkillIdPath'
+ - $ref: '#/components/parameters/SkillVersionPath'
+ get:
+ operationId: downloadSkillVersion
+ tags: [Skills]
+ summary: Download skill version
+ description: 'Download the ZIP bundle of one version.'
+ security:
+ - BearerAuth: []
+ - AgentsApiKey: []
+ responses:
+ '200':
+ description: 'Skill ZIP bundle'
+ content:
+ application/zip:
+ schema: { type: string, format: binary }
+ '401': { description: 'unauthorized: missing, malformed, or conflicting API key headers', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '402': { description: 'spending_limit_exceeded: API key spending limit or organization balance exhausted', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '403': { description: 'insufficient_scope (scoped key) or org_disabled', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '404': { description: 'not_found: the resource does not exist in this organization', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+ '503': { description: 'authorization_unavailable, admission_unavailable, executor_unavailable, storage_unavailable, or internal_error', content: { application/json: { schema: { $ref: '#/components/schemas/AgentsError' } } } }
+components:
+ securitySchemes:
+ BearerAuth:
+ type: http
+ scheme: bearer
+ description: SandBase API key. Treat the complete credential as an opaque secret.
+ AnthropicApiKey:
+ type: apiKey
+ in: header
+ name: x-api-key
+ description: "Supported only by `POST /v1/messages` for Anthropic SDK compatibility"
+ GoogleApiKey:
+ type: apiKey
+ in: header
+ name: x-goog-api-key
+ description: Supported by Google Gemini-compatible endpoints. Takes precedence over the other key locations.
+ GoogleQueryApiKey:
+ type: apiKey
+ in: query
+ name: key
+ description: Google SDK compatibility fallback. Prefer x-goog-api-key or Authorization because URLs can be logged.
+ AgentsApiKey:
+ type: apiKey
+ in: header
+ name: X-API-Key
+ description: Accepted only by Agents platform resources (/v1/agents*, /v1/services*, /v1/schedules*, /v1/mcp-connections*, /v1/secrets*, /v1/skills*). If Authorization is also sent, both must carry the same key.
+ parameters:
+ AgentIdPath:
+ name: agent_id
+ in: path
+ required: true
+ schema: { type: string }
+ description: Agent ID (`agt_` prefix).
+ AgentVersionPath:
+ name: version
+ in: path
+ required: true
+ schema: { type: integer, minimum: 1 }
+ description: Agent version number.
+ SessionIdPath:
+ name: session_id
+ in: path
+ required: true
+ schema: { type: string }
+ description: Session ID (`ses_` prefix).
+ TurnIdPath:
+ name: turn_id
+ in: path
+ required: true
+ schema: { type: string }
+ description: Turn ID from the turns list.
+ FileIdPath:
+ name: file_id
+ in: path
+ required: true
+ schema: { type: string }
+ description: Attachment ID (`fil_` prefix).
+ ServiceIdPath:
+ name: service_id
+ in: path
+ required: true
+ schema: { type: string }
+ description: Service ID (`svc_` prefix).
+ ScheduleIdPath:
+ name: schedule_id
+ in: path
+ required: true
+ schema: { type: string }
+ description: Schedule ID (`sch_` prefix).
+ RunIdPath:
+ name: run_id
+ in: path
+ required: true
+ schema: { type: string }
+ description: Run ID returned by invoke, trigger, or the runs list.
+ NotificationChannelPath:
+ name: channel
+ in: path
+ required: true
+ schema: { type: string, enum: [feishu] }
+ description: Notification channel. Only feishu is supported.
+ MCPConnectionIdPath:
+ name: connection_id
+ in: path
+ required: true
+ schema: { type: string }
+ description: MCP connection ID (`mcp_` prefix).
+ SecretIdPath:
+ name: secret_id
+ in: path
+ required: true
+ schema: { type: string }
+ description: Secret ID (`sec_` prefix).
+ SkillIdPath:
+ name: skill_id
+ in: path
+ required: true
+ schema: { type: string }
+ description: Skill ID (`skl_` prefix).
+ SkillVersionPath:
+ name: version
+ in: path
+ required: true
+ schema: { type: string }
+ description: Skill version identifier, such as "1".
+ IdempotencyKeyRequired:
+ name: Idempotency-Key
+ in: header
+ required: true
+ schema: { type: string, minLength: 1, maxLength: 128 }
+ description: Required. 1-128 characters without leading or trailing whitespace. Reuse the same key and body when retrying.
+ IdempotencyKeyOptional:
+ name: Idempotency-Key
+ in: header
+ required: false
+ schema: { type: string, minLength: 1, maxLength: 128 }
+ description: Optional. Send a stable key so retries do not create duplicates.
+ OpenAIBeta:
+ name: OpenAI-Beta
+ in: header
+ required: false
+ schema: { type: string, enum: ['agents=v1'] }
+ description: Optional OpenAI SDK header. When present it must be agents=v1.
+ AgentsLimit:
+ name: limit
+ in: query
+ required: false
+ schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
+ description: Page size. Values outside 1-100 return 400.
+ SkillLimit:
+ name: limit
+ in: query
+ required: false
+ schema: { type: integer, minimum: 0, maximum: 100, default: 20 }
+ description: Page size. Values outside 0-100 return 400.
+ AgentsAfter:
+ name: after
+ in: query
+ required: false
+ schema: { type: string }
+ description: Cursor. Pass last_id from the previous page.
+ AgentsOrder:
+ name: order
+ in: query
+ required: false
+ schema: { type: string, enum: [asc, desc], default: desc }
+ description: Sort order by creation time.
+ OffsetLimit:
+ name: limit
+ in: query
+ required: false
+ schema: { type: integer, minimum: 1, default: 20 }
+ description: Page size. Defaults to 20; non-numeric or non-positive values fall back to 20. Service, Schedule, Run, and attachment lists return 400 above 100; other offset lists cap the value at 200.
+ Offset:
+ name: offset
+ in: query
+ required: false
+ schema: { type: integer, minimum: 0, default: 0 }
+ description: Number of records to skip.
+ AgentStatusFilter:
+ name: status
+ in: query
+ required: false
+ schema: { type: string, enum: [active, archived, all], default: active }
+ description: Lifecycle filter. Other values return 400.
+ SessionLifecycleFilter:
+ name: lifecycle_status
+ in: query
+ required: false
+ schema: { type: string, enum: [active, archived, all], default: active }
+ description: Lifecycle filter. Other values return 400.
+ RunSourceStatusFilter:
+ name: status
+ in: query
+ required: false
+ schema: { type: string, enum: [active, paused, archived, all], default: active }
+ description: Lifecycle filter. Other values return 400.
+ GeminiModel:
+ name: model
+ in: path
+ required: true
+ schema: { type: string, minLength: 1 }
+ description: Bare Gemini model name, such as gemini-3.5-flash. SandBase adds the google/ catalog prefix.
+ schemas:
# --- Account --------------------------------------------------
AccountBalance:
type: object
@@ -4521,1190 +5181,732 @@ components:
required: [message, type, code, param]
properties:
message: { type: string }
- type: { type: string }
- code: { type: ['null', string] }
- param: { type: ['null', string] }
-
- APIKeyScopeError:
- type: object
- required: [error]
- properties:
- error:
- type: object
- required: [code, message, request_id]
- properties:
- code: { type: string, const: insufficient_scope }
- message: { type: string }
- request_id: { type: string }
-
- LegacyResourceError:
- type: object
- required: [code, message]
- properties:
- code: { type: integer }
- message: { type: string }
-
- # --- Services (Endpoint resources) -----------------------------
- CreateDeclarativeEndpointRequest:
- type: object
- additionalProperties: false
- required: [name, runtime, protocols]
- properties:
- name:
- type: string
- maxLength: 200
- runtime:
- type: string
- description: Enabled runtime profile. This release supports hermes.
- example: hermes
- description:
- type: string
- model:
- oneOf:
- - type: string
- - type: object
- additionalProperties: true
- system:
- type: string
- skills:
- type: [array, 'null']
- maxItems: 20
- description: |
- Full vendor_slug/plugin_slug references. Hermes runtime identity and installation lookup use each
- bundle-root SKILL.md frontmatter name; the plugin slug and runtime name may differ.
- items:
- type: string
- pattern: '^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$'
- tools:
- type: [array, 'null']
- maxItems: 1
- items:
- $ref: '#/components/schemas/EndpointToolset'
- protocols:
- type: array
- minItems: 1
- uniqueItems: true
- description: Explicit public invocation transports. Always send this field; public clients may enable REST, MCP, experimental ACP, or any supported combination.
- items:
- type: string
- enum: [rest, mcp, acp]
-
- CreateAdvancedEndpointRequest:
- type: object
- additionalProperties: false
- required: [name, agent_id, protocols]
- properties:
- name:
- type: string
- slug:
- type: string
- agent_id:
- type: string
- agent_version:
- type: integer
- minimum: 1
- protocols:
- type: array
- minItems: 1
- uniqueItems: true
- description: Explicit public invocation transports. Always send this field; public clients may enable REST, experimental ACP, or both.
- items:
- type: string
- enum: [rest, mcp, acp]
- config:
- type: object
- additionalProperties: true
- session_metadata:
- type: object
- additionalProperties: true
- description: Metadata applied when an invocation creates a new persistent Session.
- memory_config:
- type: object
- additionalProperties: true
- description: Reserved configuration that is stored with the Service but is not currently applied to Session execution.
- resource_config:
- type: object
- additionalProperties: true
- description: Reserved configuration that is stored with the Service but is not currently applied to Session execution.
- vault_config:
- type: object
- additionalProperties: true
- description: Reserved configuration that is stored with the Service but is not currently applied to Session execution.
-
- EndpointToolset:
- type: object
- additionalProperties: false
- required: [type]
- properties:
- type:
- type: string
- const: agent_toolset_20260401
-
- UpdateEndpointRequest:
- type: object
- minProperties: 1
- properties:
- name:
- type: string
- slug:
- type: string
- agent_id:
- type: string
- agent_version:
- type: integer
- minimum: 1
- expected_agent_version:
- type: integer
- minimum: 1
- description: Current bound version required when upgrading agent_version.
- status:
- type: string
- enum: [active, disabled]
- protocols:
- type: array
- minItems: 1
- uniqueItems: true
- items:
- type: string
- enum: [rest, mcp, acp]
- config: { type: object, additionalProperties: true }
- session_metadata:
- type: object
- additionalProperties: true
- description: Metadata applied when a later invocation creates a new persistent Session.
- memory_config:
- type: object
- additionalProperties: true
- description: Reserved configuration that is stored with the Service but is not currently applied to Session execution.
- resource_config:
- type: object
- additionalProperties: true
- description: Reserved configuration that is stored with the Service but is not currently applied to Session execution.
- vault_config:
- type: object
- additionalProperties: true
- description: Reserved configuration that is stored with the Service but is not currently applied to Session execution.
-
- Endpoint:
- type: object
- required: [id, type, name, slug, agent_id, agent_version, protocols, config, session_metadata, memory_config, resource_config, vault_config, status, store_status, creation_mode, run_url, acp_url, created_at, updated_at]
- properties:
- id:
- type: string
- example: ep_01abc
- type:
- type: string
- const: endpoint
- name:
- type: string
- slug:
- type: string
- agent_id:
- type: string
- agent_version:
- type: [integer, 'null']
- protocols:
- type: array
- description: Enabled transport identifiers. Public values are rest, mcp, and experimental acp; other returned values are reserved.
- items:
- type: string
- config:
- type: [object, 'null']
- additionalProperties: true
- description: Advanced Service configuration. Declarative Service responses always return null here.
- session_metadata:
- type: [object, 'null']
- additionalProperties: true
- description: Metadata applied when an invocation creates a new persistent Session.
- memory_config:
- type: [object, 'null']
- additionalProperties: true
- description: Reserved configuration stored with the Service; it is not currently applied to Session execution.
- resource_config:
- type: [object, 'null']
- additionalProperties: true
- description: Reserved configuration stored with the Service; it is not currently applied to Session execution.
- vault_config:
- type: [object, 'null']
- additionalProperties: true
- description: Reserved configuration stored with the Service; it is not currently applied to Session execution.
- status:
- type: string
- enum: [active, disabled]
- store_status:
- type: string
- enum: [private, pending_review, public, suspended]
- description: Publication and review state of the Service in the Service store.
- creation_mode:
- type: string
- enum: [declarative, advanced]
- runtime:
- type: string
- description: Present for declarative Services.
- skills:
- type: array
- description: Present for declarative Services.
- items:
- type: string
- run_url:
- type: string
- format: uri
- acp_url:
- type: string
- format: uri
- description: Experimental ACP invocation URL. Its presence does not mean ACP is enabled; inspect protocols first.
- created_at:
- type: string
- format: date-time
- updated_at:
- type: string
- format: date-time
-
- EndpointInvokeRequest:
- type: object
- properties:
- input:
- oneOf:
- - type: string
- - type: array
- items:
- type: object
- additionalProperties: true
- - type: object
- additionalProperties: true
- content:
- oneOf:
- - type: array
- items:
- type: object
- additionalProperties: true
- - type: object
- additionalProperties: true
- session_id:
- type: string
- description: Persistent Session to continue. Omit to create a new Session.
- anyOf:
- - required: [input]
- - required: [content]
-
- EndpointInvokeAccepted:
- type: object
- description: Acceptance response for standard events sent to a persistent Session.
- required: [session_id, events]
- properties:
- session_id:
- type: string
- description: Persistent public Session identity.
- events:
- type: array
- items:
- $ref: '#/components/schemas/EndpointAcceptedEvent'
-
- EndpointAcceptedEvent:
- type: object
- required: [id, type, processed_at]
- properties:
- id: { type: string }
- type: { type: string, const: user.message }
- processed_at:
- type: [string, 'null']
- format: date-time
- description: Acceptance time, or null while a native delivery outcome is still unknown.
+ type: { type: string }
+ code: { type: ['null', string] }
+ param: { type: ['null', string] }
- APIError:
+ APIKeyScopeError:
type: object
required: [error]
properties:
error:
type: object
- required: [type, message]
+ required: [code, message, request_id]
properties:
- type:
- type: string
- message:
- type: string
+ code: { type: string, const: insufficient_scope }
+ message: { type: string }
+ request_id: { type: string }
- ACPRequest:
+ MCPServer:
type: object
- required: [method]
+ required: [name, type, url]
properties:
- jsonrpc:
- type: string
- description: JSON-RPC version supplied by the client. Use 2.0.
- example: '2.0'
- id:
- description: Client request identifier echoed in the response.
- oneOf:
- - type: string
- - type: number
- - type: 'null'
- method:
- type: string
- enum: [initialize, session/new, session/prompt, session/cancel]
- params:
- type: object
- additionalProperties: true
- description: Method-specific parameters. session/prompt and session/cancel require sessionId.
- additionalProperties: true
-
- ACPResponse:
+ name: { type: string, minLength: 1, maxLength: 255 }
+ type: { type: string, enum: [url] }
+ url: { type: string, format: uri }
+ # --- Agents platform -------------------------------------------
+ AgentsError:
type: object
- required: [jsonrpc, id]
+ description: Error envelope for Agents platform resources. message is a fixed, safe sentence for each code.
+ required: [error]
properties:
- jsonrpc: { type: string, const: '2.0' }
- id:
- oneOf:
- - type: string
- - type: number
- - type: 'null'
- result:
- type: object
- additionalProperties: true
- description: Method-specific result, present on success.
error:
type: object
- required: [code, message]
+ required: [type, code, param, message]
properties:
- code: { type: integer }
+ type: { type: string, enum: [invalid_request_error, authentication_error, server_error] }
+ code:
+ type: string
+ description: Stable machine-readable code. Unknown future codes should be treated by HTTP status.
+ enum: [unauthorized, insufficient_scope, authorization_unavailable, org_disabled, spending_limit_exceeded, quota_exceeded, concurrency_limit, admission_unavailable, invalid_request, not_found, conflict, session_input_pending, session_input_expired, session_environment_unavailable, tenant_provisioning_failed, tenant_unavailable, executor_rejected, executor_unavailable, executor_contract_error, skill_not_found, skill_unavailable, skill_runtime_incompatible, storage_unavailable, internal_error]
+ param: { type: 'null' }
message: { type: string }
- data:
- type: object
- additionalProperties: true
- description: JSON-RPC error, present when the method cannot be completed.
- oneOf:
- - required: [result]
- - required: [error]
-
- MCPRequest:
+ AgentsCursorList:
type: object
- required: [method]
+ required: [object, data, has_more, first_id, last_id]
properties:
- jsonrpc: { type: string, const: '2.0' }
- id:
- oneOf:
- - type: string
- - type: number
- - type: 'null'
- method: { type: string, description: MCP method name. }
- params: { type: object, additionalProperties: true }
- additionalProperties: true
-
- MCPResponse:
+ object: { type: string, const: list }
+ data: { type: array, items: { type: object, additionalProperties: true } }
+ has_more: { type: boolean }
+ first_id: { type: [string, 'null'] }
+ last_id: { type: [string, 'null'], description: "Pass as after to read the next page." }
+ AgentsOffsetList:
type: object
- required: [jsonrpc]
+ required: [data, total, limit, offset]
properties:
- jsonrpc: { type: string, const: '2.0' }
- id:
- oneOf:
- - type: string
- - type: number
- - type: 'null'
- result: { type: object, additionalProperties: true }
- error:
+ data: { type: array, items: { type: object, additionalProperties: true } }
+ total: { type: integer }
+ limit: { type: integer }
+ offset: { type: integer }
+ EmptyObject:
+ type: object
+ additionalProperties: false
+ maxProperties: 0
+ RowVersionRequest:
+ type: object
+ additionalProperties: false
+ required: [row_version]
+ properties:
+ row_version: { type: integer, minimum: 1, description: "The row_version you last read. A stale value returns 409." }
+ DeletedFlag:
+ type: object
+ required: [deleted]
+ properties:
+ deleted: { type: boolean, const: true }
+ AgentSkillRef:
+ type: object
+ additionalProperties: false
+ required: [skill_id]
+ properties:
+ skill_id: { type: string, description: "Skill ID (`skl_` prefix) owned by your organization." }
+ version: { type: [string, 'null'], description: "Pinned Skill version. null uses the default version, resolved when a Session is created." }
+ AgentMCPConnectionRef:
+ type: object
+ additionalProperties: false
+ required: [connection_id, server_label]
+ properties:
+ connection_id: { type: string, description: "MCP connection ID (`mcp_` prefix)." }
+ server_label: { type: string, pattern: '^[a-zA-Z][a-zA-Z0-9_-]{0,63}$', description: "Label unique within the Agent." }
+ allowed_tools: { type: [array, 'null'], maxItems: 256, items: { type: string, minLength: 1, maxLength: 128 }, description: "null allows every tool; an empty array allows none." }
+ required: { type: boolean, description: "Whether the Session must load this server." }
+ AgentFunctionTool:
+ type: object
+ additionalProperties: false
+ required: [type, name, description, parameters]
+ description: Saved function tool. Runs that request a client-side tool result are not supported yet.
+ properties:
+ type: { type: string, const: function }
+ name: { type: string, minLength: 1, maxLength: 64 }
+ description: { type: string }
+ parameters: { type: object, description: "JSON Schema whose type is object." }
+ defer_loading: { type: boolean }
+ AgentWebSearchTool:
+ type: object
+ additionalProperties: false
+ required: [type]
+ description: At most one per Agent. Sessions, Services, and Schedules currently require mode disabled.
+ properties:
+ type: { type: string, const: web_search }
+ mode: { type: string, enum: [disabled, cached, live] }
+ context_size: { type: string, enum: [low, medium, high] }
+ allowed_domains: { type: array, items: { type: string } }
+ location:
type: object
- required: [code, message]
+ additionalProperties: false
properties:
- code: { type: integer }
- message: { type: string }
- data: { type: object, additionalProperties: true }
- oneOf:
- - required: [result]
- - required: [error]
-
- # --- Agents ---------------------------------------------------
- CreateAgentRequest:
+ city: { type: string }
+ country: { type: string }
+ region: { type: string }
+ timezone: { type: string }
+ AgentText:
type: object
- additionalProperties: true
- required: [model, name]
+ additionalProperties: false
properties:
- model:
- oneOf:
- - type: string
- pattern: '^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$'
- example: "anthropic/claude-sonnet-5"
- - type: object
- additionalProperties: true
- required: [id]
- properties:
- id:
- type: string
- pattern: '^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$'
- speed:
- type: string
- effort:
- description: Optional model runtime hint preserved with the Agent configuration.
- name:
- type: string
- description:
- type: [string, 'null']
- system:
- type: [string, 'null']
+ format:
+ type: object
+ additionalProperties: false
+ required: [type]
+ properties:
+ type: { type: string, enum: [text, json_schema] }
+ schema: { type: object, description: "Required for json_schema; its type must be object." }
+ verbosity: { type: string, enum: [low, medium, high] }
+ AgentReasoning:
+ type: object
+ additionalProperties: false
+ properties:
+ effort: { type: [string, 'null'], enum: [none, minimal, low, medium, high, xhigh, max, null] }
+ summary: { type: [string, 'null'], enum: [concise, detailed, auto, null] }
+ AgentConfigFields:
+ type: object
+ properties:
+ name: { type: string, maxLength: 128 }
+ model: { type: string, minLength: 1, maxLength: 200, description: "Model ID from the Models API, such as deepseek/deepseek-v4-flash." }
+ instructions: { type: string, maxLength: 262144 }
+ description: { type: string, maxLength: 4096 }
+ metadata: { type: object, maxProperties: 16, additionalProperties: { type: string, maxLength: 512 }, description: "Keys up to 64 characters." }
+ service_tier: { type: string, enum: [auto, default, flex, priority, fast], default: auto }
+ runtime_profile: { type: string, enum: [codex, claude, mcode], default: codex }
tools:
- type: [array, 'null']
- items:
- type: object
- mcp_servers:
- type: [array, 'null']
- description: MCP server configurations available to the Agent runtime.
- skills:
- type: [array, 'null']
+ type: array
+ maxItems: 64
items:
- type: object
- handoffs:
- type: [array, 'null']
- description: Agent handoff configurations.
- multiagent:
- type: [object, 'null']
- description: Reserved for Claude compatibility. Omit, send null, or send an empty object; any non-empty object returns unsupported_feature.
- metadata:
- type: [object, 'null']
- additionalProperties: true
- description: Application-defined JSON metadata. The reserved _sandbase namespace is stripped from public requests.
-
- UpdateAgentRequest:
+ oneOf:
+ - $ref: '#/components/schemas/AgentFunctionTool'
+ - $ref: '#/components/schemas/AgentWebSearchTool'
+ text: { $ref: '#/components/schemas/AgentText' }
+ reasoning: { $ref: '#/components/schemas/AgentReasoning' }
+ skills: { type: array, items: { $ref: '#/components/schemas/AgentSkillRef' } }
+ mcp_connections: { type: array, maxItems: 32, items: { $ref: '#/components/schemas/AgentMCPConnectionRef' } }
+ AgentCreateRequest:
+ allOf:
+ - $ref: '#/components/schemas/AgentConfigFields'
+ - type: object
+ required: [model]
+ properties:
+ note: { type: string, description: "Optional change note recorded on version 1." }
+ multi_agent: { type: 'null', description: "Only null is accepted." }
+ AgentUpdateRequest:
type: object
- additionalProperties: true
+ additionalProperties: false
+ description: SDK-style partial update. description and note are not accepted here; use PATCH for them.
properties:
- version:
- type: integer
- minimum: 1
- description: Current version (optimistic lock)
- name:
- type: [string, 'null']
- description:
- type: [string, 'null']
- model:
- oneOf:
- - type: string
- pattern: '^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$'
- - type: object
- additionalProperties: true
- required: [id]
- properties:
- id: { type: string, pattern: '^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$' }
- speed: { type: string }
- effort:
- description: Optional model runtime hint preserved with the Agent configuration.
- system:
- type: [string, 'null']
- tools:
- type: [array, 'null']
- skills:
- type: [array, 'null']
- mcp_servers:
- type: [array, 'null']
- handoffs:
- type: [array, 'null']
- multiagent:
- type: [object, 'null']
- description: Reserved. Omit, send null, or send an empty object; any non-empty object returns unsupported_feature.
- metadata:
- type: [object, 'null']
- description: Full replacement for caller-owned metadata. The reserved _sandbase namespace cannot be changed through this API.
-
+ name: { type: string, maxLength: 128 }
+ model: { type: string, minLength: 1, maxLength: 200 }
+ instructions: { type: string, maxLength: 262144 }
+ metadata: { type: object, maxProperties: 16, additionalProperties: { type: string, maxLength: 512 } }
+ service_tier: { type: string, enum: [auto, default, flex, priority, fast] }
+ runtime_profile: { type: string, enum: [codex, claude, mcode] }
+ tools: { type: array, maxItems: 64, items: { type: object } }
+ text: { $ref: '#/components/schemas/AgentText' }
+ reasoning: { $ref: '#/components/schemas/AgentReasoning' }
+ skills: { type: array, items: { $ref: '#/components/schemas/AgentSkillRef' } }
+ mcp_connections: { type: array, items: { $ref: '#/components/schemas/AgentMCPConnectionRef' } }
+ AgentReplaceRequest:
+ allOf:
+ - $ref: '#/components/schemas/AgentConfigFields'
+ - type: object
+ required: [row_version]
+ description: Supplied fields replace saved values; explicit null values are rejected.
+ properties:
+ note: { type: string, description: "Change note recorded on the new version." }
+ row_version: { type: integer, minimum: 1 }
+ AgentRowVersionRequest:
+ $ref: '#/components/schemas/RowVersionRequest'
+ AgentRestoreRequest:
+ type: object
+ additionalProperties: false
+ required: [source_version, row_version]
+ properties:
+ source_version: { type: integer, minimum: 1, description: "Version whose configuration becomes the new current version." }
+ row_version: { type: integer, minimum: 1 }
+ AgentCloneRequest:
+ type: object
+ additionalProperties: false
+ properties:
+ version: { type: integer, minimum: 1, description: "Catalog version to copy. Defaults to the current public version." }
+ name: { type: string, maxLength: 128, description: "Name for the new Agent." }
Agent:
type: object
- required: [id, type, name, description, model, system, tools, mcp_servers, skills, handoffs, metadata, version, created_at, updated_at, archived_at]
+ required: [id, object, model, name, instructions, tools, text, reasoning, service_tier, multi_agent, visibility, status, version, row_version, projection_status, runtime_profile, description, metadata, mcp_connections, skills, created_at, updated_at, archived_at]
properties:
- id:
- type: string
- pattern: '^agent_[0-9a-f]{8}-[0-9a-f]{3}$'
- type:
- type: string
- enum: [agent]
- name:
- type: string
- description:
- type: string
- model:
- type: object
- required: [id]
+ id: { type: string, description: "Agent ID (`agt_` prefix)." }
+ object: { type: string, const: agent }
+ model: { type: string }
+ name: { type: [string, 'null'] }
+ instructions: { type: [string, 'null'] }
+ tools: { type: array, items: { type: object } }
+ text: { $ref: '#/components/schemas/AgentText' }
+ reasoning: { type: object }
+ service_tier: { type: string }
+ multi_agent: { type: object, description: "Always reports enabled false." }
+ visibility: { type: string, enum: [private, public] }
+ status: { type: string, enum: [active, archived] }
+ version: { type: integer, description: "Current version number." }
+ row_version: { type: integer, description: "Compare-and-set token for PATCH, archive, unarchive, and restore." }
+ projection_status: { type: string, description: "Whether the current version is ready for execution. Sessions require ready." }
+ runtime_profile: { type: string, enum: [codex, claude, mcode] }
+ description: { type: string }
+ metadata: { type: object, additionalProperties: { type: string } }
+ mcp_connections: { type: array, items: { $ref: '#/components/schemas/AgentMCPConnectionRef' } }
+ skills: { type: array, items: { $ref: '#/components/schemas/AgentSkillRef' } }
+ created_at: { type: integer, description: "Unix seconds." }
+ updated_at: { type: integer, description: "Unix seconds." }
+ archived_at: { type: [integer, 'null'], description: "Unix seconds." }
+ failure_code: { type: string }
+ AgentList:
+ allOf:
+ - $ref: '#/components/schemas/AgentsCursorList'
+ - type: object
properties:
- id: { type: string }
- effort:
- description: Optional model runtime hint preserved from the submitted model object; absent for string model input.
- speed: { type: [string, 'null'], description: Optional model runtime hint preserved from the submitted model object. }
- system:
- type: string
- tools:
- type: [array, 'null']
- mcp_servers:
- type: [array, 'null']
- skills:
- type: [array, 'null']
- handoffs:
- type: [array, 'null']
- metadata:
- type: [object, 'null']
- version:
- type: integer
- minimum: 1
- created_at:
- type: string
- format: date-time
- updated_at:
- type: string
- format: date-time
- archived_at:
- type: [string, 'null']
- format: date-time
-
- AgentListPage:
+ data: { type: array, items: { $ref: '#/components/schemas/Agent' } }
+ AgentDeleted:
type: object
- required: [data]
+ required: [id, object, deleted]
properties:
- data:
- type: array
- items: { $ref: '#/components/schemas/Agent' }
- next_page:
- type: string
- description: Opaque cursor included only when another page is available.
-
- MCPServer:
+ id: { type: string }
+ object: { type: string, const: agent.deleted }
+ deleted: { type: boolean, const: true }
+ AgentVersion:
type: object
- required: [name, type, url]
+ required: [version, agent, checksum, created_at]
properties:
- name: { type: string, minLength: 1, maxLength: 255 }
- type: { type: string, enum: [url] }
- url: { type: string, format: uri }
-
- # --- Sessions -------------------------------------------------
- CreateSessionRequest:
+ version: { type: integer }
+ agent: { type: object, description: "Saved configuration document of this version." }
+ checksum: { type: string }
+ note: { type: string, description: "Present only when the change that created this version sent a note." }
+ created_by: { type: string }
+ created_at: { type: string, format: date-time, description: "ISO 8601 string, unlike the Unix-second timestamps on Agent objects." }
+ failure_code: { type: string }
+ AgentVersionList:
+ allOf:
+ - $ref: '#/components/schemas/AgentsOffsetList'
+ - type: object
+ properties:
+ data: { type: array, items: { $ref: '#/components/schemas/AgentVersion' } }
+ AgentCatalogEntry:
type: object
- additionalProperties: false
- required: [agent]
+ required: [id, object, visibility, version, name, description, model, created_at, updated_at]
properties:
- agent:
- oneOf:
- - type: string
- description: Agent ID (uses latest version)
- - type: object
- additionalProperties: false
- required: [type, id]
- properties:
- type:
- type: string
- enum: [agent]
- id:
- type: string
- version:
- type: integer
- minimum: 1
- agent_version_id:
- type: string
- description: Immutable AgentVersion resource ID. Takes precedence over agent.version when supplied.
- initial_events:
- type: array
- maxItems: 50
- items:
- $ref: '#/components/schemas/SessionEventInput'
- initial_event:
- type: array
- deprecated: true
- description: SandBase legacy alias for initial_events; do not combine both fields.
- maxItems: 50
- items:
- $ref: '#/components/schemas/SessionEventInput'
- title:
- type: string
- metadata:
- type: object
- resources:
- type: array
- description: Reserved for Claude compatibility; rejected until resource attachment is supported.
- items: { type: object, additionalProperties: true }
- vault_ids:
- type: array
- description: Reserved for Claude compatibility; rejected until vault attachment is supported.
- items: { type: string }
-
- Session:
+ id: { type: string }
+ object: { type: string, const: agent }
+ visibility: { type: string, const: public }
+ version: { type: integer }
+ name: { type: string }
+ description: { type: string }
+ model: { type: string }
+ skills: { type: array, items: { type: object } }
+ configuration: { type: object, description: "Read-only configuration (name, description, model, instructions, runtime_profile, metadata, tools, text, reasoning, skills)." }
+ created_at: { type: integer }
+ updated_at: { type: integer }
+ failure_code: { type: string }
+ AgentCatalogList:
+ allOf:
+ - $ref: '#/components/schemas/AgentsCursorList'
+ - type: object
+ properties:
+ data: { type: array, items: { $ref: '#/components/schemas/AgentCatalogEntry' } }
+ ModelProvider:
type: object
- required: [id, type, status, agent, agent_id, agent_version, agent_version_id, source, title, metadata, archived_at, created_at, updated_at]
- properties:
- id:
- type: string
- description: Persistent public Session identity.
- type:
- type: string
- enum: [session]
- agent_id:
- type: string
- agent:
- $ref: '#/components/schemas/SessionAgentProjection'
- readOnly: true
- agent_version:
- type: [integer, 'null']
- agent_version_id:
- type: string
- title:
- type: string
- status:
- type: string
- enum: [idle, running, rescheduling, terminated]
- source:
- type: string
- enum: [direct, endpoint, deployment, store_trial]
- description: Entry point that created the Session. store_trial identifies a Service store trial Session.
- metadata:
- type: [object, 'null']
- archived_at:
- type: [string, 'null']
- format: date-time
- created_at:
- type: string
- format: date-time
- updated_at:
- type: string
- format: date-time
-
- SessionAPIError:
+ additionalProperties: false
+ description: Write-only model credential. Omit it to run with the calling SandBase API key. Never returned.
+ properties:
+ type: { type: string, enum: [sandbase, custom], default: sandbase }
+ base_url: { type: string, format: uri, description: "Required for custom. HTTPS only, without credentials, query, or fragment. Not allowed for sandbase." }
+ api_key: { type: string, description: "Required for custom. Optional for sandbase, where it defaults to the calling key." }
+ SessionCreateRequest:
type: object
- required: [error]
+ additionalProperties: false
+ required: [agent_id]
properties:
- error:
+ agent_id: { type: string, description: "Saved Agent ID (`agt_` prefix). Inline Agents are not supported." }
+ version: { type: integer, minimum: 1, description: "Agent version. Defaults to the current version." }
+ title: { type: string }
+ agent: { type: object, additionalProperties: false, properties: { model: { type: string } }, description: "OpenAI SDK form of the model override." }
+ model: { type: string, maxLength: 200, description: "Model override for this Session. Must equal agent.model when both are sent." }
+ environment: { type: object, additionalProperties: false, properties: { type: { type: string, const: openai_hosted } }, description: "OpenAI SDK compatibility only. Sessions always run in the SandBase hosted cloud, so omit this field in direct HTTP calls. The OpenAI SDK requires it; {\"type\":\"openai_hosted\"} is accepted and any other value returns 400." }
+ input:
+ description: 'Optional first input: a string, or an array with exactly one message, e.g. [{"role":"user","content":[{"type":"input_text","text":"..."}]}]. A bare object returns 400.'
+ oneOf:
+ - { type: string }
+ - type: array
+ minItems: 1
+ maxItems: 1
+ items: { type: object, required: [role, content], properties: { role: { type: string, const: user }, content: { type: array, items: { type: object, required: [type, text], properties: { type: { type: string, const: input_text }, text: { type: string } } } } } }
+ metadata: { type: object, maxProperties: 16, additionalProperties: { type: string, maxLength: 512 } }
+ stream: { type: boolean, const: false, description: "true returns 400." }
+ model_provider: { $ref: '#/components/schemas/ModelProvider' }
+ SessionFailure:
+ type: object
+ required: [code, message, action]
+ properties:
+ code: { type: string, enum: [session_creation_rejected, execution_failed, model_authentication_failed, model_rate_limited, model_quota_exceeded, context_limit_exceeded, environment_connection_timeout, runtime_unavailable, model_request_failed, model_configuration_rejected, session_not_ready, session_execution_unavailable, session_configuration_unavailable, session_snapshot_unavailable] }
+ message: { type: string }
+ action: { type: string, description: "Suggested next step, such as retry_later, inspect_history, create_new_session, check_model_account, or create_session_with_updated_model_provider." }
+ retryable: { type: boolean }
+ Session:
+ type: object
+ required: [id, object, agent, agent_id, agent_version, title, model, environment, metadata, status, lifecycle_status, sync_status, execution_status, failure, row_version, required_actions, vault_ids, created_at, last_active_at, updated_at, archived_at]
+ properties:
+ id: { type: string, description: "Session ID (`ses_` prefix)." }
+ object: { type: string, const: agent.session }
+ agent: { type: object, description: "OpenAI-shaped snapshot of the effective Agent configuration (id, name, model, instructions, multi_agent, reasoning, service_tier, text, tools)." }
+ agent_id: { type: string }
+ agent_version: { type: integer }
+ title: { type: string }
+ model: { type: string }
+ environment:
type: object
- required: [type, message]
+ description: Echo of the hosted execution context, kept for OpenAI SDK compatibility. It is not a Sandbox instance and needs no action from clients.
properties:
- type: { type: string }
- message: { type: string }
-
- SessionAgentProjection:
- description: The immutable pinned Agent snapshot when available; legacy or unavailable snapshots fall back to the minimal identity projection.
- anyOf:
- - $ref: '#/components/schemas/Agent'
+ id: { type: string }
+ type: { type: string, const: openai_hosted }
+ skills: { type: array, items: { type: object } }
+ metadata: { type: object, additionalProperties: { type: string } }
+ status: { type: string, enum: [idle, in_progress, requires_action, failed], description: "Execution status. requires_action is not supported yet and surfaces as a 502 executor_contract_error." }
+ lifecycle_status: { type: string, enum: [active, archived] }
+ sync_status: { type: string }
+ execution_status: { type: string }
+ failure: { oneOf: [{ type: 'null' }, { $ref: '#/components/schemas/SessionFailure' }] }
+ row_version: { type: integer }
+ required_actions: { type: array, items: {} }
+ vault_ids: { type: array, items: { type: string } }
+ usage: { type: [object, 'null'], description: "Token usage; null until a turn reports usage." }
+ created_at: { type: integer }
+ last_active_at: { type: integer }
+ updated_at: { type: integer }
+ archived_at: { type: [integer, 'null'] }
+ source: { type: [string, 'null'], enum: [direct, service, schedule, null] }
+ source_id: { type: [string, 'null'], description: "Service or Schedule ID when source is service or schedule." }
+ source_failure_code: { type: string }
+ SessionList:
+ allOf:
+ - $ref: '#/components/schemas/AgentsCursorList'
- type: object
- required: [id, type, version]
properties:
- id: { type: string, pattern: '^agent_' }
- type: { type: string, const: agent }
- version: { type: [integer, 'null'] }
-
- SendEventsRequest:
+ data: { type: array, items: { $ref: '#/components/schemas/Session' } }
+ SessionUpdateRequest:
+ type: object
+ additionalProperties: false
+ required: [metadata]
+ properties:
+ metadata: { type: object, maxProperties: 16, additionalProperties: { type: string, maxLength: 512 }, description: "Replaces the whole metadata map." }
+ SessionRenameRequest:
+ type: object
+ additionalProperties: false
+ required: [title, row_version]
+ properties:
+ title: { type: string, maxLength: 512 }
+ row_version: { type: integer, minimum: 1 }
+ SessionRowVersionRequest:
+ $ref: '#/components/schemas/RowVersionRequest'
+ SessionEventsRequest:
type: object
- required: [events]
+ additionalProperties: false
+ description: 'Either events (exactly one event) or the SandBase extension input/file_ids.'
properties:
events:
type: array
minItems: 1
+ maxItems: 1
items:
- $ref: '#/components/schemas/SessionEventInput'
-
- SessionAgentUpdate:
+ type: object
+ additionalProperties: false
+ required: [type]
+ properties:
+ type: { type: string, enum: [agent.session.input.message, agent.session.input.cancel] }
+ input: { description: 'For input.message: a string, or exactly one user message with one input_text part. Must be omitted for input.cancel.' }
+ input: { type: string, maxLength: 262144, description: "SandBase extension. Text input, non-empty." }
+ file_ids: { type: array, maxItems: 10, items: { type: string }, description: "SandBase extension. Up to 10 distinct ready attachments." }
+ SessionDeleted:
type: object
- description: Session-local full replacement override; omitted fields preserve the pinned Agent snapshot.
+ required: [id, object, deleted]
properties:
- tools:
- type: array
- items: { type: object, additionalProperties: true }
- mcp_servers:
- type: array
- items: { $ref: '#/components/schemas/MCPServer' }
-
- SessionEventInput:
- description: >-
- Discriminated input schema. Recognition does not imply runtime support:
- user.tool_confirmation, user.custom_tool_result, user.tool_result, and
- user.define_outcome currently return HTTP 422 unsupported_event_type;
- any session_thread_id returns HTTP 422 unsupported_multiagent. The
- rejected batch is not persisted.
- discriminator:
- propertyName: type
- mapping:
- user.message: '#/components/schemas/UserMessageEventInput'
- user.interrupt: '#/components/schemas/UserInterruptEventInput'
- user.tool_confirmation: '#/components/schemas/UserToolConfirmationEventInput'
- user.custom_tool_result: '#/components/schemas/UserCustomToolResultEventInput'
- user.tool_result: '#/components/schemas/UserToolResultEventInput'
- user.define_outcome: '#/components/schemas/UserDefineOutcomeEventInput'
- interrupt.response: '#/components/schemas/SandBaseInterruptResponseEventInput'
- system.message: '#/components/schemas/SandBaseSystemMessageEventInput'
- oneOf:
- - $ref: '#/components/schemas/UserMessageEventInput'
- - $ref: '#/components/schemas/UserInterruptEventInput'
- - $ref: '#/components/schemas/UserToolConfirmationEventInput'
- - $ref: '#/components/schemas/UserCustomToolResultEventInput'
- - $ref: '#/components/schemas/UserToolResultEventInput'
- - $ref: '#/components/schemas/UserDefineOutcomeEventInput'
- - $ref: '#/components/schemas/SandBaseInterruptResponseEventInput'
- - $ref: '#/components/schemas/SandBaseSystemMessageEventInput'
-
- UserMessageEventInput:
+ id: { type: string }
+ object: { type: string, const: agent.session.deleted }
+ deleted: { type: boolean, const: true }
+ SessionTurn:
type: object
- additionalProperties: false
- required: [type, content]
+ additionalProperties: true
+ required: [id]
properties:
- type: { type: string, enum: [user.message] }
- content: { $ref: '#/components/schemas/UserMessageContent' }
-
- UserInterruptEventInput:
+ id: { type: string }
+ session_id: { type: string }
+ agent_id: { type: string }
+ status: { type: string }
+ SessionCancelAccepted:
type: object
- additionalProperties: false
- required: [type]
+ required: [session_id, accepted]
properties:
- type: { type: string, enum: [user.interrupt] }
- session_thread_id: { type: string }
-
- UserToolConfirmationEventInput:
+ session_id: { type: string }
+ accepted: { type: boolean, const: true }
+ SessionReconnect:
type: object
- description: Schema-recognized but not runtime-supported; requests currently return HTTP 422 unsupported_event_type without persistence.
- additionalProperties: false
- required: [type, tool_use_id, result]
+ required: [session, stream_url, recovery, requires_authorization_header]
properties:
- type: { type: string, enum: [user.tool_confirmation] }
- tool_use_id: { type: string }
- result: { type: string, enum: [allow, deny] }
- deny_message: { type: string, description: Allowed only when result is deny. }
- session_thread_id: { type: string }
-
- UserCustomToolResultEventInput:
+ session: { $ref: '#/components/schemas/Session' }
+ stream_url: { type: string, description: "Path of the product stream, for example /v1/agents/sessions/{session_id}/events/stream." }
+ recovery: { type: string, const: snapshot_then_live }
+ requires_authorization_header: { type: boolean, const: true }
+ SessionFileUpload:
type: object
- description: Schema-recognized but not runtime-supported; requests currently return HTTP 422 unsupported_event_type without persistence.
- additionalProperties: false
- required: [type, custom_tool_use_id]
+ required: [file]
properties:
- type: { type: string, enum: [user.custom_tool_result] }
- custom_tool_use_id: { type: string }
- content: { $ref: '#/components/schemas/ToolResultContent' }
- is_error: { type: boolean }
- session_thread_id: { type: string }
-
- UserToolResultEventInput:
+ file: { type: string, format: binary, description: 'Exactly one file part, up to 5 MiB. File names use letters, digits, spaces, dots, underscores, and hyphens (maximum 200 characters).' }
+ SessionFile:
type: object
- description: Schema-recognized but not runtime-supported; requests currently return HTTP 422 unsupported_event_type without persistence.
- additionalProperties: false
- required: [type, tool_use_id]
+ required: [id, session_id, name, mime_type, size_bytes, status, created_at, updated_at]
properties:
- type: { type: string, enum: [user.tool_result] }
- tool_use_id: { type: string }
- content: { $ref: '#/components/schemas/ToolResultContent' }
- is_error: { type: boolean }
- session_thread_id: { type: string }
-
- UserDefineOutcomeEventInput:
+ id: { type: string, description: "Attachment ID (`fil_` prefix)." }
+ session_id: { type: string }
+ name: { type: string }
+ mime_type: { type: string }
+ size_bytes: { type: integer }
+ status: { type: string, enum: [pending, ready, failed, deleted], description: "Only ready attachments can be sent or downloaded." }
+ error_code: { type: string }
+ created_at: { type: string, format: date-time }
+ updated_at: { type: string, format: date-time }
+ SessionFileList:
+ allOf:
+ - $ref: '#/components/schemas/AgentsOffsetList'
+ - type: object
+ properties:
+ data: { type: array, items: { $ref: '#/components/schemas/SessionFile' } }
+ SessionFileDeleted:
type: object
- description: Schema-recognized but not runtime-supported; requests currently return HTTP 422 unsupported_event_type without persistence.
- additionalProperties: false
- required: [type, description, rubric]
+ required: [deleted, id]
properties:
- type: { type: string, enum: [user.define_outcome] }
- description: { type: string }
- rubric: { $ref: '#/components/schemas/OutcomeRubric' }
- max_iterations: { type: integer, minimum: 1, maximum: 20, default: 3 }
-
- SandBaseInterruptResponseEventInput:
+ deleted: { type: boolean, const: true }
+ id: { type: string }
+ ScheduleSpec:
type: object
additionalProperties: false
- description: SandBase extension; not part of the Claude Managed Agents event input union.
- required: [type, interrupt_id, response]
+ required: [cron]
properties:
- type: { type: string, enum: [interrupt.response] }
- interrupt_id: { type: string }
- response: { type: object, additionalProperties: true }
-
- SandBaseSystemMessageEventInput:
+ cron: { type: string, maxLength: 256, description: 'Five-field cron expression: minute hour day-of-month month day-of-week. Seconds are not supported. An expression with no match in the next 366 days returns 400.' }
+ timezone: { type: string, maxLength: 128, default: UTC, description: "IANA timezone name, such as Asia/Shanghai." }
+ ServiceCreateRequest:
type: object
additionalProperties: false
- description: SandBase extension for a privileged mid-conversation system message.
- required: [type, content]
+ required: [name, agent_id]
properties:
- type: { type: string, enum: [system.message] }
- content:
- type: array
- minItems: 1
- items: { $ref: '#/components/schemas/TextContentBlock' }
-
- UserMessageContent:
- type: array
- minItems: 1
- items:
- oneOf:
- - $ref: '#/components/schemas/TextContentBlock'
- - $ref: '#/components/schemas/ImageContentBlock'
- - $ref: '#/components/schemas/DocumentContentBlock'
-
- ToolResultContent:
- type: array
- minItems: 1
- items:
- oneOf:
- - $ref: '#/components/schemas/TextContentBlock'
- - $ref: '#/components/schemas/ImageContentBlock'
- - $ref: '#/components/schemas/DocumentContentBlock'
- - $ref: '#/components/schemas/SearchResultContentBlock'
-
- TextContentBlock:
+ name: { type: string, minLength: 1, maxLength: 200 }
+ description: { type: string, maxLength: 16384 }
+ agent_id: { type: string }
+ agent_version: { type: integer, minimum: 1, description: "Omit to pin the current Agent version." }
+ input: { type: string, maxLength: 262144, description: "Default input for invocations that do not send one." }
+ model: { type: string, maxLength: 200, description: "Default model override." }
+ ScheduleCreateRequest:
type: object
additionalProperties: false
- required: [type, text]
+ required: [name, agent_id, input, schedule]
properties:
- type: { type: string, enum: [text] }
- text: { type: string }
-
- ImageContentBlock:
+ name: { type: string, minLength: 1, maxLength: 200 }
+ description: { type: string, maxLength: 16384 }
+ agent_id: { type: string }
+ agent_version: { type: integer, minimum: 1, description: "Omit to pin the current Agent version." }
+ input: { type: string, minLength: 1, maxLength: 262144 }
+ model: { type: string, maxLength: 200 }
+ schedule: { $ref: '#/components/schemas/ScheduleSpec' }
+ model_provider: { $ref: '#/components/schemas/ModelProvider' }
+ RunSourcePatch:
type: object
additionalProperties: false
- required: [type, source]
+ required: [row_version]
properties:
- type: { type: string, enum: [image] }
- source: { type: object, additionalProperties: true }
-
- DocumentContentBlock:
+ name: { type: string, minLength: 1, maxLength: 200 }
+ description: { type: string, maxLength: 16384 }
+ agent_id: { type: string }
+ agent_version: { type: integer, minimum: 1 }
+ input: { type: string, maxLength: 262144 }
+ row_version: { type: integer, minimum: 1 }
+ ScheduleTimingRequest:
type: object
additionalProperties: false
- required: [type, source]
+ required: [row_version, cron]
properties:
- type: { type: string, enum: [document] }
- source: { type: object, additionalProperties: true }
- context: { type: string }
- title: { type: string }
-
- SearchResultContentBlock:
+ row_version: { type: integer, minimum: 1 }
+ cron: { type: string, maxLength: 256 }
+ timezone: { type: string, maxLength: 128, default: UTC }
+ model_provider: { $ref: '#/components/schemas/ModelProvider' }
+ RunSourceDefinition:
type: object
- additionalProperties: false
- required: [type, source, title, content]
+ required: [id, kind, name, description, agent_id, agent_version, input, status, row_version, created_at, updated_at]
properties:
- type: { type: string, enum: [search_result] }
- source: { type: string }
- title: { type: string }
- content:
- type: array
- items: { $ref: '#/components/schemas/TextContentBlock' }
- citations: { type: object, additionalProperties: true }
-
- OutcomeRubric:
- oneOf:
- - $ref: '#/components/schemas/TextOutcomeRubric'
- - $ref: '#/components/schemas/FileOutcomeRubric'
-
- TextOutcomeRubric:
+ id: { type: string, description: "Service (`svc_`) or Schedule (`sch_`) ID." }
+ kind: { type: string, enum: [service, schedule] }
+ name: { type: string }
+ description: { type: string }
+ agent_id: { type: string }
+ agent_version: { type: integer, description: "Pinned Agent version." }
+ input: { type: string }
+ model: { type: string }
+ status: { type: string, enum: [active, paused, archived] }
+ row_version: { type: integer, description: "Schedule row_version also advances after every scheduled tick." }
+ schedule: { $ref: '#/components/schemas/ScheduleSpec' }
+ next_run_at: { type: string, format: date-time }
+ created_at: { type: string, format: date-time }
+ updated_at: { type: string, format: date-time }
+ RunSourceDefinitionList:
+ allOf:
+ - $ref: '#/components/schemas/AgentsOffsetList'
+ - type: object
+ properties:
+ data: { type: array, items: { $ref: '#/components/schemas/RunSourceDefinition' } }
+ AgentRunRequest:
type: object
additionalProperties: false
- required: [type, content]
properties:
- type: { type: string, enum: [text] }
- content: { type: string, maxLength: 262144 }
-
- FileOutcomeRubric:
+ input: { type: string, maxLength: 262144, description: "Overrides the saved input for this Run. The effective input must be non-empty." }
+ model: { type: string, maxLength: 200, description: "Overrides the saved model for this Run." }
+ model_provider: { $ref: '#/components/schemas/ModelProvider' }
+ AgentRun:
+ type: object
+ required: [id, source_type, source_id, source_version, agent_id, agent_version, trigger_source, status, session_id, created_at, updated_at]
+ properties:
+ id: { type: string, description: "Run ID. Treat as opaque." }
+ source_type: { type: string, enum: [service, schedule] }
+ source_id: { type: string }
+ source_version: { type: integer }
+ agent_id: { type: string }
+ agent_version: { type: integer }
+ trigger_source: { type: string, enum: [manual, scheduled] }
+ scheduled_at: { type: string, format: date-time }
+ status: { type: string, enum: [pending, succeeded, failed, cancelled, skipped], description: "skipped is used when a scheduled tick finds a pending Run." }
+ session_id: { type: [string, 'null'], description: "Session created for this Run; null until the Session exists." }
+ error_code: { type: string, description: 'For example session_creation_rejected, execution_failed, schedule_configuration_invalid, concurrency_limit (skipped ticks), or an admission code such as spending_limit_exceeded.' }
+ created_at: { type: string, format: date-time }
+ updated_at: { type: string, format: date-time }
+ AgentRunList:
+ allOf:
+ - $ref: '#/components/schemas/AgentsOffsetList'
+ - type: object
+ properties:
+ data: { type: array, items: { $ref: '#/components/schemas/AgentRun' } }
+ NotificationTargetRequest:
type: object
additionalProperties: false
- required: [type, file_id]
+ required: [webhook_url]
properties:
- type: { type: string, enum: [file] }
- file_id: { type: string }
-
- AcceptedSessionEvents:
+ webhook_url: { type: string, description: 'https://open.feishu.cn/open-apis/bot/v2/hook/ or https://open.larksuite.com/open-apis/bot/v2/hook/. Write-only.' }
+ NotificationTarget:
type: object
- required: [data]
+ required: [channel, version, updated_at]
properties:
- data:
- type: array
- items:
- $ref: '#/components/schemas/SessionEvent'
-
- SessionEvent:
+ channel: { type: string, enum: [feishu] }
+ version: { type: integer }
+ updated_at: { type: string, format: date-time }
+ NotificationTargetList:
type: object
- required: [id, type, processed_at, created_at]
+ required: [data]
properties:
- id:
- type: string
- pattern: '^sevt_'
- type:
- type: string
- content:
- description: Canonical event content. Message events use an array; tool and status events may use an object.
- oneOf:
- - type: array
- items: { type: object, additionalProperties: true }
- - type: object
- additionalProperties: true
- data:
- type: object
- description: Present only for event types whose canonical payload is not content.
- stop_reason:
- type: [object, 'null']
- model_used:
- type: [string, 'null']
- tokens_in:
- type: integer
- tokens_out:
- type: integer
- duration_ms:
- type: [integer, 'null']
- processed_at:
- type: [string, 'null']
- format: date-time
- description: Null only while a native delivery outcome is still unknown.
- created_at:
- type: string
- format: date-time
-
- CreateCredentialRequest:
+ data: { type: array, items: { $ref: '#/components/schemas/NotificationTarget' } }
+ NotificationDelivery:
type: object
- required: [scope, scope_name, secret_key, value]
+ required: [id, channel, status, attempts, delivered_at, next_attempt_at]
properties:
- agent_id: { type: string }
- scope: { type: string, minLength: 1, maxLength: 40, description: 'Caller-defined scope. Leading and trailing whitespace is removed; control characters are rejected.' }
- scope_name: { type: string, minLength: 1, maxLength: 100, description: 'Caller-defined name within the scope. Leading and trailing whitespace is removed; control characters are rejected.' }
- secret_key: { type: string, minLength: 1, maxLength: 200, description: 'Caller-defined key. Leading and trailing whitespace is removed; control characters are rejected.' }
- value: { type: string, minLength: 1, maxLength: 8192, writeOnly: true, description: Plaintext value encrypted at rest and never returned. Blank or whitespace-only values are rejected. }
- group_key: { type: string, description: Defaults to the submitted scope_name and secret_key joined by a colon. }
- strategy: { type: string, enum: [round_robin], default: round_robin }
- weight: { type: integer, minimum: 1, default: 1 }
-
- UpdateCredentialRequest:
+ id: { type: string }
+ channel: { type: string }
+ status: { type: string, enum: [pending, processing, succeeded, failed, canceled] }
+ attempts: { type: integer }
+ error_code: { type: string }
+ delivered_at: { type: [string, 'null'], format: date-time }
+ next_attempt_at: { type: string, format: date-time }
+ NotificationDeliveryList:
type: object
- minProperties: 1
+ required: [data]
properties:
- status: { type: string, enum: [active, disabled] }
- strategy: { type: string, enum: [round_robin] }
- weight: { type: integer, minimum: 1 }
-
- Credential:
+ data: { type: array, items: { $ref: '#/components/schemas/NotificationDelivery' } }
+ SecretCreateRequest:
type: object
- description: Credential metadata. The plaintext and encrypted payload are never returned.
- required: [id, org_id, agent_id, agent_version, scope, scope_name, secret_key, value_hint, group_key, strategy, weight, status, last_used_at, cooldown_until, failure_count, created_at, updated_at]
+ additionalProperties: false
+ required: [name, kind, server_url, token]
properties:
- id: { type: string, pattern: '^sec_' }
- org_id: { type: string }
- agent_id: { type: string }
- agent_version: { type: [integer, 'null'] }
- scope: { type: string }
- scope_name: { type: string }
- secret_key: { type: string }
- value_hint: { type: string, description: Masked prefix/suffix hint. Treat it as sensitive metadata even though it is not the stored plaintext. }
- group_key: { type: string }
- strategy: { type: string }
- weight: { type: integer }
- status: { type: string, enum: [active, disabled] }
- last_used_at: { type: [string, 'null'], format: date-time }
- cooldown_until: { type: [string, 'null'], format: date-time }
- failure_count: { type: integer }
- created_at: { type: string, format: date-time }
- updated_at: { type: string, format: date-time }
-
- CredentialResponse:
+ name: { type: string, minLength: 1, maxLength: 128 }
+ kind: { type: string, const: mcp_bearer }
+ server_url: { type: string, format: uri, maxLength: 2048, description: "HTTPS MCP server URL without credentials, query, or fragment. The Secret only works for this exact URL." }
+ token: { type: string, minLength: 1, maxLength: 16384, writeOnly: true }
+ SecretRotateRequest:
type: object
- description: Credential endpoints currently use the standard response envelope; the plaintext secret is never included.
- required: [code, data, message]
+ additionalProperties: false
+ required: [token, row_version]
properties:
- code: { type: integer, const: 0 }
- data: { $ref: '#/components/schemas/Credential' }
- message: { type: string, const: ok }
-
- CredentialListResponse:
+ token: { type: string, minLength: 1, maxLength: 16384, writeOnly: true }
+ row_version: { type: integer, minimum: 1 }
+ Secret:
type: object
- required: [code, data, message]
+ required: [id, name, kind, server_url, status, row_version, created_at, updated_at]
properties:
- code: { type: integer, const: 0 }
- data:
- type: array
- items: { $ref: '#/components/schemas/Credential' }
- message: { type: string, const: ok }
-
- CreateDeclarativeDeploymentRequest:
+ id: { type: string, description: "Secret ID (`sec_` prefix)." }
+ name: { type: string }
+ kind: { type: string, const: mcp_bearer }
+ server_url: { type: string }
+ status: { type: string, enum: [pending, active, disabled], description: "disabled means revoked." }
+ row_version: { type: integer }
+ created_at: { type: string, format: date-time }
+ updated_at: { type: string, format: date-time }
+ SecretList:
+ allOf:
+ - $ref: '#/components/schemas/AgentsOffsetList'
+ - type: object
+ properties:
+ data: { type: array, items: { $ref: '#/components/schemas/Secret' } }
+ MCPConnectionCreateRequest:
type: object
additionalProperties: false
- required: [name, runtime, initial_events]
+ required: [name, server_url]
properties:
- name: { type: string, minLength: 1 }
- runtime:
- type: string
- description: Enabled declarative runtime. This release supports hermes.
- example: hermes
- description: { type: string }
- model:
- oneOf:
- - type: string
- - type: object
- additionalProperties: true
- system: { type: string }
- skills:
- type: [array, 'null']
- maxItems: 20
- items:
- type: string
- pattern: '^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$'
- tools:
- type: [array, 'null']
- maxItems: 1
- items: { $ref: '#/components/schemas/EndpointToolset' }
- initial_events:
- type: array
- minItems: 1
- description: Must contain at least one user.message event.
- items: { $ref: '#/components/schemas/SessionEventInput' }
- schedule: { type: [object, 'null'] }
- timeout_policy:
- type: object
- additionalProperties: false
+ name: { type: string, minLength: 1, maxLength: 128 }
+ server_url: { type: string, format: uri, maxLength: 2048, description: "HTTPS URL without credentials, query, or fragment." }
+ secret_id: { type: string, description: "Optional active Secret for the same server_url. Omit for an anonymous server." }
+ MCPConnectionReplaceRequest:
+ allOf:
+ - $ref: '#/components/schemas/MCPConnectionCreateRequest'
+ - type: object
+ required: [row_version]
properties:
- hard_timeout_sec: { type: [integer, 'null'], minimum: 60, maximum: 604800 }
- inactivity_timeout_sec: { type: integer, minimum: 180, maximum: 3600 }
- resources: { type: array, maxItems: 0, description: Reserved; only an empty array is accepted. }
- vault_ids: { type: array, maxItems: 0, description: Reserved; only an empty array is accepted. }
-
- CreateDeploymentRequest:
+ row_version: { type: integer, minimum: 1 }
+ MCPConnection:
type: object
- required: [name, initial_events]
- anyOf:
- - required: [agent_id]
- properties:
- agent_id: {}
- - required: [agent]
- properties:
- agent: {}
+ required: [id, name, server_url, status, row_version, created_at, updated_at]
properties:
+ id: { type: string, description: "Connection ID (`mcp_` prefix)." }
name: { type: string }
- description: { type: string }
- agent_id:
- type: string
- description: Agent to run. This is the primary request field.
- agent:
- deprecated: true
- description: Compatibility alias for agent_id; do not send both fields.
- oneOf:
- - type: string
- - $ref: '#/components/schemas/AgentReference'
- agent_version: { type: integer, minimum: 1 }
- initial_events:
- type: array
- minItems: 1
- description: Must contain at least one user.message event.
- items: { $ref: '#/components/schemas/SessionEventInput' }
- schedule: { type: [object, 'null'] }
- timeout_policy:
- type: object
- additionalProperties: false
+ server_url: { type: string }
+ secret_id: { type: string }
+ status: { type: string, enum: [active, disabled] }
+ row_version: { type: integer }
+ created_at: { type: string, format: date-time }
+ updated_at: { type: string, format: date-time }
+ MCPConnectionList:
+ allOf:
+ - $ref: '#/components/schemas/AgentsOffsetList'
+ - type: object
properties:
- hard_timeout_sec: { type: [integer, 'null'], minimum: 60, maximum: 604800 }
- inactivity_timeout_sec: { type: integer, minimum: 180, maximum: 3600 }
- resources: { type: array, maxItems: 0, description: Reserved; only an empty array is accepted. }
- vault_ids: { type: array, maxItems: 0, description: Reserved; only an empty array is accepted. }
- metadata: { type: object, additionalProperties: true }
- config: { type: object }
-
- UpdateDeploymentRequest:
+ data: { type: array, items: { $ref: '#/components/schemas/MCPConnection' } }
+ SkillUploadForm:
+ type: object
+ required: [files]
+ properties:
+ files: { type: string, format: binary, description: 'Exactly one ZIP part named files (or files[]), up to 5 MiB, containing SKILL.md with name and description frontmatter.' }
+ SkillVersionUploadForm:
+ type: object
+ required: [files]
+ properties:
+ files: { type: string, format: binary, description: "Exactly one ZIP part, up to 5 MiB." }
+ default: { type: string, enum: ['true', 'false'], description: "Make this version the default." }
+ SkillUpdateRequest:
type: object
- minProperties: 1
additionalProperties: false
properties:
- name: { type: string }
- description: { type: string }
- metadata: { type: object, additionalProperties: true }
- agent:
- oneOf:
- - type: string
- - $ref: '#/components/schemas/AgentReference'
- initial_events:
- type: array
- minItems: 1
- description: Must contain at least one user.message event.
- items: { $ref: '#/components/schemas/SessionEventInput' }
- schedule: { type: [object, 'null'] }
- timeout_policy:
- type: object
- additionalProperties: false
- properties:
- hard_timeout_sec: { type: [integer, 'null'], minimum: 60, maximum: 604800 }
- inactivity_timeout_sec: { type: integer, minimum: 180, maximum: 3600 }
- expected_version: { type: integer, minimum: 1 }
- agent_binding:
- type: object
- additionalProperties: false
- required: [agent_id]
- properties:
- agent_id: { type: string }
- agent_version: { type: integer, minimum: 1 }
- agent_config: { type: object, additionalProperties: true }
- notification_settings:
- type: [object, 'null']
- description: Set to null to remove the saved Feishu webhook. A configured webhook is encrypted and never returned.
- additionalProperties: false
- required: [feishu_webhook_url]
- properties:
- feishu_webhook_url:
- type: [string, 'null']
- format: uri
- pattern: '^https://open\.feishu\.cn/open-apis/bot/v2/hook/[A-Za-z0-9_-]+$'
- writeOnly: true
- description: Official Feishu custom-bot webhook. Set to null, or set notification_settings itself to null, to remove it.
-
- Deployment:
+ default_version: { type: string }
+ status: { type: string, enum: [active, disabled] }
+ metadata: { type: object, description: "Market metadata. Requires row_version." }
+ row_version: { type: integer, minimum: 1 }
+ Skill:
type: object
- description: Deployment resource. List responses omit initial_events and config; detail responses may add resolved binding fields.
- required: [id, type, name, description, metadata, resources, vault_ids, agent, agent_id, agent_version, schedule, timeout_policy, status, version, next_run_at, creation_mode, created_at, updated_at]
+ required: [id, object, name, description, default_version, latest_version, status, visibility, row_version, metadata, created_at, updated_at]
properties:
- id: { type: string, pattern: '^depl_' }
- type: { type: string, enum: [deployment] }
+ id: { type: string, description: "Skill ID (`skl_` prefix)." }
+ object: { type: string, const: skill }
name: { type: string }
description: { type: string }
- metadata: { type: [object, 'null'], additionalProperties: true }
- resources: { type: array, items: { type: object, additionalProperties: true } }
- vault_ids: { type: array, items: { type: string } }
- agent: { $ref: '#/components/schemas/AgentReference' }
- agent_id: { type: string }
- agent_version: { type: [integer, 'null'] }
- initial_events:
- type: [array, 'null']
- items: { $ref: '#/components/schemas/SessionEventInput' }
- schedule: { type: [object, 'null'], additionalProperties: true }
- timeout_policy: { type: object, additionalProperties: true }
- config: { type: [object, 'null'], additionalProperties: true }
- status: { type: string, enum: [active, paused, archived] }
- version: { type: integer, minimum: 1 }
- next_run_at: { type: [string, 'null'], format: date-time }
- creation_mode: { type: string, enum: [declarative, advanced] }
- runtime: { type: string, description: Present for declarative Deployments. }
- skills: { type: array, items: { type: string }, description: Present for declarative Deployments. }
- agent_binding: { type: object, additionalProperties: true }
- agent_config: { type: object, additionalProperties: true }
- notification_settings:
- type: object
+ default_version: { type: string }
+ latest_version: { type: string }
+ status: { type: string }
+ visibility: { type: string, enum: [public, private] }
+ row_version: { type: integer }
+ metadata: { type: object }
+ created_at: { type: integer }
+ updated_at: { type: integer }
+ failure_code: { type: string }
+ SkillList:
+ allOf:
+ - $ref: '#/components/schemas/AgentsCursorList'
+ - type: object
properties:
- feishu_webhook_configured: { type: boolean }
- created_at: { type: string, format: date-time }
- updated_at: { type: string, format: date-time }
-
- EmptyObjectRequest:
- type: object
- maxProperties: 0
- additionalProperties: false
-
- DeploymentRun:
+ data: { type: array, items: { $ref: '#/components/schemas/Skill' } }
+ SkillVersion:
type: object
- description: Durable result of one Deployment trigger's Session creation attempt; it never mirrors later Session state.
- required: [id, type, agent, deployment_id, session_id, error, trigger_context, created_at]
+ required: [object, id, skill_id, version, name, description, created_at]
properties:
- id: { type: string, pattern: '^drun_' }
- type: { type: string, enum: [deployment_run] }
- agent: { $ref: '#/components/schemas/AgentReference' }
- deployment_id: { type: string, pattern: '^depl_' }
- session_id: { type: [string, 'null'], pattern: '^sess_' }
- error:
- type: [object, 'null']
- required: [type, code, message]
+ object: { type: string, const: skill.version }
+ id: { type: string, description: ':' }
+ skill_id: { type: string }
+ version: { type: string }
+ name: { type: string }
+ description: { type: string }
+ created_at: { type: integer }
+ failure_code: { type: string }
+ SkillVersionList:
+ allOf:
+ - $ref: '#/components/schemas/AgentsCursorList'
+ - type: object
properties:
- type: { type: string }
- code: { type: string }
- message: { type: string }
- trigger_context:
- oneOf:
- - type: 'null'
- - type: object
- required: [type]
- properties: { type: { type: string, enum: [manual] } }
- - type: object
- required: [type, scheduled_at]
- properties:
- type: { type: string, enum: [schedule] }
- scheduled_at: { type: string, format: date-time }
- created_at: { type: string, format: date-time }
-
- AgentReference:
+ data: { type: array, items: { $ref: '#/components/schemas/SkillVersion' } }
+ SkillDeleted:
type: object
- required: [id, type, version]
+ required: [id, object, deleted]
properties:
id: { type: string }
- type: { type: string, enum: [agent] }
- version: { type: integer, minimum: 1 }
+ object: { type: string, const: skill.deleted }
+ deleted: { type: boolean, const: true }
diff --git a/sandbox/index.md b/sandbox/index.md
index 7773bc9b..9977c2fa 100644
--- a/sandbox/index.md
+++ b/sandbox/index.md
@@ -43,3 +43,7 @@ An empty list does not prove zero usage, and missing events do not prove an oper
## Template visibility
Templates can be public or private to your organization. Counts are not fixed. Entries without names remain visible, with aliases or IDs used as fallback labels. Your organization’s access and template availability determine which templates you can see and use.
+
+## Sandboxes and Agent Sessions
+
+Agents Sessions run in SandBase's hosted cloud. They are separate from the Sandbox instances you create with the E2B SDK, and the Agents API does not expose Sandbox instances. See [Sessions](/agents/sessions).
diff --git a/scripts/validate-ai-readable.mjs b/scripts/validate-ai-readable.mjs
index f93bd4d0..a1d2330d 100644
--- a/scripts/validate-ai-readable.mjs
+++ b/scripts/validate-ai-readable.mjs
@@ -23,6 +23,16 @@ const forbidden = [
['anthropic/claude-sonnet-4-20250514', 'stale example model'],
['bfl/flux-1/schnell', 'stale example model'],
['GET /v1/models/{name}', 'incorrect model detail path parameter'],
+ ['/v1/sessions', 'retired Session path; use /v1/agents/sessions'],
+ ['/v1/deployments', 'retired Deployment API; use /v1/schedules'],
+ ['/v1/deployment_runs', 'retired DeploymentRun API'],
+ ['/v1/endpoints', 'retired Endpoint API; use /v1/services'],
+ ['/v1/credentials', 'retired Credentials API; use /v1/secrets'],
+ ['/v1/skills/files', 'retired Skill file upload'],
+ ['drun_', 'retired DeploymentRun ID prefix'],
+ ['depl_', 'retired Deployment ID prefix'],
+ ['initial_events', 'retired Deployment field'],
+ ['user.message', 'retired Session event type'],
]
for (const [needle, label] of forbidden) {
@@ -42,6 +52,9 @@ for (const required of [
'GET /v1/models/{id_or_name}',
'GET /v1/run/{id}',
'GET /v1/tasks/{id}/cost',
+ 'POST /v1/agents/sessions',
+ 'POST /v1/services/{service_id}/invoke',
+ 'POST /v1/schedules',
]) {
assert.ok(combined.includes(required), `AI-readable documentation must include ${required}`)
}
diff --git a/scripts/validate-api-reference-contract.mjs b/scripts/validate-api-reference-contract.mjs
index e48d35e4..e405b9e2 100644
--- a/scripts/validate-api-reference-contract.mjs
+++ b/scripts/validate-api-reference-contract.mjs
@@ -69,11 +69,7 @@ function operationParameters(pathItem, operation, document) {
const apiReferenceKeys = []
const inlineApiReferences = []
-for (const file of walk('api-reference').filter((entry) =>
- entry.endsWith('.md')
- && !entry.startsWith('api-reference/embeds/')
- && !entry.startsWith('api-reference/environments/')
-)) {
+for (const file of walk('api-reference').filter((entry) => entry.endsWith('.md'))) {
const source = readFileSync(file, 'utf8')
const match = source.match(/^apiReferenceKey:\s*["']([^"']+)["']\s*$/m)
if (match) apiReferenceKeys.push([file, match[1]])
diff --git a/scripts/validate-built-public-surface.mjs b/scripts/validate-built-public-surface.mjs
index 04833bf9..f16c9b03 100644
--- a/scripts/validate-built-public-surface.mjs
+++ b/scripts/validate-built-public-surface.mjs
@@ -30,6 +30,14 @@ for (const excludedUrl of ['/docs/README', '/docs/CONTRIBUTING', '/docs/DEPLOYME
const deploymentAlias = readFileSync(path.join(dist, 'agents', 'deployments.html'), 'utf8')
assert.match(deploymentAlias, //, 'Legacy deployment guide must be noindex')
+for (const retiredResource of ['endpoints', 'deployments', 'credentials']) {
+ const page = readFileSync(path.join(dist, 'api-reference', retiredResource, 'index.html'), 'utf8')
+ assert.match(page, //, `Retired ${retiredResource} notice must be noindex`)
+ assert.ok(!sitemap.includes(`/docs/api-reference/${retiredResource}/`), `Retired ${retiredResource} notice must not be in the sitemap`)
+}
+for (const archived of ['guides/site-agent-integration.html', 'use-cases', 'api-reference/embeds', 'api-reference/environments', 'api-reference/endpoints/mcp.html', 'api-reference/endpoints/acp.html', 'api-reference/deployments/create.html', 'api-reference/sessions/list-events.html']) {
+ assert.ok(!existsSync(path.join(dist, archived)), `${archived} must not be published`)
+}
const storeModelsAlias = readFileSync(path.join(dist, 'store', 'models.html'), 'utf8')
assert.match(storeModelsAlias, //, 'Legacy Store Models alias must be noindex')
@@ -52,7 +60,7 @@ const forbidden = [
[/\/events\/webhooks(?:\/|\b)/i, 'Sandbox event webhook API'],
[/\/v1\/endpoints\/[A-Za-z0-9_{}%.-]+\/mcp\b/i, 'Endpoint MCP transport'],
[/\/v1\/endpoint_runtime_profiles\b/i, 'Endpoint runtime-profile discovery'],
- [/\/v1\/mcp(?:\/|\b)/i, 'generic MCP transport or discovery'],
+ [/\/v1\/mcp(?!-connections)(?:\/|\b)/i, 'generic MCP transport or discovery'],
[/\/v1\/mcp\/(?:servers|[A-Za-z0-9_{}%.-]+\/config)\b/i, 'MCP discovery or runtime config'],
[/\/mcp\/[A-Za-z0-9_{}%.-]+\/sse\b/i, 'MCP SSE proxy'],
[/\/v1\/skills\/[A-Za-z0-9_{}%.-]+\/mcp-publications\b/i, 'Skill MCP publication creation'],
@@ -89,7 +97,6 @@ function inspect(directory) {
}
}
for (const [pattern, label] of forbidden) {
- if (label === 'Endpoint MCP transport' && (/^(?:api-reference\/endpoints\/(?:index|mcp)|agents\/services)\.html$/.test(relative) || /(?:api-reference_endpoints_(?:index|mcp)|agents_services)\.md\./.test(relative))) continue
if (label === 'Sandbox API') {
// The experimental documentation routes and their dedicated lazy UI chunk are public.
// Keep all former Sandbox API reference pages and unrelated bundles protected.
diff --git a/scripts/validate-doc-content.mjs b/scripts/validate-doc-content.mjs
index 083aa2df..dc4f9d6c 100644
--- a/scripts/validate-doc-content.mjs
+++ b/scripts/validate-doc-content.mjs
@@ -7,10 +7,7 @@ const roots = ['admin', 'agents', 'sandbox', 'api-reference', 'for-agents', 'get
const excluded = [
'agents/deployments.md',
'agents/endpoint-quickstart.md',
- 'api-reference/embeds/',
- 'api-reference/environments/',
'api-reference/webhooks.md',
- 'guides/site-agent-integration.md',
'setup/cli.md',
'setup/groups.md',
]
@@ -34,6 +31,8 @@ const forbidden = [
[/\/v1\/environments\b/, 'non-public Environment management API'],
[/\/v1\/embeds\b/, 'retired Embed Config API'],
[/\/v1\/endpoints\/\{[^}]+\}\/mcp\b/, 'non-public Endpoint MCP transport'],
+ [/\b(?:GET|POST|PUT|PATCH|DELETE) \/v1\/(?:sessions|endpoints|deployments|deployment_runs|credentials|skills\/files)\b/, 'retired Agents route; use the sandbase-agents resources'],
+ [/\bdrun_|\bdepl_|\binitial_events\b|"user\.message"/, 'retired Deployment or Session contract'],
[/https:\/\/api\.sandbase\.ai\/sandboxes\b/, 'non-public Sandbox API'],
[/\bEmbed Configs?\b/i, 'retired Embed Config product'],
[/\bEndpoints API\b/, 'legacy product label; use Services API'],
@@ -63,7 +62,6 @@ let inspected = 0
for (const filename of files) {
const source = readFileSync(filename, 'utf8')
for (const [pattern, label] of forbidden) {
- if (label === 'non-public Endpoint MCP transport' && ['api-reference/endpoints/index.md', 'api-reference/endpoints/mcp.md', 'agents/services.md'].includes(filename)) continue
assert.ok(!pattern.test(source), `${filename} exposes ${label}: ${pattern}`)
}
inspected += 1
@@ -133,17 +131,21 @@ assert.match(agentOverview, /pinned to that version/, 'Agent guide must explain
const credentialGuide = readFileSync('agents/api-credentials.md', 'utf8')
assert.match(credentialGuide, /Developer → Credentials/, 'Credential guide must use current Console navigation')
assert.doesNotMatch(credentialGuide, /Agents → \[Your Agent\] → Credentials/, 'Credential guide must not use retired Agent-level navigation')
-assert.match(credentialGuide, /Workspace[\s\S]+Agent[\s\S]+Service/, 'Credential guide must describe the current scope choices')
+assert.match(credentialGuide, /mcp_bearer/, 'Credential guide must describe MCP Bearer Secrets')
+assert.match(credentialGuide, /not environment variables/, 'Credential guide must not imply runtime environment injection')
const servicesGuide = readFileSync('agents/services.md', 'utf8')
-assert.match(servicesGuide, /202 Accepted/, 'Services guide must document asynchronous REST acceptance')
-assert.match(servicesGuide, /same Service/, 'Services guide must explain Session continuation binding')
+assert.match(servicesGuide, /202 Accepted/, 'Services guide must document asynchronous invocation')
+assert.match(servicesGuide, /Idempotency-Key/, 'Services guide must document idempotent invocation')
+assert.doesNotMatch(servicesGuide, /session_id`? to continue|continue (?:a|the) Session/i, 'Services no longer continue Sessions')
const schedulesGuide = readFileSync('agents/schedules.md', 'utf8')
-assert.match(schedulesGuide, /public DeploymentRun object has no `status` field/, 'Schedules guide must match the public DeploymentRun schema')
-assert.match(schedulesGuide, /status=pending\|succeeded\|failed/, 'Schedules guide must document trigger-outcome status filters')
+assert.match(schedulesGuide, /`cron`/, 'Schedules guide must document cron timing')
+assert.match(schedulesGuide, /`timezone`/, 'Schedules guide must document the timezone')
+assert.match(schedulesGuide, /skipped/, 'Schedules guide must document skipped Runs')
const sessionsGuide = readFileSync('agents/sessions.md', 'utf8')
-assert.match(sessionsGuide, /does not expose a `status` field/, 'Sessions guide must not invent a DeploymentRun response status')
+assert.match(sessionsGuide, /\/v1\/agents\/sessions/, 'Sessions guide must use the current Session path')
+assert.match(sessionsGuide, /live-only/, 'Sessions guide must explain that the native stream is live-only')
const setupGuide = readFileSync('setup/index.md', 'utf8')
assert.doesNotMatch(setupGuide, /^The installer requires Node\.js 20 or newer\./m, 'Setup prerequisites must remain client-specific')
diff --git a/scripts/validate-http-references.mjs b/scripts/validate-http-references.mjs
index 7afd57b2..869b65f3 100644
--- a/scripts/validate-http-references.mjs
+++ b/scripts/validate-http-references.mjs
@@ -25,13 +25,6 @@ const roots = ['agents', 'api-reference', 'for-agents', 'getting-started', 'guid
const references = []
function walk(directory) {
- const relativeDirectory = path.relative(root, directory)
- const hiddenApiReferenceRoots = ['api-reference/embeds', 'api-reference/environments']
- if (
- hiddenApiReferenceRoots.some(
- (hiddenRoot) => relativeDirectory === hiddenRoot || relativeDirectory.startsWith(`${hiddenRoot}/`),
- )
- ) return
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
if (entry.name === '_archived') continue
const filename = path.join(directory, entry.name)
diff --git a/scripts/validate-public-api-surface.mjs b/scripts/validate-public-api-surface.mjs
index c89ed3f7..843ce1c5 100644
--- a/scripts/validate-public-api-surface.mjs
+++ b/scripts/validate-public-api-surface.mjs
@@ -1,6 +1,6 @@
import { maskApprovedSandboxPageHrefs, sandboxAPIPath } from './public-sandbox-page-links.mjs'
import assert from 'node:assert/strict'
-import { readdirSync, readFileSync } from 'node:fs'
+import { existsSync, readdirSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { parse } from 'yaml'
@@ -130,16 +130,22 @@ const requiredPublicPaths = [
'/v1/images/edits:',
'/v1/assets:',
'/v1/assets/{id}:',
- '/v1/skills/files:',
+ '/v1/agents:',
+ '/v1/agents/{agent_id}:',
+ '/v1/agents/catalog:',
+ '/v1/agents/sessions:',
+ '/v1/agents/sessions/{session_id}/events:',
+ '/v1/services:',
+ '/v1/services/{service_id}/invoke:',
+ '/v1/schedules:',
+ '/v1/schedules/{schedule_id}/runs:',
+ '/v1/schedules/{schedule_id}/timing:',
+ '/v1/secrets:',
+ '/v1/secrets/{secret_id}/rotate:',
+ '/v1/mcp-connections:',
+ '/v1/mcp-connections/{connection_id}/enable:',
'/v1/skills:',
- '/v1/skills/{id}:',
- '/v1/credentials:',
- '/v1/credentials/{id}:',
- '/v1/credentials/{id}/rotate:',
- '/v1/deployments/{id}/pause:',
- '/v1/deployments/{id}/unpause:',
- '/v1/deployments/{id}/archive:',
- '/v1/deployments/{id}/notifications/feishu/test:',
+ '/v1/skills/{skill_id}/versions:',
]
for (const requiredPath of requiredPublicPaths) {
@@ -152,7 +158,8 @@ for (const [publicPath, pathItem] of Object.entries(openapiDocument.paths)) {
const operation = pathItem[method]
if (!operation) continue
for (const [status, response] of Object.entries(operation.responses)) {
- if (Number(status) >= 200 && Number(status) < 300 && status !== '204') {
+ // x-sandbase-empty-body marks implemented 2xx acknowledgements that carry no body.
+ if (Number(status) >= 200 && Number(status) < 300 && status !== '204' && response['x-sandbase-empty-body'] !== true) {
assert.ok(
Object.values(response.content ?? {}).some((media) => media.schema),
`${method.toUpperCase()} ${publicPath} ${status} must document its implemented success envelope`,
@@ -184,117 +191,11 @@ const jsonErrorSchema = (method, publicPath, status) =>
const jsonSuccessSchema = (method, publicPath, status = '200') =>
openapiDocument.paths[publicPath]?.[method]?.responses?.[status]?.content?.['application/json']?.schema
-for (const publicPath of ['/v1/sessions', '/v1/sessions/{id}/events', '/v1/deployments']) {
- assert.ok(
- jsonSuccessSchema('get', publicPath)?.required?.includes('data'),
- `GET ${publicPath} must require the always-emitted data collection`,
- )
-}
-
-for (const status of ['400', '500', '503']) {
- assert.equal(
- jsonErrorSchema('post', '/v1/skills/files', status)?.$ref,
- '#/components/schemas/TaskCostError',
- `Skill file upload ${status} must document its implemented flat error envelope`,
- )
-}
-for (const [method, publicPath, statuses] of [
- ['post', '/v1/skills', ['400', '500']],
- ['get', '/v1/skills', ['500']],
- ['get', '/v1/skills/{id}', ['404', '500']],
- ['put', '/v1/skills/{id}', ['400', '404', '500']],
- ['delete', '/v1/skills/{id}', ['404', '500']],
-]) {
- for (const status of statuses) {
- assert.equal(
- jsonErrorSchema(method, publicPath, status)?.$ref,
- '#/components/schemas/APIError',
- `${method.toUpperCase()} ${publicPath} ${status} must document its implemented typed error envelope`,
- )
- }
-}
-assert.deepEqual(
- jsonErrorSchema('post', '/v1/skills', '401')?.oneOf?.map((schema) => schema.$ref),
- ['#/components/schemas/TaskCostError', '#/components/schemas/APIError'],
- 'Skill creation must document both middleware and creator-context authentication envelopes',
-)
-for (const [method, publicPath, statuses] of [
- ['post', '/v1/credentials', ['400', '500']],
- ['get', '/v1/credentials', ['500']],
- ['get', '/v1/credentials/{id}', ['404', '500']],
- ['patch', '/v1/credentials/{id}', ['400', '404', '500']],
- ['post', '/v1/credentials/{id}/rotate', ['400', '404', '500']],
-]) {
- for (const status of statuses) {
- assert.equal(
- jsonErrorSchema(method, publicPath, status)?.$ref,
- '#/components/schemas/LegacyResourceError',
- `${method.toUpperCase()} ${publicPath} ${status} must document its implemented legacy error envelope`,
- )
- }
-}
-for (const [method, publicPath, statuses] of [
- ['post', '/v1/deployments', ['400', '404', '415', '422', '500']],
- ['get', '/v1/deployments', ['400', '500']],
- ['get', '/v1/deployments/{id}', ['404', '500']],
- ['patch', '/v1/deployments/{id}', ['400', '404', '409', '422', '500']],
- ['post', '/v1/deployments/{id}', ['400', '404', '409', '422', '500']],
- ['delete', '/v1/deployments/{id}', ['404', '409', '500']],
-]) {
- for (const status of statuses) {
- assert.equal(
- jsonErrorSchema(method, publicPath, status)?.$ref,
- '#/components/schemas/APIError',
- `${method.toUpperCase()} ${publicPath} ${status} must document its implemented typed error envelope`,
- )
- }
-}
-for (const [method, publicPath, statuses] of [
- ['post', '/v1/deployments/{id}/run', ['400', '404', '409', '500', '503']],
- ['post', '/v1/deployments/{id}/pause', ['404', '409', '500']],
- ['post', '/v1/deployments/{id}/unpause', ['404', '409', '500']],
- ['post', '/v1/deployments/{id}/archive', ['404', '409', '500']],
- ['post', '/v1/deployments/{id}/notifications/feishu/test', ['400', '404', '409', '502']],
- ['post', '/v1/deployments/{id}/runs', ['400', '404', '409', '500', '503']],
- ['get', '/v1/deployments/{id}/runs', ['400', '500', '503']],
- ['get', '/v1/deployments/{id}/runs/{drun_id}', ['404', '409', '503']],
- ['get', '/v1/deployment_runs', ['400', '500', '503']],
- ['get', '/v1/deployment_runs/{id}', ['404', '503']],
-]) {
- for (const status of statuses) {
- assert.equal(
- jsonErrorSchema(method, publicPath, status)?.$ref,
- '#/components/schemas/APIError',
- `${method.toUpperCase()} ${publicPath} ${status} must document its implemented typed error envelope`,
- )
- }
-}
-for (const [method, publicPath, statuses] of [
- ['patch', '/v1/endpoints/{id}', ['400', '404', '409', '422', '500']],
- ['post', '/v1/endpoints/{id}', ['400', '404', '409', '422', '500']],
- ['delete', '/v1/endpoints/{id}', ['404', '500']],
-]) {
- for (const status of statuses) {
- assert.equal(
- jsonErrorSchema(method, publicPath, status)?.$ref,
- '#/components/schemas/APIError',
- `${method.toUpperCase()} ${publicPath} ${status} must document its implemented typed error envelope`,
- )
- }
-}
-for (const status of ['404', '409', '500']) {
- assert.equal(
- jsonErrorSchema('delete', '/v1/sessions/{id}', status)?.$ref,
- '#/components/schemas/SessionAPIError',
- `DELETE /v1/sessions/{id} ${status} must document its implemented typed error envelope`,
- )
-}
assert.match(config, /'_archived\/\*\*'/, 'Archived API pages must stay excluded from the public build')
-assert.match(config, /'guides\/site-agent-integration\.md'/, 'Legacy Site Agent guide must stay excluded from the public build')
-assert.match(config, /'use-cases\/\*\*'/, 'Legacy Site Agent use cases must stay excluded from the public build')
+for (const archivedSource of ['guides/site-agent-integration.md', 'use-cases', 'api-reference/embeds', 'api-reference/environments', 'api-reference/endpoints/mcp.md', 'api-reference/endpoints/acp.md']) {
+ assert.ok(!existsSync(path.join(root, archivedSource)), `${archivedSource} is retired and must live under _archived/`)
+}
assert.match(config, /'api-reference\/webhooks\.md'/, 'Sandbox event webhook reference must stay excluded from the public build')
-assert.match(config, /'api-reference\/embeds\/\*\*'/, 'Unmaintained Embed Config pages must stay excluded from the public build')
-assert.match(config, /'api-reference\/environments\/\*\*'/, 'Internal Environment API pages must stay excluded from the public build')
assert.match(config, /'agents\/endpoint-quickstart\.md'/, 'Retired Service quickstart must stay excluded from the public build')
assert.match(config, /'admin\/api-keys\.md'/, 'Duplicate API Keys guide must stay excluded from the public build')
assert.match(config, /'setup\/cli\.md'/, 'Duplicate CLI setup page must stay excluded from the public build')
@@ -311,22 +212,18 @@ assert.match(platformSidebar, /text: 'Services'/, 'Platform API sidebar must use
assert.doesNotMatch(platformSidebar, /text: 'Endpoints'/, 'Platform API sidebar must not expose the legacy Endpoints product name')
assert.match(platformSidebar, /text: 'Schedules'/, 'Platform API sidebar must use the Schedules product name')
assert.doesNotMatch(platformSidebar, /text: 'Deployments'/, 'Platform API sidebar must not expose the compatibility Deployment resource as the product name')
-for (const anchor of [
- 'create-a-credential',
- 'list-credentials',
- 'get-a-credential',
- 'update-a-credential',
- 'rotate-a-credential',
+for (const [resource, anchors] of [
+ ['mcp-connections', ['create-a-connection', 'list-connections', 'get-a-connection', 'replace-a-connection', 'enable-a-connection', 'disable-a-connection']],
+ ['secrets', ['create-a-secret', 'list-secrets', 'get-a-secret', 'rotate-a-secret', 'revoke-a-secret']],
]) {
- assert.match(
- platformSidebar,
- new RegExp(`/api-reference/credentials/#${anchor}`),
- `Credentials sidebar must expose the ${anchor} operation`,
- )
+ const overview = readFileSync(new URL(`../api-reference/${resource}/index.md`, import.meta.url), 'utf8')
+ for (const anchor of anchors) {
+ assert.match(platformSidebar, new RegExp(`/api-reference/${resource}/#${anchor}`), `${resource} sidebar must expose the ${anchor} operation`)
+ assert.match(overview, new RegExp(`^## ${anchor.replace(/-/g, ' ')}$`, 'mi'), `${resource} overview must keep the ${anchor} anchor`)
+ }
}
-assert.match(openapi, /^ \/v1\/endpoints\/\{[^}]+\}\/mcp:$/m, 'Endpoint MCP transport must be documented')
-assert.match(openapi, /^ post:/m, 'Endpoint MCP transport must support POST')
-assert.match(openapi, /^ delete:/m, 'Endpoint MCP transport must support DELETE')
+assert.doesNotMatch(platformSidebar, /text: 'Credentials'|\/api-reference\/(?:credentials|endpoints|deployments)\//, 'Platform API sidebar must not link retired resources')
+assert.doesNotMatch(openapi, /^ \/v1\/(?:endpoints|deployments|deployment_runs|sessions|credentials|skills\/files)(?:[/{:]|$)/m, 'Retired Agents resources must not be public')
assert.doesNotMatch(openapi, /^ \/v1\/endpoint_runtime_profiles:$/m, 'Endpoint runtime profiles that reveal MCP transport must not be public')
assert.doesNotMatch(openapi, /^\s+mcp_url:$/m, 'Endpoint MCP transport URL must not be public')
assert.doesNotMatch(openapi, /^ \/v1\/generations(?:\/\{[^}]+\})?:$/m, 'Withdrawn generation paths must not be public')
@@ -584,91 +481,81 @@ for (const content of [apiKeyGuide, authenticationReference]) {
assert.match(content, /64 hexadecimal characters/, 'API key docs must describe the current sk- key format')
assert.match(content, /(?:Existing|Legacy) `sk-sb-\.\.\.` keys remain/, 'API key docs must preserve legacy-key guidance')
}
-const agentListPage = openapi.match(/^ AgentListPage:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(agentListPage, /required: \[data\]/, 'Agent list responses must require data')
-assert.doesNotMatch(agentListPage, /required: \[[^\]]*next_page/, 'Agent list next_page must be optional on the final page')
-const nestedDeploymentRuns = openapi.match(/^ \/v1\/deployments\/\{id\}\/runs:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-assert.match(nestedDeploymentRuns, /name: limit[\s\S]*?name: page/, 'Nested DeploymentRun lists must document implemented cursor pagination')
-assert.match(nestedDeploymentRuns, /next_page:\n\s+type: string/, 'Nested DeploymentRun lists must document their emitted cursor')
-const triggerDeploymentRun = nestedDeploymentRuns.match(/^ post:\n[\s\S]*?(?=^ get:)/m)?.[0] ?? ''
-assert.match(triggerDeploymentRun, /EmptyObjectRequest/, 'Deployment triggers must document their empty-object-only request body')
-for (const status of ['200', '400', '401', '402', '403', '404', '409', '500', '503']) {
- assert.match(triggerDeploymentRun, new RegExp(`'${status}':`), `Deployment triggers must document ${status} responses`)
-}
-const listNestedDeploymentRuns = nestedDeploymentRuns.match(/^ get:\n[\s\S]*/m)?.[0] ?? ''
-assert.match(listNestedDeploymentRuns, /required: \[data\]/, 'Nested DeploymentRun lists must require data')
-for (const status of ['200', '400', '401', '402', '403', '500', '503']) {
- assert.match(listNestedDeploymentRuns, new RegExp(`'${status}':`), `Nested DeploymentRun lists must document ${status} responses`)
-}
-assert.doesNotMatch(listNestedDeploymentRuns, /'404':/, 'Nested DeploymentRun lists return an empty page for an unknown Deployment')
-const triggerDeploymentRunAlias = openapi.match(/^ \/v1\/deployments\/\{id\}\/run:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-assert.match(triggerDeploymentRunAlias, /EmptyObjectRequest/, 'The Deployment trigger alias must preserve the empty-object-only request body')
-for (const status of ['200', '400', '401', '402', '403', '404', '409', '500', '503']) {
- assert.match(triggerDeploymentRunAlias, new RegExp(`'${status}':`), `The Deployment trigger alias must document ${status} responses`)
-}
-const nestedDeploymentRun = openapi.match(/^ \/v1\/deployments\/\{id\}\/runs\/\{drun_id\}:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-for (const status of ['200', '401', '402', '403', '404', '409', '503']) {
- assert.match(nestedDeploymentRun, new RegExp(`'${status}':`), `Nested DeploymentRun reads must document ${status} responses`)
-}
-const deploymentsPath = openapi.match(/^ \/v1\/deployments:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-assert.match(deploymentsPath, /name: status\n\s+in: query\n\s+style: form\n\s+explode: true\n\s+schema:\n\s+type: array/, 'Deployment status filters must be repeatable')
-const deploymentRunsPath = openapi.match(/^ \/v1\/deployment_runs:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-for (const field of ['trigger_type', 'status']) {
- assert.match(deploymentRunsPath, new RegExp(`name: ${field}\\n\\s+in: query[\\s\\S]*?type: array`), `DeploymentRun ${field} filters must be repeatable`)
-}
-for (const field of ['created_at_gt', 'created_at_gte', 'created_at_lt', 'created_at_lte']) {
- assert.match(deploymentRunsPath, new RegExp(`name: ${field}, in: query, deprecated: true`), `DeploymentRun lists must document the ${field} compatibility filter`)
-}
-for (const status of ['200', '400', '401', '402', '403', '500', '503']) {
- assert.match(deploymentRunsPath, new RegExp(`'${status}':`), `Global DeploymentRun lists must document ${status} responses`)
-}
-const globalDeploymentRun = openapi.match(/^ \/v1\/deployment_runs\/\{id\}:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-for (const status of ['200', '401', '402', '403', '404', '503']) {
- assert.match(globalDeploymentRun, new RegExp(`'${status}':`), `Global DeploymentRun reads must document ${status} responses`)
-}
-assert.doesNotMatch(globalDeploymentRun, /'409':/, 'Global DeploymentRun reads return pending records instead of a conflict')
-const emptyObjectRequest = openapi.match(/^ EmptyObjectRequest:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(emptyObjectRequest, /maxProperties: 0/, 'Deployment triggers must reject request fields')
-const deploymentRunSchema = openapi.match(/^ DeploymentRun:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(deploymentRunSchema, /trigger_context:\n\s+oneOf:\n\s+- type: 'null'/, 'DeploymentRun trigger context must tolerate invalid legacy JSON projected as null')
-assert.doesNotMatch(deploymentRunSchema, /^ status:/m, 'DeploymentRun responses must not expose internal creation status')
-const deploymentsPathForCreate = openapi.match(/^ \/v1\/deployments:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-const createDeploymentOperation = deploymentsPathForCreate.match(/^ post:\n[\s\S]*?(?=^ get:)/m)?.[0] ?? ''
-for (const mediaType of ['application/json', 'application/yaml', 'application/x-yaml']) {
- assert.match(createDeploymentOperation, new RegExp(mediaType.replace('/', '\\/') + ':'), `Deployment creation must document ${mediaType}`)
-}
-for (const status of ['200', '400', '401', '402', '403', '404', '415', '422', '500']) {
- assert.match(createDeploymentOperation, new RegExp(`'${status}':`), `Deployment creation must document ${status} responses`)
-}
-assert.match(createDeploymentOperation, /CreateDeclarativeDeploymentRequest/, 'Deployment creation must document declarative runtime definitions')
-const declarativeDeploymentRequest = openapi.match(/^ CreateDeclarativeDeploymentRequest:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(declarativeDeploymentRequest, /required: \[name, runtime, initial_events\]/, 'Declarative Deployments must require runtime and initial events')
-assert.doesNotMatch(declarativeDeploymentRequest, /mcp_servers:|protocols:/, 'Declarative Deployment creation must not expose hidden transports')
-assert.doesNotMatch(declarativeDeploymentRequest, /metadata:/, 'Declarative Deployment creation must not document ignored metadata')
-const advancedDeploymentRequest = openapi.match(/^ CreateDeploymentRequest:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(advancedDeploymentRequest, /anyOf:[\s\S]*?- required: \[agent_id\][\s\S]*?- required: \[agent\]/, 'Advanced Deployment creation must allow the implemented Agent compatibility alias')
-for (const field of ['agent_version', 'timeout_policy']) {
- assert.match(advancedDeploymentRequest, new RegExp(`^ ${field}:`, 'm'), `Advanced Deployment creation must document ${field}`)
-}
-for (const field of ['resources', 'vault_ids']) {
- assert.match(advancedDeploymentRequest, new RegExp(`${field}: \\{ type: array, maxItems: 0`), `Advanced Deployment creation must reject non-empty ${field}`)
+// --- Agents platform (sandbase-agents) contract -------------------------------
+const resolveSchema = (schema) => schema?.$ref ? schema.$ref.replace(/^#\//, '').split('/').reduce((current, segment) => current?.[segment], openapiDocument) : schema
+const agentsPlatformPrefix = /^\/v1\/(?:agents|services|schedules|secrets|mcp-connections|skills)(?:\/|$)/
+const agentsSchema = (name) => openapi.match(new RegExp(`^ ${name}:\\n[\\s\\S]*?(?=^ [A-Za-z#])`, 'm'))?.[0] ?? ''
+let agentsOperationCount = 0
+for (const [publicPath, pathItem] of Object.entries(openapiDocument.paths)) {
+ if (!agentsPlatformPrefix.test(publicPath)) continue
+ for (const method of publicMethods) {
+ const operation = pathItem[method]
+ if (!operation) continue
+ agentsOperationCount += 1
+ assert.deepEqual(operation.security, [{ BearerAuth: [] }, { AgentsApiKey: [] }], `${method.toUpperCase()} ${publicPath} must document Bearer and X-API-Key authentication`)
+ for (const [status, response] of Object.entries(operation.responses)) {
+ if (Number(status) < 400) continue
+ assert.equal(
+ response.content?.['application/json']?.schema?.$ref,
+ '#/components/schemas/AgentsError',
+ `${method.toUpperCase()} ${publicPath} ${status} must document the Agents error envelope`,
+ )
+ }
+ }
}
-const feishuTestPath = openapi.match(/^ \/v1\/deployments\/\{id\}\/notifications\/feishu\/test:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-assert.match(feishuTestPath, /maxProperties: 0/, 'Feishu notification tests must reject custom request fields')
-assert.match(feishuTestPath, /This endpoint never accepts a webhook URL or custom message/, 'Feishu notification tests must document the saved-target-only boundary')
-for (const status of ['200', '400', '401', '402', '403', '404', '409', '502']) {
- assert.match(feishuTestPath, new RegExp(`'${status}':`), `Feishu notification test must document ${status} responses`)
+assert.ok(agentsOperationCount >= 90, `Agents platform OpenAPI must cover every public route (found ${agentsOperationCount})`)
+assert.match(openapi, /AgentsApiKey:\n\s+type: apiKey\n\s+in: header\n\s+name: X-API-Key/, 'AgentsApiKey must describe the X-API-Key header')
+for (const [method, publicPath] of [
+ ['get', '/v1/agents'], ['get', '/v1/agents/sessions'], ['get', '/v1/agents/sessions/{session_id}/items'],
+ ['get', '/v1/agents/sessions/{session_id}/turns'], ['get', '/v1/skills'], ['get', '/v1/agents/catalog'],
+]) {
+ const schema = resolveSchema(jsonSuccessSchema(method, publicPath))
+ assert.ok(schema?.allOf?.some((part) => part.$ref === '#/components/schemas/AgentsCursorList') || schema === openapiDocument.components.schemas.AgentsCursorList, `GET ${publicPath} must use the cursor list envelope`)
+}
+for (const publicPath of ['/v1/services', '/v1/schedules', '/v1/services/{service_id}/runs', '/v1/schedules/{schedule_id}/runs', '/v1/secrets', '/v1/mcp-connections', '/v1/agents/{agent_id}/versions']) {
+ const schema = resolveSchema(jsonSuccessSchema('get', publicPath))
+ assert.ok(schema?.allOf?.some((part) => part.$ref === '#/components/schemas/AgentsOffsetList'), `GET ${publicPath} must use the offset list envelope`)
+}
+assert.match(agentsSchema('AgentsCursorList'), /required: \[object, data, has_more, first_id, last_id\]/, 'Cursor lists must require the emitted envelope')
+assert.match(agentsSchema('AgentsOffsetList'), /required: \[data, total, limit, offset\]/, 'Offset lists must require the emitted envelope')
+assert.match(agentsSchema('AgentsError'), /required: \[type, code, param, message\]/, 'Agents errors must document the emitted envelope')
+assert.match(agentsSchema('Agent'), /runtime_profile: \{ type: string, enum: \[codex, claude, mcode\] \}/, 'Agent runtime profiles must match the implementation')
+assert.match(agentsSchema('Session'), /status: \{ type: string, enum: \[idle, in_progress, requires_action, failed\]/, 'Session status must match the implementation')
+assert.match(agentsSchema('Session'), /lifecycle_status: \{ type: string, enum: \[active, archived\] \}/, 'Session lifecycle must match the implementation')
+assert.doesNotMatch(agentsSchema('SessionCreateRequest'), /initial_events|environment_id/, 'Session creation must not expose retired fields')
+assert.match(agentsSchema('AgentRun'), /enum: \[pending, succeeded, failed, cancelled, skipped\]/, 'Run status must match the implementation')
+assert.doesNotMatch(agentsSchema('ServiceCreateRequest'), /^ (?:schedule|model_provider):/m, 'Service creation rejects schedule and model_provider')
+assert.match(agentsSchema('ScheduleCreateRequest'), /required: \[name, agent_id, input, schedule\]/, 'Schedule creation must require input and timing')
+assert.match(agentsSchema('SecretCreateRequest'), /kind: \{ type: string, const: mcp_bearer \}/, 'Secrets only support MCP Bearer tokens')
+assert.doesNotMatch(agentsSchema('Secret'), /^ token:/m, 'Secret responses must never expose the token')
+for (const [method, publicPath] of [['delete', '/v1/secrets/{secret_id}'], ['delete', '/v1/mcp-connections/{connection_id}']]) {
+ const response = openapiDocument.paths[publicPath]?.[method]?.responses?.['204']
+ assert.ok(response && !response.content, `${method.toUpperCase()} ${publicPath} must document its empty 204 response`)
+}
+assert.equal(openapiDocument.paths['/v1/agents/sessions/{session_id}/events']?.post?.responses?.['202']?.['x-sandbase-empty-body'], true, 'Session input acceptance has no body')
+for (const publicPath of ['/v1/agents/sessions/{session_id}/events', '/v1/agents/sessions/{session_id}/events/stream']) {
+ assert.ok(openapiDocument.paths[publicPath]?.get?.responses?.['200']?.content?.['text/event-stream'], `GET ${publicPath} must document SSE output`)
+}
+for (const [method, publicPath] of [
+ ['post', '/v1/services'], ['post', '/v1/schedules'], ['post', '/v1/services/{service_id}/invoke'], ['post', '/v1/schedules/{schedule_id}/runs'],
+ ['post', '/v1/secrets'], ['post', '/v1/mcp-connections'], ['post', '/v1/agents/sessions/{session_id}/cancel'], ['post', '/v1/agents/sessions/{session_id}/files'],
+]) {
+ assert.ok(
+ (openapiDocument.paths[publicPath]?.[method]?.parameters ?? []).some((parameter) => parameter.$ref === '#/components/parameters/IdempotencyKeyRequired'),
+ `${method.toUpperCase()} ${publicPath} must require Idempotency-Key`,
+ )
}
-const updateDeploymentSchema = openapi.match(/^ UpdateDeploymentRequest:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-for (const field of ['resources', 'vault_ids']) {
- assert.doesNotMatch(updateDeploymentSchema, new RegExp(`^ ${field}:`, 'm'), `Deployment updates must not advertise always-rejected ${field}`)
+for (const status of ['429']) {
+ for (const publicPath of ['/v1/services/{service_id}/invoke', '/v1/schedules/{schedule_id}/runs']) {
+ assert.ok(openapiDocument.paths[publicPath].post.responses[status], `POST ${publicPath} must document ${status} concurrency_limit`)
+ }
}
-assert.match(updateDeploymentSchema, /initial_events:\n\s+type: array\n\s+minItems: 1/, 'Deployment updates must require non-empty initial events')
-assert.match(updateDeploymentSchema, /notification_settings:\n\s+type: \[object, 'null'\]/, 'Deployment notifications must allow null to clear the saved target')
-assert.match(updateDeploymentSchema, /required: \[feishu_webhook_url\]/, 'A non-null notification_settings object must contain only its implemented webhook field')
-assert.match(updateDeploymentSchema, /pattern: '\^https:\/\/open\\\.feishu\\\.cn\/open-apis\/bot\/v2\/hook\//, 'Deployment notifications must publish the enforced Feishu webhook origin and path')
-assert.match(sidebar, /\/api-reference\/deployments\/test-feishu-notification/, 'Sidebar must link to the Feishu notification test reference')
-assert.match(sidebar, /\/api-reference\/endpoints\/acp/, 'Sidebar must link to the Endpoint ACP reference')
+assert.doesNotMatch(openapi, /environment_id|environment_binding/, 'Public OpenAPI must not expose internal Environment bindings')
+const agentReferenceText = ['create', 'get', 'list', 'update', 'replace', 'get-version', 'versions']
+ .map((page) => readFileSync(new URL(`../api-reference/agents/${page}.md`, import.meta.url), 'utf8'))
+ .join('\n')
+assert.doesNotMatch(agentReferenceText, /"model":\s*(?:"|\{"id":\s*")claude-sonnet-4/, 'Agent examples must use implemented vendor/model identities')
+assert.doesNotMatch(agentReferenceText, /\bagent_[0-9a-f]{8}\b|"system":/, 'Agent examples must use the current agt_ IDs and instructions field')
assert.doesNotMatch(modelSidebar, /text: 'Official Native API'/, 'Model API main navigation must not duplicate the custom Official Native API sidebar')
assert.match(officialNativeSidebar, /Official Native API/, 'Custom sidebar must expose Official Native API')
assert.match(officialNativeSidebar, /official-native-api\/bytedance\/seedance-2\.5-official/, 'Official Native API sidebar must expose Seedance 2.5')
@@ -688,147 +575,6 @@ assert.match(officialNativeSidebar, /GPT Image 2\.5 Flare/, 'Official Native API
assert.match(officialNativeSidebar, /official-native-api\/openai\/gpt-image-2\.5-flare/, 'Official Native API sidebar must link GPT Image 2.5 Flare')
assert.match(officialNativeSidebar, /GPT Image 2\.5 Sunburst/, 'Official Native API sidebar must expose GPT Image 2.5 Sunburst')
assert.match(officialNativeSidebar, /official-native-api\/openai\/gpt-image-2\.5-sunburst/, 'Official Native API sidebar must link GPT Image 2.5 Sunburst')
-const scheduleOverview = readFileSync(new URL('../api-reference/deployments/index.md', import.meta.url), 'utf8')
-assert.match(scheduleOverview, /POST \/v1\/deployments\/\{deployment_id\}\/runs/, 'Schedule overview must document the preferred plural trigger path')
-assert.match(scheduleOverview, /POST \/v1\/deployments\/\{deployment_id\}\/run` remains a compatibility alias/, 'Schedule overview must label the singular trigger path as compatibility-only')
-for (const anchor of ['create-a-service', 'list-services', 'get-a-service', 'update-a-service', 'delete-a-service', 'invoke-with-rest']) {
- assert.match(sidebar, new RegExp(`/api-reference/endpoints/#${anchor}`), `Sidebar must expose the Service ${anchor} operation`)
-}
-const deploymentSchema = openapi.match(/^ Deployment:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-for (const [publicPath, pathItem] of Object.entries(openapiDocument.paths)) {
- if (!publicPath.startsWith('/v1/deployment')) continue
- for (const method of publicMethods) {
- const operation = pathItem[method]
- if (!operation) continue
- assert.equal(
- operation.responses['401'].content?.['application/json']?.schema?.$ref,
- '#/components/schemas/TaskCostError',
- `${method.toUpperCase()} ${publicPath} must document the implemented string-error authentication envelope`,
- )
- }
-}
-assert.match(deploymentSchema, /required: \[id, type, name, description, metadata, resources, vault_ids, agent, agent_id, agent_version, schedule, timeout_policy, status, version, next_run_at, creation_mode, created_at, updated_at\]/, 'Deployment responses must require fields emitted by every list and detail projection')
-assert.doesNotMatch(deploymentSchema, /environment_id|environment_binding/, 'Public Schedule responses must not expose internal Environment bindings')
-const deploymentResourcePath = openapi.match(/^ \/v1\/deployments\/\{id\}:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-for (const status of ['400', '401', '402', '403', '404', '409', '422', '500']) {
- assert.match(deploymentResourcePath, new RegExp(`'${status}':`), `Deployment resource operations must collectively document ${status}`)
-}
-for (const suffix of ['pause', 'unpause', 'archive']) {
- const lifecyclePath = openapi.match(new RegExp(`^ /v1/deployments/\\{id\\}/${suffix}:\\n[\\s\\S]*?(?=^ /)`, 'm'))?.[0] ?? ''
- for (const status of ['200', '401', '402', '403', '404', '409', '500']) {
- assert.match(lifecyclePath, new RegExp(`'${status}':`), `Deployment ${suffix} must document ${status}`)
- }
-}
-const agentSchema = openapi.match(/^ Agent:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-for (const field of ['tools', 'mcp_servers', 'skills', 'handoffs']) {
- assert.match(agentSchema, new RegExp(`${field}:\\n\\s+type: \\[array, 'null'\\]`), `Agent ${field} must allow the serializer's null output`)
-}
-assert.match(agentSchema, /metadata:\n\s+type: \[object, 'null'\]/, 'Agent metadata must allow the serializer\'s null output')
-assert.doesNotMatch(agentSchema, /^ (?:runtime_profile|multiagent):/m, 'Agent responses must not advertise fields omitted by the serializer')
-assert.match(agentSchema, /required: \[id, type, name, description, model, system, tools, mcp_servers, skills, handoffs, metadata, version, created_at, updated_at, archived_at\]/, 'Agent responses must require every field always emitted by the serializer')
-assert.match(agentSchema, /effort:\n\s+description: Optional model runtime hint preserved/, 'Agent model effort responses must allow the implemented pass-through value')
-assert.match(agentSchema, /id:\n\s+type: string\n\s+pattern: '\^agent_\[0-9a-f\]\{8\}-\[0-9a-f\]\{3\}\$'/, 'Agent IDs must document the implemented generated shape')
-const updateAgentSchema = openapi.match(/^ UpdateAgentRequest:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-for (const field of ['tools', 'skills', 'mcp_servers', 'handoffs']) {
- assert.match(updateAgentSchema, new RegExp(`${field}:\\n\\s+type: \\[array, 'null'\\]`), `Agent update ${field} must allow explicit null replacement`)
-}
-assert.match(updateAgentSchema, /metadata:\n\s+type: \[object, 'null'\]/, 'Agent update metadata must allow explicit null replacement')
-assert.match(updateAgentSchema, /additionalProperties: true/, 'Agent updates must allow ignored compatibility fields')
-assert.match(updateAgentSchema, /name:\n\s+type: \[string, 'null'\]/, 'Agent updates must allow null scalar fields to preserve their values')
-const createAgentSchema = openapi.match(/^ CreateAgentRequest:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(createAgentSchema, /additionalProperties: true/, 'Agent creation must allow ignored compatibility fields')
-assert.match(createAgentSchema, /pattern: '\^\[A-Za-z0-9\]/, 'Agent model identities must require implemented vendor/model syntax')
-assert.match(createAgentSchema, /tools:\n\s+type: \[array, 'null'\]/, 'Agent creation must distinguish omitted default tools from explicit null')
-assert.doesNotMatch(createAgentSchema, /maxItems: (?:20|128)|maxLength: (?:256|2048|100000)/, 'Agent creation must not advertise limits that are not enforced')
-const agentsPath = openapi.match(/^ \/v1\/agents:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-const createAgent = agentsPath.match(/^ post:\n[\s\S]*?(?=^ get:)/m)?.[0] ?? ''
-for (const status of ['200', '400', '401', '402', '403', '422', '500']) {
- assert.match(createAgent, new RegExp(`'${status}':`), `Agent creation must document ${status} responses`)
-}
-const listAgents = agentsPath.match(/^ get:\n[\s\S]*/m)?.[0] ?? ''
-for (const status of ['200', '400', '401', '402', '403', '500']) {
- assert.match(listAgents, new RegExp(`'${status}':`), `Agent listing must document ${status} responses`)
-}
-const agentPath = openapi.match(/^ \/v1\/agents\/\{id\}:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-for (const status of ['200', '400', '401', '402', '403', '404', '500']) {
- assert.match(agentPath.match(/^ get:\n[\s\S]*?(?=^ post:)/m)?.[0] ?? '', new RegExp(`'${status}':`), `Agent reads must document ${status} responses`)
-}
-const updateAgent = agentPath.match(/^ post:\n[\s\S]*/m)?.[0] ?? ''
-assert.match(updateAgent, /a no-op returns the current Agent without incrementing/, 'Agent updates must document no-op version semantics')
-for (const status of ['200', '400', '401', '402', '403', '404', '409', '422', '500']) {
- assert.match(updateAgent, new RegExp(`'${status}':`), `Agent updates must document ${status} responses`)
-}
-const archiveAgentPath = openapi.match(/^ \/v1\/agents\/\{id\}\/archive:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-for (const status of ['200', '401', '402', '403', '404', '409', '500']) {
- assert.match(archiveAgentPath, new RegExp(`'${status}':`), `Agent archival must document ${status} responses`)
-}
-const agentVersionsPath = openapi.match(/^ \/v1\/agents\/\{id\}\/versions:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-for (const status of ['200', '400', '401', '402', '403', '404', '500']) {
- assert.match(agentVersionsPath, new RegExp(`'${status}':`), `Agent version listing must document ${status} responses`)
-}
-const agentVersionPath = openapi.match(/^ \/v1\/agents\/\{id\}\/versions\/\{version\}:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-for (const status of ['200', '401', '402', '403', '404', '500']) {
- assert.match(agentVersionPath, new RegExp(`'${status}':`), `Agent version reads must document ${status} responses`)
-}
-const updateAgentReference = readFileSync(new URL('../api-reference/agents/update.md', import.meta.url), 'utf8')
-assert.match(updateAgentReference, /null is treated as omitted and preserves the current value/, 'Agent string updates must document their implemented null semantics')
-assert.doesNotMatch(updateAgentReference, /Send null or an empty string to clear/, 'Agent string updates must not claim null clears pointer fields')
-const agentReferenceText = ['create', 'get', 'list', 'update', 'get-version', 'versions']
- .map((page) => readFileSync(new URL(`../api-reference/agents/${page}.md`, import.meta.url), 'utf8'))
- .join('\n')
-assert.doesNotMatch(agentReferenceText, /"model":\s*(?:"|\{"id":\s*")claude-sonnet-4/, 'Agent examples must use implemented vendor/model identities')
-const sessionSchema = openapi.match(/^ Session:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(sessionSchema, /metadata:\n\s+type: \[object, 'null'\]/, 'Session metadata must allow the serializer\'s null output')
-assert.match(sessionSchema, /required: \[[^\]]*title[^\]]*metadata[^\]]*archived_at[^\]]*updated_at[^\]]*\]/, 'Session responses must require fields always emitted by the serializer')
-assert.match(sessionSchema, /source:\n\s+type: string\n\s+enum: \[direct, endpoint, deployment, store_trial\]/, 'Session source must include Service store trials')
-const sessionEventSchema = openapi.match(/^ SessionEvent:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(sessionEventSchema, /required: \[id, type, processed_at, created_at\]/, 'Session Events must require the serializer\'s created_at field')
-const sessionEventStream = openapi.match(/^ \/v1\/sessions\/\{id\}\/events\/stream:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-assert.match(sessionEventStream, /does not synthesize preview deltas/, 'Session event_delta must not promise unimplemented preview deltas')
-const sessionStreamReference = generatedReferenceSpecs.match(/"sessions\/stream": \{[\s\S]*?(?=\n \},\n "skills\/create")/)?.[0] ?? ''
-assert.match(sessionStreamReference, /id: sevt_01/, 'Session SSE examples must include the emitted id field')
-assert.match(sessionStreamReference, /created_at/, 'Session SSE examples must include the emitted created_at field')
-const credentialSchema = openapi.match(/^ Credential:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(credentialSchema, /id: \{ type: string, pattern: '\^sec_' \}/, 'Managed Credential IDs must use the implemented sec_ prefix')
-assert.match(credentialSchema, /required: \[[^\]]*last_used_at[^\]]*cooldown_until[^\]]*failure_count[^\]]*\]/, 'Credential responses must require fields always emitted by the serializer')
-assert.doesNotMatch(credentialSchema, /^ (?:value|value_encrypted):/m, 'Credential responses must never expose plaintext or encrypted values')
-const createCredentialSchema = openapi.match(/^ CreateCredentialRequest:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(createCredentialSchema, /strategy: \{ type: string, enum: \[round_robin\]/, 'Credential creation must document the only implemented strategy')
-assert.match(createCredentialSchema, /weight: \{ type: integer, minimum: 1/, 'Credential creation must require a positive weight')
-assert.match(createCredentialSchema, /Defaults to the submitted scope_name and secret_key joined by a colon/, 'Credential creation must document its derived group key')
-const credentialsPath = openapi.match(/^ \/v1\/credentials:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-const createCredential = credentialsPath.match(/^ post:\n[\s\S]*?(?=^ get:)/m)?.[0] ?? ''
-for (const status of ['201', '400', '401', '402', '403', '500']) {
- assert.match(createCredential, new RegExp(`'${status}':`), `Credential creation must document ${status} responses`)
-}
-const listCredentials = credentialsPath.match(/^ get:\n[\s\S]*/m)?.[0] ?? ''
-for (const status of ['200', '401', '402', '403', '500']) {
- assert.match(listCredentials, new RegExp(`'${status}':`), `Credential listing must document ${status} responses`)
-}
-const credentialPath = openapi.match(/^ \/v1\/credentials\/\{id\}:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-assert.doesNotMatch(credentialPath, /^ delete:/m, 'Credentials do not implement deletion')
-const getCredential = credentialPath.match(/^ get:\n[\s\S]*?(?=^ patch:)/m)?.[0] ?? ''
-for (const status of ['200', '401', '402', '403', '404', '500']) {
- assert.match(getCredential, new RegExp(`'${status}':`), `Credential reads must document ${status} responses`)
-}
-const updateCredential = credentialPath.match(/^ patch:\n[\s\S]*/m)?.[0] ?? ''
-for (const status of ['200', '400', '401', '402', '403', '404', '500']) {
- assert.match(updateCredential, new RegExp(`'${status}':`), `Credential updates must document ${status} responses`)
-}
-const rotateCredential = openapi.match(/^ \/v1\/credentials\/\{id\}\/rotate:\n[\s\S]*?(?=^ \/|^components:)/m)?.[0] ?? ''
-assert.match(rotateCredential, /pattern: '\^sec_'/, 'Credential rotation IDs must use the implemented sec_ prefix')
-assert.match(rotateCredential, /reset failure_count to 0 and clear cooldown_until/, 'Credential rotation must document health-state reset semantics')
-for (const status of ['200', '400', '401', '402', '403', '404', '500']) {
- assert.match(rotateCredential, new RegExp(`'${status}':`), `Credential rotation must document ${status} responses`)
-}
-const skillSchema = openapi.match(/^ Skill:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(skillSchema, /id: \{ type: string, format: uuid \}/, 'Skill resources must document their UUID identity')
-assert.match(skillSchema, /required: \[[^\]]*icon_url[^\]]*preview_urls[^\]]*\]/, 'Skill responses must require fields always emitted by every projection')
-const skillUploadPath = openapi.match(/^ \/v1\/skills\/files:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-assert.match(skillUploadPath, /preview image[\s\S]*?10 MB/, 'Skill uploads must document supported preview images and their size limit')
-const skillReferences = generatedReferenceSpecs.match(/"skills\/create": \{[\s\S]*?(?=\n "inference\/responses")/)?.[0] ?? ''
-assert.doesNotMatch(skillReferences, /skill_01/, 'Skill reference examples must not invent a skill_ ID prefix')
-assert.match(skillReferences, /550e8400-e29b-41d4-a716-446655440000/, 'Skill reference examples must use a UUID')
const modelDetailSchema = openapi.match(/^ ModelDetail:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
assert.match(modelDetailSchema, /id:\n\s+type: string\n\s+format: uuid/, 'Model detail IDs must document the implemented UUID identity')
assert.match(modelDetailSchema, /required: \[[^\]]*run_count[^\]]*sort_order[^\]]*created_at[^\]]*examples[^\]]*\]/, 'Model details must require fields always emitted by the serializer')
@@ -876,7 +622,7 @@ const accountHistorySchema = openapi.match(/^ AccountHistoryItem:\n[\s\S]*?(?
for (const field of ['latency_ms', 'user_cost', 'cache_creation_tokens', 'api_key_prefix']) {
assert.match(accountHistorySchema, new RegExp(`required: \\[[^\\]]*${field}`), `Account history must require emitted ${field}`)
}
-const accountHistoryReference = generatedReferenceSpecs.match(/"account\/history": \{[\s\S]*?(?=\n "endpoints\/acp")/)?.[0] ?? ''
+const accountHistoryReference = generatedReferenceSpecs.match(/"account\/history": \{[\s\S]*?(?=\n "models\/audio")/)?.[0] ?? ''
assert.match(accountHistoryReference, /"name": "timezone"/, 'Account history reference must list the timezone compatibility parameter')
assert.match(accountHistoryReference, /Timestamps remain UTC/, 'Account history reference must not imply timezone conversion')
assert.doesNotMatch(accountHistoryReference, /openai\/gpt-4o"/, 'Account history reference must not use the retired default GPT-4o example')
@@ -888,180 +634,8 @@ for (const status of ['200', '401', '403', '404']) {
}
assert.match(taskCostPath, /'403':[\s\S]*APIKeyScopeError/, 'Task cost scope failures must use their implemented nested error shape')
assert.doesNotMatch(taskCostPath, /'500':/, 'Task cost lookup does not expose an internal-error branch')
-const sessionsPath = openapi.match(/^ \/v1\/sessions:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-const createSession = sessionsPath.match(/^ post:\n[\s\S]*?(?=^ get:)/m)?.[0] ?? ''
-assert.match(createSession, /Unknown top-level fields are rejected/, 'Session creation must document strict unknown-field handling')
-assert.match(createSession, /unsupported_feature, agent_runtime_environment_mismatch/, 'Session creation must document every implemented 422 class')
-for (const status of ['200', '400', '401', '402', '403', '404', '422', '500', '502', '503']) {
- assert.match(createSession, new RegExp(`'${status}':`), `Session creation must document ${status} responses`)
-}
-const listSessions = sessionsPath.match(/^ get:\n[\s\S]*/m)?.[0] ?? ''
-for (const status of ['200', '400', '401', '402', '403', '500']) {
- assert.match(listSessions, new RegExp(`'${status}':`), `Session listing must document ${status} responses`)
-}
-const sessionPath = openapi.match(/^ \/v1\/sessions\/\{id\}:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-const getSession = sessionPath.match(/^ get:\n[\s\S]*?(?=^ post:)/m)?.[0] ?? ''
-for (const status of ['200', '401', '402', '403', '404', '500']) {
- assert.match(getSession, new RegExp(`'${status}':`), `Session reads must document ${status} responses`)
-}
-const updateSession = sessionPath.match(/^ post:\n[\s\S]*?(?=^ delete:)/m)?.[0] ?? ''
-assert.match(updateSession, /shallow-merge metadata/, 'Session updates must document metadata merge semantics')
-assert.match(updateSession, /null removes a key/, 'Session updates must document metadata key deletion')
-assert.match(updateSession, /additionalProperties: true/, 'Session updates must allow ignored compatibility fields')
-for (const status of ['200', '400', '401', '402', '403', '404', '500']) {
- assert.match(updateSession, new RegExp(`'${status}':`), `Session updates must document ${status} responses`)
-}
-const deleteSession = sessionPath.match(/^ delete:\n[\s\S]*/m)?.[0] ?? ''
-for (const status of ['200', '401', '402', '403', '404', '409', '500']) {
- assert.match(deleteSession, new RegExp(`'${status}':`), `Session deletion must document ${status} responses`)
-}
-const archiveSessionPath = openapi.match(/^ \/v1\/sessions\/\{id\}\/archive:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-for (const status of ['200', '401', '402', '403', '404', '500']) {
- assert.match(archiveSessionPath, new RegExp(`'${status}':`), `Session archival must document ${status} responses`)
-}
-const sessionEventsPath = openapi.match(/^ \/v1\/sessions\/\{id\}\/events:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-for (const status of ['200', '400', '401', '402', '403', '404', '409', '422', '500']) {
- assert.match(sessionEventsPath.match(/^ post:\n[\s\S]*?(?=^ get:)/m)?.[0] ?? '', new RegExp(`'${status}':`), `Session Event send must document ${status} responses`)
-}
-for (const status of ['200', '400', '401', '402', '403', '404', '500']) {
- assert.match(sessionEventsPath.match(/^ get:\n[\s\S]*/m)?.[0] ?? '', new RegExp(`'${status}':`), `Session Event listing must document ${status} responses`)
-}
-const sessionStreamPath = openapi.match(/^ \/v1\/sessions\/\{id\}\/events\/stream:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-for (const status of ['200', '400', '401', '402', '403', '404', '500']) {
- assert.match(sessionStreamPath, new RegExp(`'${status}':`), `Session Event streaming must document ${status} responses`)
-}
-const publicSessionSchema = openapi.match(/^ Session:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(publicSessionSchema, /SessionAgentProjection/, 'Session responses must use the implemented full-or-minimal Agent projection')
-const sessionAgentProjection = openapi.match(/^ SessionAgentProjection:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(sessionAgentProjection, /legacy or unavailable snapshots fall back/, 'Session Agent projection must document its fallback behavior')
-assert.match(sessionAgentProjection, /required: \[id, type, version\]/, 'Session Agent fallback must require every serializer field')
assert.doesNotMatch(openapi, /^ \/v1\/skills\/\{id\}\/mcp-publications:$/m, 'Skill MCP publication management must not be public')
assert.doesNotMatch(openapi, /^ \/v1\/skill-mcp(?:-publications)?(?:\/|:)/m, 'Skill MCP transports and publications must not be public')
-const skillFilesPath = openapi.match(/^ \/v1\/skills\/files:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-for (const status of ['201', '400', '401', '402', '403', '500', '503']) {
- assert.match(skillFilesPath, new RegExp(`'${status}':`), `Skill file uploads must document ${status} responses`)
-}
-const skillsPath = openapi.match(/^ \/v1\/skills:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-const createSkill = skillsPath.match(/^ post:\n[\s\S]*?(?=^ get:)/m)?.[0] ?? ''
-assert.match(createSkill, /SkillCreated/, 'Skill creation must use its compact response projection')
-for (const status of ['201', '400', '401', '402', '403', '500']) {
- assert.match(createSkill, new RegExp(`'${status}':`), `Skill creation must document ${status} responses`)
-}
-const listSkills = skillsPath.match(/^ get:\n[\s\S]*/m)?.[0] ?? ''
-assert.match(listSkills, /SkillListItem/, 'Skill lists must use their list-item response projection')
-for (const status of ['200', '401', '402', '403', '500']) {
- assert.match(listSkills, new RegExp(`'${status}':`), `Skill lists must document ${status} responses`)
-}
-const skillPath = openapi.match(/^ \/v1\/skills\/\{id\}:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-const getSkill = skillPath.match(/^ get:\n[\s\S]*?(?=^ put:)/m)?.[0] ?? ''
-for (const status of ['200', '401', '402', '403', '404', '500']) {
- assert.match(getSkill, new RegExp(`'${status}':`), `Skill reads must document ${status} responses`)
-}
-const updateSkill = skillPath.match(/^ put:\n[\s\S]*?(?=^ delete:)/m)?.[0] ?? ''
-assert.match(updateSkill, /Omitted name, description, and categories are cleared/, 'Skill PUT must document its replacement-style display fields')
-for (const status of ['200', '400', '401', '402', '403', '404', '500']) {
- assert.match(updateSkill, new RegExp(`'${status}':`), `Skill updates must document ${status} responses`)
-}
-const deleteSkill = skillPath.match(/^ delete:\n[\s\S]*/m)?.[0] ?? ''
-for (const status of ['200', '401', '402', '403', '404', '500']) {
- assert.match(deleteSkill, new RegExp(`'${status}':`), `Skill deletion must document ${status} responses`)
-}
-const skillCreatedSchema = openapi.match(/^ SkillCreated:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(skillCreatedSchema, /required: \[id, name, display_name, vendor_slug, plugin_slug, icon_url, preview_urls, created_at\]/, 'Skill creation responses must require exactly their emitted fields')
-const skillListItemSchema = openapi.match(/^ SkillListItem:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(skillListItemSchema, /required: \[id, name, display_name, vendor_slug, plugin_slug, description, icon_url, preview_urls, created_at, updated_at\]/, 'Skill list items must require every emitted field')
-const publicSkillSchema = openapi.match(/^ Skill:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(publicSkillSchema, /required: \[id, name, display_name, vendor_slug, plugin_slug, description, categories, icon_url, preview_urls, skill_file_url, git_url, created_at, updated_at\]/, 'Skill detail responses must require every emitted field')
-const createSkillSchema = openapi.match(/^ CreateSkillRequest:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.doesNotMatch(createSkillSchema, /format: uri/, 'Skill registration does not validate submitted source strings as URLs')
-const updateSkillSchema = openapi.match(/^ UpdateSkillRequest:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(updateSkillSchema, /Omitted name, description, and categories are cleared/, 'Skill update schemas must document mixed replacement semantics')
-assert.doesNotMatch(updateSkillSchema, /^ git_url:/m, 'Skill updates must not advertise the ignored git_url field')
-const endpointSchema = openapi.match(/^ Endpoint:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-for (const field of ['session_metadata', 'memory_config', 'resource_config', 'vault_config']) {
- assert.match(endpointSchema, new RegExp(`${field}:\\n\\s+type: \\[object, 'null'\\]`), `Endpoint ${field} must allow the serializer's null output`)
-}
-assert.match(endpointSchema, /store_status:\n\s+type: string\n\s+enum: \[private, pending_review, public, suspended\]/, 'Endpoint responses must document the serializer\'s store status')
-assert.match(endpointSchema, /required: \[id, type, name, slug, agent_id, agent_version, protocols, config, session_metadata, memory_config, resource_config, vault_config, status, store_status, creation_mode, run_url, acp_url, created_at, updated_at\]/, 'Endpoint responses must require every field always emitted by the serializer')
-assert.doesNotMatch(endpointSchema, /environment_id|environment_binding/, 'Public Service responses must not expose internal Environment bindings')
-assert.match(endpointSchema, /config:\n\s+type: \[object, 'null'\]/, 'Endpoint responses must document advanced config and declarative null output')
-assert.match(endpointSchema, /acp_url:[\s\S]*?presence does not mean ACP is enabled/, 'Endpoint ACP URLs must not imply the protocol is enabled')
-assert.doesNotMatch(endpointSchema, /^ mcp_url:/m, 'Endpoint responses must not document the hidden MCP transport URL')
-assert.doesNotMatch(endpointSchema, /protocols:[\s\S]*?enum: \[rest, acp\]/, 'Endpoint responses must not claim internal transport values are normalized to the public request enum')
-for (const schemaName of ['CreateDeclarativeEndpointRequest', 'CreateAdvancedEndpointRequest']) {
- const schema = openapi.match(new RegExp(`^ ${schemaName}:\\n[\\s\\S]*?(?=^ [A-Za-z])`, 'm'))?.[0] ?? ''
- assert.match(schema, /required: \[[^\]]*protocols[^\]]*\]/, `${schemaName} must require explicit public protocols to avoid hidden transport defaults`)
- assert.match(schema, /protocols:\n\s+type: array\n\s+minItems: 1\n\s+uniqueItems: true/, `${schemaName} must require a non-empty unique protocol list`)
- assert.match(schema, /enum: \[rest, mcp, acp\]/, `${schemaName} must expose only public invocation transports`)
- assert.doesNotMatch(schema, /default: \[rest\]/, `${schemaName} must not claim an unimplemented REST-only server default`)
- assert.doesNotMatch(schema, /environment_id/, `${schemaName} must not expose the withdrawn Environment field`)
-}
-for (const schemaName of ['UpdateEndpointRequest', 'CreateSessionRequest', 'UpdateDeploymentRequest', 'CreateDeploymentRequest']) {
- const schema = openapi.match(new RegExp(`^ ${schemaName}:\\n[\\s\\S]*?(?=^ [A-Za-z])`, 'm'))?.[0] ?? ''
- assert.doesNotMatch(schema, /environment_id/, `${schemaName} must not expose the withdrawn Environment field`)
-}
-const endpointsPath = openapi.match(/^ \/v1\/endpoints:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-const createEndpointOperation = endpointsPath.match(/^ post:\n[\s\S]*?(?=^ get:)/m)?.[0] ?? ''
-assert.match(createEndpointOperation, /responses:\n\s+'201':/, 'Endpoint creation must document its implemented 201 response')
-assert.doesNotMatch(createEndpointOperation, /responses:\n\s+'200':/, 'Endpoint creation must not document an unimplemented 200 response')
-for (const status of ['400', '401', '402', '403', '404', '409', '415', '422', '500']) {
- assert.match(createEndpointOperation, new RegExp(`'${status}':`), `Endpoint creation must document ${status} responses`)
-}
-const listEndpointOperation = endpointsPath.match(/^ get:\n[\s\S]*$/m)?.[0] ?? ''
-for (const status of ['200', '400', '401', '402', '403', '500']) {
- assert.match(listEndpointOperation, new RegExp(`'${status}':`), `Endpoint listing must document ${status} responses`)
-}
-const endpointResourcePath = openapi.match(/^ \/v1\/endpoints\/\{id\}:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-const getEndpointOperation = endpointResourcePath.match(/^ get:\n[\s\S]*?(?=^ patch:)/m)?.[0] ?? ''
-assert.match(getEndpointOperation, /EndpointId/, 'Endpoint reads must use the Endpoint path parameter')
-assert.doesNotMatch(getEndpointOperation, /EnvironmentId|Environment updated/, 'Endpoint reads must not inherit Environment contracts')
-for (const status of ['200', '401', '402', '403', '404']) {
- assert.match(getEndpointOperation, new RegExp(`'${status}':`), `Endpoint reads must document ${status} responses`)
-}
-for (const method of ['patch', 'post']) {
- const next = method === 'patch' ? 'post' : 'delete'
- const operation = endpointResourcePath.match(new RegExp(`^ ${method}:\\n[\\s\\S]*?(?=^ ${next}:)`, 'm'))?.[0] ?? ''
- assert.match(operation, /UpdateEndpointRequest/, `${method.toUpperCase()} Endpoint updates must use the implemented request schema`)
- assert.doesNotMatch(operation, /UpdateEnvironmentRequest|Environment updated/, `${method.toUpperCase()} Endpoint updates must not inherit Environment contracts`)
- for (const status of ['200', '400', '401', '402', '403', '404', '409', '422', '500']) {
- assert.match(operation, new RegExp(`'${status}':`), `${method.toUpperCase()} Endpoint updates must document ${status} responses`)
- }
- assert.match(operation, /'401':[\s\S]*?TaskCostError/, `${method.toUpperCase()} Endpoint updates must document the implemented string-error envelope for authentication failures`)
-}
-const deleteEndpointOperation = endpointResourcePath.match(/^ delete:\n[\s\S]*/m)?.[0] ?? ''
-assert.match(deleteEndpointOperation, /required: \[id, deleted\]/, 'Endpoint deletion must use its implemented response projection')
-assert.doesNotMatch(deleteEndpointOperation, /environment_deleted|EnvironmentId/, 'Endpoint deletion must not inherit Environment contracts')
-for (const status of ['200', '401', '402', '403', '404', '500']) {
- assert.match(deleteEndpointOperation, new RegExp(`'${status}':`), `Endpoint deletion must document ${status} responses`)
-}
-assert.match(deleteEndpointOperation, /'401':[\s\S]*?TaskCostError/, 'Endpoint deletion must document the implemented string-error envelope for authentication failures')
-const endpointACPPath = openapi.match(/^ \/v1\/endpoints\/\{id\}\/acp:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-assert.match(endpointACPPath, /ACPRequest/, 'Endpoint ACP must document its JSON-RPC request envelope')
-assert.match(endpointACPPath, /application\/x-ndjson:/, 'Endpoint ACP must document prompt streaming as NDJSON')
-for (const status of ['200', '400', '401', '402', '403', '404', '500']) {
- assert.match(endpointACPPath, new RegExp(`'${status}':`), `Endpoint ACP must document ${status} responses`)
-}
-const acpRequestSchema = openapi.match(/^ ACPRequest:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(acpRequestSchema, /enum: \[initialize, session\/new, session\/prompt, session\/cancel\]/, 'Endpoint ACP must document every implemented method')
-const acpResponseSchema = openapi.match(/^ ACPResponse:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(acpResponseSchema, /oneOf:\n\s+- required: \[result\]\n\s+- required: \[error\]/, 'Endpoint ACP must distinguish JSON-RPC success and error responses')
-const endpointRunPath = openapi.match(/^ \/v1\/endpoints\/\{id\}\/run:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
-for (const status of ['202', '400', '401', '402', '403', '404', '409', '500', '503']) {
- assert.match(endpointRunPath, new RegExp(`'${status}':`), `Endpoint REST invocation must document ${status} responses`)
-}
-for (const status of ['422', '502']) {
- assert.doesNotMatch(endpointRunPath, new RegExp(`'${status}':`), `Endpoint REST invocation must not document unimplemented direct ${status} responses`)
-}
-assert.match(endpointRunPath, /When both input and content are supplied, content takes precedence/, 'Endpoint REST invocation must document implemented input precedence')
-const endpointInvokeRequest = openapi.match(/^ EndpointInvokeRequest:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(endpointInvokeRequest, /- type: object\n\s+additionalProperties: true/, 'Endpoint REST invocation must allow a single content block object')
-const endpointInvokeAccepted = openapi.match(/^ EndpointInvokeAccepted:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(endpointInvokeAccepted, /EndpointAcceptedEvent/, 'Endpoint REST acceptance must use its emitted compact event projection')
-const endpointAcceptedEvent = openapi.match(/^ EndpointAcceptedEvent:\n[\s\S]*?(?=^ [A-Za-z])/m)?.[0] ?? ''
-assert.match(endpointAcceptedEvent, /required: \[id, type, processed_at\]/, 'Endpoint REST accepted events must require exactly their emitted identity fields')
-for (const field of ['memory_config', 'resource_config', 'vault_config']) {
- assert.match(endpointSchema, new RegExp(`${field}:[\\s\\S]*?not currently applied to Session execution`), `Endpoint ${field} must be documented as reserved`)
-}
const assetRegistrationPath = openapi.match(/^ \/v1\/assets:\n[\s\S]*?(?=^ \/)/m)?.[0] ?? ''
assert.match(assetRegistrationPath, /responses:\n\s+'200':/, 'Asset registration must document the implemented 200 response')
assert.doesNotMatch(assetRegistrationPath, /\s+'201':/, 'Asset registration must not document an unimplemented 201 response')
@@ -1095,7 +669,6 @@ assert.doesNotMatch(openapi, /^ \/default\/v1(?:\/|:)/m, 'Internal Console path
const unpublishedFiles = new Set([
'api-reference/webhooks.md',
- 'guides/site-agent-integration.md',
])
function inspectPublishedSources(directory) {
@@ -1108,7 +681,7 @@ function inspectPublishedSources(directory) {
}
if (!/\.(?:md|mdx|txt)$/.test(entry.name)) continue
const relative = path.relative(root, filename)
- if (unpublishedFiles.has(relative) || relative.startsWith('use-cases/') || relative.startsWith('api-reference/embeds/') || relative.startsWith('api-reference/environments/')) continue
+ if (unpublishedFiles.has(relative)) continue
const content = readFileSync(filename, 'utf8')
assert.doesNotMatch(content, /sk-sb-(?:YOUR|your|xxx)/, `${relative} must use the current sk- placeholder for new API keys`)
assert.doesNotMatch(content, /\/default\/v1(?:\/|\b)/, `${relative} must not expose internal Console API paths`)
@@ -1134,11 +707,12 @@ function inspectPublishedSources(directory) {
if (!['sandbox/index.md', 'sandbox/playground.md'].includes(relative)) {
assert.doesNotMatch(maskApprovedSandboxPageHrefs(content), sandboxAPIPath, `${relative} must not expose sandbox API paths`)
}
- if (!['api-reference/endpoints/index.md', 'api-reference/endpoints/mcp.md', 'agents/services.md'].includes(relative)) {
- assert.doesNotMatch(content, /\/v1\/endpoints\/[^\s`"']+\/mcp\b/i, `${relative} must not expose Endpoint MCP transport`)
+ assert.doesNotMatch(content, /\/v1\/endpoints\/[^\s`"']+\/mcp\b/i, `${relative} must not expose Endpoint MCP transport`)
+ if (relative !== 'scripts/validate-public-api-surface.mjs') {
+ assert.doesNotMatch(content, /(?:GET|POST|PUT|PATCH|DELETE) \/v1\/(?:sessions|endpoints|deployments|deployment_runs|credentials|skills\/files)\b/, `${relative} must not document retired Agents routes as current`)
}
assert.doesNotMatch(content, /\/v1\/endpoint_runtime_profiles\b/i, `${relative} must not expose Endpoint runtime profiles that reveal MCP transport`)
- assert.doesNotMatch(content, /\/v1\/mcp(?:\/|\b)/i, `${relative} must not expose the generic MCP transport or its discovery routes`)
+ assert.doesNotMatch(content, /\/v1\/mcp(?!-connections)(?:\/|\b)/i, `${relative} must not expose the generic MCP transport or its discovery routes`)
assert.doesNotMatch(content, /\/v1\/mcp\/(?:servers|[^\s/]+\/config)\b/i, `${relative} must not expose MCP discovery or runtime config routes`)
assert.doesNotMatch(content, /\/mcp\/[^\s/]+\/sse\b/i, `${relative} must not expose the MCP SSE proxy`)
assert.doesNotMatch(content, /\/v1\/skills\/[^\s/]+\/mcp-publications\b/i, `${relative} must not expose Skill MCP publication creation`)
diff --git a/scripts/validate-seo.mjs b/scripts/validate-seo.mjs
index 226bd1b8..151b7581 100644
--- a/scripts/validate-seo.mjs
+++ b/scripts/validate-seo.mjs
@@ -6,10 +6,7 @@ import YAML from 'yaml'
const roots = ['admin', 'agents', 'sandbox', 'api-reference', 'for-agents', 'getting-started', 'guides', 'models', 'setup', 'store', 'model-api-reference']
const excluded = [
'agents/endpoint-quickstart.md',
- 'api-reference/embeds/',
- 'api-reference/environments/',
'api-reference/webhooks.md',
- 'guides/site-agent-integration.md',
'setup/cli.md',
'setup/groups.md',
]
diff --git a/store/agents.md b/store/agents.md
index 4fdaa329..9795d50b 100644
--- a/store/agents.md
+++ b/store/agents.md
@@ -1,32 +1,32 @@
---
title: Agents
-description: Browse public Agent examples, test them, clone them, and publish your own Service.
+description: Browse public Agent examples, clone them into your organization, test them in a Session, and deploy your copy as a Service or Schedule.
---
# Agents
Store Agents are working examples.
-They show a useful pattern: which Model to use, which APIs to call, which Skills help, and how the workflow is shaped.
+They show a useful pattern: which Model to use, which Skills and MCP tools help, and how the workflow is shaped.
## What you can do with a Store Agent
You can:
- inspect the configuration
-- test the Agent
-- clone it into your workspace
-- customize it in Build Agent
-- publish your version as a Service for application access or selection in Workspace Services
-- schedule your version for recurring work
+- clone it into your organization
+- customize your copy in Build Agent
+- test your copy in a Session
+- deploy your version as a Service for application access
+- deploy your version as a Schedule for recurring work
## Public Agent vs your Agent
-A public Agent is a starting point.
+A public Agent is a starting point. It comes from the [Agent catalog](/api-reference/agents/catalog) and is read-only.
-Your Agent is the version you own, edit, test, publish as a Service, and schedule.
+Your Agent is the clone you own, edit, test, and deploy as a Service or Schedule.
-If you need to change the prompt, tools, model, credentials, schedule, or output format, clone the public Agent first.
+If you need to change the instructions, model, Skills, MCP connections, or output format, clone the public Agent first with `POST /v1/agents/catalog/{agent_id}/clone`.
## Next steps
diff --git a/store/apis.md b/store/apis.md
index d80501b7..97f23ec2 100644
--- a/store/apis.md
+++ b/store/apis.md
@@ -5,7 +5,7 @@ description: Browse ready-to-use APIs for search, scrape, data, media, SaaS, and
# API Catalog
-APIs give your Agent access to the real world.
+APIs give your application and connected AI tools access to the real world.
Use APIs for things like:
@@ -18,11 +18,12 @@ Use APIs for things like:
## How APIs are used
-You can use an API in three ways:
+You can use an API in two ways:
1. **Call it directly** from your application.
2. **Add it to Workspace Services** so a connected AI tool can discover it.
-3. **Add it to an Agent** when the Agent needs tools.
+
+Store APIs are not attached to Agents as tools. An Agent reaches external systems through [MCP connections](/agents/mcp-tools#mcp-connections) and reusable [Skills](/agents/mcp-tools).
## API vs Model
@@ -30,7 +31,7 @@ Models think and generate.
APIs fetch data, take actions, or call external systems.
-Both can be used by an Agent.
+An Agent uses a Model directly; external tools come from its MCP connections.
## Next steps
diff --git a/store/index.md b/store/index.md
index 6be60e18..536816d8 100644
--- a/store/index.md
+++ b/store/index.md
@@ -14,8 +14,8 @@ You can start from four kinds of things:
| Kind | What it is | What you do with it |
|---|---|---|
| **Models** | AI models for text, image, audio, and video | Call them directly, or use them inside an Agent |
-| **APIs** | Ready-to-use capabilities like search, scrape, data, media, and SaaS actions | Copy a request, add to Setup, or give to an Agent |
-| **Agents** | Proven Agent examples and best-practice workflows | Test them, clone them, or publish your own Service |
+| **APIs** | Ready-to-use capabilities like search, scrape, data, media, and SaaS actions | Copy a request or add to Setup |
+| **Agents** | Proven Agent examples and best-practice workflows | Clone them, test them, and deploy your copy as a Service or Schedule |
| **Skills** | Reusable task packages for Agents | Attach them when building an Agent |
## How to choose
@@ -32,7 +32,7 @@ If you are building your own Agent and want reusable implementation knowledge, u
|---|---|---|
| Add one capability to an AI tool | Model or API | Add it to [Setup](/setup/) |
| Call a capability from code | Model or API | Open its detail page, then use the [API Reference](/api-reference/) |
-| Automate a multi-step job | Agent | Clone, test, and publish it in [Build Agent](/agents/) |
+| Automate a multi-step job | Agent | Clone, test, and deploy it from [Build Agent](/agents/) |
| Reuse a proven workflow | Skill | Attach it while building an Agent |
When two entries look similar, compare their input, output, pricing, latency, and authentication requirements before installing or cloning them.
@@ -41,27 +41,28 @@ When two entries look similar, compare their input, output, pricing, latency, an
Models are AI engines for text generation and reasoning, image generation or editing, audio, and video.
-You can call a Model directly, add it to Setup for a connected AI tool, or select it when building an Agent. The Model determines how an Agent thinks; APIs and Skills determine what it can do.
+You can call a Model directly, add it to Setup for a connected AI tool, or select it when building an Agent. The Model determines how an Agent thinks; Skills and MCP tools determine what it can do.
Most text Models use the OpenAI-compatible Chat Completions API. See [Supported Models](/models/supported) and the [Model capability matrix](/models/capabilities).
## APIs
-APIs give your application or Agent access to real-world data and actions, including web search, scraping, business data, media generation, SaaS tools, and provider-specific capabilities.
+APIs give your application or connected AI tools access to real-world data and actions, including web search, scraping, business data, media generation, SaaS tools, and provider-specific capabilities.
-Use an API in one of three ways:
+Use an API in one of two ways:
1. call it directly from your application
2. add it to Setup so a connected AI tool can use it
-3. add it to an Agent as a tool
+
+Agents reach external systems through [MCP connections](/agents/mcp-tools#mcp-connections).
Models think and generate; APIs fetch data, take actions, or connect external systems. See the [API Reference](/api-reference/) for request formats and endpoints.
## Agents
-Store Agents are working examples that combine a Model, APIs, Skills, and workflow instructions.
+Store Agents are working examples that combine a Model, Skills, and workflow instructions.
-You can inspect and test a public Agent, clone it into your workspace, customize it, and publish or schedule your version. Clone the public Agent first when you need to change its prompt, tools, model, credentials, schedule, or output format.
+You can inspect a public Agent, clone it into your workspace, customize it, and deploy your copy as a Service or Schedule. Clone the public Agent first when you need to change its instructions, model, Skills, MCP tools, or output format.
## Skills
@@ -81,7 +82,7 @@ Open a Model or API detail page, copy a working request, and call it with your S
### Build your own Agent
-Clone an Agent example or start from scratch in **Build Agent**. Add Models, APIs, and Skills, test it, then publish it as a Service or create a Schedule.
+Clone an Agent example or start from scratch in **Build Agent**. Choose a Model, attach Skills and [MCP connections](/agents/mcp-tools#mcp-connections), test it in a Session, then deploy it as a Service or Schedule.
## Before you add an item