Skip to content

Agent API ​

Setup ​

1. Add an API key to config.json

json
{
  "gateway": {
    "api": {
      "keys": [
        {
          "key": "my-secret-key-123",
          "description": "My app",
          "agents": ["alfred"]
        },
        {
          "key": "admin-key-456",
          "description": "Admin — full access",
          "agents": "*",
          "admin": true
        },
        {
          "key": "automation-key-789",
          "description": "Automation — may use tools",
          "agents": ["alfred"],
          "allow_tools": true
        }
      ]
    }
  }
}

agents can be an array of agent IDs or "*" for access to all agents. Administrative operations additionally require admin: true; write operations require write: true or admin access. Keys support ${ENV_VAR} interpolation.

Tool access (Read, Bash, Grep, etc.) is configured server-side. An explicit agent allow_tools value takes precedence; otherwise the API uses the key's allow_tools flag. No request-body field grants tool access. Set the agent value to false to enforce conversational-only behavior.

2. Restart the gateway

bash
npm start

GET /api/v1/agents ​

List agents accessible by the provided API key.

bash
curl -H "X-Api-Key: my-secret-key-123" \
  http://localhost:10850/api/v1/agents | jq
json
{
  "agents": [
    { "id": "alfred", "name": null, "description": "Personal assistant", "model": "claude-sonnet-4-6", "allow_tools": false }
  ]
}

name is an optional display name (null when unset); the UI falls back to id in that case.

orchestration_enabled reports the effective gateway mode for this Agent. When true, clients must continue observing session history after an acknowledgement stream ends: completed tasks produce later assistant replies. False or absent retains legacy request/response behavior.


POST /api/v1/agents ​

Create a new agent entry in config.json. Requires admin key. Also creates the workspace directory with stub files (AGENTS.md, SOUL.md, USER.md, MEMORY.md).

Request body:

FieldRequiredDescription
idYesAgent ID — pattern [a-z][a-z0-9_-]{1,31}
descriptionYesHuman-readable description
modelNoClaude model ID (default: claude-sonnet-4-6)
allow_toolsNoWhether the agent may invoke tools when accessed via the API channel (default: true). Pass false to create a conversational-only agent.
bash
curl -X POST \
  -H "X-Api-Key: admin-key-456" \
  -H "Content-Type: application/json" \
  -d '{"id": "my-bot", "description": "My new bot", "model": "claude-sonnet-4-6"}' \
  http://localhost:10850/api/v1/agents | jq
json
{ "agent": { "id": "my-bot", "description": "My new bot", "model": "claude-sonnet-4-6", "allow_tools": true } }

Error responses:

StatusWhen
400Invalid id format, missing description, or non-boolean allow_tools
403Not an admin key
409Agent ID already exists
501Gateway started without a config path

PATCH /api/v1/agents/:agentId ​

Update an agent's display name, description, model, allow_tools flag, or connector enablement. Requires write access to the agent; the connectors field additionally requires admin (see below). Only fields provided are updated.

Request body (all optional):

FieldTypeDescription
namestring | nullDisplay name shown in the UI instead of id. Empty string or null clears it (falls back to id)
descriptionstringNew description
modelstringNew Claude model ID
allow_toolsbooleanOverride tool access for this agent
connectorsobjectAdmin only — a non-admin key gets 403. Per-connector enablement, { "<connectorId>": { "enabled": boolean } }. Merged into the agent's existing map, not replacing it — send only the ids you are changing. enabled must be a boolean, and each key must be a valid connector id (^[a-z0-9][a-z0-9-]*$, max 64 chars), or the request is 400

connectors is the one field on this route that is admin-gated, because it is the only one that reaches a credential somebody else owns: every mutating route under Connectors API is admin-only, and enabling a connector here resolves that connector's secret into the agent's MCP config at spawn. Under gateway.connectorsDefaultEnabled: false a write key scoped to a single agent could otherwise hand that agent any token an admin had connected.

An agent's own flag always wins. Without one, the agent gets a connected connector unless the connector itself stores defaultEnabled: false, or the gateway sets gateway.connectorsDefaultEnabled: false. A connector added without a scope reaches every agent; restrict it with defaultEnabled: false or with PUT /api/v1/connectors/:id/agents. A paused connector (PATCH /api/v1/connectors/:id) reaches no agent whatever this flag says.

Ids are validated for shape only, not existence — pre-setting {"enabled": false} for a connector nobody has added yet is a legitimate way to keep it off an agent from the moment it appears.

Changing connectors restarts the agent's sessions so the new MCP set takes effect: a running session's MCP subprocess has the old connector set baked into its env and cannot be hot-patched. The restart is lossless — a busy session finishes its current turn and restarts after it, an idle channel session is armed to restart on its next message, and only api / heartbeat sessions (which respawn fresh on next use anyway) are stopped right away. Nothing in flight is killed.

