Part 6 — Building the screens

← Contents · Previous: Part 5 — Call flow reference · Next: Appendix

How a client app turns the engine's API into screens for the businesses it serves: the flow editor, the calls page and call detail, the reports, the live screens, and the screens around Agent Phone (§6.7). Each section names the endpoints to use, what to do, and what not to do. The demo console (console/) is a working example of every screen here.


6.0 The shape of a client app

 browser (business staff)  ──►  client app backend  ──►  Ringmaster engine (/v1, API key)
                                       ▲
                                       └──  webhooks (signed) from the engine

Do

Don't


6.1 The flow editor

Load

What Endpoint Cache
Block library (types, groups, exits, approval) GET /v1/scripts/schema Per schemaVersion; reload when it changes
The account's flows and their versions GET /v1/accounts/{id}/scripts (D1) —
A version's blocks GET …/scripts/{ref}/versions/{v} (D6) Versions never change: cache forever
Pickers: departments, audio, knowledge docs, app actions I2, E2, F2, GET /v1/actions Short
Other flows for Schedule call (OUTGOING) and Transfer (SHARED) D1, then D6 of the chosen version —

Draw the blocks

Build the library from the schema's blocks, grouped by group, with ⚠ on needsApproval. Each block's ports (its exits) come from its settings:

Block Ports
menu each options[].id (default: the key) · noInput · notUnderstood (only when speech is on) · later
collect ok · failed · later
poll next · noInput · later
pick chosen · empty · noInput · later
ask answered · silence
connect noAnswer · each wr.options[].exit
schedule scheduled · notUnderstood (only with ask) · declined (only with confirm) · refused
request mode: continue → next; else success · failure · timeout · each wr.options[].exit
app_action from the registered action (GET /v1/actions): no wait → next; wait → each outcomes[] · failed · timeout · each wr.options[].exit
condition test.kind: hours → open · closed; others → yes · no
goto tooMany (when maxLoops > 0); draw target as a dashed line, not a port
transfer the exit of each Return block in the chosen SHARED version
ai_agent goal · human · vm · cb · nohelp · time · err
say · optout next
voicemail · goodbye · return none: an end block

Show the later port as optional ("if the caller says later"). Show ports with nothing connected in a different colour: the call ends there.

Keep the layout yourself

The engine stores the flow, not the drawing. A top-level layout key is dropped on push. Store block positions in the client app, by account, flow ref and block id. Since ids stay the same across versions, the drawing carries over.

Edit

Check, save, publish

 every change (debounced ~1 s)  →  POST /v1/scripts/validate {definition}   → show issues on the blocks
 Save                           →  POST …/scripts/{ref}/versions {version: latest+1, runnable, definition}
 Approve (risky versions)       →  POST …/versions/{v}/runnable
 Go live                        →  PUT /v1/numbers/{numberId}/script {scriptVersionId}   (incoming)
                                    or pick the version in a call-out                    (outgoing)

Delete

DELETE …/scripts/{ref} (D4). A 409 SCRIPT_IN_USE lists what uses the flow (numbers, callouts, scripts): show the list with links. A flow that has run calls is archived, not erased: its call history stays readable.

Per-block stats on the canvas

GET …/versions/{v}/stats (D9), for the version shown. Draw on each block:

Stats appear a few minutes after calls end, and only for that version: compare versions side by side, not mixed.

Try it

Push the version and put it on a test number, or call it with a quick call to your own phone (POST /v1/calls, needs CALLOUTS). Then open the call's timeline (§6.2): it shows every block, sentence, key and match.

Don't


6.2 Calls page and call detail

The calls list

GET /v1/calls?accountId=…&page=…&size=50 (G1), newest first. Filters: from / to, type, result, status, number, calloutId.

Column From Show as
When startedAt Local time of the business; "Today 14:05"
Who from (incoming) / to (outgoing) Your contact's name if you have one, else the number
Line to (incoming) / from (outgoing) The number's name in your app
Type type Incoming · Call-out · OTP · Agent call
Result result A coloured label (table below)
Handled by handledBy (+ agent from the timeline) Flow · Agent Asha · AI · Voicemail
Length durations.totalSeconds 2:31
🔊 · 📝 recorded · transcript Icons; play inline

Result labels

