<Gather> that plays
your greeting and collects one keypress, then runs the branch for that digit. This
API is the authoring layer on top of WaveML — you describe the menu as JSON and
Wave turns it into the call flow.
This page starts with the single-level menu — one greeting, one set of keypad
options, each ending the call or handing it off. For sub-menus, “press 9 for the
main menu” and repeat-on-silence, write a multi-level menu (a graph) — see
Multi-level menus (graph) below. A visual flow
builder is a separate, later feature.
Authentication & tiers
All/v1/callflows endpoints take an API key with the callflows:read or
callflows:write scope, or a dashboard session. You can author and test
menus with a sandbox key (sk_sandbox_); serving a menu to a real inbound
call needs a production key (sk_live_), a production-tier org, and a
bound number (see Bind a number below).
The menu definition
A menu is a JSONdefinition: a greeting, the keypad branches, and an optional
fallback for when the caller presses nothing.
Prompt and actions
A prompt and every branch action is one of these. A branch action is a terminal leaf — it ends the call or hands it off.dial targets are restricted to Saudi (KSA) phone numbers — a toll-fraud
guard, since a <Dial> bridges the caller out to a new leg.Branch input (your webhook payload)
Give a branch aninput object and Wave merges it into the call.input
webhook when the caller picks that option — your
integration’s hook for what the caller chose.
- Up to 20 fields; values are string (≤512), number, or boolean.
- The keys
call_id,digits,nodeandlimit_reachedare reserved (Wave sets them) and rejected.
Create a menu
POST /v1/callflows — create a menu by name. Pass "activate": true to make it
live immediately.
201 Created returns the menu, including its version history:
name returns 409 — use PUT to add a version instead.
List and fetch menus
GET /v1/callflows lists one row per menu (its active or latest version):
GET /v1/callflows/{name} returns the full menu — the active (or latest) version’s
definition plus the complete versions history. 404 if the menu doesn’t exist.
Add ?version=N to read one version instead — for example a saved draft that is
not live, or an older version: GET /v1/callflows/auth-ivr?version=3. The
response has the same shape; is_active tells you whether that version is live.
404 if that version doesn’t exist; 400 if N is not a positive integer.
Each flow has a kind: menu (single-level) or graph (multi-level). The
definition comes back in the shape you wrote it: a menu as its prompt,
gather, branches and no_input, and a graph with its ui fields. So you can
send a definition that you read back to PUT with no change.
A flow that Wave set up for you (not through this API) can read back in a
simpler form: each option shows only its main action. If you PUT it back, the
new version stores that simpler form.
Versioning and rollback
Menus are immutable and append-only. Editing never mutates a version in place — it appends a new one.PUT /v1/callflows/{name}with a newdefinition(and optionalactivate) adds the next version.404if the menu doesn’t exist.- To avoid lost updates, also send
base_version: the version you read and edited. If someone saved a later version first, thePUTreturns409CALLFLOW_VERSION_CONFLICTand writes nothing; the message names the latest version. Read that version, apply your change again, and send it as the newbase_version. Withoutbase_version,PUTalways appends (as before). POST /v1/callflows/{name}/activatewith{ "version": N }makes versionNlive — rollback is just activating an older version.404if that version doesn’t exist.
Bind a number
POST /v1/callflows/{name}/bind-number points one of your Wave numbers at the
menu’s active version — inbound calls to that number then run the menu.
Returns
{ "bound": true }. Numbers are Wave-provided — see Virtual
Numbers.
Retire a menu
DELETE /v1/callflows/{name} soft-retires a menu: it stops serving callers and any
bound number is unlinked, but the version history is kept (you can recreate it
later). Returns { "retired": true }; 404 if the menu doesn’t exist.
The call.input webhook
When a caller presses a digit, Wave sends a call.input webhook. Its
data carries the pressed digits plus that branch’s input object — so your
backend learns exactly what the caller chose:
Multi-level menus (graph)
A graph is a menu with more than one level. You write it as a set of named nodes. Each node does one thing and points to thenext node. The call starts
at the start node.
Send a graph to the same routes as a menu: POST /v1/callflows and
PUT /v1/callflows/{name}, with "kind": "graph" in the definition. Activate,
rollback, retire and bind work the same as for a menu.
Example: two levels, “press 9 for the main menu”
Nodes
Node ids use lower-case letters and digits, 1–12 characters (main, sales2).
Every node can also have ui: { "x": number, "y": number, "label"?: string } —
layout data for a visual builder. label is the node’s display name in any
language (for example "القائمة الرئيسية"), 1–40 characters after spaces are
trimmed, with no control characters or bidi overrides. Wave trims label and
otherwise stores ui and gives it back unchanged. It does not change the
call: the node id stays the routing key, and label never goes to the caller or
to your webhook.
No input, wrong key, and the loop limit
- No key before
timeout_seconds: the call goes to the menu’sno_inputnode. - A key that is not an option: the call goes to the menu’s
invalidnode. - If you do not set
no_inputorinvalid, Wave plays the same menu again. - A call can take 20 menu turns at most. At the next menu, the call goes to
your
on_limitnode. If there is noon_limit, the call ends.
Rules that Wave checks when you save
Wave rejects a graph with a400 and names the field that is wrong (for example
definition.nodes.sales.options.0.next) when:
startoron_limitis not a node, or anextpoints to a node that does not exist.- A node cannot be reached from
start. (Nodes that onlyon_limitreaches are permitted.) - The graph has more than 50 nodes, or a menu has more than 12 options or the same digit two times.
- A loop does not go through a
menunode. (The call would never stop.) - The
on_limitpath can reach amenunode. - A
dialtarget is not a KSA number, or anaudio_ref(in aplaynode, a menu prompt ormoh) is not a file that your org uploaded.
call.input for a graph
Wave sends one call.input webhook each time the caller answers a
menu node — also for no input and for a wrong key. data has the digits, the
option’s input, and node (the id of the menu node). When the call gets to the
20-turn limit, data also has "limit_reached": true.
dial or enqueue node, the call ends. Wave does not send another
call.input for it.
Audio prompts
Aplay prompt or action plays a pre-recorded file instead of TTS — good for a
produced greeting, and it sidesteps synthesis latency. Upload the file first, then
reference the returned audio_ref.
POST /v1/callflows/audio takes a base64-encoded WAV file (8 kHz mono 16-bit
PCM recommended — telephony quality) and returns a Wave-hosted audio_ref:
audio_ref in a play:
Audio upload is rolling out. Until it’s enabled for your account, the upload
returns
503 and menus use say (TTS) prompts. An audio_ref must be a file
you uploaded — arbitrary URLs are rejected.
