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 switched on by setting 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); on every request the server computes the fingerprint of the key you sent and compares it with the stored one.
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
How to get the key hash
You can compute the key’s fingerprint 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 a .env file).
Codes returned when authentication fails
- 401 Unauthorized — the key is missing or wrong;
- 503 Service Unavailable — the
AGENT_API_KEYvariable is not set: “Remote API is disabled”, the API is off entirely.
/api/remote commands
An endpoint is the address a program calls on the server to fetch something or make something happen. Every /api/remote command is 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:
/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.
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;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.