The Agent API is a way to send commands to the agent and get answers back without a browser. Other programs can create chats, send prompts, and collect results through a set of API commands — from build pipelines, external tools, and wrappers. The same key also opens the extended surface (chats, skills, models, settings, git, analytics, and speech recognition) and a subscription to live events — that is exactly how Research connects to its input field, microphone, and analytics. The sections below are a developer reference for these addresses; if you are new to the term, an API is simply a set of addresses programs use to talk to each other, and the one idea worth keeping is this: a program talks to the agent by the same rules a person follows in the interface.
Authentication
The API is opened by an access key, and there are two ways to get one. The first is to set the environment variable AGENT_API_KEY: it stores not the key itself but its fingerprint (a SHA-256 hash — a short string the key cannot be recovered from), and on every request the server computes the fingerprint of the key you sent and compares it with the stored one. The second is to issue named keys right in the agent’s interface (Settings → Configuration → Access Keys): then every device or program has its own key, and revoking one does not touch the rest. Details — on the Access Keys page.
The key is passed in the X-API-Key header — a service field of the request where the program states its access key:
X-API-Key: my-secret-api-key
Live connections (WebSocket/SignalR) cannot carry headers, so the same key is also accepted as the api_key address parameter:
/svelteChatHub?api_key=my-secret-api-key
Named keys instead of the variable
The easiest way to issue a key is right in the agent: open Settings → the Configuration group → the Access Keys card and press “Generate Key”. The key is shown once — copy it right away and hand it to the device or program it was created for. Only a salted hash remains in the project’s keys.yml file: the salt is a shared random string, and the key itself cannot be recovered from the file, so it is safe to keep and commit to a repository. Access can be revoked by simply deleting the key from the list, without touching the others.
For requests there is no difference: a named key is sent in the same X-API-Key header. It looks like agent_ plus 32 more random characters; the prefix makes the key easy to recognize among other keys and tokens. How to issue a key, how it shows up in the list, and what exactly it opens — on the Access Keys page.
How to get the key hash
If a single key from the environment variable suits you better, its fingerprint can be computed on the application’s login page (the “SHA256 generator” button under the login form) or by hand:
# Linux / macOS
echo -n "my-secret-api-key" | sha256sum | awk '{print $1}'
# Windows PowerShell
$hash = [System.BitConverter]::ToString([System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes("my-secret-api-key"))).Replace("-", "").ToLower()
$hash
Put the resulting fingerprint into AGENT_API_KEY (an application-level variable set before launch — as a system environment variable, a command-line argument, or through the .env file in the project root).
Codes returned when authentication fails
- 401 Unauthorized — the key is missing or wrong: “Invalid or missing API key”. A wrong key is rejected on any API address — even when a valid login token is passed alongside;
- 503 Service Unavailable — the Remote API is off: the
AGENT_API_KEYvariable is not set and thekeys.ymlfile defines no keys. The message names both sources: “AGENT_API_KEY environment variable is not set and keys.yml defines no keys”.
/api/remote commands
Every command is its own address — the one a program calls on the server to fetch something or make something happen. All of the /api/remote commands are described below.
Creating a chat
POST /api/remote/chats
Creates a new chat. A prompt, when supplied, is queued and processed automatically — it is never sent directly. The chat can also be linked to a bot (botId); the link is silent: it never posts to Telegram and can never break chat creation, and its outcome comes back in the botLinked and botLinkNote flags.
Request body
prompt(optional) — the initial message; with it the chat is created with the statusqueued, without it —created;title(optional) — the chat’s title (defaults to “Remote API Chat”);model(optional) — the model for the chat (see Models);skill(optional) — a skill; absent means “none” (no inheritance);systemPrompt(optional) — a system prompt, stored on the chat;botId(optional) — the bot id for the silent chat link;app(optional) — a short id of the application creating the chat, for examplePortal.
The app is an optional mark of which program created the chat. After trimming, 1 to 64 characters are allowed: an empty value means “no app”, anything longer is an input error and the chat is not created. The mark is set at creation and never changes by itself afterwards; a subagent inherits the app of its parent chat. Using this mark, an application sees only its own chats and knows right away how many of them there are.
Response (201 Created)
{
"chatId": 123,
"url": "/123",
"status": "queued",
"createdAt": "2026-09-04T10:00:00Z",
"botLinked": null,
"botLinkNote": ""
}
Example
curl -X POST http://localhost:5173/api/remote/chats \
-H "X-API-Key: my-secret-api-key" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Explain what this code does",
"title": "Code Analysis",
"model": "claude-sonnet"
}'
Chat status
GET /api/remote/chats/{chatId}/status
Returns the current processing status: processing — the agent is working, queued — the chat is idle but its queue holds messages, otherwise completed. Along with the status come the title, the timestamps, the message count, the accumulated cost, and the chat’s app — an empty string means “no app”. The error field in the response is always empty.
{
"chatId": 123,
"status": "processing",
"title": "Code Analysis",
"createdAt": "2026-09-04T10:00:00Z",
"updatedAt": "2026-09-04T10:00:15Z",
"messageCount": 2,
"totalCost": 0.005,
"app": "Portal"
}
Example:
curl http://localhost:5173/api/remote/chats/123/status \
-H "X-API-Key: my-secret-api-key"
Chat messages
GET /api/remote/chats/{chatId}/messages?since={timestamp}
Incremental message reading — the program picks up only what appeared after a given timestamp instead of re-parsing the whole history. The since parameter (ISO 8601) returns messages from the given timestamp inclusively and is echoed back in the response; an invalid since value is silently ignored — the full history returns. Content is simplified by type: a user message gives its text; an agent reply gives its text with button markup stripped; an error gives the error text; a tool use becomes Tool: <name>.
{
"chatId": 123,
"messages": [
{
"id": 456,
"role": "user",
"content": "Explain what this code does",
"createdAt": "2026-09-04T10:00:00Z",
"tokens": { "input": 120, "output": 0 },
"cost": 0.0001
},
{
"id": 457,
"role": "assistant",
"content": "This module handles ...",
"createdAt": "2026-09-04T10:00:20Z",
"tokens": { "input": 140, "output": 320 },
"cost": 0.003
}
],
"since": "2026-09-04T10:00:00Z"
}
Examples:
# All messages
curl http://localhost:5173/api/remote/chats/123/messages \
-H "X-API-Key: my-secret-api-key"
# Only the new ones from a given timestamp
curl "http://localhost:5173/api/remote/chats/123/messages?since=2026-09-04T10:00:10Z" \
-H "X-API-Key: my-secret-api-key"
The prompt queue
Five commands under /api/remote/chats/{chatId}/queue manage the prompts waiting to be sent to the agent. Unlike the internal queue commands, the remote ones are stricter: an unknown chat, or a prompt that is not in the queue (never was there, belongs to another chat, or has already been dispatched), is a 404; empty or whitespace-only text is a 400.
Listing the queue
GET /api/remote/chats/{chatId}/queue
The queue in dispatch order: id, prompt, isPaused, model (a per-message override), createdAt. Paused prompts are included and marked with isPaused.
{
"chatId": 123,
"prompts": [
{
"id": 456,
"prompt": "Refactor the auth module",
"isPaused": false,
"model": "",
"createdAt": "2026-09-04T10:00:00Z"
},
{
"id": 457,
"prompt": "Then update the README",
"isPaused": true,
"model": "claude-sonnet",
"createdAt": "2026-09-04T10:01:00Z"
}
]
}
Append a prompt to the end of the queue
POST /api/remote/chats/{chatId}/queue
Appends a prompt to the end of the queue (like any other message source). prompt is required and non-empty; model is an optional model override for that one message. The 201 response is the added prompt with its id.
curl -X POST http://localhost:5173/api/remote/chats/123/queue \
-H "X-API-Key: my-secret-api-key" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Now add unit tests for the refactored module"
}'
Rewrite the text of one prompt
PUT /api/remote/chats/{chatId}/queue/{promptId}
Changes only the text: the row’s id, position, model, and pause state are preserved. 400 for empty text; 404 when the prompt is not in this chat’s queue or has already been dispatched.
curl -X PUT http://localhost:5173/api/remote/chats/123/queue/456 \
-H "X-API-Key: my-secret-api-key" \
-H "Content-Type: application/json" \
-d '{ "prompt": "Refactor the auth module and update the docs" }'
Delete one prompt
DELETE /api/remote/chats/{chatId}/queue/{promptId}
Removes the prompt from the queue; success is 204 No Content.
curl -X DELETE http://localhost:5173/api/remote/chats/123/queue/456 \
-H "X-API-Key: my-secret-api-key"
Replace the whole queue
PUT /api/remote/chats/{chatId}/queue
The submitted array becomes the whole queue. The replacement rules:
- an item whose
idmatches an existing row keeps its identity — the same row, model, and pause state (unlessisPausedis set explicitly); - an item without an
id(or withid: 0) creates a new prompt; - existing prompts the array does not mention are deleted — including ones other sources added after your last read (last writer wins);
- the array’s order becomes the dispatch order — reordering the array reorders the queue;
- an empty array (
[]) clears the queue; - the id of an already dispatched prompt is skipped (not resurrected) — sending it again would duplicate a message the agent is already processing.
400 errors: the array is missing; any item has empty text; the same positive id appears twice.
curl -X PUT http://localhost:5173/api/remote/chats/123/queue \
-H "X-API-Key: my-secret-api-key" \
-H "Content-Type: application/json" \
-d '{
"prompts": [
{ "id": 457, "prompt": "Refactor the auth module (edited)" },
{ "prompt": "Then update the README" }
]
}'
The bot list
GET /api/remote/bots
Read-only: every configured bot returns with id, title, enabled, running, and pollingError — enough for an external program to show bots with status dots. No tokens, no prompts, no management: bots are configured only in the Agent interface.
{
"bots": [
{
"id": "release-bot",
"title": "Release Bot",
"enabled": true,
"running": true,
"pollingError": null
}
]
}
Chat statuses
created | chat created without an initial prompt, waiting for input |
queued | chat created with a prompt, the message is in the queue |
processing | the agent is processing a message |
completed | all messages are processed, the chat is idle |
error | a processing error (in the remote API the error field in the response is always empty) |
The extended surface unlocked by the API key
The same key opens far more than /api/remote. Sent to these paths, it acts under the Manager role — the same as signing in as the manager. A named key from keys.yml carries exactly the same rights, and deleting or regenerating it takes effect immediately, without restarting the agent:
/api/chats,/api/chat— reading chats and working with them;/svelteChatHub— the live SignalR connection (see below);/api/skills— skills (list, select);/api/models— models (list, select);/api/autofix— autofix status and toggle;/api/settings— settings (settings/all,settings/set);/api/skills-settings— skill colors fromskills.yml;/api/git— status and commit of the working repository;/api/MessageQueue— edit, delete, and pause the message queue;/api/analytics— chat analytics (read-only);/api/chat-labels— chat labels: list, create, change, and assign (thelabels.ymlfile);/api/files— file uploads into the agent’s working folder (images and clipboard files);/api/speechrecognition— audio transcription through the speech recognition engines;/api/build— builds: trigger, status, and output (trigger,status,output);/api/deploy— deployments: start, stop, restart, status, and output;/api/logs— server logs by day: the list of days, the latest lines of a day, and the file download (read-only).
Your chats and their number. GET /api/remote/chats — a paged chat list in the same shape the web client uses: the page parameter counts from 1 (default 1), pageSize is 1 to 200 rows (default 50), and an optional app narrows both the page and the total count. The response carries chats — the list rows — and totalCount — how many chats match the request in total. GET /api/remote/chats/counts answers how many chats each app has, plus unassigned (“no app”) and total (the overall sum). This way an application reads only its own chats and knows their number right away, without paging through the whole list.
GET /api/remote/chats?page=1&pageSize=50&app=Portal
{
"chats": [ { "id": 123, "title": "Code Analysis", "app": "Portal" } ],
"totalCount": 12
}
GET /api/remote/chats/counts
{
"counts": { "Portal": 12, "BuildBot": 3 },
"unassigned": 7,
"total": 22
}
The app filter in the regular reads. The same server-side app parameter is understood by the paged chat list, search, the updates feed, and the set of active chats: the comparison is case-insensitive against the trimmed id, an empty parameter means no filter, and subagents are still excluded unless asked for explicitly. The total count in the paged list is computed with the filter applied.
Builds and deployments. POST /api/build/trigger/{name} starts a build, GET /api/build/status[/{name}] returns its state and the error and warning counts, and GET /api/build/output/{name} — the output text. For deployments: POST /api/deploy/start|stop|restart/{name}, GET /api/deploy/status[/{name}], GET /api/deploy/output/{name}. Triggering a build that is already running is ignored, starting a deployment again changes nothing, and restart means stop, a short pause, and start. An unknown build name answers 404, while an unknown deployment name still answers success — nothing starts, so check the result through the status address.
When the key is not configured or not sent, these paths work as before — through the normal interface sign-in. Nothing breaks: API-key access and user logins coexist.
Server logs. GET /api/logs returns the list of saved days, newest first, each with its date, size, and last-modified time. GET /api/logs/{date}?lines= returns the last lines of a day: 1000 by default, 1 to 10000 allowed — together with the returned line count and the flag saying the file holds more lines than were returned. GET /api/logs/{date}/download returns the whole day’s file as plain text under the name {date}.log. The date is strictly YYYY-MM-DD: any other form is an input error, and a day with no file is a 404. Reading writes and deletes nothing — the logs are only ever read.
Subscribing to live events (SignalR)
Instead of polling the status forever, an external system can subscribe to live events — receive notifications the moment something happens in the chats. Connect to /svelteChatHub, passing the key in the X-API-Key header or as the api_key address parameter (for WebSocket the address parameter is mandatory — headers cannot be set):
const connection = new signalR.HubConnectionBuilder()
.withUrl("http://localhost:5173/svelteChatHub?api_key=my-secret-api-key")
.build();
connection.on("messageUpdate", (event) => {
console.log("New message in chat", event.chatId, event.message.content);
});
connection.start();
Events arrive on changes: new messages, chat updates (including processing statuses), message-queue and prompt-queue changes, and changes to settings, skills, and models. Connections reconnect gracefully — a drop does not lose the subscription. This is exactly how Research shows the Agent interface.
An example workflow
A typical scenario: create a chat → poll the status → collect the messages → append a prompt to the queue → replace the queue. The full cycle in Python:
import time, requests
API_KEY = "my-secret-api-key"
BASE = "http://localhost:5173/api/remote"
H = {"X-API-Key": API_KEY}
# 1. Create a chat — the prompt is queued
resp = requests.post(f"{BASE}/chats", headers=H, json={
"prompt": "Add error handling to the login endpoint",
"title": "Error Handling",
"model": "claude-sonnet"
})
chat_id = resp.json()["chatId"]
# 2. Poll the status until it finishes
while True:
status = requests.get(f"{BASE}/chats/{chat_id}/status", headers=H).json()["status"]
if status in ("completed", "error"):
break
time.sleep(2)
# 3. Collect the replies (incrementally via since)
messages = requests.get(f"{BASE}/chats/{chat_id}/messages", headers=H).json()["messages"]
for msg in messages:
if msg["role"] == "assistant":
print(msg["content"])
# 4. Append a follow-up to the end of the queue
requests.post(f"{BASE}/chats/{chat_id}/queue", headers=H,
json={"prompt": "Now add unit tests"})
# 5. Replace the whole queue
requests.put(f"{BASE}/chats/{chat_id}/queue", headers=H, json={
"prompts": [
{"prompt": "Fix the auth flow"},
{"prompt": "Update the README"}
]
})
Error format
All errors follow one format, {error, message, details}:
{
"error": "NotFound",
"message": "Chat 123 not found",
"details": ""
}
Unauthorized— 401, the key is missing or wrong;BadRequest— 400, an invalid request body (an empty prompt, a duplicate id, a missing array);NotFound— 404, a chat or prompt not found;Remote API is disabled— 503, theAGENT_API_KEYvariable is not set andkeys.ymldefines no keys;InternalServerError— 500, an unexpected server error (detailscarries the exception text).
Related sections
- Remote Control through Yandex.Disk — the file queue and the AgentRemote desktop app;
- Managing the queue from the interface — the prompts dialog and the server-side AutoSend;
- Models — which models can be passed in
model. - Access Keys — issuing keys in the interface, the
keys.ymlfile, and the safety rules.