Agent API
The agent API lets your own scripts, CI jobs and tools talk to the agent over HTTP. You authenticate with a personal API token, send a message, and get the answer back, either when the turn is finished or streamed as it happens. Everything a run does is an ordinary conversation turn: the same tools, permissions, Projects and usage accounting as a message you type in the web app.
The examples use https://elowen.example.com; replace it with the address you open Elowen on. All paths go through that same host under /api.
Personal API tokens
A token acts as you. It has exactly the access your account has, administrator rights included, so treat it like a password.
Create a token
- Open Account → API tokens.
- Choose New token, give it a name you will recognise later, and optionally pick the last day it should work. Leave the date empty for a token that works until you delete it. The token stops working at the end of that day in UTC.
- Copy the token from the window that opens. It is shown only once. Elowen stores a hash of it, not the token itself, so nobody can display it again. If you lose it, choose Create a new value in the token's panel header: the token gets a brand-new value, which is shown once and put on your clipboard. Everything still using the old value stops working immediately; the name, limits and recorded usage are kept.
Every token starts with elw_, so a leaked token is easy to recognise in a log or a paste. Chatbot accounts cannot have tokens.
Usage and limits
Everything about a token lives in one panel. When you have several tokens, pick which one the panel shows in its header. The panel states the token's last use and expiry, its turns, answered turns, failed turns and cost per day for the period you pick, and the limits it runs under. Days are counted in UTC.
Choose Limits to cap a token. Every limit is optional, and each has its own Unlimited switch:
| Limit | What happens when it is reached |
|---|---|
| Turns per day | Further runs are refused until the next UTC day. |
| Cost per day | Further runs are refused until the next UTC day. |
| Requests per minute | Every request made with this token is refused until the minute is over. |
| Concurrent turns | A new run is refused while this many runs are still going. |
If a turn used tokens but its provider reported no price, that day's cost is unknown. Elowen never treats unknown as zero: the panel shows Cost unknown, and a token with a cost limit is refused for the rest of that UTC day. The cost limit counts what turns have already used, so runs that are still going when a new one starts can take the day slightly past it.
If Elowen restarts while a run is going, that run is counted as failed and its spend can no longer be attributed to the token. The day it started and the day of the restart then count as cost unknown, so a token with a cost limit is refused for the rest of those UTC days.
Delete a token
Choose Delete in the token's panel header. Everything using it stops working immediately, and its recorded usage is deleted. Deleting your account deletes all of your tokens.
What a token cannot do
A token cannot sign out, change your password, impersonate another account, or create, change, regenerate or delete tokens, including itself. Those need you signed in to the web app, and the API answers them with 403.
Authentication
Send the token in the Authorization header of every request:
export ELOWEN_URL=https://elowen.example.com
export ELOWEN_TOKEN=elw_… # the token you copied
curl -sS "$ELOWEN_URL/api/auth/me" -H "Authorization: Bearer $ELOWEN_TOKEN"
A missing, unknown, deleted or expired token gets 401 {"error":"unauthorized"}. The same happens after you Copy a new value for a token: the old value is dead from that moment.
Run the agent
POST /api/v1/agent/runs sends one message and runs one agent turn.
| Field | Type | Meaning |
|---|---|---|
message | string, required | What you want the agent to do. |
conversationId | string, optional | Continue this conversation. Without it, the run starts a new one. |
model | string, optional | Run on this model. See Models. |
stream | boolean, optional | true streams progress as server-sent events. |
Wait for the answer
curl -sS "$ELOWEN_URL/api/v1/agent/runs" \
-H "Authorization: Bearer $ELOWEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message":"Summarise the failed builds from yesterday"}'
{ "conversationId": "brain-12-…", "reply": "Two builds failed…", "usage": { "tokens": 1840, "costUsd": 0.012 } }
usage.costUsd is null when the provider reported no price.
The call waits up to two minutes. If the turn takes longer, you get 202 with { "conversationId": "…", "reply": null, "usage": null }. The turn keeps running and its answer lands in that conversation; for long work, stream instead.
Stream the turn
curl -sSN "$ELOWEN_URL/api/v1/agent/runs" \
-H "Authorization: Bearer $ELOWEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message":"Check the open pull requests","stream":true}'
The response is text/event-stream. Each event has a name and a JSON data line:
event: start
data: {"conversationId":"brain-12-…"}
event: tool
data: {"phase":"start","id":"t1","name":"Bash","detail":"gh pr list"}
event: tool
data: {"phase":"end","id":"t1","ok":true}
event: delta
data: {"text":"There are three open"}
event: delta
data: {"text":" pull requests…"}
event: final
data: {"conversationId":"brain-12-…","reply":"There are three open pull requests…","usage":{"tokens":2210,"costUsd":0.019}}
| Event | Data |
|---|---|
start | { conversationId }, always first. |
delta | { text }, the next piece of the answer. |
tool | { phase: "start", id?, name, detail? } when a tool starts, { phase: "end", id?, ok } when it ends. |
error | { message } when the turn fails. The stream then ends without final. |
final | { conversationId, reply, usage } when the turn is finished. |
If you disconnect, the turn keeps running and its answer lands in the conversation.
Conversations
Every run answers with the conversationId it used. Send it back to continue the same conversation, with its history and context:
curl -sS "$ELOWEN_URL/api/v1/agent/runs" \
-H "Authorization: Bearer $ELOWEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message":"Open an issue for the second one","conversationId":"brain-12-…"}'
- Without
conversationId, each run starts a new conversation. - You can continue any of your own conversations, including ones you started in the web app. Another account's conversation, or an id that does not exist, gets
404 {"error":"unknown conversation"}. - One conversation runs one turn at a time. A run into a conversation that is already working, whether on another run or on a message typed in the web app, is refused with
409 {"error":"conversation_busy"}and counts nothing. Wait for the other turn to finish, then send it again, or start a new conversation. - API runs never change which conversation your web app or CLI opens on.
- API conversations appear in your conversation history like any other.
Models
List the models you can use
curl -sS "$ELOWEN_URL/api/brain/models" -H "Authorization: Bearer $ELOWEN_TOKEN"
[
{ "provider": "anthropic", "providerLabel": "Anthropic", "model": "claude-sonnet-4-5", "exec": "anthropic/claude-sonnet-4-5", "contextWindow": 200000, "…": "…" }
]
The list contains only the models your account is allowed to use. The exec value, <provider>/<model>, is what you pass when you pick one.
Pick a model for one run
Add model to the run. The conversation switches to that model before the turn starts, and it stays on that model for later runs:
curl -sS "$ELOWEN_URL/api/v1/agent/runs" \
-H "Authorization: Bearer $ELOWEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message":"Review this diff","model":"anthropic/claude-sonnet-4-5"}'
Switch a conversation's model
POST /api/brain/model switches an existing conversation, the same way the model picker in the web app does. Always pass the conversation as session; without it, the switch applies to the conversation your web app has open.
curl -sS "$ELOWEN_URL/api/brain/model" \
-H "Authorization: Bearer $ELOWEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model":"anthropic/claude-sonnet-4-5","session":"brain-12-…"}'
{ "model": "claude-sonnet-4-5" }
Both ways use the same rules as the web app. A model you are not allowed to use, or one that cannot take over this conversation, gets 409 with the same error message the web app shows, and no turn runs.
Errors
Every error body is JSON with an error field.
| Status | When | Body |
|---|---|---|
400 | The request body is not valid JSON, a field has the wrong type, or it contains a field the endpoint does not accept. | { "error": "…" } describing the field |
401 | The token is missing, unknown, deleted or expired, or its value was replaced. | { "error": "unauthorized" } |
403 | The endpoint needs a signed-in browser session (see What a token cannot do), or your account may not use it. | { "error": "forbidden" } |
404 | conversationId is not one of your conversations. | { "error": "unknown conversation" } |
409 | The conversation is already running a turn. | { "error": "conversation_busy" } |
409 | The requested model was refused. | { "error": "…" }, the same message the web app shows |
429 | A token limit was reached. | { "error": "…", "reason": "…" } and a Retry-After header |
500 | The turn failed or could not start. With "stream": true, a turn that could not start answers with this status instead of a stream. | { "error": "…", "conversationId": "…" } |
503 | The agent is not available on this server. | { "error": "brain unavailable" } |
Limit refusals
A 429 always carries Retry-After, the number of seconds to wait, and a reason a script can act on:
error | reason | Meaning | Retry-After |
|---|---|---|---|
limit_exhausted | requests_per_minute | Too many requests this minute. | Seconds until the minute is over. |
limit_exhausted | daily_turns | Today's turn limit is reached. | Seconds until the next UTC day. |
limit_exhausted | daily_cost | Today's cost limit is reached. | Seconds until the next UTC day. |
limit_unverifiable | daily_cost_unverifiable | A turn today reported no cost, so the cost limit cannot be checked. | Seconds until the next UTC day. |
limit_exhausted | concurrent_turns | Too many runs are still going. | 1 |
curl -sS -D - "$ELOWEN_URL/api/v1/agent/runs" \
-H "Authorization: Bearer $ELOWEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message":"One more"}'
HTTP/1.1 429 Too Many Requests
Retry-After: 52380
Content-Type: application/json
{"error":"limit_exhausted","reason":"daily_turns"}
A run refused by a limit creates nothing: no conversation, no turn, and nothing is counted against the token's turns. A run refused as conversation_busy, for an unknown conversation or model, or because its turn could not start is not counted either.