Skip to content
ivralo
Sections

Docs / Reference

Local API

ivralo serve runs a local HTTP API on this machine's loopback address, for clients that are not a terminal: an editor, a script, Alertmanager. Every route is a call the CLI also makes. This page lists the routes, the stream format, approvals and errors.

Starting it

ivralo serve

It runs in the foreground, in the workspace you start it in, until Ctrl+C or SIGTERM. Nothing installs it as a service; run it under your own service manager if you want it kept up. Nothing listens once it ends.

SettingValue
Address127.0.0.1 only. There is no option to bind anywhere else.
Port--port, else IVRALO_API_PORT (61017). A port already taken exits with 1.
WorkspaceThe one it was started in, and no other.
Exit130 after Ctrl+C, 143 after SIGTERM.

To reach it from another machine, use an ssh tunnel:

ssh -L 61017:127.0.0.1:61017 you@server

Token, version and workspace

Every request but GET /version carries three things:

HeaderValue
AuthorizationBearer and the token in api-token in your ivralo home.
X-Ivralo-Api-Version2. A request for another version, or none, is refused with 409.
X-Ivralo-WorkspaceOptional. The directory the client works in; a different one from the server's is refused with 409.

The token file is owner-only. ivralo init --token makes it, or the first ivralo serve does; ivralo serve --new-token replaces it, and every client then needs the new one. ivralo never prints it. The Python worker refuses to read it by name, which is a guardrail, not a boundary.

Before the token is checked, two more checks run on every request: the caller must be a loopback address, and the Host header must be 127.0.0.1, localhost or [::1]. That second check is what a web page using DNS rebinding cannot fake. Both refuse with 403.

TOKEN=$(cat ~/ivralo/api-token)
curl -s http://127.0.0.1:61017/version
curl -s http://127.0.0.1:61017/health \
  -H "Authorization: Bearer $TOKEN" -H "X-Ivralo-Api-Version: 2"

Routes

Method and pathWhat it doesSame as
GET /versionivralo's version and the API version: {"ivralo": "…", "api": 2}. No token.
GET /healthThe workspace, the model, whether the database answers, what is running, which edits are accepted for this server's life.
POST /requestsRun one request and stream it.ivralo ask
POST /requests/{id}/steerSend a message to a running request; it rides the next tool result.typing during a turn
DELETE /requests/{id}Cancel a running request. Closing the stream does the same.Esc
POST /approvals/{id}Answer an approval the stream asked for.the approval dialog
GET /sessionsRecent sessions. ?limit= from 1 to 200, 20 by default.ivralo sessions
GET /sessions/{id}/runsA session's runs. ?limit= as above.ivralo runs
GET /runs/{run}/traceA run's tool calls. ?session= names the session for last.ivralo trace --json
GET /changesWhat each turn changed, newest first. ?limit=, 50 by default.ivralo changes --json
GET /changes/{n}/diffOne change's diff; 1 is the newest./diff n
POST /whyWhere a claim came from.ivralo why --json
GET /changesetsThe staged changesets.ivralo changeset list
POST /changesets/{id}/landLand one in the working tree.ivralo changeset land
POST /changesets/{id}/dropThrow one away.ivralo changeset drop
POST /changesets/{id}/releaseClear the mark of a turn that no longer runs, so the changeset can be landed or dropped.ivralo changeset release
POST /verifyRun the project's checks and stream the result.ivralo verify
POST /incidentInvestigate an evidence bundle and stream the report.ivralo incident --format json
POST /alerts/alertmanagerTake in an Alertmanager webhook.
POST /alerts/pagerdutyTake in a PagerDuty V3 webhook.
GET /jobsThis workspace's jobs.ivralo jobs
GET /jobs/{id}One job in full.ivralo job
POST /jobs/{id}/cancelCancel a job.ivralo cancel
POST /jobs/{id}/resumeResume a job, streamed.ivralo resume

The alert routes are described on Alert intake.

Request bodies

Every body is a JSON object.

