---
title: "API"
id: "1701"
type: "page"
slug: "api"
published_at: "2026-09-20T20:15:32+00:00"
modified_at: "2026-11-14T11:18:05+00:00"
url: "https://xedant.com/agents/agent/docs/api/"
markdown_url: "https://xedant.com/agents/agent/docs/api.md"
excerpt: "The Agent API is a way to send commands to the agent and get answers…"
---

# API

[https://xedant.com/agents/agent/docs/api/.md](https://xedant.com/agents/agent/docs/api/.md)

**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](/agents/agent/docs/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](/agents/agent/docs/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_KEY` variable is not set and the `keys.yml` file 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 status `queued`, without it — `created`;
- `title` (optional) — the chat’s title (defaults to “Remote API Chat”);
- `model` (optional) — the model for the chat (see [Models](/agents/agent/docs/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 example `Portal`.

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 `id` matches an existing row keeps its identity — the same row, model, and pause state (unless `isPaused` is set explicitly);
- an item without an `id` (or with `id: 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 from `skills.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 (the `labels.yml` file);
- `/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, the `AGENT_API_KEY` variable is not set and `keys.yml` defines no keys;
- `InternalServerError` — 500, an unexpected server error (`details` carries the exception text).

## Related sections

- [Remote Control through Yandex.Disk](/agents/agent/docs/remote-control) — the file queue and the AgentRemote desktop app;
- [Managing the queue from the interface](/agents/agent/docs/prompts) — the prompts dialog and the server-side AutoSend;
- [Models](/agents/agent/docs/models) — which models can be passed in `model`.
- [Access Keys](/agents/agent/docs/access-keys) — issuing keys in the interface, the `keys.yml` file, and the safety rules.

**[← Remote Control](/agents/agent/docs/remote-control)**

**[Prompts →](/agents/agent/docs/prompts)**
