The Xedant 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 Xedant Research Agent 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.
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, and the accumulated cost. 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
}
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 Xedant 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.
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.
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 Agent shows the Xedant 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.