result Label Colour
HANDLED_BY_SCRIPT Served by the flow green
HANDLED_BY_AGENT Talked to an agent green
HANDLED_BY_AI Served by AI green
VOICEMAIL Left a message blue
MISSED Nobody answered red
ABANDONED Hung up while waiting red
HUNG_UP_EARLY Hung up at once grey
REJECTED Refused (limits, suspended) grey
ANSWERED (outgoing) Answered green
NO_ANSWER · BUSY · DECLINED Not answered · Busy · Declined amber
UNREACHABLE · INVALID_NUMBER · FAILED · NOT_DIALED Couldn't reach · Wrong number · Failed · Not called grey

Call detail

One page per call, from four calls, loaded together:

Part Endpoint Notes
Header: who, when, result, lengths, flow version, outcome tags GET /v1/calls/{id} (G2) durations as a bar: waiting · talking · on hold · AI
Call log GET /v1/calls/{id}/timeline (G3) Below
Recording GET /v1/calls/{id}/recording (G4) Proxy it through your backend; 404 = none or not ready
Transcript and summary GET /v1/calls/{id}/transcript (G5) 404 until summary.ready; show "Being transcribed…" if recorded

providerCost is what the call cost to run (carrier, AI). Show it to the client app's own staff, not to the business, unless that is how you bill.

The call log (timeline → readable lines)

The timeline is the call's log: every step with its time. Show it as lines, offsetMs as 0:12. Group lines under the block they happened in (block). Show the readable ones by default; put the rest behind "Show technical details".

Event Line Default
CALL_RECEIVED / DIALING / ANSWERED "Call from +255745… to Mapokezi line" / "Calling…" / "Answered" shown
BLOCK_ENTER Block heading: the block's name and type as heading
SAY 🔈 "Karibu PMK Chemicals." (data.text) shown
PLAY 🔈 the audio's name shown
KEY ⌨ "Pressed 2" (red if valid: false) shown
SPEECH / AI_HEARD 🗣 "nataka kununua" shown
MATCH → "Chose Mauzo (by voice, 92%)" shown
INPUT ⌨ "Entered 1234 (valid)" or •••• shown
NO_INPUT / NOT_UNDERSTOOD / RETRY "No answer — asked again (2)" shown
POLL / PICK "Answered 4 (Nzuri)" / "Chose Arusha" shown
CONDITION "Opening hours: open" shown
QUEUED / RING / ACCEPTED / RING_TIMEOUT "Waiting for Mapokezi" / "Ringing Asha" / "Asha answered" / "Asha didn't answer" shown
WAIT_OPTION "Pressed 9 while waiting → Voicemail" shown
VOICEMAIL "Left a message" with the player shown
CALL_SCHEDULED "Callback booked for tomorrow 09:00" shown
ACTION_REQUESTED / ACTION_RESULT "Asked the app: pay" / "pay → paid" shown
REQUEST "GET shop.example → 200 in 340 ms" shown
AI_SAID / AI_STAGE / AI_TOOL 🤖 the sentence / stage / tool shown
EXIT_UNCONNECTED ⚠ "The flow ends here (exit noInput not connected)" shown: it's a flow bug the business can fix
ERROR ⚠ data.code explained shown
ENDED "Ended: caller hung up" (endReason, endedBy) shown
WORKER_* · AI_USAGE · WAIT_UPDATE · TRANSFER_IN/OUT · VARIABLE_MISSING · others as they are hidden

Don't


6.3 Reports

Account dashboard

GET /v1/accounts/{id}/stats?from=…&to=…&groupBy=day (N1).

Tile / chart Formula
Calls incoming + outgoing
Didn't get a person missed + abandoned: callers who waited for an agent and gave up or weren't answered; as a share of incoming
Left a message voicemail
Served without a person (byScript + byAi) / (byScript + byAgent + byAi + voicemail)
Hung up at once hungUpEarly / incoming (a long or unclear greeting)
Average call length seconds / answered (answered counts incoming calls and answered outgoing ones)
Busiest hours groupBy=hour: a heat map by hour
Per line groupBy=number
By type groupBy=type

Range: at most 400 days. Default view: the last 7 days.

Flow performance

Call-outs

Screen part Endpoint
List and status GET /v1/callouts?accountId=… (H5)
Header: status, counts, pause reason GET /v1/callouts/{id} (H7)
Progress bar, by result, by try, by hour GET /v1/callouts/{id}/stats (H8)
People and their result GET /v1/callouts/{id}/recipients?status=… (H6)
Download GET /v1/callouts/{id}/export.csv (H9)

