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.
| Setting | Value |
|---|---|
| Address | 127.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. |
| Workspace | The one it was started in, and no other. |
| Exit | 130 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:
| Header | Value |
|---|---|
Authorization | Bearer and the token in api-token in your ivralo home. |
X-Ivralo-Api-Version | 2. A request for another version, or none, is refused with 409. |
X-Ivralo-Workspace | Optional. 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 path | What it does | Same as |
|---|---|---|
GET /version | ivralo's version and the API version: {"ivralo": "…", "api": 2}. No token. | |
GET /health | The workspace, the model, whether the database answers, what is running, which edits are accepted for this server's life. | |
POST /requests | Run one request and stream it. | ivralo ask |
POST /requests/{id}/steer | Send 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 /sessions | Recent sessions. ?limit= from 1 to 200, 20 by default. | ivralo sessions |
GET /sessions/{id}/runs | A session's runs. ?limit= as above. | ivralo runs |
GET /runs/{run}/trace | A run's tool calls. ?session= names the session for last. | ivralo trace --json |
GET /changes | What each turn changed, newest first. ?limit=, 50 by default. | ivralo changes --json |
GET /changes/{n}/diff | One change's diff; 1 is the newest. | /diff n |
POST /why | Where a claim came from. | ivralo why --json |
GET /changesets | The staged changesets. | ivralo changeset list |
POST /changesets/{id}/land | Land one in the working tree. | ivralo changeset land |
POST /changesets/{id}/drop | Throw one away. | ivralo changeset drop |
POST /changesets/{id}/release | Clear the mark of a turn that no longer runs, so the changeset can be landed or dropped. | ivralo changeset release |
POST /verify | Run the project's checks and stream the result. | ivralo verify |
POST /incident | Investigate an evidence bundle and stream the report. | ivralo incident --format json |
POST /alerts/alertmanager | Take in an Alertmanager webhook. | |
POST /alerts/pagerduty | Take in a PagerDuty V3 webhook. | |
GET /jobs | This workspace's jobs. | ivralo jobs |
GET /jobs/{id} | One job in full. | ivralo job |
POST /jobs/{id}/cancel | Cancel a job. | ivralo cancel |
POST /jobs/{id}/resume | Resume a job, streamed. | ivralo resume |
The alert routes are described on Alert intake.
Request bodies
Every body is a JSON object.
| Route | Field | Default | Meaning |
|---|---|---|---|
POST /requests | text | required | The request. |
images | [] | Paths of images to attach. | |
session | the shared one | The session to run in. | |
attended | true | true 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. | |
plan | false | Draft a plan first; it becomes a plan approval. Needs attended. | |
changeset | the setting | auto 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. | |
trust | false | true trusts the folder, recorded as given through the API. | |
POST /requests/{id}/steer | text | required | The message. |
POST /approvals/{id} | decision | required | confirm, reject or answer. |
scope | call | With confirm: call, turn, edits or always. | |
note | empty | With reject: what to do instead. The model reads it. | |
text | empty | With answer: the answer to a question. | |
POST /why | claim | required | A number or a phrase. |
run | last | The run. | |
session | the shared one | The session. | |
POST /verify | paths | what git says changed | The changed files. |
scope | targeted | targeted or full. | |
attended | true | false refuses the commands that would ask. | |
trust | false | As for requests. | |
POST /incident | bundle | incident/ | The evidence bundle, a folder in the workspace. |
hosted | false | Let a model that leaves the machine read the evidence for this run. It also needs masking on. | |
session | a fresh one | The session to run in. | |
attended | true | true turns the Investigator's questions into approvals. | |
POST /jobs/{id}/resume | answer | none | For a parked job: approve, approve_job or reject. |
note | empty | With 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:
| Group | Kinds |
|---|---|
| The turn | run, call_started, call_completed, call_failed, reasoning, content, completed, run_failed, memory, context |
| The server | accepted, 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:
| Type | What it holds | Answers |
|---|---|---|
call | One tool call with its facts and, for an edit, its diff. | confirm with the scopes listed, or reject |
question | A question an agent put to you. | answer or reject |
commands | The commands /verify would run that ask. | confirm or reject |
plan | A drafted plan. | confirm or reject |
integration | A 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.mddeclares does not run."trust": trueis 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.
| Status | When |
|---|---|
| 401 | The token is missing or wrong. |
| 403 | The caller is not on loopback, or the Host header is not a loopback name. |
| 404 | No such request, approval, run, changeset or job. |
| 409 | Wrong 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. |
| 413 | An alert body larger than 1 MiB. |
| 422 | The body is not JSON, a field has the wrong type or value, or a scope the approval does not offer. |
| 503 | The 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.