Brenian

The API.

Everything the app does with your agent, your own software can do with a key: open sessions, send turns, answer its questions, read what it wrote, keep projects, publish a page and refresh it. This page is the whole of it. Anything not here is not available to a key.

Keys and where to send them

Make a key in the app under Settings → Developer. It is shown once. Revoke it there at any time; a revoked key stops working at once. Your address must be verified before a key can be made.

Send it as a bearer token to the app's origin, under the /agent prefix. Every path on this page is relative to that prefix.

curl https://brenian.com/agent/v1/runs \
  -H "Authorization: Bearer apk_…" \
  -H "Content-Type: application/json" \
  -d '{"objective": "List the five largest quakes this week, with a source for each."}'

Answers are JSON, except a turn, which streams. A missing or revoked key answers 401. Everything a key does is charged to your account exactly as the app is.

Identity

A key is either unbound or bound to a subject, chosen when it is made.

Credits, limits and the queue

A turn starts only with credits on the account, and it carries your balance with it as a ceiling: the agent stops when the ceiling is reached, so a runaway turn costs at most what you had.

answerwhenwhat to do
402no credits leftbuy a pack in the app; credits in the body is the balance
429too many turns in a minute, or too many sessions working at once, for the whole accountwait Retry-After seconds and send it again
202the machine your workspace runs on is full, or your workspace is being updatedthe turn is kept as a ticket; see below

A 202 means we are busy, not you. The body is { "queued": true, "ticket": { "id", "status", "ahead", "message", "createdAt" }, "instance": "waiting" | "updating" }. The ticket is your turn exactly as sent; it starts on its own when a slot frees, and its runId appears on the ticket once it has. Read and withdraw tickets on the app's own origin, outside the /agent prefix, with the same key:

GET    /v1/queue              your tickets: { tickets: [{ id, status, ahead, message, runId?, error? }] }
GET    /v1/queue/:ticketId    one ticket
DELETE /v1/queue/:ticketId    withdraw it while waiting, or dismiss it once settled

A ticket that cannot start within the maximum wait settles as failed with error: "… send it again".

Sessions

A session is one conversation with the agent and its own workspace of files. It is opened with a first objective, and every later message is a turn on the same session.

POST /v1/runs
{
  "objective": "Compare the three cheapest flights from Denver to Lisbon in March.",
  "stream": true,
  "projectId": "…",          // optional: work in a project's shared workspace
  "clientId": "…",           // optional; forced to the key's subject when the key is bound
  "config": { "modelId": "…", "reasoningEffort": "low" | "medium" | "high" },
  "limits": { "maxWallClockMs": 600000, "maxToolCalls": 40, "maxSpendUsd": 0.5 }
}
GET  /v1/runs                     every session, newest first; ?projectId=… narrows to one project
GET  /v1/runs/:id                 the session: { id, status, objective, messages, events, result, error,
                                  createdAt, completedAt, projectId, llmConfig: { modelId }, totals,
                                  artifacts, pendingUserQuestion, sessionLimits, draft }
DELETE /v1/runs/:id               delete the session and its files
POST /v1/runs/:id/stop            stop the turn in flight; status becomes "stopped"

status is running, completed, failed, stopped or awaiting_user_input. A turn that stopped at a limit still reads completed; result.stopReason says which. totals carries the tokens and cost so far. artifacts lists the deliverables the session made, each with a workspace path.

Turns and the event stream

POST /v1/chat
{ "message": "Now put that in a table, cheapest first.", "runId": "…", "config": { "modelId": "…" } }

Sends the next message to an existing session, or opens a new one when runId is left out. The answer is an SSE stream of JSON events; each carries type and at (epoch milliseconds).

eventmeaning
run_start, run_continuedthe session's id is in runId
iteration_start, iteration_decision, thinkinga step begins, what the agent decided, a preview of its reasoning
tool_start, tool_complete, tool_errora tool call; toolName, toolInput, outputSummary
plan_updatethe plan, in full, in plan
token_usageone model call's tokens and cost, in diagnostics
artifact_createda file worth keeping was written; diagnostics.path
ask_user_requested, ask_user_resumedthe agent asked you something and paused, or went on after your answer
final_answerthe answer, in message
run_completethe turn ended; status as above

Lost the stream? GET /v1/runs/:id/events replays every event so far and keeps tailing while the turn runs. The session record's draft holds what the agent was typing when you reconnected.

POST /v1/runs/:id/continue   { "objective": "…", "limits"?: … }   a follow-up on a finished session
POST /v1/runs/:id/rerun                                           the same objective again, as a new session
POST /v1/runs/:id/edit       { "messageIndex": n, "message": "…" } rewind to a message and go on from there
POST /v1/runs/:id/retry      { "messageIndex": n }                 rewind and run that message again

Questions the agent asks

The agent may stop to ask you one thing. The session's status becomes awaiting_user_input, the stream emits ask_user_requested then run_complete, and the session record carries the question:

"pendingUserQuestion": { "question": "…", "options": ["A", "B"], "optionImages": ["site/a.png", "site/b.png"], "context": "…" }

Answer with the text, or with the chosen option word for word. The turn goes on from exactly where it paused. Answering a session that is not waiting answers 409.

POST /v1/runs/:id/answer   { "answer": "B" }

A POST /v1/chat to a waiting session does the same thing with its message, so your ordinary chat path works. optionImages, when present, is one image per option, in order; fetch each from the session's files.

A session's model and limits

PUT /v1/runs/:id/model    { "modelId": "…" }
                          → { "appliesAt": "next_iteration" | "next_turn" }
