Agents API
An agent is the unit InfraVoice runs on a call: a system prompt, provider slots (LLM, speech-to-text, voice), optional tools, knowledge bases and a phone binding. You create an agent, wire it up, then publish it so live calls pick up the change.
Base URL:
https://your-infravoice-host/api/v1Auth:Authorization: Bearer usf-…— the single organization key from Account settings. It authenticates as your organization's owner, soworkspace_iddefaults to your primary workspace almost everywhere (a dashboard JWT works identically). The same key works on every other page in this reference too. Response envelope: every endpoint on this page is served by a Go service, so the payload is always{"success":true,"data":{…},"error":null,"meta":…,"timestamp":…}. The Python-backed services (telephony, call bridge, voice pipeline) return raw JSON with no envelope — if you write one client for the whole platform, branch on the presence ofsuccess/datarather than assuming either shape. Drafts: every write on this page edits a draft. Nothing reaches a live call untilPOST /api/v1/agents/{id}/versions/publish.
Endpoints
Agent CRUD
| Method | Endpoint | Purpose |
|---|---|---|
POST | /api/v1/agents | Create an agent (draft) |
GET | /api/v1/agents | List agents in a workspace |
GET | /api/v1/agents/{id} | Agent + providers + telephony + knowledge bases |
PUT | /api/v1/agents/{id} | Partial update of the draft |
DELETE | /api/v1/agents/{id} | Delete the agent |
Versions
| Method | Endpoint | Purpose |
|---|---|---|
GET | /api/v1/agents/{id}/versions | List published versions + draft_dirty |
GET | /api/v1/agents/{id}/versions/diff | Diff two versions, or a version vs. the draft |
GET | /api/v1/agents/{id}/versions/{version} | One version's full snapshot |
POST | /api/v1/agents/{id}/versions/publish | Snapshot the draft and make it live |
POST | /api/v1/agents/{id}/versions/{version}/rollback | Restore an old version and republish it |
Providers and credentials
| Method | Endpoint | Purpose |
|---|---|---|
POST | /api/v1/agents/{id}/providers | Set (upsert) one slot |
GET | /api/v1/agents/{id}/providers | List configured slots |
DELETE | /api/v1/agents/{id}/providers/{type} | Remove a slot |
GET | /api/v1/providers | Browse the catalog of supported providers (may be empty — see below) |
GET | /api/v1/providers/{id} | One catalog entry, by its UUID |
POST | /api/v1/providers/user-config | Legacy: store a key/model per account or agent_id |
GET | /api/v1/providers/user-config | Legacy: list those configs (never returns a key) |
GET | /api/v1/providers/resolve | Debug: effective config for a type (+ optional agent_id) |
POST | /api/v1/agents/credentials | Create a reusable workspace credential |
GET | /api/v1/agents/credentials | List credentials (requires workspace_id) |
GET | /api/v1/agents/credentials/{credID} | Fetch one |
PUT | /api/v1/agents/credentials/{credID} | Update or rotate |
DELETE | /api/v1/agents/credentials/{credID} | Delete and unlink from agents |
Prompts, tools, knowledge, checklist
| Method | Endpoint | Purpose |
|---|---|---|
POST | /api/v1/agents/snippets | Create a workspace prompt snippet |
GET | /api/v1/agents/snippets | List snippets (requires workspace_id), merged with seeds |
GET | /api/v1/agents/snippets/{id} | Fetch one snippet |
PUT | /api/v1/agents/snippets/{id} | Update a snippet |
DELETE | /api/v1/agents/snippets/{id} | Delete a snippet |
GET | /api/v1/agents/{id}/handbook | Resolved snippets attached to an agent, in order |
PUT | /api/v1/agents/{id}/handbook | Replace the agent's ordered snippet list |
POST | /api/v1/agents/{id}/tools | Create a tool |
GET | /api/v1/agents/{id}/tools | List the agent's tools |
GET | /api/v1/agents/{id}/tools/{toolID} | Fetch one tool |
PUT | /api/v1/agents/{id}/tools/{toolID} | Update a tool |
DELETE | /api/v1/agents/{id}/tools/{toolID} | Delete a tool |
POST | /api/v1/agents/{id}/knowledge-bases | Attach a KB with a delivery mode |
GET | /api/v1/agents/{id}/knowledge-bases | List bindings |
PUT | /api/v1/agents/{id}/knowledge-bases/{kbID} | Change mode or enable/disable |
DELETE | /api/v1/agents/{id}/knowledge-bases/{kbID} | Detach |
GET | /api/v1/agents/{id}/checklist | Read the live-call checklist template |
PUT | /api/v1/agents/{id}/checklist | Replace the ordered pointers |
Telephony, flow mode, tests
| Method | Endpoint | Purpose |
|---|---|---|
PUT | /api/v1/agents/{id}/telephony | Set the telephony config |
GET | /api/v1/agents/{id}/telephony | Read it back (token masked) |
GET | /api/v1/agents/{id}/mode | Read the current mode |
PUT | /api/v1/agents/{id}/mode | Switch single_prompt / flow |
GET | /api/v1/agents/{id}/flow | Get the latest flow graph |
PUT | /api/v1/agents/{id}/flow | Save the graph (creates a new flow version) |
POST | /api/v1/agents/{id}/flow/validate | Dry-run validation without saving |
GET | /api/v1/agents/{id}/test-cases | List test cases |
POST | /api/v1/agents/{id}/test-cases | Create a test case |
PUT | /api/v1/agents/{id}/test-cases/{tcId} | Update a test case |
DELETE | /api/v1/agents/{id}/test-cases/{tcId} | Delete a test case |
POST | /api/v1/agents/{id}/test-runs | Start a run |
GET | /api/v1/agents/{id}/test-runs | List runs (paginated) |
Secrets are one-way. Provider
api_key,twilio_auth_tokenand toolauth_tokenare encrypted at rest (AES-256-GCM) and only ever read back masked (********+ last 4; a bare********inside version snapshots). No endpoint returns a stored secret in plaintext — if you lose a key, rotate it at the source and write a new one.
Create an agent
An agent lives in exactly one workspace. Requests for an agent outside your workspaces return 404, not 403 — the platform does not confirm that a foreign id exists.
Quickstart
The shortest path from a bare organization key to an agent that can answer a phone call.
1. Create the agent. Only name and system_prompt are required.
curl -X POST https://your-infravoice-host/api/v1/agents \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Support Line",
"system_prompt": "You are Ava, the support agent for Acme. Be brief and warm. Never invent order numbers.",
"welcome_message": "Hi, this is Ava at Acme — how can I help?",
"language": "en",
"allow_human_transfer": true
}'Keep data.id — every call below is scoped to it.
2. Set the three required providers (llm, asr, tts), one request per slot:
AGENT=agt_0d5f... # data.id from step 1
KEY="Authorization: Bearer usf-YOUR_ORG_KEY"
curl -X POST https://your-infravoice-host/api/v1/agents/$AGENT/providers \
-H "$KEY" -H "Content-Type: application/json" \
-d '{"provider_type":"llm","provider_name":"openai","model_name":"gpt-4o-mini","api_key":"sk-...","credential_label":"OpenAI – prod"}'
curl -X POST https://your-infravoice-host/api/v1/agents/$AGENT/providers \
-H "$KEY" -H "Content-Type: application/json" \
-d '{"provider_type":"asr","provider_name":"deepgram","model_name":"nova-2","api_key":"...","credential_label":"Deepgram – prod"}'
curl -X POST https://your-infravoice-host/api/v1/agents/$AGENT/providers \
-H "$KEY" -H "Content-Type: application/json" \
-d '{"provider_type":"tts","provider_name":"elevenlabs","voice_id":"21m00Tcm...","api_key":"...","credential_label":"ElevenLabs – prod"}'3. (Optional) Add tools, knowledge bases and a handbook — POST /agents/{id}/tools, POST /agents/{id}/knowledge-bases, PUT /agents/{id}/handbook.
4. Bind a phone number and transfer behaviour.
curl -X PUT https://your-infravoice-host/api/v1/agents/$AGENT/telephony \
-H "$KEY" -H "Content-Type: application/json" \
-d '{
"twilio_account_sid": "AC...",
"twilio_auth_token": "...",
"twilio_phone_number": "+14155550123",
"call_direction": "inbound",
"human_transfer_mode": "phone",
"human_transfer_number": "+14155550188"
}'5. Publish. Until you do, everything above is a draft and calls behave as if none of it happened.
curl -X POST https://your-infravoice-host/api/v1/agents/$AGENT/versions/publish \
-H "$KEY" -H "Content-Type: application/json" \
-d '{"notes":"initial launch"}'{
"success": true,
"data": {
"version_number": 1,
"published_at": "2026-07-26T10:14:22Z",
"message": "Version 1 is now live."
},
"error": null
}Create fields
POST https://your-infravoice-host/api/v1/agents
| Field | Required | Description |
|---|---|---|
name | yes | Display name. 400 MISSING_FIELDS if empty. |
system_prompt | yes | The agent's instructions — personality, scope and rules. |
workspace_id | no | Defaults to your primary workspace. A workspace you are not a member of returns 403. |
description | no | Internal note, never spoken. |
welcome_message | no | The first line the agent speaks when a call connects. |
language | no | Primary language hint, e.g. en. |
max_turns | no | Hard ceiling on conversation turns before the call ends. |
connection_type | no | How the agent is reached (telephony vs. web session). |
interrupt_enabled | no | Whether the caller can barge in over the agent's speech. |
vad_threshold | no | Voice-activity sensitivity — lower is more sensitive to quiet speech. |
silence_timeout_ms | no | How long silence must last before the turn is treated as finished. |
allow_human_transfer | no | Enables the hand-off-to-human tool. Pair with the telephony config. |
metadata | no | Free-form object holding the Advanced Pipeline Settings. |
Everything the dashboard calls "advanced" lives in metadata, so the API surface stays stable as tuning knobs come and go: llm_temperature, llm_max_tokens, the context_* window controls, tts_* playback controls, dtmf_* keypad handling, amd_* answering-machine detection, noise_cancellation_enabled. Unknown keys are stored, not rejected — a typo is silently ignored, so check the object you get back.
Response — 201 with the full agent:
{
"success": true,
"data": {
"id": "agt_0d5f7c1a",
"workspace_id": "ws_2b91",
"user_id": "usr_77c1",
"name": "Support Line",
"system_prompt": "You are Ava, the support agent for Acme. …",
"welcome_message": "Hi, this is Ava at Acme — how can I help?",
"language": "en",
"max_turns": 0,
"interrupt_enabled": true,
"vad_threshold": 0.5,
"silence_timeout_ms": 800,
"connection_type": "telephony",
"is_active": true,
"allow_human_transfer": true,
"mode": "single_prompt",
"metadata": { "llm_temperature": 0.4 },
"handbook_snippet_ids": [],
"created_at": "2026-07-26T10:02:11Z",
"updated_at": "2026-07-26T10:02:11Z"
},
"error": null
}New agents start in mode: "single_prompt" — one system prompt drives the whole conversation. The graph-based alternative is described under Flow mode and tests.
List agents
GET https://your-infravoice-host/api/v1/agents
| Query | Description |
|---|---|
workspace_id | Auto-filled from your primary workspace when omitted. A workspace you are not a member of returns 403. |
limit | Any value greater than 0 turns on pagination. Capped at 200. |
offset | Row offset for pagination. |
With limit set, the envelope's meta carries {page, per_page, total, total_pages}; without it you get the unpaginated array.
curl -H "Authorization: Bearer usf-YOUR_ORG_KEY" \
"https://your-infravoice-host/api/v1/agents?limit=50"Get, update, delete
GET /api/v1/agents/{id} returns the assembled view a dashboard needs — the agent row plus its sub-resources:
{
"success": true,
"data": {
"agent": { "id": "agt_0d5f7c1a", "name": "Support Line", "mode": "single_prompt" },
"providers": [
{ "provider_type": "llm", "provider_name": "openai", "model_name": "gpt-4o-mini", "api_key": "********f3a1", "credential_id": "cred_9a2", "credential_label": "OpenAI – prod" },
{ "provider_type": "asr", "provider_name": "deepgram", "model_name": "nova-2", "api_key": "********8c40" },
{ "provider_type": "tts", "provider_name": "elevenlabs", "voice_id": "21m00Tcm", "api_key": "********11de" }
],
"telephony": { "twilio_phone_number": "+14155550123", "twilio_auth_token": "********9b2c" },
"knowledge_bases": []
},
"error": null
}PUT /api/v1/agents/{id} is a partial update: every field is optional and an omitted field is left unchanged. You can update name, description, system_prompt, welcome_message, language, max_turns, connection_type, interrupt_enabled, vad_threshold, silence_timeout_ms, is_active, allow_human_transfer, metadata and handbook_snippet_ids (sending [] clears the list; sending a list replaces it wholesale).
curl -X PUT https://your-infravoice-host/api/v1/agents/$AGENT \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{"system_prompt":"You are Ava … Always confirm the caller'\''s order number before looking it up."}'DELETE /api/v1/agents/{id} removes the agent and returns {"message":"agent deleted"}.
Publishing & versions
Every write on this page — the agent row, providers, tools, telephony, knowledge-base bindings, the handbook — edits the draft. The draft is what GET /agents/{id} returns. It is not what answers calls: calls are served by the most recently published version, an immutable snapshot taken at publish time. Any mutation flips draft_dirty to true and invalidates the voice pipeline's cache for that agent, so the next call re-reads configuration — that is what makes a published change take effect immediately; it does not promote a draft.
Saving is not shipping. A prompt edit, a swapped voice, a new tool, a changed transfer number — none of it affects a live call until
POST /api/v1/agents/{id}/versions/publish. If a change "didn't take", checkGET /api/v1/agents/{id}/versionsfordraft_dirty: truefirst.
The safe sequence: edit the draft → GET /versions and confirm draft_dirty → GET /versions/diff to review → POST /versions/publish with notes → if it goes wrong, POST /versions/{version}/rollback.
List versions
curl -H "Authorization: Bearer usf-YOUR_ORG_KEY" \
https://your-infravoice-host/api/v1/agents/$AGENT/versions{
"success": true,
"data": {
"agent_id": "agt_0d5f7c1a",
"live_version": 3,
"draft_dirty": true,
"versions": [
{ "version_number": 3, "status": "published", "published_at": "2026-07-25T16:40:02Z", "published_by": "usr_77c1", "notes": "shorter greeting", "is_live": true },
{ "version_number": 2, "status": "published", "published_at": "2026-07-24T09:12:44Z", "published_by": "usr_77c1", "notes": "added order-lookup tool", "is_live": false },
{ "version_number": 1, "status": "published", "published_at": "2026-07-23T11:01:09Z", "published_by": "usr_77c1", "notes": "initial launch", "is_live": false }
]
},
"error": null
}Diff before publishing
GET /api/v1/agents/{id}/versions/diff takes from and to, each an integer version number or the literal draft (0 also means draft). With both omitted it defaults to from=live_version, to=draft — exactly the "what am I about to ship?" question.
curl -H "Authorization: Bearer usf-YOUR_ORG_KEY" \
"https://your-infravoice-host/api/v1/agents/$AGENT/versions/diff"{
"success": true,
"data": {
"from": 3,
"to": "draft",
"changes": [
{ "field": "system_prompt", "action": "changed", "before": "You are Ava …", "after": "You are Ava … Always confirm the caller's order number …" },
{ "field": "agent.metadata.llm_temperature", "action": "changed", "before": 0.7, "after": 0.4 },
{ "field": "tools.lookup_order", "action": "added", "before": null, "after": { "endpoint_url": "https://api.acme.com/orders" } }
],
"summary": "2 changed, 1 added"
},
"error": null
}Diffs cover agent scalars, agent.metadata.<key>, providers.<type>, tools.<name> and telephony.
Secret rotations are invisible in a diff. Key fields are stripped from both sides before comparison, so changing only an
api_keyproduces an empty diff. Publish it anyway — the snapshot does capture the new key.
Publish and inspect
POST /api/v1/agents/{id}/versions/publish with an optional {"notes":"…"}. The new version_number is the previous maximum plus one, published_by comes from the token identity, and the snapshot captures the agent, providers, tools, telephony and knowledge-base bindings together — you cannot publish half a change.
GET /api/v1/agents/{id}/versions/{version} returns that snapshot:
{
"success": true,
"data": {
"id": "ver_31ac",
"agent_id": "agt_0d5f7c1a",
"version_number": 2,
"status": "published",
"snapshot": {
"agent": { "name": "Support Line", "system_prompt": "You are Ava …" },
"providers": [ { "provider_type": "llm", "provider_name": "openai", "api_key": "********" } ],
"tools": [],
"telephony": { "twilio_phone_number": "+14155550123", "twilio_auth_token": "********" },
"knowledge_bases": []
},
"published_at": "2026-07-24T09:12:44Z",
"published_by": "usr_77c1",
"notes": "added order-lookup tool",
"created_at": "2026-07-24T09:12:44Z"
},
"error": null
}A non-integer {version} returns 400 invalid_version.
Roll back
POST /api/v1/agents/{id}/versions/{version}/rollback restores that snapshot into the draft tables and immediately publishes it as a new version. History stays linear: rolling back to v2 while v4 is live produces v5 whose contents equal v2. v2 does not "become live again".
Rollback overwrites your draft. Any unpublished work in progress is destroyed. Diff
draftagainst the live version first if you are not sure what you are about to lose.
Providers & credentials
A provider slot tells the agent which vendor and model to use for one job. Slots are keyed one row per type — posting the same provider_type twice updates it rather than adding a second.
| Slot | Job | Required to take a call |
|---|---|---|
llm | Generates replies and decides when to call a tool | yes |
asr | Transcribes the caller's speech | yes |
tts | Speaks the agent's replies | yes |
analysis_llm | Optional second model for post-call analysis and QA, so heavy analysis never competes with the conversational model | no |
A missing provider fails the call with 422. An agent without all three of
llm,asrandttsis not "degraded" — the voice pipeline rejects the session up front with 422 instead of retrying forever. Configure all three, then publish, before you point a phone number at the agent.
Set a provider
POST https://your-infravoice-host/api/v1/agents/{id}/providers
| Field | Required | Description |
|---|---|---|
provider_type | yes | One of llm, asr, tts, analysis_llm, opener_llm. Validated: anything else returns 400 INVALID_PROVIDER_TYPE and writes nothing. |
provider_name | yes | Vendor id from the catalog, e.g. openai, deepgram, elevenlabs. |
api_key | no | The vendor key. Encrypted at rest; only ever read back masked. See the three write modes below. |
credential_id | no | Link an existing reusable workspace credential instead of sending a key. |
save_as_credential | no | Defaults to true whenever a new api_key is sent — the key becomes a reusable credential automatically. Set false to keep it on this agent only. |
credential_label | no | Name for the auto-created credential. Worth setting; the default label is not self-explanatory in a long list. |
base_url | no | Override for OpenAI-compatible or self-hosted endpoints. Falls back to the linked credential's base_url. Must be a public http(s)/ws(s) address — a private, loopback or cloud-metadata host is rejected with 400 INVALID_BASE_URL. |
model_name | no | e.g. gpt-4o-mini, nova-2. |
voice_id | no | TTS voice identifier. |
language | no | Per-provider language hint. |
config | no | Free-form object for vendor-specific options. |
How the key field behaves — three distinct modes, and mixing them up is the usual cause of "my key disappeared":
credential_idset — the effective key comes from that reusable workspace credential. A credential belonging to another workspace is rejected with 403FORBIDDEN.api_keyset — stored encrypted. Becausesave_as_credentialdefaults totrue, it is normally also promoted to a workspace credential and linked, so the next agent can reuse it.api_keyempty or omitted — the existing key or credential link is preserved. This is deliberate: it lets you changemodel_nameorvoice_idwithout re-sending the secret.
Response — 200 with the slot, key masked:
{
"success": true,
"data": {
"provider_type": "llm",
"provider_name": "openai",
"model_name": "gpt-4o-mini",
"base_url": "",
"api_key": "********f3a1",
"credential_id": "cred_9a2e",
"credential_label": "OpenAI – prod",
"config": {}
},
"error": null
}Changing a model_name without touching the key:
curl -X POST https://your-infravoice-host/api/v1/agents/$AGENT/providers \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{"provider_type":"llm","provider_name":"openai","model_name":"gpt-4o"}'DELETE /api/v1/agents/{id}/providers/{type} clears a slot and returns {"message":"provider removed"}. Deleting llm, asr or tts from a published agent and republishing will break its calls.
analysis_llm is written exactly like the others. The analysis schema — the structured fields extracted from each conversation — is not configured here; it belongs to the conversation/QA surface. See Conversations. What lives on the agent is only the model that does the work, plus the live-call checklist.
Provider catalog
GET https://your-infravoice-host/api/v1/providers?type=tts lists the vendors a platform administrator has registered, with their model and language lists. It is a read-only, tenant-wide catalog containing no secrets — use it to populate a picker or to check a provider_name before you write it.
The catalog is optional, and on many deployments it is empty. It is seeded by platform administrators, not created by using the product, so
GET /api/v1/providerslegitimately answers"data": nullandGET /api/v1/providers/{id}then has no id to be called with — every lookup returns404 NOT_FOUND. Nothing on this page depends on it:POST /agents/{id}/providerstakes aprovider_namestring, never a catalog id. Read the catalog if it is populated; do not build a required step on it. Catalog ids are plain UUIDs — theprv_…shape in older examples was never real, so do not validate on a prefix.
{
"success": true,
"data": [
{
"id": "9f1c4e2a-6b70-4a51-9d3e-0c2f5a7b8e11",
"name": "elevenlabs",
"provider_type": "tts",
"description": "ElevenLabs neural voices",
"base_url": "https://api.elevenlabs.io",
"auth_type": "api_key",
"is_active": true,
"default_config": {},
"supported_models": ["eleven_turbo_v2", "eleven_multilingual_v2"],
"supported_languages": ["en", "es", "fr"]
}
],
"error": null
}Two older endpoints, POST/GET /api/v1/providers/user-config, store a key against your account rather than an agent. /agents/{id}/providers plus credentials is the surface to build on.
POST /providers/user-configneeds aprovider_idfrom the shared provider catalog (GET /api/v1/providers) — it is a foreign key, not a free-form vendor name. An id that is not in the catalog returns 404PROVIDER_NOT_FOUND, and on a deployment whose catalog has not been seeded the catalog is empty, so this endpoint cannot be used at all there. Use/agents/{id}/providers, which takes aprovider_nameand needs no catalog. Because ausf-key authenticates as your organization's owner, all key-authenticated calls share oneuser-confignamespace.
Debugging what a slot resolves to
GET https://your-infravoice-host/api/v1/providers/resolve?type=llm&agent_id={id} answers "which vendor, model and auth type would actually be used?". It masks the api_key, and returns 404 NOT_CONFIGURED when nothing is set for that type.
{
"success": true,
"data": {
"provider_type": "llm",
"provider_name": "openai",
"base_url": "",
"api_key": "********f3a1",
"model_name": "gpt-4o-mini",
"auth_type": "bearer",
"config": null,
"source": "agent_providers"
}
}source tells you which store answered: agent_providers for a slot written by POST /agents/{id}/providers, user_config for the legacy per-account row. The per-agent slot wins when both exist, because that is the one the runtime reads. Pass agent_id — without it only the legacy store is consulted, and a modern agent has nothing there.
An empty base_url is not an error: it means the slot names no override and the runtime uses its own built-in endpoint for that provider_name. auth_type reports bearer unless the catalog holds an entry for that vendor saying otherwise.
Resolving is workspace-scoped like every other by-id read: an agent_id in another workspace returns 403, not the config.
Reusable credentials
Putting the same OpenAI key on twelve agents means rotating it twelve times. A workspace credential stores the secret once; agents link to it by credential_id, and a rotation propagates to all of them and busts each one's pipeline cache.
POST https://your-infravoice-host/api/v1/agents/credentials
| Field | Required | Description |
|---|---|---|
provider_type | yes | llm, asr, tts or analysis_llm. |
provider_name | yes | Vendor id, e.g. openai. |
label | yes | Human name shown in the dashboard. |
api_key | no | The secret. Write-only — encrypted at rest and never echoed back. |
workspace_id | no | Defaults to your primary workspace; membership enforced with 403. |
base_url, model_name, voice_id, language, config | no | Defaults inherited by every agent that links this credential. |
curl -X POST https://your-infravoice-host/api/v1/agents/credentials \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{"provider_type":"llm","provider_name":"openai","label":"OpenAI – prod","api_key":"sk-...","model_name":"gpt-4o-mini"}'{
"success": true,
"data": {
"id": "cred_9a2e",
"workspace_id": "ws_2b91",
"provider_type": "llm",
"provider_name": "openai",
"label": "OpenAI – prod",
"model_name": "gpt-4o-mini",
"masked_key": "********f3a1",
"is_active": true,
"created_at": "2026-07-26T10:20:03Z"
},
"error": null
}Attach it to as many agents as you like — no key in the request body:
curl -X POST https://your-infravoice-host/api/v1/agents/$AGENT/providers \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{"provider_type":"llm","provider_name":"openai","credential_id":"cred_9a2e"}'PUT /api/v1/agents/credentials/{credID} rotates the secret for every linked agent at once. All fields are optional; omitting api_key (or sending it empty) keeps the current key, so you can rename a credential without handling the secret.
curl -X PUT https://your-infravoice-host/api/v1/agents/credentials/cred_9a2e \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{"api_key":"sk-new-key-...","label":"OpenAI – prod (rotated Jul)"}'
GET /api/v1/agents/credentialsrequiresworkspace_id. Unlike most list endpoints it is not auto-filled — call it as?workspace_id=ws_…&type=llm, or you get400 MISSING_PARAM. Readworkspace_idoff any agent you already have. The same applies toGET /api/v1/agents/snippets. Listing returnsmasked_keyonly; there is no read path for the plaintext, by design.
Tools & knowledge
Four sub-resources shape what the agent knows and can do mid-call: the handbook (shared prompt text), custom tools (HTTP calls), knowledge bases, and the live-call checklist.
These endpoints replace, they don't append.
handbook.snippet_ids,checklist.pointersandagent.handbook_snippet_idsall overwrite the existing list. Read, modify, write.
Prompt snippets & handbook
system_prompt is the whole instruction set in the simple case. When several agents must share the same rules — compliance language, escalation policy, brand voice — put those paragraphs in snippets and attach them as a handbook. Snippet bodies are prepended to the system prompt, in the order you list them, at the start of each session.
Create with title, body and an optional category; workspace_id defaults to your primary workspace.
curl -X POST https://your-infravoice-host/api/v1/agents/snippets \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Escalation policy","category":"policy","body":"If the caller mentions a legal complaint, do not answer — transfer to a human immediately."}'Listing merges your snippets with platform-shipped ones. Those carry is_seed: true and are read-only: editing or deleting one returns 403 READ_ONLY. Copy a seed into a new snippet if you need to change its wording.
curl -X PUT https://your-infravoice-host/api/v1/agents/$AGENT/handbook \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{"snippet_ids":["snp_escalation","snp_brand_voice"]}'{
"success": true,
"data": {
"agent_id": "agt_0d5f7c1a",
"snippets": [
{ "id": "snp_escalation", "title": "Escalation policy", "body": "If the caller mentions …", "category": "policy", "is_seed": false },
{ "id": "snp_brand_voice", "title": "Brand voice", "body": "Warm, concise, never salesy.", "category": "style", "is_seed": true }
],
"missing_ids": []
},
"error": null
}missing_ids lists attached ids whose snippet has since been deleted — a deleted snippet does not silently vanish from the agent, it shows up here so you can notice. Order in snippet_ids is the concatenation order into the prompt; null or an omitted list is treated as empty. This endpoint is equivalent to sending handbook_snippet_ids on PUT /agents/{id}.
Custom tools
A tool lets the agent do something mid-call: look up an order, book a slot, write to your CRM. The contract:
- The
function_schemayou provide is handed to the LLM as a callable function. Itsdescriptionand per-parameter descriptions are the only thing the model has to go on — write them for a reader who cannot see your API. - Mid-call, the model decides to call the tool and produces arguments matching that schema.
- InfraVoice issues an HTTP request to
endpoint_urlusinghttp_method, yourheaders, and the credential implied byauth_type/auth_token, shaping the body fromrequest_body. response_pathpoints at the part of the JSON payload worth handing to the model, so a large payload doesn't flood the conversation.- That value goes back into the conversation and the agent speaks its next turn.
Because a caller is waiting through step 3, timeout_ms is a latency decision, not just a safety net: keep it tight (a second or two) and have your endpoint return something the agent can say rather than something it has to wait for. retry_count multiplies that wait, so use it only for genuinely idempotent calls.
| Field | Required | Description |
|---|---|---|
name | yes | Function name the model calls. Keep it snake_case and descriptive. |
description | yes | When the model should use this tool. The most important field on the object. |
function_schema | no | JSON Schema of the function's parameters, in the standard function-calling shape. |
endpoint_url | no | The HTTPS endpoint InfraVoice calls. |
http_method | no | GET, POST, … |
headers | no | Static headers sent with every invocation. Headers that would impersonate an internal caller (X-Internal-Service, Host, Cookie, Connection, Content-Length, Transfer-Encoding, Upgrade) are stripped before the request is sent. |
auth_type | no | How auth_token is presented to your endpoint. |
auth_token | no | Secret. Encrypted at rest, returned masked. |
request_body | no | Body template sent to your endpoint. |
response_path | no | Path into the JSON response selecting the value handed back to the model. |
timeout_ms | no | Per-invocation timeout. The caller hears this as silence. |
retry_count | no | Retries on failure. |
connector_binding | no | Binds the tool to a managed connector instead of a raw URL. |
is_active | no (update only) | Disable a tool without deleting it. |
curl -X POST https://your-infravoice-host/api/v1/agents/$AGENT/tools \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "lookup_order",
"description": "Look up the status of a customer order by its order number. Use this whenever the caller asks where their order is.",
"function_schema": {
"type": "object",
"properties": {
"order_number": { "type": "string", "description": "The order number, e.g. AC-10293" }
},
"required": ["order_number"]
},
"endpoint_url": "https://api.acme.com/v1/orders/status",
"http_method": "POST",
"auth_type": "bearer",
"auth_token": "acme_live_...",
"request_body": { "order": "{{order_number}}" },
"response_path": "data.status",
"timeout_ms": 2500,
"retry_count": 0
}'{
"success": true,
"data": {
"id": "tool_44b1",
"agent_id": "agt_0d5f7c1a",
"name": "lookup_order",
"description": "Look up the status of a customer order …",
"endpoint_url": "https://api.acme.com/v1/orders/status",
"http_method": "POST",
"auth_type": "bearer",
"auth_token": "********e_...",
"response_path": "data.status",
"timeout_ms": 2500,
"retry_count": 0,
"is_active": true
},
"error": null
}PUT /api/v1/agents/{id}/tools/{toolID} takes the same fields, all optional, plus is_active. DELETE returns {"message":"tool deleted"}.
Knowledge bases
Knowledge bases are created and populated on the Knowledge Base surface; here you only bind an existing one to an agent and choose how its content is delivered into the conversation.
curl -X POST https://your-infravoice-host/api/v1/agents/$AGENT/knowledge-bases \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{"knowledge_base_id":"kb_7f21","mode":"system_context"}'{
"success": true,
"data": {
"id": "akb_5c30",
"agent_id": "agt_0d5f7c1a",
"knowledge_base_id": "kb_7f21",
"mode": "system_context",
"is_active": true,
"created_at": "2026-07-26T10:31:44Z",
"updated_at": "2026-07-26T10:31:44Z"
},
"error": null
}mode has exactly two values, and it is the only field here that can be wrong:
mode | Delivery |
|---|---|
system_context (default when omitted) | The knowledge base's content is folded into the agent's system prompt for every turn. |
tool | The agent is given a lookup tool and queries the knowledge base only when it decides it needs to. |
Anything else returns 400 INVALID_MODE.
{kbID}is the binding id, not the knowledge-base id. Update and detach take theidreturned by the attach call (akb_…above), notknowledge_base_id. Using the KB id there is the most common 404 on this resource.
PUT accepts mode and is_active, both optional — flipping is_active to false is the way to test whether a KB is helping or hurting without losing the binding. DELETE returns {"message":"knowledge base detached"}.
Live-call checklist
A checklist is an ordered list of things the agent is supposed to accomplish on a call — verify identity, confirm the address, offer the upgrade. During a live call, a team leader watching the monitor sees each pointer tick off as it is satisfied. PUT replaces the whole list; a pointer sent without an id gets a generated uuid, so keep ids stable when editing an existing checklist and historical calls stay comparable.
curl -X PUT https://your-infravoice-host/api/v1/agents/$AGENT/checklist \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{"pointers":[
{"label":"Verified caller identity","hint":"Full name and order number confirmed"},
{"label":"Explained the resolution","hint":"Caller repeated it back or agreed"},
{"label":"Offered the callback option"}
]}'{
"success": true,
"data": {
"agent_id": "agt_0d5f7c1a",
"pointers": [
{ "id": "9f1c…", "label": "Verified caller identity", "hint": "Full name and order number confirmed" },
{ "id": "2a70…", "label": "Explained the resolution", "hint": "Caller repeated it back or agreed" },
{ "id": "c418…", "label": "Offered the callback option", "hint": "" }
],
"is_active": true,
"created_at": "2026-07-26T10:35:00Z",
"updated_at": "2026-07-26T10:35:00Z"
},
"error": null
}GET returns an empty pointers array when nothing is configured, rather than a 404.
Telephony binding
The per-agent telephony sub-resource decides which number the agent answers on and what happens when it hands a caller to a human.
| Field | Description |
|---|---|
twilio_account_sid | Twilio account the number belongs to. |
twilio_auth_token | Secret — encrypted at rest, always returned masked. |
twilio_phone_number | The number in E.164, e.g. +14155550123. |
webhook_url | Where telephony events are delivered. |
call_direction | inbound (default), outbound or both. Lowercase — "INBOUND" is rejected with 400 INVALID_TELEPHONY. |
human_transfer_number | The number a caller is transferred to in phone mode. E.164. |
human_transfer_mode | phone (default — dial a number) or panel (route to a live agent in the agent panel). Any other value returns 400 INVALID_TELEPHONY. |
panel_project_id | Required for panel mode: the project whose human agents receive the call. Must belong to your workspace; a foreign project id is rejected. |
transcribe_human_segment | Default false. Transcribe the portion of the call after a human takes over. |
generate_call_summary | Default true. Produce a whole-call summary when the call ends. |
curl -H "Authorization: Bearer usf-YOUR_ORG_KEY" \
https://your-infravoice-host/api/v1/agents/$AGENT/telephonyGET returns 404 NOT_FOUND when the agent has no telephony row yet — expected for a brand-new agent, not an error to alarm at. As everywhere on this page, the change is a draft until you publish. For number provisioning and call control see the Telephony API.
Flow mode & tests
Instead of one system prompt, an agent can run a flow: a graph of nodes (welcome, conversation, question, decision, tool call, variable, transfer, end) with edges between them. The agent's mode decides which engine runs.
Flows live under the agent. Every flow route is a sub-resource of
/api/v1/agents/{id}— there is no top-level flows collection to call.
Save and validate a flow
GET /flow never 404s: an agent with no flow yet returns an empty version-0 graph, so a builder UI has something to render. PUT /flow stores a new version each time (MAX(version)+1) and rejects an empty node list with 400 EMPTY_FLOW, so a blank canvas can never silently replace a working flow. An invalid graph, though, does save — is_valid simply comes back false with validation_errors populated, which is why you should call /flow/validate first:
curl -X POST https://your-infravoice-host/api/v1/agents/$AGENT/flow/validate \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{"graph":{"nodes":[{"id":"n1","type":"welcome","position":{"x":0,"y":0},"data":{"text":"Hi, Acme here."}}],"edges":[],"start_node_id":"n1"},"variables":[]}'{
"success": true,
"data": {
"is_valid": false,
"errors": [
{ "node_id": "n1", "edge_id": "", "code": "NO_OUTGOING_EDGE", "message": "welcome node has no outgoing edge" }
]
},
"error": null
}| Code | Meaning |
|---|---|
NO_START_NODE | start_node_id is missing or points at nothing. |
INVALID_SOURCE / INVALID_TARGET | An edge references a node that isn't in the graph. |
NO_OUTGOING_EDGE | A welcome, conversation, question, decision, tool_call or variable node is a dead end. |
UNREACHABLE | A non-terminal node has no inbound edge. Terminal types are transfer and end. |
MISSING_FIELD | welcome/conversation without text or system_prompt, question without question, tool_call without tool_id. |
Switching mode is a separate call — saving a flow does not activate it:
curl -X PUT https://your-infravoice-host/api/v1/agents/$AGENT/mode \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{"mode":"flow"}'Any value other than single_prompt or flow returns 400 INVALID_MODE. Then publish a version, as usual.
Simulation tests
Before publishing a prompt change you can run the agent against scripted personas. Test cases are stored per agent; a run executes some or all of them and is dispatched to the simulator asynchronously. A test case takes name (required), plus persona, goal, assertions[], max_turns (default 20) and tags[]. Updates apply only the non-empty fields you send.
curl -X POST https://your-infravoice-host/api/v1/agents/$AGENT/test-runs \
-H "Authorization: Bearer usf-YOUR_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{"agent_version":3,"case_ids":[]}'An empty case_ids runs every case. An agent with no test cases returns 400 NO_CASES. Poll GET /api/v1/agents/{id}/test-runs (limit defaults to 20, with meta pagination) for status.
Errors
Errors use the same envelope with success: false: {"success":false,"data":null,"error":{"code":"…","message":"…"}}.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | MISSING_FIELDS | name or system_prompt missing on agent create. |
| 400 | MISSING_PARAM | A required query parameter is absent — most often workspace_id on the credentials and snippets list endpoints. |
| 400 | EMPTY_FLOW | PUT /flow with an empty node list. |
| 400 | INVALID_MODE | On the agent: mode was neither single_prompt nor flow. On a knowledge-base binding: mode was neither system_context nor tool. |
| 400 | INVALID_PROVIDER_TYPE | provider_type on a provider slot was not one of llm, asr, tts, analysis_llm, opener_llm. |
| 400 | INVALID_TELEPHONY | PUT /telephony with an unsupported call_direction or human_transfer_mode, or panel mode without panel_project_id. |
| 400 | INVALID_BASE_URL | A provider or credential base_url that is malformed or points at a private/internal address. |
| 400 | invalid_version | A non-integer version number in a version path. |
| 400 | NO_CASES | A test run was started for an agent with no test cases. |
| 401 | — | Missing or malformed Authorization header, or an unknown/revoked usf- key. |
| 403 | FORBIDDEN | You are not a member of the target workspace, or you linked a credential_id from another workspace. |
| 403 | READ_ONLY | Attempt to edit or delete a platform seed snippet. |
| 404 | NOT_FOUND | The id does not exist or belongs to another workspace — foreign ids are deliberately indistinguishable from missing ones. Also returned by GET /telephony before any telephony row exists, and by PUT /knowledge-bases/{kbID} when {kbID} is not one of this agent's binding ids. |
| 404 | PROVIDER_NOT_FOUND | POST /providers/user-config with a provider_id that is not in the shared provider catalog. |
| 404 | NOT_CONFIGURED | GET /providers/resolve found nothing configured for that type. |
| 422 | — | Raised by the voice pipeline, not this API: a call was attempted for an agent missing one of llm, asr or tts. Fix the provider slots and publish. |