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/v1

Authentication

Send an API key as a bearer token. Three key roles cover the surface:

KeyFormatUsed by
Personal keyxag_personal_<prefix>_<secret>/v1/me, /v1/agents*, /v1/templates*
Runtime key — agentxag_<prefix>_<secret>/v1/chat/* (creating and continuing tasks)
Runtime key — workforcexag_<prefix>_<secret>/v1/workforces/*, plus continuing and reading its own tasks
Authorization: Bearer xag_Ab3xY9_Qw7Rt2Kp9Lm4Nz8Vc1Bd6Hf5Jg0Xs3Tr

Both 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

CodeHTTPMeaning
invalid_api_key401Any 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_input422, also 400 / 413Body or parameter validation failed. 400 for client-side rejections (e.g. inaccessible file ids); 413 when an upload exceeds the size cap.
agent_not_found404Agent does not exist or is not bound to this key. Also returned when a workforce key sends agent_id.
workforce_not_found404Workforce does not exist or is not bound to this key. Also returned when an agent key sends workforce_id.
template_not_found404Template id is unknown or unavailable.
task_not_found404Task does not exist, does not belong to this key, or was not created through the SDK.
file_not_found404A file id in message.files is inaccessible. Returned on workforce-run creation; the task endpoints report this as invalid_input (400) instead.
task_busy409Task is still running and cannot accept new input yet. Retryable — poll status, then retry.
internal_error500, also 503Server-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.

CodeMeaning
workforce_archivedThe workforce is archived. Restore it or target another workforce.
workforce_not_activeThe workforce is not in active status (for example, still a draft). Publish it first.
workforce_config_changedThe run's pinned configuration no longer matches the workforce. Start a new run.
idempotency_conflictThe 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.

CodeHTTPMeaning
connector_not_found404The referenced connector was not found or is not accessible.
invalid_runtime_context400Malformed runtime context (duplicate reference, bad shape, unsupported selector).
missing_runtime_context400A required runtime context value was not supplied.
runtime_context_immutable409Runtime context cannot be changed after the task is created.
runtime_secret_not_allowed400A runtime secret is not permitted for this entry point.
runtime_secret_unavailable400A required runtime secret is unavailable.
connector_runtime_unavailable503Runtime 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

Auth: Personal keyxag_personal_<prefix>_<secret>

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

Auth: Personal keyxag_personal_<prefix>_<secret>

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

Auth: Personal keyxag_personal_<prefix>_<secret>

FieldTypeRequiredDescription
namestringYesDisplay name. 1–200 characters.
descriptionstringNoShort summary of the agent.
instructionsstringNoSystem prompt for the agent.
execution_modestringNoOne of flash, balanced, think, auto. Default balanced.
modelsobjectNoModel assignments by slot (integer model ids).
knowledge_basesarrayNoKnowledge bases to retrieve from.
skillsarrayNoSkills the agent can recall.
tool_categoriesarrayNoTool categories the agent may use.
suggested_promptsarrayNoStarter prompts shown to users.
generate_runtime_keybooleanNoReturn 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

Auth: Personal keyxag_personal_<prefix>_<secret>

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

Auth: Personal keyxag_personal_<prefix>_<secret>

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

Auth: Personal keyxag_personal_<prefix>_<secret>

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}

Auth: Personal keyxag_personal_<prefix>_<secret>

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

Auth: Runtime keyxag_<prefix>_<secret>

Requires an agent-bound runtime key. A workforce-bound key is rejected with 404 agent_not_found — use workforce runs instead.

FieldTypeRequiredDescription
agent_idintegerYesAgent to run. Must match the agent bound to the runtime key.
messageobjectYesFirst user message — see the message object.
metadataobjectNoFree-form correlation data (e.g. request_id). Accepted and validated; not interpreted by the server.
connector_runtime_contextarrayNoPer-run configuration for MCP servers or custom APIs — see connector runtime context. Fixed for the life of the task.

The message object

FieldTypeRequiredDescription
rolestringNoMust be user (the default). Any other value is rejected.
contentstringYesThe message text. Must be non-empty.
filesarrayNoFile 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}

Auth: Runtime keyxag_<prefix>_<secret>

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

Auth: Runtime keyxag_<prefix>_<secret>

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

Auth: Runtime keyxag_<prefix>_<secret>

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 toSendMust not send
An agentagent_id — required, and must match the keyworkforce_id
A workforceNothing — or workforce_id matching the keyagent_id
MistakeResult
Agent key without agent_id422 invalid_input — message agent_id is required
Agent key with a wrong agent_id404 agent_not_found
Agent key that also sends workforce_id404 workforce_not_found
Workforce key that sends agent_id404 agent_not_found
Workforce key with a mismatched workforce_id404 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

Auth: Runtime keyxag_<prefix>_<secret>

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.

ParameterInRequiredDescription
filesform-dataYesOne or more files. Repeat the field to upload several at once.
task_idqueryNoAttach 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

Auth: Runtime keyxag_<prefix>_<secret>

Starts a run of a multi-agent workforce. Requires a workforce-bound runtime key, and the path workforce_id must match that key.

FieldTypeRequiredDescription
messageobjectYesThe opening user message — same shape as a task message, including optional files.
execution_modestringNoOverride the mode for this run: flash, balanced, think, or auto.
idempotency_keystringNoUp 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" }
    }
  ]
}
FieldTypeRequiredDescription
connector_ref.connector_typestringYesmcp or custom_api.
connector_ref.connector_idintegerYesThe connector to configure.
contextobjectNoNon-secret runtime values.
secretsobjectNoSecret runtime values. Redacted from task step data.
auth_selectorobjectNoSelects 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.

Next Steps