Ava API
Ava is the assistant that builds, edits and operates agents from a plain-language command. Over HTTP it is a chat: you open one, send a message, and read the turn back as a stream. A turn stops at up to three gates — questions, the plan, the changeset — and you resume it with a decision. Nothing is applied to the workspace until you approve it.
Base URL:
https://ultravoice-dev.us.inc/api/v1/voikaAuth:Authorization: Bearer <JWT>— your dashboard login token, not theusf-organization key. Get one fromPOST /api/v1/auth/login(see Authentication). The token names the workspaces you belong to; every request also says which workspace it acts in (workspace_idin the body, or as a query parameter onGETandDELETE). Response envelope: every JSON endpoint answers{"success":true,"data":{…},"error":null}. Errors carry the HTTP status and{"detail":"…"}. Streaming endpoints answer withtext/event-stream. Gates: a turn is not finished untilawaitingisnull. While it isquestion,plan_readyorchangeset_ready, the chat is waiting for you.
Endpoints
Chats
| Method | Endpoint | Purpose |
|---|---|---|
GET | /api/v1/voika/chats | List the caller's chats in a workspace |
POST | /api/v1/voika/chats | Open a chat, optionally bound to an existing agent |
GET | /api/v1/voika/chats/{id} | Transcript, proposals and whether a turn is running |
DELETE | /api/v1/voika/chats/{id} | Delete a chat and everything under it |
Turns
| Method | Endpoint | Purpose |
|---|---|---|
POST | /api/v1/voika/chats/{id}/conductor/stream | Send a message and stream the turn (spends one turn) |
POST | /api/v1/voika/chats/{id}/messages | Same, without streaming — answers when the turn stops |
POST | /api/v1/voika/chats/{id}/resume/stream | Answer questions, approve, revise or cancel the plan, decide a changeset |
POST | /api/v1/voika/chats/{id}/resume | Same, without streaming |
POST | /api/v1/voika/chats/{id}/changesets/{cid}/decide/stream | Accept or reject proposed changes, per change or all at once |
POST | /api/v1/voika/chats/{id}/changesets/{cid}/decide | Same, without streaming |
Plan, agent state and history
| Method | Endpoint | Purpose |
|---|---|---|
PUT | /api/v1/voika/chats/{id}/plan-draft | Save canvas edits to merge into the plan on approval |
DELETE | /api/v1/voika/chats/{id}/plan-draft | Discard the draft |
GET | /api/v1/voika/chats/{id}/plan.md | The plan as Markdown |
GET | /api/v1/voika/chats/{id}/plans | Every stored plan document for the chat (one per plan gate and per commit) |
GET | /api/v1/voika/chats/{id}/plans/{name} | One stored plan document |
GET | /api/v1/voika/chats/{id}/artifact | Committed and staged agent, readiness, open changeset, current gate |
GET | /api/v1/voika/chats/{id}/versions | Version history |
POST | /api/v1/voika/chats/{id}/undo/stream | Roll back to the previous version (or a named one) |
POST | /api/v1/voika/chats/{id}/undo | Same, without streaming |
Utilities
| Method | Endpoint | Purpose |
|---|---|---|
POST | /api/v1/voika/simulate | Walk a flow graph without an LLM and report the path taken |
GET | /api/v1/voika/health | Liveness (no auth) |
GET | /api/v1/voika/ready | Readiness — 503 when the database is unreachable (no auth) |
Authentication
Ava acts as you: it reads and writes the workspace with your permissions, so it authenticates with your login token rather than the organization key. Sign in and keep access_token:
curl -X POST https://ultravoice-dev.us.inc/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "…"}'{
"access_token": "eyJhbGciOiJIUzI1NiJ9…",
"refresh_token": "…",
"expires_in": 3600,
"token_type": "Bearer",
"user": { "id": "usr_…", "email": "you@example.com" },
"workspace": { "id": "00f89f1a-3259-4e92-9419-d1ff3e24a024", "name": "Acme" }
}Send it on every request as Authorization: Bearer <access_token>, and refresh it with POST /api/v1/auth/refresh ({"refresh_token": "…"}) before expires_in runs out. The examples below use $TOKEN for the access token, $WS for the workspace id (data.workspace.id above), and $CHAT / $CS for a chat and a changeset once you have them.
Rules the service enforces on every call:
workspace_idmust be one of the workspaces in your token (403otherwise).- A chat can only be read or driven by the user who opened it, in the workspace it was opened in.
- The
usf-organization key is not accepted here.
Quickstart
The shortest path from a sentence to a published agent. Every id on this page — workspace, chat, changeset, agent — is a UUID; the values below are placeholders. Set WS, CHAT and CS as you go.
1. Open a chat.
curl -X POST https://ultravoice-dev.us.inc/api/v1/voika/chats \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"workspace_id": "'$WS'", "title": "Booking agent"}'Keep data.id as CHAT — every call below is scoped to it.
2. Send the command and read the stream. -N keeps curl from buffering.
curl -N -X POST https://ultravoice-dev.us.inc/api/v1/voika/chats/$CHAT/conductor/stream \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"workspace_id": "'$WS'", "content": "Create an appointment booking agent for Northside Dental"}'The stream ends with one done frame. Read awaiting on it.
3. Answer questions while awaiting is question. choice on the done frame holds the question, its options and its key.
curl -N -X POST https://ultravoice-dev.us.inc/api/v1/voika/chats/$CHAT/resume/stream \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"workspace_id": "'$WS'", "resume": {"answer": "Book, reschedule and cancel cleanings"}}'4. Approve the plan when awaiting is plan_ready. This is the turn that builds: the stream carries a graph_patch frame per step drawn, then stops at the changeset.
curl -N -X POST https://ultravoice-dev.us.inc/api/v1/voika/chats/$CHAT/resume/stream \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"workspace_id": "'$WS'", "resume": {"action": "execute"}}'5. Accept the changes when awaiting is changeset_ready. changeset.id on the done frame is CS.
curl -N -X POST https://ultravoice-dev.us.inc/api/v1/voika/chats/$CHAT/changesets/$CS/decide/stream \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"workspace_id": "'$WS'", "accept_all": true}'The final done frame has awaiting: null, commit_result.ok: true, a new version and artifact.committed.agent_id — the id of the agent you can now call, bind and publish through the Agents API.
Chats
Open a chat
POST https://ultravoice-dev.us.inc/api/v1/voika/chats
| Field | Required | Description |
|---|---|---|
workspace_id | yes | Workspace the chat works in |
title | no | Shown in the chat list; defaults to New chat, cut at 200 characters |
agent_id | no | Bind the chat to an existing agent so edits and questions target it. Must be an agent in this workspace |
Response — 200 with the chat:
{
"success": true,
"data": {
"id": "7c1f0b2e-4d6a-4f8b-9e3c-2a5d7b9c1e42",
"title": "Booking agent",
"workspace_id": "'$WS'",
"organization_id": "org_…",
"user_id": "usr_…",
"credits_left": 30,
"created_at": "2026-09-07T10:00:00Z",
"updated_at": "2026-09-07T10:00:00Z"
},
"error": null
}A workspace keeps 10 chats per user; opening an eleventh deletes the least recently updated one.
List chats
GET https://ultravoice-dev.us.inc/api/v1/voika/chats?workspace_id=$WS&q=booking
q filters on the title. Returns up to 50 chats, most recently updated first, each in the shape above.
Read a chat
GET https://ultravoice-dev.us.inc/api/v1/voika/chats/$CHAT?workspace_id=$WS
{
"success": true,
"data": {
"chat": { "id": "7c1f0b2e-4d6a-4f8b-9e3c-2a5d7b9c1e42", "title": "Booking agent", "credits_left": 27 },
"messages": [
{ "id": "msg_1", "role": "user", "content": "Create an appointment booking agent…", "meta": {}, "created_at": "…" },
{ "id": "msg_2", "role": "assistant", "content": "Here is the plan…", "meta": { "plan": {}, "events": [] }, "created_at": "…" }
],
"proposals": [],
"running": false,
"own_key": false,
"caller": { "first_name": "Amar", "email": "you@example.com", "organization_id": "org_…" }
},
"error": null
}messages are in order. An assistant message's meta carries what the turn produced — events, choice, plan, changeset_id, readiness, version, commit_result, suggestions — so a client can redraw the conversation without replaying it. running is true while a turn is in flight. own_key is true when the workspace runs Ava on its own model credential, in which case turns are not charged.
Delete a chat
DELETE https://ultravoice-dev.us.inc/api/v1/voika/chats/$CHAT?workspace_id=$WS
Response — {"deleted": true}. Messages, plans, changesets and versions go with it. 409 while a turn is running.
Send a message
POST https://ultravoice-dev.us.inc/api/v1/voika/chats/{id}/conductor/stream
| Field | Required | Description |
|---|---|---|
workspace_id | yes | Must match the chat's workspace |
content | yes | The command, 1–8,000 characters. A pasted curl command is understood as a tool definition |
mention | no | Point the turn at something: {"kind": "agent", "id": "agt_…", "label": "Katy"} or {"kind": "call", "id": "call_…", "label": "…"} |
Sending a message spends one turn of the chat's 30 (402 when they are gone, unless the workspace uses its own key). Resuming a gate never spends a turn. Only one turn runs per chat at a time; a second message while one is running gets 409.
The same body on POST …/chats/{id}/messages returns the turn result as ordinary JSON once the turn stops at a gate or finishes. The same goes for the other non-streaming twins (resume, decide, undo). They are convenient for scripts, but you get no progress and the request stays open for as long as the turn runs — up to a few minutes while a plan is being written — so give your HTTP client a generous read timeout and prefer the streaming endpoints for anything interactive.
Reading the stream
Streaming endpoints answer with Content-Type: text/event-stream. Each frame is event: <name> followed by one data: line of JSON. A comment line : ping arrives after every 15 seconds of silence — keep the connection open through it; planning can take a minute without any other frame.
| Event | When | Payload |
|---|---|---|
status | Continuously | One progress event: {"type": …, "label": …, "at": …, …} — see the types below |
artifact | After each build or edit step | The whole staged (or committed) agent: {"type": "artifact", "artifact": {…}, "committed": true} |
changeset | When the review gate opens | {"type": "changeset_ready", "changeset": {…}, "blocking": [...], "lock": true} |
done | Exactly once, last | The turn result (below). awaiting says whether a gate is open |
error | Instead of done | {"message": "…", "detail": "…"} — the stream closes |
status types, roughly in the order they appear:
type | Extra fields | Meaning |
|---|---|---|
plan_of_record | stages: [{id, label}], path | The stages this turn will go through |
reasoning_start · reasoning_delta · reasoning | text, step_id, duration_ms | Streamed prose while Ava thinks; reasoning_delta.text is the token stream |
reasoning_step | step_id | Progress through a long structured output, e.g. "Laying out the steps · 7" |
narration | — | A one-line status for the activity log |
subtask | id, parent_id, status, detail | Parallel work items and their state |
tool_call · tool_result | tool, args / tool, ok | Platform reads and writes |
graph_patch | op, node or edge | One node or transition to draw on the canvas, live |
artifact_patch | — | A variable, tool or knowledge base declared on the staged agent; label says which |
self_correct | problems: [...] | The validator found problems in the draft flow and a repair pass is running |
verify | report, ok | The validation report for the staged flow, before the review gate |
readiness | readiness, stage | Whether the agent can take a call, checked at plan, changeset, commit and undo |
commit_partial | applied, failed, not_applied, skipped | Some writes landed and some did not |
suggestions | suggestions: [{id, label, message}] | Follow-ups the client can offer as one-click messages |
The done frame
The same object is the data of the non-streaming endpoints.
{
"assistant_message": { "role": "assistant", "content": "Here is the plan…", "meta": {} },
"phase": "planning",
"intent": "build",
"awaiting": "plan_ready",
"choice": null,
"plan": { "flow_design": {}, "agent": {}, "tools": [], "variables": [], "knowledge_bases": [], "summary_md": "…", "open_items": [] },
"changeset": null,
"composer_locked": false,
"artifact": { "committed": {}, "staged": {} },
"readiness": null,
"verify_report": null,
"commit_result": null,
"version": 1,
"events": [],
"suggestions": [ { "id": "s1", "label": "Bind a number to it", "message": "Bind +1 (415) 555-0182 to the booking agent" } ],
"done": false,
"error": ""
}| Field | Meaning |
|---|---|
awaiting | question, plan_ready, changeset_ready, or null when the turn is finished |
intent | Which path triage chose: build, edit, ops or answer |
choice | The open question when awaiting is question |
plan | The plan when awaiting is plan_ready |
changeset | The proposed changes, with id, when awaiting is changeset_ready |
composer_locked | true while a changeset is open — decide it before sending anything else |
commit_result | After a build: {ok, applied, failed, not_applied, skipped, version, undo_available, agent_id} — each entry of applied is {step, method, path, status} |
readiness | {can_speak, roles, telephony, flow_valid, flow_errors, missing, fix_hints} |
version | The agent version after this turn |
suggestions | Follow-ups written by Ava for this reply |
Gates
Every gate is resumed with POST …/chats/{id}/resume/stream and a body of {"workspace_id": "'$WS'", "resume": …}. What goes in resume depends on the gate.
Questions — awaiting: "question"
choice on the done frame:
{
"question": "What should the agent do on a call?",
"options": [ { "label": "Book appointments", "value": "book" }, { "label": "Answer questions", "value": "answer" } ],
"allow_other": true,
"step": 1,
"total": 3,
"question_id": "q_purpose",
"key": "purpose",
"questions": []
}Accepted resume shapes:
| Shape | Use |
|---|---|
"Book appointments" | A bare string answers the open question |
{"answer": "Book appointments"} | Same |
{"answers": {"purpose": "book", "hours": "9–6 weekdays"}} | Several at once, keyed by question_id or key |
{"answers": [{"question_id": "q_purpose", "answer": "book"}]} | Same, as a list |
Answering a question that was already answered edits the earlier answer. Answers are the only resume payload that is written into the transcript as a user message.
Plan — awaiting: "plan_ready"
plan on the done frame holds the design: flow_design (nodes, edges, start_node_id), agent, tools, variables, knowledge_bases, summary_md and open_items. At this gate nothing has been built; artifact.staged is still empty.
resume | Effect |
|---|---|
{"action": "execute"} | Build it. approve, build, go, yes and confirm mean the same |
{"action": "cancel"} | Stop the turn. abort, stop and reject mean the same |
{"action": "revise", "feedback": "Drop the voicemail branch"} | Rewrite the plan. Any other text — or a bare string — is treated as a revision, never as approval |
{"action": "execute", "plan": {…}} | Build with your edited design instead of the proposed one |
If you saved a plan draft before approving, it is merged in automatically and cleared afterwards. Only flow_design, agent, tools, variables and knowledge_bases are taken from an edited plan, and an edit whose flow_design.nodes is empty is ignored.
Changeset — awaiting: "changeset_ready"
changeset on the done frame:
{
"id": "a9e4c6d2-1b3f-4e7a-8c5d-6f2b0a1c3d94",
"summary": "6 steps, 7 transitions, 1 tool",
"total": 14,
"locked": true,
"blocking": [],
"groups": [
{
"group": "nodes", "label": "Steps", "count": 6, "accepted": 0,
"ops": [
{ "id": "op_a1", "scope": "node", "op": "add", "target_path": "flow.nodes.greet", "label": "Greet the caller", "before": null, "after": {}, "status": "proposed" }
]
}
]
}Groups are global, nodes, edges and tools. blocking lists readiness problems that would stop the agent from taking a call, each as {code, role, message, fix}.
Decide with the dedicated endpoint:
POST https://ultravoice-dev.us.inc/api/v1/voika/chats/{id}/changesets/{cid}/decide/stream
| Field | Required | Description |
|---|---|---|
workspace_id | yes | |
ops | one of | Per-change verdicts: [{"id": "op_a1", "status": "accepted"}, {"id": "op_b2", "status": "rejected"}] |
accept_all | one of | Apply every change |
reject_all | one of | Apply nothing and close the changeset |
Any change you do not name is rejected. A partial list is always safe. 404 if the changeset does not belong to the chat; 409 if it was already decided.
The same decision can go through resume/stream as {"resume": {"ops": [...]}}, {"resume": {"accept_all": true}} or {"resume": {"reject_all": true}}.
After a decision the stream narrates the writes as they land, adds a readiness frame, and ends with done: commit_result.applied lists every platform write as {step, method, path, status}, failed and not_applied list what did not, and version is the new version.
Plan draft, agent state and versions
Plan draft
PUT https://ultravoice-dev.us.inc/api/v1/voika/chats/{id}/plan-draft
{ "workspace_id": "'$WS'", "plan": { "flow_design": { "nodes": [], "edges": [], "start_node_id": "greet" } } }Saves canvas edits made while the plan gate is open. plan.flow_design.nodes must contain at least one step (400 otherwise). Response — {"saved": true}. DELETE …/plan-draft?workspace_id=$WS discards it.
Plan as Markdown
GET https://ultravoice-dev.us.inc/api/v1/voika/chats/{id}/plan.md?workspace_id=$WS
Returns the plan as text/markdown — save the body as plan.md. This endpoint is not wrapped in the envelope. 404 when the chat has no plan yet.
Every plan Ava shows at the plan gate, and every plan it builds at a commit, is also kept as a file in object storage and served only through this endpoint — the response is the document itself, never a bucket URL or a signed link, so nothing in your client ever addresses the storage directly. plan.md returns the newest stored document; if you have edited the plan in the draft editor since, it is rendered live with your edits instead, and the file catches up at the next gate or commit.
GET https://ultravoice-dev.us.inc/api/v1/voika/chats/{id}/plans?workspace_id=$WS lists the stored documents, oldest first — 20260925T081530123Z-plan.md for a plan gate, …-committed-v3.md for a commit — and GET …/plans/{name} returns one of them. The assistant message that produced a document carries its key as meta.plan_file.
Agent state
GET https://ultravoice-dev.us.inc/api/v1/voika/chats/{id}/artifact?workspace_id=$WS
{
"success": true,
"data": {
"enabled": true,
"agent_id": "agt_0d5f…",
"committed": { "agent_id": "agt_0d5f…", "mode": "flow", "flow": { "nodes": [], "edges": [] } },
"staged": {},
"changeset": null,
"composer_locked": false,
"awaiting": null,
"plan": null,
"plan_draft": {},
"readiness": { "can_speak": true, "roles": {}, "telephony": "bound", "flow_valid": true, "flow_errors": [], "missing": [], "fix_hints": [], "source": "artifact", "checked_at": "…" },
"version": 2,
"undo_available": true
},
"error": null
}The one call to rehydrate a client: the agent as committed and as staged, whether a gate is open and which, the open changeset, readiness and the current version. It never advances the turn.
Versions
GET https://ultravoice-dev.us.inc/api/v1/voika/chats/{id}/versions?workspace_id=$WS
{ "current": 2, "versions": [ { "version": 2, "reason": "commit", "agent_id": "agt_0d5f…", "changeset_id": "a9e4c6d2-1b3f-4e7a-8c5d-6f2b0a1c3d94", "readiness": {}, "created_at": "…" }, { "version": 1, "reason": "baseline", "agent_id": "agt_0d5f…", "changeset_id": null, "readiness": {}, "created_at": "…" } ] }reason is baseline (the chat's starting point), commit (an applied changeset), partial (a changeset that only partly landed), reconcile (the agent was re-read from the platform before an edit, so changes made elsewhere are part of the history) or undo. Version numbers therefore step by more than one across an edit turn: a reconcile snapshot is recorded first, then the commit.
Undo
POST https://ultravoice-dev.us.inc/api/v1/voika/chats/{id}/undo/stream
{ "workspace_id": "'$WS'", "to_version": 1 }Omit to_version to go back one step — the version just before the current one, which after an edit is the reconcile snapshot taken at the start of that edit. The stream shows the writes being reverted and ends with done carrying version, reverted_to, applied, not_undone and the new readiness; the history gains a version with reason: "undo".
Undo needs an earlier version that already had the agent. Right after the changeset that created an agent there is nothing to go back to — the call answers 409 and undo_available on the artifact is false. From the next applied changeset on, undo is available. 409 also while a review is still open.
Simulate a flow
POST https://ultravoice-dev.us.inc/api/v1/voika/simulate
| Field | Required | Description |
|---|---|---|
workspace_id | yes | |
graph | yes | The flow as it sits on the canvas: nodes, edges, start_node_id |
answers | no | For branching steps, the answer to take at each one, keyed by node id |
Walks the graph without a model or a chat and reports the path: {ok, outcome, error, trace: [...], visited: [...], branch_points: [...]}. Nothing is billed.
Tool secrets
When a message or an answer contains a curl command with a bearer token, Ava reads the token but keeps it out of everything you can see: it is not in the plan, not in the changeset, not in any stream frame and not in the stored transcript. It is attached to the tool only at the moment the tool is written to the platform, which stores it encrypted, and it is discarded from the chat right after. There is no endpoint for it — paste the curl and the right thing happens.
Errors
| HTTP | Where | Meaning and fix |
|---|---|---|
400 | Any | workspace_id missing; resume missing; no decision supplied; a plan draft with no steps; an invalid agent_id |
401 | Any | No Authorization header, or a token that is invalid or expired — sign in again |
402 | Send a message | The chat has used its 30 turns — open a new chat, or run the workspace on its own key |
403 | Any | The workspace is not in your token, or the chat belongs to another user or organization |
404 | Chats, changesets | Unknown or malformed chat id, changeset not on this chat, agent not in this workspace, or plan.md for a chat with no plan |
409 | Turns, changesets, undo | A turn is still running; the changeset was already decided; a review is open; nothing to undo |
422 | Any | Body validation — most often content outside 1–8,000 characters |
503 | Send a message, ready | The service is not configured with a platform key, or the database is unreachable |
An error raised after a stream has opened arrives as an error frame instead of an HTTP status. A stream that ends without done or error was cut by the network: read the chat (GET …/chats/{id}) — running tells you whether the turn is still going — and then GET …/artifact for the current gate. Only resend a message when the stream failed before its first frame; otherwise the turn is already running and a second request is refused with 409.