While it runs, update the page from callout.progress (every 30 s) instead of polling. Show callout.paused reasons as actions: VERSION_REVOKED (choose another version), FEATURE_OFF (ask the owner), ACCOUNT_SUSPENDED / CLIENT_SUSPENDED (resumes when lifted), CAN_CALL:<reason> (your own can-call check said no). Quiet hours don't pause a call-out: it waits and goes on by itself.

Agents

GET /v1/accounts/{id}/agents/stats (I13): per agent offered, answered, missed, declined, talkSeconds, and the declines by reason (declinedWithCustomer, declinedBreak, declinedWrongDepartment, declinedOther). Answer rate = answered / offered. Show the reasons next to declined, so a manager sees why (four "with a customer" in a busy shop is not the same as four "other"). Live status comes from agent.status; live declines from call.ring_declined.

Exports

GET /v1/calls/export.csv?accountId=…&from=…&to=… (G6) and H9. Stream them through your backend as downloads; up to 100,000 rows, oldest first.

Don't


6.4 Live screens

The agent desk

Everything is in Part 2D and the desk signals in Part 3. In short:

  1. Agent signs in to the client app → backend PUT …/agents/{ref} (once) and POST …/agents/{ref}/lobby-token.
  2. The page joins the LiveKit lobby room with that token and listens for ring, ring_cancel, connecting.
  3. Start shift button: unlocks the browser's sound (a ring needs a user gesture) and sets status AVAILABLE.
  4. On ring: show the caller and the flow's notes, ring, flash the title. Accept → backend POST /v1/calls/{id}/accept → join the call room with the returned token. Decline → POST …/decline.
  5. In the call: Hold, Transfer (to a department), Hang up, each through the backend.
  6. Renew the lobby token before it expires (600 s).
  7. Next to Agent Phone: stay quiet when a ring says "audible": "phone", and offer Take on waiting callers (§6.7.11).

Queue wall

GET …/departments/{id}/queue (I6) for the waiting callers. Refresh it when agent.status, call.started or call.ended arrive for that account, with a slow timer as backup (≥ 10 s).

Don't


6.5 Which webhook updates which screen

Webhook Update
call.started / call.answered Live calls list; queue wall
call.block The caller's answers as they happen (a menu choice, a number, a poll answer); act on them in your app (open a ticket)
call.ended Calls list, call detail, your own call store (reports, search)
call.missed "Missed calls" inbox; notify the business
call.scheduled Scheduled callbacks list
voicemail.received · recording.ready Voicemail inbox; the call's player
summary.ready Transcript and summary on the call; voicemail preview
ai.unanswered "Questions the AI couldn't answer": suggestions for the knowledge base
action.requested Not a screen: do the action and answer (2E)
request.failed An alert for the business's integration
callout.progress · callout.paused · callout.completed · callout.recipient_results Call-out page; your own record of who was reached
agent.status Agent list, desk, queue wall, the agent's status switch
call.ring_declined Manager's live board (declines with reasons)
agent.device_paired · agent.device_revoked Connect dialog, device lists, security notices (§6.7)
agent.shift_started · agent.shift_ended · station.paired · station.removed Shared computers page; "Start my shift" on the agent's home
media.ready · media.failed Audio library (processing → ready)

Webhooks can arrive twice or out of order: store by id, and compare createdAt before overwriting newer data.


6.6 Don't, in one place

  1. Never put the API key or webhook secret in a browser, app or log.
  2. Never edit a stored version: push a new one.
  3. Never change an existing block's id: stats and links are by id.
  4. Never let users type ids (departments, audio, flows, actions): use pickers.
  5. Don't poll for what a webhook tells you (calls, recordings, summaries, call-out progress, agent status).
  6. Don't fail on unknown fields, values, events or webhook types.
  7. Don't offer what the client app's features don't allow (GET /v1/ping → features).
  8. Don't put a version live that is waiting for approval; don't approve in the same click as saving a risky version.
  9. Don't show •••• values unmasked, or engine costs to businesses unless you bill by them.
  10. Don't build reports by paging through GET /v1/calls.
  11. Don't send or store Agent Phone pairing codes, and don't ignore a TOKEN_REUSED revoke (§6.7.12).

6.7 Screens for Agent Phone

Agent Phone is the desktop app agents keep running to pick up calls (Part 2F, AGENT-PHONE.md). You don't build the app. You build the screens around it on your dashboard: signing an agent's computer in, the business's shared computers, shifts, the business's switches, and the notices. Everything here goes through your backend with your API key, like every other screen in this part.

 your dashboard (browser) ──► your backend ──► engine /v1/accounts/…   (API key: codes, devices, stations, shifts, settings)
                                    ▲
                                    └── webhooks: agent.device_*, agent.shift_*, station.*, agent.status, call.ring_declined

 Agent Phone (desktop) ───────────────────────► engine /v1/agent/…     (its own device token; you never see it)