RouteFieldDefaultMeaning
POST /requeststextrequiredThe request.
images[]Paths of images to attach.
sessionthe shared oneThe session to run in.
attendedtruetrue builds the team as the session does: calls that ask become approvals in the stream. false builds it as ivralo ask does: nothing pauses, and what would ask is refused.
planfalseDraft a plan first; it becomes a plan approval. Needs attended.
changesetthe settingauto stages the edits and lands them when the checks pass, keep stages them and never lands, off lets them land in place. Not with plan.
trustfalsetrue trusts the folder, recorded as given through the API.
POST /requests/{id}/steertextrequiredThe message.
POST /approvals/{id}decisionrequiredconfirm, reject or answer.
scopecallWith confirm: call, turn, edits or always.
noteemptyWith reject: what to do instead. The model reads it.
textemptyWith answer: the answer to a question.
POST /whyclaimrequiredA number or a phrase.
runlastThe run.
sessionthe shared oneThe session.
POST /verifypathswhat git says changedThe changed files.
scopetargetedtargeted or full.
attendedtruefalse refuses the commands that would ask.
trustfalseAs for requests.
POST /incidentbundleincident/The evidence bundle, a folder in the workspace.
hostedfalseLet a model that leaves the machine read the evidence for this run. It also needs masking on.
sessiona fresh oneThe session to run in.
attendedtruetrue turns the Investigator's questions into approvals.
POST /jobs/{id}/resumeanswernoneFor a parked job: approve, approve_job or reject.
noteemptyWith reject.

The stream

POST /requests, /verify, /incident and /jobs/{id}/resume answer with application/x-ndjson: one JSON object per line, each with a seq number and a kind.

{"seq":1,"kind":"accepted","request":"8f2c…"}
{"seq":2,"kind":"run","run_id":"…","session_id":"…"}
{"seq":3,"kind":"call_started","call":"…","agent":"Ivralo","tool":"read_file","args":{"path":"src/app.py"},…}
{"seq":4,"kind":"approval","approval":"a1…","type":"call","expires_in":1800,…,"answers":["confirm","reject"],"scopes":["call","turn","edits"]}
{"seq":5,"kind":"decided","approval":"a1…","decision":"confirm","scope":"call","by":"…"}
{"seq":9,"kind":"result","result":{"ok":true,"reply":"…","files":[…],…},"undelivered":[]}

Fields are shortened here with ….

The kinds a stream carries:

GroupKinds
The turnrun, call_started, call_completed, call_failed, reasoning, content, completed, run_failed, memory, context
The serveraccepted, approval, decided, steering, limit, heartbeat, step_started, step_ended, step_result, integrated, instance, say, reply, job, cancelled, error, result

A stream ends with result, error or cancelled. A request's result record holds, under result, the same object ivralo ask --format json prints, and under undelivered any steering message that never reached the model. An investigation's is {"kind":"result","incident":{…}}, a verification's carries verification, and a resume ends with a job record. A heartbeat is sent after 15 quiet seconds; it is also how the server notices a client that went away.

Every string is written as escaped ASCII. A reply that holds a newline and something shaped like a record is still one string inside one line.

Approvals

When a call needs a person, the stream carries an approval record and the run waits. Its type says what it is:

TypeWhat it holdsAnswers
callOne tool call with its facts and, for an edit, its diff.confirm with the scopes listed, or reject
questionA question an agent put to you.answer or reject
commandsThe commands /verify would run that ask.confirm or reject
planA drafted plan.confirm or reject
integrationA coder's patch, before it lands.confirm (scope call or edits) or reject

The scopes are the dialog's keys: call is y, turn is a, edits is s and always is p. A send lists only call. Edits accepted with edits stay accepted while this ivralo serve runs.

curl -s -X POST http://127.0.0.1:61017/approvals/a1… \
  -H "Authorization: Bearer $TOKEN" -H "X-Ivralo-Api-Version: 2" \
  -d '{"decision": "reject", "note": "Change only the test, not the handler."}'

One approval is open at a time per request. An approval nobody answers within IVRALO_API_APPROVAL_TIMEOUT seconds (1800) is rejected, with a note the model reads.

Limits

  • One at a time. One request, verification, investigation or resume runs at once. A second gets 409, with the running request's id.
  • The workspace's lease. A request holds the workspace's job lease while it runs. When a job holds the lease, a request gets 409 until the job ends. See Jobs.
  • Trust. An API request is unattended by default, so a folder with no record is untrusted and what its IVRALO.md declares does not run. "trust": true is the token holder's yes.
  • A job's stream. Cancelling a resume's stream leaves the job running; stop it with POST /jobs/{id}/cancel.

Errors

A refusal is one JSON object with one sentence, {"error": "…"}, and never says anything about the token.

StatusWhen
401The token is missing or wrong.
403The caller is not on loopback, or the Host header is not a loopback name.
404No such request, approval, run, changeset or job.
409Wrong or missing API version; another workspace; something else is running; a job holds the lease; an approval already decided; a changeset that cannot land over your own edits (the body lists both sides); an investigation that cannot start outside a repository or on a hosted model without permission.
413An alert body larger than 1 MiB.
422The body is not JSON, a field has the wrong type or value, or a scope the approval does not offer.
503The database could not be read.

Clients

ivralo ask --via-api sends a request to the running server and answers its approvals in your terminal; it exits 2 when there is no server, no token or a refusal. Any HTTP client works the same way.