Claude Code agent as an HTTP API.
Runs the Claude Code CLI agent via the Agent SDK with a worker pool, queue, and multi-turn sessions.
curl / bot / claude-code-web
↓ HTTP (x-api-key or Bearer token)
claude-code-api (worker pool + queue)
↓ Agent SDK
Claude Code CLI (agent execution)
git clone https://github.com/exitxio/claude-code-api.git
cd claude-code-api
cp .env.example .env
# Edit .env — set NEXTAUTH_SECRET (required)Generate a secret:
openssl rand -base64 32pnpm docker:up| Script | Command |
|---|---|
pnpm docker:up |
Build & start containers |
pnpm docker:down |
Stop containers |
pnpm docker:logs |
Follow container logs |
pnpm docker:prod |
Start with pre-built GHCR image |
curl http://localhost:8080/healthdocker run -d -p 8080:8080 \
-e NEXTAUTH_SECRET=your-secret \
-e API_KEYS=sk-my-secret-key \
-v claude-auth:/home/node/.claude \
-v agent-home:/home/node/users \
ghcr.io/exitxio/claude-code-api:latestTwo authentication methods are supported (checked in order):
Set API_KEYS env var with comma-separated keys:
# Simple — userId defaults to "api"
API_KEYS=sk-my-secret-key
# Prefixed — userId derived from prefix (prefix:key)
# "myapp" and "bot" become the userId for each key
API_KEYS=myapp:sk-key1,bot:sk-key2Note: The
x-api-keyheader takes only the key part, not the prefix. The prefix is only used in the env var to map keys to userIds.
# If API_KEYS=myapp:sk-key1 → send sk-key1, not myapp:sk-key1
curl -X POST http://localhost:8080/run \
-H "x-api-key: sk-key1" \
-H "Content-Type: application/json" \
-d '{"prompt": "What is 2+2?"}'Used by claude-code-web for internal communication. Requires NEXTAUTH_SECRET to be shared between api and web.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/health |
No | Health check — worker pool status |
POST |
/run |
Yes | Execute a prompt |
GET |
/status |
Yes | Detailed queue and session status |
DELETE |
/session |
Yes | Close a named session |
GET |
/user-claude |
Yes | Read user's CLAUDE.md |
PUT |
/user-claude |
Yes | Save user's CLAUDE.md |
GET |
/auth/status |
Yes | Claude OAuth status |
POST |
/auth/login |
Yes | Start Claude OAuth flow |
POST |
/auth/exchange |
Yes | Complete Claude OAuth flow |
{
"prompt": "Explain this code",
"sessionId": "optional-session-id",
"timeoutMs": 120000
}Response:
{
"success": true,
"output": "This code does...",
"durationMs": 5432,
"timedOut": false
}- Without
sessionId: uses a stateless worker from the pool (one-shot) - With
sessionId: creates/reuses a persistent session with conversation history
| Variable | Default | Description |
|---|---|---|
NEXTAUTH_SECRET |
required | Secret for HMAC token verification |
API_KEYS |
— | Comma-separated API keys (see Authentication) |
CLAUDE_MODEL |
claude-sonnet-4-6 |
Claude model to use |
POOL_SIZE |
1 |
Number of pre-warmed workers |
PORT |
8080 |
Host port mapping |
USE_CLAUDE_API_KEY |
— | Set to 1 to use ANTHROPIC_API_KEY instead of OAuth |
By default, the server uses Claude OAuth (subscription-based). Credentials are stored in a Docker volume (claude-auth).
Option A: OAuth (subscription) — Use the claude-code-web UI, or authenticate directly via curl:
# 1. Get OAuth URL
curl -X POST http://localhost:8080/auth/login \
-H "x-api-key: YOUR_API_KEY"
# → {"url":"https://claude.ai/oauth/authorize?..."}
# 2. Open the URL in a browser → sign in → copy the code from the callback page
# (looks like: aBcDeFg...#xYz123...)
# 3. Exchange the code for tokens
curl -X POST http://localhost:8080/auth/exchange \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"code": "PASTE_THE_FULL_CODE#STATE_HERE"}'
# → {"success":true}Credentials persist in the claude-auth Docker volume. You only need to do this once.
Option B: API key (pay-per-token) — Set ANTHROPIC_API_KEY and USE_CLAUDE_API_KEY=1 in environment. No OAuth needed.
claude-code-api and claude-code-web run as separate Docker Compose stacks, connected via a shared Docker network.
claude-code-api (port 8080) ──┐
├── exitx network
claude-code-web (port 3000) ──┘
claude-code-api creates and owns the exitx network:
# claude-code-api/docker-compose.yml
networks:
exitx:
name: exitx
driver: bridgeclaude-code-web joins as external:
# claude-code-web/docker-compose.yml
services:
web:
environment:
- AUTOMATION_SERVER_URL=http://claude-code-api:8080
networks:
- exitx
networks:
exitx:
external: trueStart order: api first, then web.
# 1. Start API
cd claude-code-api && pnpm docker:up
# 2. Start Web
cd claude-code-web && pnpm docker:upHealth check (no auth required):
curl http://localhost:8080/healthSingle-shot prompt:
curl -X POST http://localhost:8080/run \
-H "x-api-key: sk-my-secret-key" \
-H "Content-Type: application/json" \
-d '{"prompt": "What is 2+2?"}'
# {"success":true,"output":"Four.","durationMs":1116,"timedOut":false}Multi-turn conversation (use the same sessionId):
curl -X POST http://localhost:8080/run \
-H "x-api-key: sk-my-secret-key" \
-H "Content-Type: application/json" \
-d '{"prompt": "My name is Alice.", "sessionId": "session-1"}'
curl -X POST http://localhost:8080/run \
-H "x-api-key: sk-my-secret-key" \
-H "Content-Type: application/json" \
-d '{"prompt": "What is my name?", "sessionId": "session-1"}'
# {"success":true,"output":"Your name is Alice.","durationMs":...}Check status:
curl http://localhost:8080/status \
-H "x-api-key: sk-my-secret-key"Use docker-compose.prod.yml for production deployment with resource limits and log rotation:
pnpm docker:prodpnpm install
# Set env vars (NEXTAUTH_SECRET, etc.)
pnpm devMIT