The dashboard and the app never talk to each other. They meet in the engine: you start something (a pairing, a shift, a setting), the engine tells the app within a second, and the engine tells you (webhook) what the app did.

6.7.1 Who sees which screen

Screen Agent (signed in) Manager / owner Section
Download Agent Phone ✓ ✓ 6.7.2
Connect Agent Phone (my computer) ✓ for themselves ✓ for any agent 6.7.3
My devices / an agent's devices ✓ their own ✓ all 6.7.4
Shared computers (add, settings, remove) – ✓ 6.7.5
Start / end my shift on a shared computer ✓ for themselves ✓ for any agent 6.7.6
Agent Phone settings (the business's switches) – ✓ owner 6.7.7
My status (Available / Away) ✓ ✓ for any agent 6.7.8

Your own login and roles decide this; the engine only knows your API key. Always check on your backend that the signed-in person may act for the agent ref they send.

6.7.2 Download Agent Phone

A page or a button on the agent's profile: "Download Agent Phone for Windows / macOS / Linux", with the short version of what it does ("rings on your computer even when this page is closed").

6.7.3 Connect Agent Phone

Where: the agent's profile ("My calls" / "My computer"), and on a manager's agent page.

Flow

[Connect Agent Phone]  → your backend: POST /v1/accounts/{accountId}/agents/{ref}/devices/pairings   (2F K1)
                       ← { code: "482913", link: "ringmaster-agent://pair?code=482913&host=…", expiresAt }

┌──────────────────────────────────────────────┐
│ Connect Agent Phone                          │
│                                              │
│   [ Open Agent Phone ]   ← the link          │
│                                              │
│   or type this code in Agent Phone:          │
│          4 8 2   9 1 3                       │
│   Expires in 4:52 · [New code]               │
│                                              │
│ Don't have it yet? Download Agent Phone      │
└──────────────────────────────────────────────┘
  1. The Open Agent Phone button is the link. On a computer with Agent Phone installed it opens the app and pairs it at once; elsewhere nothing happens, which is why the code is shown too.
  2. Show the code large, in two groups of three, with a countdown to expiresAt (5 minutes). When it runs out, replace the code with "This code expired" and a New code button (a new POST).
  3. Wait for agent.device_paired for that agent. Then close the dialog with "Connected: Agent Phone on reception-pc" and refresh the device list (6.7.4). Push it to the open page over your own socket; don't poll the engine.

Do

Don't

Errors: 404 agent not found. When pairing is refused at the app (shown in the app, not to you): TOO_MANY_DEVICES (the agent already has 5: remove one in 6.7.4), expired or used code.

6.7.4 Devices

Where: under Connect Agent Phone on the agent's profile, and on the manager's agent page.

GET /v1/accounts/{accountId}/agents/{ref}/devices (2F K2):

Column From Show as
Name name "Agent Phone on reception-pc"
System platform + appVersion "Windows · 0.2.0"
Last seen lastSeenAt "2 minutes ago" (relative, the viewer's time zone)
Where lastIp for managers only
Remove

Remove → confirm ("Asha's laptop will be signed out at once and stop ringing") → DELETE …/devices/{deviceId} (2F K3). The app goes back to its pairing screen within a second. Use it for a lost laptop, a person who left, or a device the agent doesn't recognise.

Keep the list current from agent.device_paired / agent.device_revoked. Show revoked reasons in words:

reason Say
REMOVED "Removed by Juma" (who clicked: your own audit)
SIGNED_OUT "Signed out on the computer"
UNUSED "Signed out: not used for 30 days"
AGENT_REMOVED "Signed out: the agent was removed"
TOKEN_REUSED "Signed out for safety: this computer's sign-in was copied." Alert the owner/manager; ask the agent to check the computer before connecting again

6.7.5 Shared computers

For a computer several agents use in turns (the reception PC). Where: a manager's "Shared computers" page.

Action Endpoint (2F K4)
List, with who is on shift GET /v1/accounts/{accountId}/stations
Add shared computer POST …/stations/pairings → show the code and link exactly as in 6.7.3, with the note "Open Agent Phone on the shared computer and type this code"
Shift ends after N minutes without calls PATCH …/stations/{stationId} {"idleEndMin": 120} (0 = never)
Shift ends at a time of day PATCH …/stations/{stationId} {"endAtTime": "17:30"} ("" = never)
End the current shift DELETE …/stations/{stationId}/shift
Remove the computer DELETE …/stations/{stationId} (signed out at once)
Shared computers                                        [Add shared computer]
┌──────────────┬──────────────────────────┬───────────────┬──────────┬────────────┐
│ Computer     │ On shift                 │ Ends if idle  │ Ends at  │            │
├──────────────┼──────────────────────────┼───────────────┼──────────┼────────────┤
│ Reception PC │ Asha · since 08:02       │ 120 min       │ 17:30    │ End · ⋯    │
│ Store PC     │ nobody                   │ 60 min        │ –        │ ⋯          │
└──────────────┴──────────────────────────┴───────────────┴──────────┴────────────┘

Keep it live from station.paired, station.removed, agent.shift_started, agent.shift_ended (Part 3).

6.7.6 Start my shift

A shared computer rings for nobody until an agent starts a shift from your dashboard. Your login is the proof of who they are: there is no PIN in Agent Phone.

Where: the agent's dashboard home, near the top, while they are not on shift anywhere:

Start my shift on:  [ Reception PC (nobody) ▾ ]  [Start]

Don't let one agent start a shift for another unless the signed-in person is a manager.

6.7.7 Agent Phone settings

Where: the business's settings, a section "Agent Phone" (owner or manager). One PATCH /v1/accounts/{accountId} with all five switches together (they are replaced as a whole) and, if you show it, the recording notice (Part 2A):

{ "agentPhone": { "showTeam": true, "showCallerNumbers": true, "allowDecline": true, "allowCallBack": true, "allowDialPad": true },
  "recording": { "mode": "LIVE_PARTS", "noticeText": "Mazungumzo haya yanarekodiwa kwa ajili ya ubora wa huduma.",
                 "noticeMediaId": null, "transcribe": true, "agentCallNotice": false } }
Switch Label to show What it means for agents
showTeam Show the team Agents see who else in their departments is available, on a call or away
showCallerNumbers Show caller numbers Off: numbers appear as +2557•••••250 everywhere in the app; names from your contacts still show
allowDecline Agents may decline calls Off: no Decline button; calls are assigned (big call centres)
allowCallBack Agents may call people back Call-back buttons on recent calls
allowDialPad Agents may dial any number The dial pad and "New call" in the app
recording.agentCallNotice Say the recording notice on calls agents make Off by default; turn on where the law asks people to be told

6.7.8 Status

Agents go Away and Available on your dashboard (PUT …/agents/{ref}/status, 2D I10). Agent Phone shows the status and only offers I'm back when the agent is away; it never sets Away.

6.7.9 Caller names (your contacts URL)

If your app knows its customers, answer the engine's contacts URL (Part 3 §3.4) and agents see "John Mushi · VIP customer" instead of a number, on the ring, the call, the queue and in search; flows can greet callers by name. The Ringmaster owner sets the URL once.

Do

Don't put things in note the agent shouldn't see (balances, medical details) unless they need them on the call.

6.7.10 Notices from the engine

Webhook Tell How
agent.device_paired The agent (and their manager): "New computer connected: Agent Phone on reception-pc. Not you? Remove it." In-app notice; e-mail if you e-mail security notices
agent.device_revoked with TOKEN_REUSED Owner/manager: security alert (6.7.4) Prominent, until read
agent.device_revoked (other reasons) Update the device list Quiet
agent.shift_started / agent.shift_ended Shared computers page; the agent's home Quiet
station.paired / station.removed Shared computers page Quiet
call.ring_declined Manager's live board: "Asha declined (with a customer)" Live counter; the reasons add up in agent stats

6.7.11 A browser desk next to Agent Phone

If you also keep the browser agent desk (6.4), it must behave when the agent has Agent Phone open:

6.7.12 Don't (Agent Phone)

  1. Don't send pairing codes by message or keep them; show them once, on screen, to the right person.
  2. Don't let a signed-in person pair, remove devices, start shifts or set switches for others unless your roles allow it.
  3. Don't ignore TOKEN_REUSED: someone copied a sign-in.
  4. Don't ring in the browser when the ring says audible: "phone".
  5. Don't keep a device token or a station token anywhere: your app never needs one.
  6. Don't block a call on your contacts URL: answer fast or not at all.

Revision #2
Created 13 November 2026 18:54:28 by Admin Qbit
Updated 14 November 2026 12:25:04 by Admin Qbit