PUT /v1/runs/:id/limits   { "maxWallClockMs": 600000, "maxToolCalls": 40, "maxSpendUsd": 0.125 }   {} clears
                          → { ok, sessionLimits, appliesAt }

Both work while a turn is running: the loop reads them at its next step. Limits are per turn. When one is reached the agent writes a progress report, what is done, what is not and what comes next, and waits for continue, which grants the same limits again. maxSpendUsd is in provider dollars, the unit the ceiling is measured in; your balance still caps every turn.

Files a session wrote

GET /v1/runs/:id/files             the workspace tree: { name, type: "file" | "directory", path, children }
GET /v1/runs/:id/files/:path       one file, with its own content type (images, audio, PDF included)

By convention the deliverables are under artifacts/; everything else is working notes. In a project, a session's files are the project's (below).

Projects

A project is a workspace several sessions share: one file tree, a brief every session reads, and, once the agent has built one, a page with a recipe to refresh it by.

GET    /v1/projects                 { projects: [{ id, name, createdAt, updatedAt, sessionCount, diskUsageBytes, working, from? }] }
POST   /v1/projects                 { "name": "…" } → 201, the record
GET    /v1/projects/:id             the record + sessions, working, brief, recipe, inputs
PATCH  /v1/projects/:id             { "name": "…" }
DELETE /v1/projects/:id             deletes the workspace; its sessions stay, detached
GET    /v1/projects/:id/files[/:path]      the shared tree, and files from it
POST   /v1/projects/:id/sessions    { "runId": "…" }   move a standalone session's files into the project

Pass projectId when opening a session to work in a project. Several sessions may work in one at once; each keeps its own conversation. working lists the sessions running there now. A refresh (below) needs the project quiet and answers 409 { projectBusy } otherwise, and while a refresh runs every other turn in the project answers the same.

The brief, the recipe, your details

Three ordinary files at the project's root, with a door for each so software can read and write them without touching the tree.

PUT /v1/projects/:id/brief     { "text": "…" }     PROJECT.md: what the project is for; "" removes it
PUT /v1/projects/:id/recipe    { "text": "…" }     RECIPE.md: how the page is built and refreshed; "" removes it
GET /v1/projects/:id/inputs                        { inputs: [{ name, kind, ask, example, value, hosts, set }], missing: [names] }
PUT /v1/projects/:id/inputs    { "values": { "area": "Austin, TX", "github_token": "ghp_…" } }

The inputs are what is yours in a recipe: the agent writes the questions, you write the answers, and every run uses them. An input of kind text keeps its value in the file. An input of kind secret is a credential: its value is never in the file or in any answer, it is kept encrypted and sent only to the hosts the input names, and set says whether one is stored. Sending "" for a secret removes it. A project copied from the Cookbook runs no turn while any input is unanswered: 409 { error, missingInputs }.

Publishing and refreshing a page

POST /v1/projects/:id/publish            no body
   → 200 { ok: true, site: { id, slug, url, visibility, current } }
   → 422 { ok: false, step, problems: ["…"] }
POST /v1/projects/:id/run                no body → 202 { id, status: "running", objective, projectId, refresh: true, eventsUrl }

Publish runs the gate on the project's site/ in order, refusing on the first step that fails, in words you can act on: slot (a free site slot and a site address), layout, policy (nothing loaded from another host, no inline script), links, sources (every figure has a source), secrets (no stored credential in any file), render (every page opens in a browser without errors). Then the site ships as the next version at its address. Who may see it, a share link, listing and the Cookbook are set in the app.

Run opens a refresh session that follows the recipe with your details: a new dated snapshot, the checks, a rebuild only if they pass, and the site is published when the session ends. It is a session like any other: watch it on /v1/runs/:id/events. 422 until a first session has written a recipe; 409 while an input is unanswered or a session is working in the project.

Scores

GET /v1/scores/:runId                     { runId, autoScore?: { overall, … }, userScore?: { rating, notes } }
PUT /v1/scores/:runId/user   { "rating": 0.8, "notes": "…" }     your rating, 0 to 1

Every finished session with tool activity is graded in the background; the grade is not charged to you.

What the agent knows about you

After a session the agent notes how you like work done. The notes live in your workspace and reach only your later sessions.

GET    /v1/harness/learnings                  { learnings: [{ id, lesson, appliesWhen, kind, inUse, sessions, … }], threshold }
PATCH  /v1/harness/learnings/:id              { "lesson"?, "appliesWhen"?, "inUse"? }   correct one, or put it to work or stop it
DELETE /v1/harness/learnings/:id              forget it

Models, storage, credentials

GET /v1/models      { models: [{ id, label, kind, description, provider, model, callable, inputCostPerMTok, outputCostPerMTok, quality, observed, … }], defaultModelId }
GET /v1/storage     { bytes, quotaBytes, overQuota, breakdown, sessions, retentionDays }
GET /v1/secrets     { secrets: [{ projectId, projectName, name, ask, hosts, setAt, lastUsed }], available }

The model list is what your sessions may run on; pick one by id in a session's config.modelId. Storage is the same measurement the app shows as "% full"; nothing is refused for being over the soft quota. The credentials list is every secret input across your projects, metadata only, never a value; replace or remove one through its project's inputs.

What a key cannot do

Anything that changes what you are charged or what your workspace is: adding or pricing models, limits, prompts, tools. Those answer 403 at the gateway. Credits and packs, site visibility, share links, schedules, the Cookbook and account settings are in the app with your login, not behind a key. The refresh schedule in particular is set only from the project page, on purpose: the agent has no tool for it, and neither does a key.

A note on versions: this page describes the API as the app uses it today. When something here changes, the app changes with it, and this page says so.