bash
curl -X PATCH \
  -H "X-Api-Key: admin-key-456" \
  -H "Content-Type: application/json" \
  -d '{"connectors": {"github": {"enabled": false}}}' \
  http://localhost:10850/api/v1/agents/alfred | jq
bash
curl -X PATCH \
  -H "X-Api-Key: admin-key-456" \
  -H "Content-Type: application/json" \
  -d '{"model": "claude-opus-4-8"}' \
  http://localhost:10850/api/v1/agents/alfred | jq
json
{ "agent": { "id": "alfred", "name": null, "description": "Personal assistant", "model": "claude-opus-4-8", "allow_tools": false } }

PUT /api/v1/agents/:agentId/avatar ​

Upload or replace an agent's avatar image. Requires write access to the agent.

Request: raw image binary as the request body.

ConstraintValue
Allowed typesimage/jpeg, image/png, image/webp, image/gif
Max size5 MB
Type detectionMagic bytes (ignores Content-Type header)
bash
curl -X PUT \
  -H "X-Api-Key: write-key" \
  --data-binary @avatar.png \
  http://localhost:10850/api/v1/agents/alfred/avatar | jq
json
{ "avatarUrl": "/api/v1/agents/alfred/avatar" }

The file is written to ~/.claude-gateway/agents/{agentId}/avatar.{ext} and the avatar field in config.json is updated. If an old avatar exists with a different extension, it is removed.

Error responses:

StatusWhen
400Empty body or file too small
403Write permission required
413File exceeds 5 MB
415Unrecognised image format

DELETE /api/v1/agents/:agentId/avatar ​

Remove an agent's avatar. Requires write access. Returns 204 No Content on success.

bash
curl -X DELETE \
  -H "X-Api-Key: write-key" \
  http://localhost:10850/api/v1/agents/alfred/avatar

PUT /api/v1/agents/:agentId/pstack ​

Set up pstack for an agent: its workers load the pstack@pstack-claude Claude Code plugin with these role models. Requires write access to the agent (an admin key may change any agent). The plugin must already be installed through Claude Code. A field in the body replaces that field of agents[].pstack and a field left out keeps its saved value ("roles": {} resets every role to its skill default); changes apply to the agent's next worker.

bash
curl -X PUT -H "X-Api-Key: write-key" -H "Content-Type: application/json" \
  -d '{"roles":{"why investigators":"gpt-6-sol[1m]"},"defaultEffort":"high"}' \
  http://localhost:10850/api/v1/agents/alfred/pstack
json
{ "agentId": "alfred", "pstack": { "roles": { "why investigators": "gpt-6-sol[1m]" }, "defaultEffort": "high" } }

Errors:

StatusBodyWhen
400{ "error": "<message>", "code": "<CODE>" }code is MODEL_ALIAS_NOT_ALLOWED (an alias where a model ID is required; the message names the ID), UNKNOWN_MODEL, INVALID_EFFORT (an @<level> on a role value that is not a level), UNKNOWN_PSTACK_ROLE, INVALID_PSTACK_CONFIG (an unknown field, roles that is not a JSON object, a defaultEffort that is not session or a level, or a list of models on a role that is not a panel), PSTACK_NOT_INSTALLED or PSTACK_UNSUPPORTED_AGENT (an app agent, whose workers run in its container)
403{ "error": "Write permission required" }The key cannot write this agent; checked first, so a write key that is not an admin key gets 403 for an agent that does not exist
404{ "error": "Agent '<id>' not found" }No such agent
500{ "error": "Failed to save pstack configuration" }config.json could not be read or written; the cause is logged
501{ "error": "Agent management not available (no configPath)" }The gateway runs without a config file

CLI: claude-gateway pstack enable <agentId> [--roles <json>] [--defaultEffort <level>].


DELETE /api/v1/agents/:agentId/pstack ​

Remove pstack from an agent: its next worker does not load the plugin. Requires write access. Returns { "agentId": "alfred", "pstack": null }; 403, 404 and 501 as for PUT.

CLI: claude-gateway pstack disable <agentId>.


GET /api/v1/agents/:agentId/avatar ​

Serve the agent's avatar image. Requires read access to the agent.

  • Cache-Control: private, max-age=3600
  • Returns the raw image bytes with the correct Content-Type
  • Returns 404 if no avatar has been set or the file is missing
bash
curl -H "X-Api-Key: my-key" \
  http://localhost:10850/api/v1/agents/alfred/avatar -o avatar.png

Wizard API — multi-step agent creation ​

The Wizard API mirrors the interactive claude-gateway agents create terminal wizard but is consumable by web UIs and automation. State is kept in memory with a 30-minute TTL (refreshed on each step transition); nothing is written to disk until the /confirm step.

State machine:

start → (optional avatar upload) → confirm → (optional channel) → (verify) → complete

POST /api/v1/agents/wizard/start ​

Auth: admin key.

Calls Claude to generate workspace markdown files based on your prompt. Returns a wizardId for subsequent steps.

Request body:

