Flows
A flow binds an agent to actual telephony. There are two
kinds, set by kind at creation and immutable afterwards:
| Kind | What it is |
|---|---|
inbound | A phone line. Calls to phone_number are answered by this agent |
outbound | A campaign. The agent dials a contact list — see Campaigns |
Status
| Status | Meaning |
|---|---|
draft | Configured but inert. Inbound does not answer; outbound does not dial |
active | Live |
paused | Was active, stopped |
completed | A campaign that has finished its list |
These four are enforced by a database constraint; any other value is rejected.
List flows
GET /api/voice/flows?kind=outbound&status=active&include_metrics=true
| Param | Notes |
|---|---|
kind | inbound or outbound |
status | draft, active, paused |
include_metrics | Attaches a live rollup computed from the flow's calls |
{
"flows": [
{
"uuid": "f1g2h3i4-j5k6-7890-abcd-ef1234567890",
"kind": "outbound",
"name": "September winbacks",
"status": "active",
"phone_number": null,
"customer_count": 240,
"branches": ["winback", "north"],
"start_date": "2026-09-01",
"end_date": "2026-09-30",
"time_window": "{\"from\":\"09:00\",\"to\":\"18:00\"}",
"config": {
"script_text": "Hi {name}, this is Aria from Pragati AI.",
"no_answer": { "max_retries": 2, "retry_interval_minutes": 120 },
"calls_per_minute": 10
},
"widget_key": null,
"embed_enabled": false,
"voice_agent": { "uuid": "a1b2...", "name": "Aria" },
"metrics": {
"calls_today": 42, "total_calls": 240, "avg_handle_time": 97,
"missed_rate": 0.39, "connect_rate": 0.61
},
"created_at": "2026-09-01T08:00:00Z",
"updated_at": "2026-09-20T09:15:44Z"
}
]
}
Get a flow
GET /api/voice/flows/{uuid} — also accepts include_metrics.
Create a flow
POST /api/voice/flows
{
"kind": "outbound",
"name": "September winbacks",
"voice_agent_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"workflow_id": "w1x2y3z4-a5b6-7890-abcd-ef1234567890",
"branches": ["winback", "north"],
"start_date": "2026-09-01",
"end_date": "2026-09-30",
"time_window": "{\"from\":\"09:00\",\"to\":\"18:00\"}",
"config": {
"script_text": "Hi {name}, this is Aria from Pragati AI.",
"no_answer": { "max_retries": 2, "retry_interval_minutes": 120 },
"calls_per_minute": 10
}
}
| Field | Type | Required | Notes |
|---|---|---|---|
kind | string | ✓ | inbound or outbound |
name | string | ✓ | |
status | string | — | Defaults to draft |
voice_agent_id | string | — | The agent's UUID, resolved and checked against your org |
workflow_id | string | — | Workflow UUID — decides handoff behaviour |
phone_number | string | — | Inbound only: the DID this flow answers |
branches | string[] | — | Outbound only: customer tags to call. Empty means the whole org |
start_date / end_date | date | — | YYYY-MM-DD |
time_window | string | — | A JSON string, not an object — see below |
config | object | — | Per-kind settings — see below |
time_window is a JSON-encoded stringThe column is text, so send "{\"from\":\"09:00\",\"to\":\"18:00\"}" rather
than a nested object. A window that will not parse is treated as "any time"
rather than refusing to call at all.
Update a flow
PATCH /api/voice/flows/{uuid} — same fields.
Updates are applied field by field: a field you omit, or send as null, keeps
what is already stored rather than being cleared. Send a new value to change
one.
Toggle a flow
POST /api/voice/flows/{uuid}/toggle
Flips active ↔ paused. A draft flow activates. Activation is refused when
the flow could not actually work:
| Status | Cause |
|---|---|
400 | {"detail": "assign a voice agent before activating this flow"} |
400 | {"detail": "assign a phone number before activating an inbound flow"} |
Delete a flow
DELETE /api/voice/flows/{uuid} → 204
Inbound configuration
{
"kind": "inbound",
"name": "Support line",
"voice_agent_id": "a1b2...",
"phone_number": "+915550010000",
"status": "active",
"config": {
"greeting": "Thank you for calling Pragati AI, how can I help?",
"filler_phrase": "One moment…",
"call_intent": "Answer product questions and book demos.",
"cta_message": "Shall I book you a demo this week?"
}
}
| Key | Purpose |
|---|---|
greeting | Spoken verbatim on answer, before the model is involved |
filler_phrase | Played while a slow turn is being generated |
call_intent | What this line is for; goes into the system prompt |
cta_message | The close the agent works toward |
language_code | Overrides the agent's default |
enable_rag / datasource_ids | Knowledge-base grounding |
How an inbound call is matched
The dialled number is matched on its last ten digits, because providers
disagree about the country prefix — the same line arrives as +915550010000,
915550010000, or 5550010000 depending on the trunk.
With no greeting, the model produces the opening line itself — and if that
line happens to contain a closing phrase such as "thank you for calling", the
call is hung up immediately. A configured greeting is spoken verbatim and
bypasses that check entirely. See Calls.
Outbound configuration
| Key | Default | Purpose |
|---|---|---|
script_text | — | The opening line. {name} and {phone} are substituted |
no_answer.max_retries | 0 | Extra attempts for someone who did not pick up |
no_answer.retry_interval_minutes | 120 | Minimum gap before retrying |
calls_per_minute | 10 | Dialling pace |
Placeholders with no value are left as written — an agent saying Hello {name}
is a visible bug someone will fix, while "Hello ," just sounds broken and gets
blamed on the phone line.
Flow metrics
With include_metrics=true:
{
"date": "2026-09-22T00:00:00Z",
"calls_today": 42,
"calls_made": 240,
"total_calls": 240,
"avg_handle_time": 97,
"missed_rate": 0.39,
"connect_rate": 0.61,
"answer_rate": 0.61,
"ptp_rate": 0,
"rpc_rate": 0,
"peak_hour": 0,
"sla_breached": 0
}
calls_made and answer_rate mirror total_calls and connect_rate.
ptp_rate, rpc_rate, peak_hour, and sla_breached are present on the
rollup and reserved for collections reporting.
Computed live from the flow's calls rather than read from a counter, so a
corrected or backfilled call is reflected immediately. A call with duration = 0
counts as missed.