Workspace SDK
The versioned SDK under /v1 is the stable surface for programmatic integration. Every endpoint documents its request body, a sample successful response, and the errors it can return.
Base URL
Self-hosted: http://localhost:8000/v1
Production: https://your-domain.com/v1Authentication
Send an API key as a bearer token. Three key roles cover the surface:
| Key | Format | Used by |
|---|---|---|
| Personal key | xag_personal_<prefix>_<secret> | /v1/me, /v1/agents*, /v1/templates* |
| Runtime key — agent | xag_<prefix>_<secret> | /v1/chat/* (creating and continuing tasks) |
| Runtime key — workforce | xag_<prefix>_<secret> | /v1/workforces/*, plus continuing and reading its own tasks |
Authorization: Bearer xag_Ab3xY9_Qw7Rt2Kp9Lm4Nz8Vc1Bd6Hf5Jg0Xs3TrBoth runtime key roles share the same wire format — a runtime key is bound to either one agent or one workforce when it is minted, and that binding determines which endpoints accept it.
Which key?
Manage agents and templates with a personal key; run and track work with the runtime key minted for that agent or workforce. Using the wrong key role returns 401 invalid_api_key.
What makes a key stop working
Every authentication failure returns the same 401 invalid_api_key — deliberately, so keys cannot be probed. A key is rejected when it is missing or malformed, is the wrong role for the endpoint, has been revoked, has been paused, or (personal keys only) has passed its expires_at. Runtime keys do not expire on their own.
Error Format
Every /v1 error uses one envelope. Pin your client against the stable code; the message is human-readable and may change.
{
"error": {
"code": "invalid_input",
"message": "Request body failed validation."
}
}Some errors add an optional details object with diagnostic context. The field is omitted entirely when there is nothing to report — never null — and its contents are open-ended, so treat it as debugging information rather than something to branch on:
{
"error": {
"code": "invalid_runtime_context",
"message": "Runtime context is not valid for this connector.",
"details": {
"reason": "duplicate_connector_ref",
"connector_ref": { "connector_type": "mcp", "connector_id": 7 }
}
}
}Core codes
| Code | HTTP | Meaning |
|---|---|---|
invalid_api_key | 401 | Any authentication failure — missing, malformed, wrong-type, expired, paused, or revoked key. One opaque code by design; the message is always Invalid or revoked API key. |
invalid_input | 422, also 400 / 413 | Body or parameter validation failed. 400 for client-side rejections (e.g. inaccessible file ids); 413 when an upload exceeds the size cap. |
agent_not_found | 404 | Agent does not exist or is not bound to this key. Also returned when a workforce key sends agent_id. |
workforce_not_found | 404 | Workforce does not exist or is not bound to this key. Also returned when an agent key sends workforce_id. |
template_not_found | 404 | Template id is unknown or unavailable. |
task_not_found | 404 | Task does not exist, does not belong to this key, or was not created through the SDK. |
file_not_found | 404 | A file id in message.files is inaccessible. Returned on workforce-run creation; the task endpoints report this as invalid_input (400) instead. |
task_busy | 409 | Task is still running and cannot accept new input yet. Retryable — poll status, then retry. |
internal_error | 500, also 503 | Server-side failure; the detail is sanitized. 503 signals a temporarily unavailable dependency (e.g. file storage). |
Workforce run conflicts — all 409, and unlike task_busy these are permanent: retrying the same request will not succeed.
| Code | Meaning |
|---|---|
workforce_archived | The workforce is archived. Restore it or target another workforce. |
workforce_not_active | The workforce is not in active status (for example, still a draft). Publish it first. |
workforce_config_changed | The run's pinned configuration no longer matches the workforce. Start a new run. |
idempotency_conflict | The idempotency_key was already used by a run whose task no longer exists. Use a new key. |
Connector runtime — returned when a request carries connector_runtime_context.
| Code | HTTP | Meaning |
|---|---|---|
connector_not_found | 404 | The referenced connector was not found or is not accessible. |
invalid_runtime_context | 400 | Malformed runtime context (duplicate reference, bad shape, unsupported selector). |
missing_runtime_context | 400 | A required runtime context value was not supplied. |
runtime_context_immutable | 409 | Runtime context cannot be changed after the task is created. |
runtime_secret_not_allowed | 400 | A runtime secret is not permitted for this entry point. |
runtime_secret_unavailable | 400 | A required runtime secret is unavailable. |
connector_runtime_unavailable | 503 | Runtime context is temporarily unavailable. Retry later. |
Reserved codes
Handle these defensively — they are part of the contract but are not emitted by the /v1 endpoints in this deployment: rate_limited (429), quota_exceeded and client_quota_exceeded (402, emitted by hosted deployments), scheduled_secret_unavailable, mcp_oauth_authorization_failed, delegated_authorization_failed.
Pin against the code, not the status
The code is the stable contract; message is human-readable and may change. The HTTP status is chosen per endpoint, so a single code can appear with more than one status (invalid_input and internal_error both do).
Identity
Get identity — GET /v1/me
Returns the user identity behind a personal key. Useful to verify a key and log which prefix is in use.
Response (200):
{
"principal_type": "user",
"user_id": 7,
"username": "ada",
"email": "ada@example.com",
"key_prefix": "Kp9Lm4"
}Errors: invalid_api_key (401).
Agents
Agent management uses a personal key.
List agents — GET /v1/agents
Response (200): an array of agent summaries.
[
{
"id": 42,
"name": "Research Assistant",
"description": "Competitor analysis on demand",
"logo_url": null,
"status": "active",
"created_at": "2026-06-23T12:00:00Z",
"updated_at": "2026-06-23T12:00:00Z",
"widget_enabled": false,
"allowed_domains": []
}
]Errors: invalid_api_key (401).
Create an agent — POST /v1/agents
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Display name. 1–200 characters. |
| description | string | No | Short summary of the agent. |
| instructions | string | No | System prompt for the agent. |
| execution_mode | string | No | One of flash, balanced, think, auto. Default balanced. |
| models | object | No | Model assignments by slot (integer model ids). |
| knowledge_bases | array | No | Knowledge bases to retrieve from. |
| skills | array | No | Skills the agent can recall. |
| tool_categories | array | No | Tool categories the agent may use. |
| suggested_prompts | array | No | Starter prompts shown to users. |
| generate_runtime_key | boolean | No | Return a runtime key in the response. Default true. |
Request:
{
"name": "Research Assistant",
"description": "Competitor analysis on demand",
"instructions": "Produce concise, sourced competitor reports.",
"execution_mode": "balanced",
"knowledge_bases": [],
"skills": ["evidence-based-rag"],
"tool_categories": ["basic", "file"],
"suggested_prompts": ["Summarize the market"],
"generate_runtime_key": true
}Response (200): the created agent, plus the runtime key when generate_runtime_key is true.
{
"agent": {
"id": 42,
"user_id": 7,
"name": "Research Assistant",
"description": "Competitor analysis on demand",
"instructions": "Produce concise, sourced competitor reports.",
"execution_mode": "balanced",
"models": null,
"knowledge_bases": [],
"skills": ["evidence-based-rag"],
"tool_categories": ["basic", "file"],
"suggested_prompts": ["Summarize the market"],
"logo_url": null,
"status": "active",
"published_at": null,
"created_at": "2026-06-23T12:00:00Z",
"updated_at": "2026-06-23T12:00:00Z",
"widget_enabled": false,
"allowed_domains": []
},
"api_key": {
"full_key": "xag_Ab3xY9_Qw7Rt2Kp9Lm4Nz8Vc1Bd6Hf5Jg0Xs3Tr",
"key_prefix": "Ab3xY9",
"created_at": "2026-06-23T12:00:00Z"
}
}Save the runtime key
api_key.full_key is returned only once. Store it securely — you cannot retrieve it again, only rotate it.
Errors: invalid_api_key (401), invalid_input (400 — e.g. duplicate name or invalid knowledge base / tool).
Create from a template — POST /v1/agents/from-template
Request: template_id is required; other fields override the template defaults.
{
"template_id": "marketing-content-agent",
"name": "My Marketing Agent",
"generate_runtime_key": true
}Response (200): same shape as Create an agent — an agent object with an optional api_key.
Errors: invalid_api_key (401), template_not_found (404), invalid_input (400).
Rotate a runtime key — POST /v1/agents/{agent_id}/api-key
Issues a fresh runtime key for the agent, revoking the previous one.
Response (200):
{
"full_key": "xag_Ab3xY9_Qw7Rt2Kp9Lm4Nz8Vc1Bd6Hf5Jg0Xs3Tr",
"key_prefix": "Ab3xY9",
"created_at": "2026-06-23T12:05:00Z"
}Errors: invalid_api_key (401), agent_not_found (404).
Templates
Template reads use a personal key.
List templates — GET /v1/templates
Response (200):
[
{
"id": "marketing-content-agent",
"name": "Marketing Content Agent",
"category": "Marketing",
"featured": true,
"description": "Drafts on-brand marketing copy.",
"features": ["Web search", "Brand voice"],
"connections": [],
"setup_time": "5 min setup",
"tags": ["marketing", "content"],
"author": "Xagent",
"version": "1.0.0"
}
]Errors: invalid_api_key (401).
Get a template — GET /v1/templates/{template_id}
Response (200): the summary above plus an agent_config object used to provision the agent.
{
"id": "marketing-content-agent",
"name": "Marketing Content Agent",
"category": "Marketing",
"featured": true,
"description": "Drafts on-brand marketing copy.",
"features": ["Web search", "Brand voice"],
"connections": [],
"setup_time": "5 min setup",
"tags": ["marketing", "content"],
"author": "Xagent",
"version": "1.0.0",
"agent_config": {
"execution_mode": "balanced",
"tool_categories": ["basic"],
"skills": []
}
}Errors: invalid_api_key (401), template_not_found (404).
Tasks
Task endpoints use a runtime key. Task creation is asynchronous: it returns 202 Accepted and you poll for the result.
SDK tasks are a separate space
These endpoints only see tasks created through the SDK. A task started from the web app is not visible here — requesting it returns 404 task_not_found.
Create a task — POST /v1/chat/tasks
Requires an agent-bound runtime key. A workforce-bound key is rejected with 404 agent_not_found — use workforce runs instead.
| Field | Type | Required | Description |
|---|---|---|---|
| agent_id | integer | Yes | Agent to run. Must match the agent bound to the runtime key. |
| message | object | Yes | First user message — see the message object. |
| metadata | object | No | Free-form correlation data (e.g. request_id). Accepted and validated; not interpreted by the server. |
| connector_runtime_context | array | No | Per-run configuration for MCP servers or custom APIs — see connector runtime context. Fixed for the life of the task. |
The message object
| Field | Type | Required | Description |
|---|---|---|---|
| role | string | No | Must be user (the default). Any other value is rejected. |
| content | string | Yes | The message text. Must be non-empty. |
| files | array | No | File ids from POST /v1/chat/files to attach to this turn. |
Request:
curl -X POST https://your-domain.com/v1/chat/tasks \
-H "Authorization: Bearer xag_Ab3xY9_Qw7Rt2Kp9Lm4Nz8Vc1Bd6Hf5Jg0Xs3Tr" \
-H "Content-Type: application/json" \
-d '{
"agent_id": 42,
"message": { "role": "user", "content": "Summarize the EV battery market." },
"metadata": { "request_id": "req_0c1d" }
}' Response (202 Accepted):
{
"task_id": 101,
"agent_id": 42,
"status": "running",
"created_at": "2026-06-23T12:00:00Z",
"run_id": "run_9f2c1a",
"state_version": 1,
"control_state": "running"
}run_id identifies this execution attempt, state_version increments as the task's state changes, and control_state reports the execution controller's view (for example running or pause_requested). Treat all three as opaque strings/counters rather than fixed enumerations.
Errors: invalid_api_key (401), agent_not_found (404 — body agent_id missing from a workforce key or not matching this key), invalid_input (422 — empty content or wrong role; 400 — inaccessible file ids), plus the connector runtime codes when connector_runtime_context is supplied.
Get task status — GET /v1/chat/tasks/{task_id}
Poll until status is completed or failed. input / output reflect the latest turn.
Response (200):
{
"task_id": 101,
"agent_id": 42,
"workforce_id": null,
"status": "completed",
"run_id": "run_9f2c1a",
"state_version": 4,
"control_state": "completed",
"input": "Summarize the EV battery market.",
"output": "The EV battery market is projected to...",
"error": null,
"created_at": "2026-06-23T12:00:00Z",
"completed_at": "2026-06-23T12:00:47Z"
}status is one of pending, running, paused, completed, or failed. workforce_id is populated only for tasks reached with a workforce-bound key.
completed_at is approximate
completed_at is derived from the last-updated time of the task once it reaches a terminal state, so treat it as an approximation rather than an exact completion timestamp.
Errors: invalid_api_key (401), task_not_found (404).
List steps — GET /v1/chat/tasks/{task_id}/steps
Returns the full known timeline in started_at order. Each step is one of four types — thinking, tool_call, agent_delegation, message — with a type-specific data object.
Response (200):
{
"task_id": 101,
"agent_id": 42,
"steps": [
{
"id": "thinking:a1",
"type": "thinking",
"status": "completed",
"started_at": "2026-06-23T12:00:01Z",
"completed_at": "2026-06-23T12:00:03Z",
"data": { "phase": "planning" }
},
{
"id": "tool_call:b2",
"type": "tool_call",
"status": "completed",
"started_at": "2026-06-23T12:00:04Z",
"completed_at": "2026-06-23T12:00:09Z",
"data": {
"name": "web_search",
"args": { "q": "EV battery market size" },
"result": { "hits": 5 }
}
}
]
}Errors: invalid_api_key (401), task_not_found (404).
Append a message — POST /v1/chat/tasks/{task_id}/messages
Continue an existing task with another user turn. The task must not be mid-run — a running task returns task_busy (409); poll its status, then append. Accepts either runtime key role, but which owner field you send depends on the key:
| Your key is bound to | Send | Must not send |
|---|---|---|
| An agent | agent_id — required, and must match the key | workforce_id |
| A workforce | Nothing — or workforce_id matching the key | agent_id |
| Mistake | Result |
|---|---|
Agent key without agent_id | 422 invalid_input — message agent_id is required |
Agent key with a wrong agent_id | 404 agent_not_found |
Agent key that also sends workforce_id | 404 workforce_not_found |
Workforce key that sends agent_id | 404 agent_not_found |
Workforce key with a mismatched workforce_id | 404 workforce_not_found |
Request (agent-bound key):
{
"agent_id": 42,
"message": { "role": "user", "content": "Now focus on solid-state batteries." }
}Response (202 Accepted):
{
"task_id": 101,
"agent_id": 42,
"workforce_id": null,
"status": "running",
"accepted_at": "2026-06-23T12:05:00Z",
"run_id": "run_7b41de",
"state_version": 5,
"control_state": "running"
}accepted_at is when the server accepted the turn, which is not necessarily the stored timestamp of the message itself.
Errors: invalid_api_key (401), agent_not_found / workforce_not_found (404, see the table above), task_not_found (404), task_busy (409), runtime_context_immutable (409 — the connector runtime context cannot be changed after creation), invalid_input (422; 400 for inaccessible file ids).
Files
Upload files — POST /v1/chat/files
Upload one or more files, then reference the returned ids in message.files on a task turn. Accepts either runtime key role. Send the files as multipart form data under the repeated field name files.
| Parameter | In | Required | Description |
|---|---|---|---|
| files | form-data | Yes | One or more files. Repeat the field to upload several at once. |
| task_id | query | No | Attach the upload to an existing task you own. Omit it when uploading before the task exists. |
Request:
curl -X POST "https://your-domain.com/v1/chat/files" \
-H "Authorization: Bearer xag_Ab3xY9_Qw7Rt2Kp9Lm4Nz8Vc1Bd6Hf5Jg0Xs3Tr" \
-F "files=@quarterly-report.pdf" \
-F "files=@figures.xlsx" Response (200):
{
"files": [
{
"file_id": "file_3ad91c",
"filename": "quarterly-report.pdf",
"file_size": 248311,
"mime_type": "application/pdf"
},
{
"file_id": "file_5be72f",
"filename": "figures.xlsx",
"file_size": 18244,
"mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
}
]
}Pass those ids on the next turn:
{
"agent_id": 42,
"message": {
"role": "user",
"content": "Summarize the attached report.",
"files": ["file_3ad91c", "file_5be72f"]
}
}File ids resolve all-or-nothing
If any id in message.files is unknown, not yours, or already bound to another task, the whole turn is rejected with 400 invalid_input and a message listing the offending ids. Nothing is created.
Errors: invalid_api_key (401), task_not_found (404 — unknown task_id), invalid_input (400 unsupported file type; 413 too large), internal_error (503 — File storage is temporarily unavailable.).
Workforces
Start a workforce run — POST /v1/workforces/{workforce_id}/runs
Starts a run of a multi-agent workforce. Requires a workforce-bound runtime key, and the path workforce_id must match that key.
| Field | Type | Required | Description |
|---|---|---|---|
| message | object | Yes | The opening user message — same shape as a task message, including optional files. |
| execution_mode | string | No | Override the mode for this run: flash, balanced, think, or auto. |
| idempotency_key | string | No | Up to 128 characters. Makes the call safe to retry — see below. |
Request:
curl -X POST https://your-domain.com/v1/workforces/7/runs \
-H "Authorization: Bearer xag_Wk8pQ2_Lm3Nv6Rt9Bd1Hf4Jg7Xs0Ae5Cz2Yu" \
-H "Content-Type: application/json" \
-d '{
"message": { "role": "user", "content": "Draft the Q3 competitor brief." },
"idempotency_key": "brief-2026-q3-001"
}' Response (202 Accepted):
{
"workforce_run_id": 55,
"workforce_id": 7,
"task_id": 812,
"agent_id": 91,
"status": "running",
"created": true,
"created_at": "2026-08-10T09:00:00Z",
"run_id": "run_2c8f40",
"state_version": 1,
"control_state": "running"
}agent_id is the workforce's manager agent. Track progress with the same task endpoints using the returned task_id.
Retrying safely with idempotency_key
Reusing an idempotency_key for the same workforce returns the original run unchanged with "created": false — no second run starts, no extra turn is queued, and the call is not metered again. Omit the key and every request starts a new run. If the original run has since been deleted the key cannot be replayed, and the request fails with 409 idempotency_conflict — use a new key.
Errors: invalid_api_key (401 — including an agent-bound key), workforce_not_found (404), workforce_archived / workforce_not_active / idempotency_conflict (409, all permanent), file_not_found (404), task_busy (409), invalid_input (422).
Connector Runtime Context
Both write endpoints optionally accept connector_runtime_context — per-run configuration and credentials for an MCP server or custom API the agent may call. Each entry identifies one connector:
{
"agent_id": 42,
"message": { "role": "user", "content": "File the ticket." },
"connector_runtime_context": [
{
"connector_ref": { "connector_type": "mcp", "connector_id": 7 },
"context": { "workspace": "acme" },
"secrets": { "api_token": "..." },
"auth_selector": { "account": "primary" }
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
| connector_ref.connector_type | string | Yes | mcp or custom_api. |
| connector_ref.connector_id | integer | Yes | The connector to configure. |
| context | object | No | Non-secret runtime values. |
| secrets | object | No | Secret runtime values. Redacted from task step data. |
| auth_selector | object | No | Selects which stored credential to use. |
Set once, at creation
Runtime context is fixed when the task is created. Sending a different context on a later turn returns 409 runtime_context_immutable. Unknown fields anywhere in this object are rejected with 422.