InfraVoice

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/voika Auth: Authorization: Bearer <JWT> — your dashboard login token, not the usf- organization key. Get one from POST /api/v1/auth/login (see Authentication). The token names the workspaces you belong to; every request also says which workspace it acts in (workspace_id in the body, or as a query parameter on GET and DELETE). Response envelope: every JSON endpoint answers {"success":true,"data":{…},"error":null}. Errors carry the HTTP status and {"detail":"…"}. Streaming endpoints answer with text/event-stream. Gates: a turn is not finished until awaiting is null. While it is question, plan_ready or changeset_ready, the chat is waiting for you.


Endpoints

Chats

MethodEndpointPurpose
GET/api/v1/voika/chatsList the caller's chats in a workspace
POST/api/v1/voika/chatsOpen 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

MethodEndpointPurpose
POST/api/v1/voika/chats/{id}/conductor/streamSend a message and stream the turn (spends one turn)
POST/api/v1/voika/chats/{id}/messagesSame, without streaming — answers when the turn stops
POST/api/v1/voika/chats/{id}/resume/streamAnswer questions, approve, revise or cancel the plan, decide a changeset
POST/api/v1/voika/chats/{id}/resumeSame, without streaming
POST/api/v1/voika/chats/{id}/changesets/{cid}/decide/streamAccept or reject proposed changes, per change or all at once
POST/api/v1/voika/chats/{id}/changesets/{cid}/decideSame, without streaming

Plan, agent state and history

MethodEndpointPurpose
PUT/api/v1/voika/chats/{id}/plan-draftSave canvas edits to merge into the plan on approval
DELETE/api/v1/voika/chats/{id}/plan-draftDiscard the draft
GET/api/v1/voika/chats/{id}/plan.mdThe plan as Markdown
GET/api/v1/voika/chats/{id}/plansEvery 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}/artifactCommitted and staged agent, readiness, open changeset, current gate
GET/api/v1/voika/chats/{id}/versionsVersion history
POST/api/v1/voika/chats/{id}/undo/streamRoll back to the previous version (or a named one)
POST/api/v1/voika/chats/{id}/undoSame, without streaming

Utilities

MethodEndpointPurpose
POST/api/v1/voika/simulateWalk a flow graph without an LLM and report the path taken
GET/api/v1/voika/healthLiveness (no auth)
GET/api/v1/voika/readyReadiness — 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:

Shell
curl -X POST https://ultravoice-dev.us.inc/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "…"}'
JSON
{
  "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_id must be one of the workspaces in your token (403 otherwise).
  • 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.

Shell
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.

Shell
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.

Shell
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.

Shell
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.

Shell
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

FieldRequiredDescription
workspace_idyesWorkspace the chat works in
titlenoShown in the chat list; defaults to New chat, cut at 200 characters
agent_idnoBind the chat to an existing agent so edits and questions target it. Must be an agent in this workspace

Response — 200 with the chat:

JSON
{
  "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

JSON
{
  "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

FieldRequiredDescription
workspace_idyesMust match the chat's workspace
contentyesThe command, 1–8,000 characters. A pasted curl command is understood as a tool definition
mentionnoPoint 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.

EventWhenPayload
statusContinuouslyOne progress event: {"type": …, "label": …, "at": …, …} — see the types below
artifactAfter each build or edit stepThe whole staged (or committed) agent: {"type": "artifact", "artifact": {…}, "committed": true}
changesetWhen the review gate opens{"type": "changeset_ready", "changeset": {…}, "blocking": [...], "lock": true}
doneExactly once, lastThe turn result (below). awaiting says whether a gate is open
errorInstead of done{"message": "…", "detail": "…"} — the stream closes

status types, roughly in the order they appear:

typeExtra fieldsMeaning
plan_of_recordstages: [{id, label}], pathThe stages this turn will go through
reasoning_start · reasoning_delta · reasoningtext, step_id, duration_msStreamed prose while Ava thinks; reasoning_delta.text is the token stream
reasoning_stepstep_idProgress through a long structured output, e.g. "Laying out the steps · 7"
narration—A one-line status for the activity log
subtaskid, parent_id, status, detailParallel work items and their state
tool_call · tool_resulttool, args / tool, okPlatform reads and writes
graph_patchop, node or edgeOne 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_correctproblems: [...]The validator found problems in the draft flow and a repair pass is running
verifyreport, okThe validation report for the staged flow, before the review gate
readinessreadiness, stageWhether the agent can take a call, checked at plan, changeset, commit and undo
commit_partialapplied, failed, not_applied, skippedSome writes landed and some did not
suggestionssuggestions: [{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.

JSON
{
  "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": ""
}
FieldMeaning
awaitingquestion, plan_ready, changeset_ready, or null when the turn is finished
intentWhich path triage chose: build, edit, ops or answer
choiceThe open question when awaiting is question
planThe plan when awaiting is plan_ready
changesetThe proposed changes, with id, when awaiting is changeset_ready
composer_lockedtrue while a changeset is open — decide it before sending anything else
commit_resultAfter 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}
versionThe agent version after this turn
suggestionsFollow-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:

JSON
{
  "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:

ShapeUse
"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.

resumeEffect
{"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:

JSON
{
  "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

FieldRequiredDescription
workspace_idyes
opsone ofPer-change verdicts: [{"id": "op_a1", "status": "accepted"}, {"id": "op_b2", "status": "rejected"}]
accept_allone ofApply every change
reject_allone ofApply 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

JSON
{ "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

JSON
{
  "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

JSON
{ "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

JSON
{ "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

FieldRequiredDescription
workspace_idyes
graphyesThe flow as it sits on the canvas: nodes, edges, start_node_id
answersnoFor 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

HTTPWhereMeaning and fix
400Anyworkspace_id missing; resume missing; no decision supplied; a plan draft with no steps; an invalid agent_id
401AnyNo Authorization header, or a token that is invalid or expired — sign in again
402Send a messageThe chat has used its 30 turns — open a new chat, or run the workspace on its own key
403AnyThe workspace is not in your token, or the chat belongs to another user or organization
404Chats, changesetsUnknown or malformed chat id, changeset not on this chat, agent not in this workspace, or plan.md for a chat with no plan
409Turns, changesets, undoA turn is still running; the changeset was already decided; a review is open; nothing to undo
422AnyBody validation — most often content outside 1–8,000 characters
503Send a message, readyThe 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.