API Reference¶
This document provides a complete reference for the AI Sessions REST API and WebSocket protocol.
Base URL¶
| Environment | URL |
|---|---|
| Production | https://ai.limacharlie.io |
| Staging | https://ai-staging.limacharlie.io |
Authentication¶
All API requests require a valid LimaCharlie JWT token in the Authorization header:
For WebSocket connections, you can also pass the token as a query parameter:
Rate Limits¶
| Operation | Limit |
|---|---|
| Registration | 10 requests/minute per user |
| Session creation | 10 requests/minute per user |
| WebSocket messages | 100 messages/second per connection |
REST API Endpoints¶
Registration¶
Register User¶
Register the authenticated user for the AI Sessions platform.
Response: 200 OK¶
Error Responses:
401: Invalid or missing JWT token403: Email domain not in allowed list409: User already registered
Deregister User¶
Deregister the user and delete all associated data. This terminates all active sessions and deletes stored credentials.
Response: 200 OK¶
Sessions¶
List Sessions¶
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
status |
string | Filter by status: starting, running, ended |
limit |
integer | Max results (default 50, max 200) |
cursor |
string | Pagination cursor |
Response: 200 OK¶
{
"sessions": [
{
"id": "abc123",
"status": "running",
"region": "us-central1",
"created_at": "2025-01-15T10:30:00Z",
"started_at": "2025-01-15T10:30:05Z",
"lc_auth_type": "jwt",
"allowed_tools": ["Bash", "Read"],
"denied_tools": ["Write"]
}
],
"next_cursor": "xyz789"
}
Create Session¶
Request Body:
{
"lc_credentials": {
"type": "org_api_key",
"org_api_key": "xxxxxxxx"
},
"provider": "openai",
"allowed_tools": ["Bash", "Read", "Write"],
"denied_tools": ["WebFetch"],
"system_prompt_suffix": "For this session, only investigate hosts tagged prod-web."
}
system_prompt_suffix is optional text appended to the system prompt for this session only. When the profile also has a suffix, the profile suffix comes first, then a blank line, then this value. Maximum 65536 bytes; a longer value returns 400 system_prompt_suffix_too_long.
provider is optional: one of anthropic, openai, google, openrouter. It overrides the profile's provider for this session; when both are omitted the session runs on Claude. See AI Providers.
Response: 201 Created¶
{
"session": {
"id": "abc123",
"status": "starting",
"region": "us-central1",
"created_at": "2025-01-15T10:30:00Z"
}
}
Error Responses:
400: Invalid request body403: Not registered, or no stored credentials for the session's provider409: Maximum concurrent sessions (10) reached
Get Session¶
Response: 200 OK¶
{
"session": {
"id": "abc123",
"status": "running",
"region": "us-central1",
"created_at": "2025-01-15T10:30:00Z",
"started_at": "2025-01-15T10:30:05Z",
"terminated_at": null,
"end_reason": null,
"exit_code": null,
"error_message": null,
"allowed_tools": ["Bash", "Read"],
"denied_tools": ["Write"]
}
}
Rename Session¶
Update a session's user-facing name. The name is metadata over the session
record, so renaming works in every state, ended included.
Request Body¶
Leading and trailing whitespace is trimmed. The trimmed name must be non-empty and at most 200 characters.
Response: 200 OK¶
Returns the updated session in the same shape as Get Session.
{
"session": {
"id": "abc123",
"name": "Ransomware triage - host WIN-DC01",
"status": "running",
"region": "us-central1",
"created_at": "2025-01-15T10:30:00Z"
}
}
Error Responses:
400: Missing or malformed body, empty name (name_required), or a name over 200 characters (name_too_long)403: The session belongs to another user404: No such session
Terminate Session¶
Response: 200 OK¶
Delete Session Record¶
Delete a terminated session from history. Only sessions in the ended state can be deleted.
Response: 200 OK¶
Fork Preflight Check¶
Inspect a source session before forking. Reports whether the source can be forked, a suggested name, and any MCP servers the source uses that are missing from the forker's profile (these must be acknowledged when forking).
Forking your own session needs no special permission. Forking a session owned by the organization (an API/D&R session) requires the ai_agent.set permission on the org.
Query Parameters¶
| Parameter | Description |
|---|---|
profile_id |
The forker's profile ID. Defaults to the caller's default profile if omitted. |
Response: 200 OK¶
{
"source_session_id": "abc123",
"source_session_type": "user",
"source_status": "ended",
"source_name": "Investigation 2025-01-15",
"source_lifetime_days": 3,
"default_fork_name": "Fork of Investigation 2025-01-15",
"source_mcp_servers": ["virustotal"],
"missing_mcps": ["virustotal"],
"is_forkable": true,
"is_forkable_reason": "",
"history_available": true
}
source_session_type is user or api. When missing_mcps is non-empty, pass those names in acknowledge_missing_tools on the fork request.
Fork Session¶
Create a new session forked from the given source. The fork inherits the source's conversation context but starts with the forker's profile. The source must be in a forkable state (dormant or ended, within its retention window).
All fields are optional:
{
"name": "Continued investigation",
"profile_id": "profile-xyz",
"initial_prompt": "Pick up where the previous session left off and check the new IOCs.",
"acknowledge_missing_tools": ["virustotal"]
}
| Field | Type | Description |
|---|---|---|
name |
string | Name for the forked session (defaults to a derived name). |
profile_id |
string | Profile to use for the fork (defaults to the caller's default profile). |
initial_prompt |
string | Initial prompt for the forked session. |
acknowledge_missing_tools |
array | MCP servers present in the source but missing from the fork profile, acknowledged by the caller. |
Response: 201 Created¶
Returns the new session object, the same shape as Create Session. The new session's forked_from_session_id field references the source.
Errors¶
| Status | Meaning |
|---|---|
403 |
Forking an org-owned session without the ai_agent.set permission. |
409 |
Source is not in a forkable state, or a create/fork/resume is already in progress. |
410 |
Source workspace archive is no longer available. |
412 |
Source uses MCP servers missing from the fork profile; acknowledge them via acknowledge_missing_tools. |
429 |
Maximum concurrent sessions reached. |
Profiles¶
List Profiles¶
Response: 200 OK¶
{
"profiles": [
{
"id": "profile123",
"name": "Investigation",
"description": "Profile for security investigations",
"is_default": true,
"allowed_tools": ["Bash", "Read"],
"denied_tools": ["Write"],
"permission_mode": "acceptEdits",
"provider": "anthropic",
"model": "claude-sonnet-4-20250514",
"max_turns": 100,
"max_budget_usd": 10.0,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
]
}
Create Profile¶
Request Body:
{
"name": "Investigation",
"description": "Profile for security investigations",
"allowed_tools": ["Bash", "Read", "Grep"],
"denied_tools": ["Write", "Edit"],
"permission_mode": "acceptEdits",
"provider": "anthropic",
"model": "claude-sonnet-4-20250514",
"max_turns": 100,
"max_budget_usd": 10.0,
"system_prompt_suffix": "You are assisting the SOC team. Always cite sensor IDs in findings.",
"mcp_servers": {
"virustotal": {
"type": "http",
"url": "https://vt-mcp.example.com",
"headers": {
"x-apikey": "hive://secret/vt-api-key"
}
}
},
"is_default": false
}
The system_prompt_suffix is free-form text appended to the agent's system prompt for sessions launched from this profile (max 65536 bytes). It is copied onto the session when the session is created, so editing the profile later does not change sessions already running. To add text for a single session, pass system_prompt_suffix on POST /v1/sessions; it is appended after the profile suffix, separated by a blank line, and has the same 65536-byte cap.
Profile-specific Error Responses:
400 system_prompt_suffix_too_long: the supplied suffix exceeds 65536 bytes (also returned byPUT /v1/profiles/{profileId}andPOST /v1/sessions)
Response: 201 Created¶
provider is optional: one of anthropic, openai, google, openrouter (default anthropic). model must be valid for the chosen provider; when omitted, the provider's default model is used. See AI Providers.
Error Responses:
400: Invalid request body409: Maximum profiles (10) reached
Get Profile¶
Update Profile¶
Delete Profile¶
Note: The default profile cannot be deleted.
Set Default Profile¶
Capture Session as Profile¶
Request Body:
Profile Memory Bank¶
Each profile can carry a small set of markdown files that get mounted into the runner's /workspace/.memory/ whenever a session is launched from the profile. See Profile Memory Bank for an overview and limits.
List memory entries¶
Returns metadata only. Bodies are excluded, so the response stays small for profiles with many entries.
{
"memories": [
{
"path": "preferences.md",
"size": 412,
"content_hash": "9b2f…",
"created_at": "2026-05-01T10:14:32Z",
"updated_at": "2026-05-04T08:02:11Z"
}
]
}
Read a memory entry¶
The path is URL-encoded; it must satisfy the naming rules.
Create or update a memory entry¶
Request Body:
Returns 201 on insert, 200 on update or no-op (when the supplied content matches the existing body byte-for-byte).
{
"path": "projects/acme.md",
"size": 63,
"content_hash": "9b2f…",
"created": true,
"changed": true,
"created_at": "2026-05-01T10:14:32Z",
"updated_at": "2026-05-01T10:14:32Z"
}
created is true on first insert. changed is false when the content matched the stored entry and nothing was written.
| Failure | Response |
|---|---|
| Invalid path | 400 invalid_memory_path |
| Body exceeds 64 KiB | 413 memory_content_too_large |
| Profile already holds 100 entries | 409 max_memories_reached |
| Profile aggregate would exceed 5 MiB | 409 memory_quota_exceeded |
Delete a memory entry¶
Deleting a profile also deletes every entry in its bank.
Claude Authentication¶
Start OAuth Flow¶
Response: 200 OK¶
{
"oauth_session_id": "oauth123",
"expires_in": 300,
"message": "Poll /auth/claude/url for the OAuth URL"
}
Get OAuth URL¶
Response: 200 OK (URL Ready)¶
{
"status": "url_ready",
"url": "https://console.anthropic.com/oauth/authorize?...",
"message": "Visit the URL to authorize"
}
Response: 200 OK (Pending)¶
Submit OAuth Code¶
Request Body:
Response: 200 OK¶
Store API Key¶
Request Body:
Response: 200 OK¶
Store Bedrock Credentials¶
Routes Claude through AWS Bedrock for this user. The body matches the BedrockConfig struct used by the org-side SessionRequest. Supply either (access_key_id + secret_access_key) (with optional session_token for STS / SSO temporary credentials) or bearer_token. region is always required.
Request Body:
{
"region": "us-east-1",
"access_key_id": "AKIA...",
"secret_access_key": "...",
"session_token": "..."
}
session_token is optional. To authenticate with a Bedrock API key instead, replace the three credential fields with "bearer_token": "...".
Response: 200 OK¶
Error Responses:
400 invalid_bedrock_config: missingregion, missing both credential pair and bearer token, mismatched access-key pair, orsession_tokenwithout the access-key pair403 not_registered: user has not calledPOST /v1/registeryet
Storing Bedrock credentials replaces any previously stored API key, OAuth token, or Vertex config. A user holds one Claude credential at a time. See Alternative AI Providers for IAM, region, and model ID details.
Store Vertex Credentials¶
Routes Claude through Google Cloud Vertex AI for this user. service_account_json is the literal contents of the service-account JSON key, meaning the entire downloaded file as a JSON string.
Request Body:
{
"project_id": "my-gcp-project",
"region": "us-east5",
"service_account_json": "{\"type\":\"service_account\",\"project_id\":\"...\",\"private_key\":\"...\"}"
}
Response: 200 OK¶
Error Responses:
400 invalid_vertex_config: missingproject_id,region, orservice_account_json403 not_registered: user has not calledPOST /v1/registeryet
Storing Vertex credentials replaces any previously stored API key, OAuth token, or Bedrock config. The service-account JSON is encrypted at rest and is never returned by the status endpoint. See Alternative AI Providers for GCP-side setup, region selection, and model ID format.
Get Credential Status¶
Response: 200 OK¶
credential_type is one of api_key, oauth_token, bedrock, or vertex, whichever was stored most recently. Secrets themselves (API keys, OAuth tokens, AWS keys, service-account JSON) are never returned.
Delete Credentials¶
Provider Credentials¶
Credentials for the non-Claude providers are managed under /v1/credentials. Request body shapes and examples are on the AI Providers page; the full schemas are in the OpenAPI specification served by the service at GET /openapi.
Get Credential Status (All Providers)¶
Response: 200 OK¶
{
"providers": {
"anthropic": {"has_credentials": true, "type": "oauth_token", "created_at": "2026-08-01T12:00:00Z"},
"openai": {"has_credentials": true, "type": "api_key", "created_at": "2026-08-10T09:30:00Z"},
"google": {"has_credentials": false},
"openrouter": {"has_credentials": false}
}
}
Store and Delete Provider Credentials¶
POST /v1/credentials/openai # OpenAI API key
POST /v1/credentials/openai/azure # Azure OpenAI configuration
DELETE /v1/credentials/openai
POST /v1/credentials/google # Google AI Studio API key
POST /v1/credentials/google/vertex # Vertex AI service account (Gemini)
DELETE /v1/credentials/google
POST /v1/credentials/openrouter # OpenRouter API key
DELETE /v1/credentials/openrouter
Storing a credential replaces any previous credential for that provider. DELETE removes it immediately.
File Transfer¶
Request Upload URL¶
Request Body:
Response: 200 OK¶
{
"upload_url": "https://storage.googleapis.com/...",
"upload_id": "upload123",
"target_path": "/workspace/uploads/data.csv",
"expires_at": "2025-01-15T11:30:00Z"
}
Error Responses:
413: File size exceeds limit (100 MB)
Notify Upload Complete¶
Request Body:
Response: 200 OK¶
Request Download URL¶
Request Body:
Response: 200 OK¶
WebSocket Protocol¶
Connection¶
Endpoint:
Authentication:
- Header:
Authorization: Bearer <JWT> - Query parameter:
?token=<JWT>
Connection Errors¶
| Code | Description |
|---|---|
| 4001 | Invalid or missing authentication |
| 4003 | Session belongs to different user |
| 4004 | Session not found |
| 4009 | Session not running |
| 4100 | Session ended |
| 4101 | Connection reset |
| 4500 | Internal error |
Message Format¶
All messages are JSON objects:
{
"type": "message_type",
"timestamp": "2025-01-15T10:30:00Z",
"session_id": "abc123",
"payload": { ... }
}
Client to Server Messages¶
prompt¶
Send a user prompt to Claude.
interrupt¶
Interrupt the current Claude operation.
heartbeat¶
Keep the connection alive (send every 30 seconds).
upload_request¶
Request a signed URL for file upload.
{
"type": "upload_request",
"payload": {
"request_id": "req_123",
"filename": "data.csv",
"content_type": "text/csv",
"size": 1024
}
}
upload_complete¶
Notify that file upload has completed.
{
"type": "upload_complete",
"payload": {
"request_id": "req_123",
"filename": "data.csv",
"path": "/workspace/uploads/data.csv"
}
}
download_request¶
Request a signed URL for file download.
{
"type": "download_request",
"payload": {
"request_id": "req_456",
"path": "/workspace/output.txt"
}
}
Server to Client Messages¶
assistant¶
Claude's response content.
{
"type": "assistant",
"timestamp": "2025-01-15T10:30:00Z",
"payload": {
"content": [
{
"type": "text",
"text": "Here are the files in the current directory:\n\n- file1.txt\n- file2.py"
}
],
"model": "claude-sonnet-4-20250514"
}
}
tool_use¶
Claude is invoking a tool.
{
"type": "tool_use",
"timestamp": "2025-01-15T10:30:01Z",
"payload": {
"id": "tool_abc123",
"name": "Bash",
"input": {
"command": "ls -la"
}
}
}
tool_result¶
Result of a tool execution.
{
"type": "tool_result",
"timestamp": "2025-01-15T10:30:02Z",
"payload": {
"tool_use_id": "tool_abc123",
"content": "total 16\ndrwxr-xr-x 2 user user 4096 Jan 15 10:00 .\n..."
}
}
user¶
Echo of user input (for display purposes).
{
"type": "user",
"timestamp": "2025-01-15T10:30:00Z",
"payload": {
"text": "List all files in the current directory"
}
}
system¶
System messages from Claude.
{
"type": "system",
"timestamp": "2025-01-15T10:30:00Z",
"payload": {
"message": "Working directory: /workspace"
}
}
result¶
Final result of a Claude operation.
{
"type": "result",
"timestamp": "2025-01-15T10:35:00Z",
"payload": {
"success": true,
"summary": "Listed directory contents successfully"
}
}
session_status¶
Session status update.
{
"type": "session_status",
"timestamp": "2025-01-15T10:30:00Z",
"payload": {
"status": "running"
}
}
session_end¶
Session has ended.
{
"type": "session_end",
"timestamp": "2025-01-15T10:35:00Z",
"payload": {
"reason": "completed",
"exit_code": 0
}
}
End Reasons:
completed: Session completed normallyfailed: Session encountered an execution errorjob_completed: Session runner process exiteduser_requested: User terminated the sessionorg_api_requested: Session was terminated via the org APImax_duration_exceeded: Session exceeded its maximum durationstartup_timeout: Session failed to start within the allowed timeheartbeat_stale: Lost connection to the session runner
session_error¶
An error occurred in the session.
{
"type": "session_error",
"timestamp": "2025-01-15T10:35:00Z",
"payload": {
"error": "Claude process died unexpectedly",
"details": "Exit code: 1"
}
}
error¶
General error message.
{
"type": "error",
"timestamp": "2025-01-15T10:35:00Z",
"payload": {
"message": "Rate limit exceeded",
"code": "rate_limited"
}
}
Error Codes:
session_not_found: Session no longer existssession_not_running: Session is not in running statesession_crashed: Session process crashedinvalid_message: Malformed message receivedrate_limited: Too many messages sent
upload_url¶
Response to upload_request.
{
"type": "upload_url",
"timestamp": "2025-01-15T10:30:00Z",
"payload": {
"request_id": "req_123",
"upload_id": "upload_789",
"url": "https://storage.googleapis.com/...",
"target_path": "/workspace/uploads/data.csv",
"expires_at": "2025-01-15T11:30:00Z"
}
}
download_url¶
Response to download_request.
{
"type": "download_url",
"timestamp": "2025-01-15T10:30:00Z",
"payload": {
"request_id": "req_456",
"url": "https://storage.googleapis.com/...",
"expires_at": "2025-01-15T11:30:00Z"
}
}
Connection Management¶
Heartbeat¶
- Client: Send
heartbeatmessage every 30 seconds - Server: Sends WebSocket ping frames every 30 seconds
- Timeout: Connection closed after 60 seconds of inactivity
Reconnection¶
If the connection is lost:
- Reconnect using the same session ID
- Server sends any buffered messages (up to 60 seconds old)
- If session has ended, server sends
session_endmessage
Message Size¶
Maximum message size is 1 MB. Use file transfer for larger payloads.
Example: Complete Session Flow¶
const jwt = 'your-limacharlie-jwt';
const baseUrl = 'https://ai.limacharlie.io';
// 1. Create session
const createResp = await fetch(`${baseUrl}/v1/sessions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${jwt}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
allowed_tools: ['Bash', 'Read', 'Write']
})
});
const { session } = await createResp.json();
// 2. Wait for session to be running
let status = 'starting';
while (status === 'starting') {
await new Promise(r => setTimeout(r, 1000));
const resp = await fetch(`${baseUrl}/v1/sessions/${session.id}`, {
headers: { 'Authorization': `Bearer ${jwt}` }
});
const data = await resp.json();
status = data.session.status;
}
// 3. Connect via WebSocket
const ws = new WebSocket(
`wss://ai.limacharlie.io/v1/sessions/${session.id}/ws?token=${jwt}`
);
// 4. Set up heartbeat
const heartbeat = setInterval(() => {
ws.send(JSON.stringify({ type: 'heartbeat' }));
}, 30000);
// 5. Handle messages
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case 'assistant':
console.log('Claude:', msg.payload.content);
break;
case 'tool_use':
console.log('Using tool:', msg.payload.name);
break;
case 'tool_result':
console.log('Tool result:', msg.payload.content);
break;
case 'session_end':
console.log('Session ended:', msg.payload.reason);
clearInterval(heartbeat);
break;
case 'error':
console.error('Error:', msg.payload.message);
break;
}
};
// 6. Send a prompt
ws.send(JSON.stringify({
type: 'prompt',
payload: { text: 'Hello! List the files in the current directory.' }
}));
// 7. Later: interrupt if needed
// ws.send(JSON.stringify({ type: 'interrupt' }));
// 8. Cleanup
ws.onclose = () => {
clearInterval(heartbeat);
};