FieldRequiredDescription
idYesAgent ID — pattern [a-z][a-z0-9_-]{1,31}
promptYesNatural-language description of the agent
bash
curl -X POST \
  -H "X-Api-Key: admin-key-456" \
  -H "Content-Type: application/json" \
  -d '{"id": "cryptobot", "prompt": "A helpful assistant that specialises in crypto analysis, speaks Thai..."}' \
  http://localhost:10850/api/v1/agents/wizard/start | jq
json
{
  "wizardId": "550e8400-e29b-41d4-a716-446655440000",
  "agentId": "cryptobot",
  "files": {
    "AGENTS.md": "# Agent: Cryptobot\n\n...",
    "SOUL.md": "...",
    "USER.md": "...",
    "MEMORY.md": ""
  },
  "expiresAt": "2026-05-15T09:03:00Z"
}

Error responses:

StatusWhen
400Invalid id format or missing prompt
403Not an admin key
409Agent already exists, or a confirmed/complete wizard is already in progress for this ID (a pending draft is replaced instead)
429Too many wizard starts in progress (max 2 concurrent)
500Claude generation failed

PUT /api/v1/agents/wizard/:wizardId/avatar ​

Auth: admin key. Optional step before /confirm.

Upload an avatar for the agent being created. The image is held in memory and written to disk during /confirm.

Request: raw image binary (same constraints as the regular avatar upload — 5 MB max, jpeg/png/webp/gif).

json
{ "preview": true }

POST /api/v1/agents/wizard/:wizardId/confirm ​

Auth: admin key.

Write workspace files and avatar to disk, add the agent to config.json, and trigger a hot-reload so the agent starts automatically.

Request body:

FieldRequiredDescription
filesNoMap of filename → content. If omitted, the files generated in /start are used. Must include AGENTS.md.
bash
curl -X POST \
  -H "X-Api-Key: admin-key-456" \
  -H "Content-Type: application/json" \
  -d '{"files": {"AGENTS.md": "# Agent: Cryptobot\n\n...", "SOUL.md": "..."}}' \
  http://localhost:10850/api/v1/agents/wizard/550e8400.../confirm | jq
json
{
  "agentId": "cryptobot",
  "avatarUrl": "/api/v1/agents/cryptobot/avatar",
  "next": "channel via POST /api/v1/agents/wizard/.../channel, or skip via POST .../complete"
}

POST /api/v1/agents/wizard/:wizardId/channel ​

Auth: admin key. Optional step after /confirm.

Token-only connect. Verify a Telegram or Discord bot token, persist it to the agent config, seed a secure access.json, and hot-start the receiver so the bot comes online immediately. No pairing code is minted here — pairing is incoming-first and happens later: the owner (or a group member) DMs the bot, a one-time code lands in Pending, and an admin approves it from the agent's Channels card (see the Telegram/Discord Channel APIs below). This mirrors the LINE flow.

Request body:

FieldRequiredDescription
channelYes"telegram" or "discord"
botTokenYesBot token from BotFather / Discord Developer Portal

Response:

json
{
  "channel": "telegram",
  "botName": "@my_crypto_bot",
  "connected": true
}

The wizard advances to step complete. A brand-new connection is seeded with a closed-but-pairing-on access.json (dmPolicy: "allowlist", pairing: true), so the owner can DM the bot and self-approve via a code.


POST /api/v1/agents/wizard/:wizardId/complete ​

Auth: admin key.

Finalise the wizard and clean up state. Can be called after /confirm to skip channel setup entirely, or after /channel (the agent keeps whatever channel was connected). Rejected with 409 only while the wizard is still in step pending (the workspace must be confirmed first).

json
{ "agentId": "cryptobot" }

POST /api/v1/agents/describe/rewrite ​

Auth: admin key.

Stateless helper for the "describe your agent" step of agent creation. Calls Claude to clean up a rough draft description into clearer, moderately clarified text — no wizardId/agentId involved, single string in, single string out.

Request body:

FieldRequiredDescription
textYesDraft description to re-write (max 8,000 characters)
bash
curl -X POST \
  -H "X-Api-Key: admin-key-456" \
  -H "Content-Type: application/json" \
  -d '{"text": "bot that like. helps with crypto stuff and is fun to talk 2"}' \
  http://localhost:10850/api/v1/agents/describe/rewrite | jq
json
{
  "text": "A friendly assistant that helps with crypto-related questions in a fun, conversational tone."
}

Error responses:

StatusWhen
400Missing/empty text, or text exceeds 8,000 characters
403Not an admin key
429Too many re-write requests in progress (max 2 concurrent)
500Claude generation failed
502Claude produced no usable output (empty after fence-stripping)

DELETE /api/v1/agents/:agentId ​

Remove an agent from config.json and stop the running process. Requires admin key. Does not delete the workspace directory.

bash
curl -X DELETE \
  -H "X-Api-Key: admin-key-456" \
  http://localhost:10850/api/v1/agents/my-bot | jq
json
{ "success": true, "id": "my-bot" }

Claude Code inside.