Kinds of agent
One field — mode — decides how much of the platform a call actually
uses. It is worth setting deliberately, because the difference between the modes is not a
setting or two: it is whether a model is in the loop at all.
The four modes
| Mode | What happens on a turn | Reach for it when |
|---|---|---|
conversationthe default | Speech is transcribed, a model answers, a voice speaks it. Tools, knowledge and flows are all available. | The caller is going to say something you cannot enumerate in advance. |
keypad | A menu, and nothing else. No speech recognition, no model, no network call inside a turn — the reply is already rendered, so it lands in milliseconds. | "Press 1 to confirm." The whole job is a fixed menu, and you want it to keep working on the day a speech vendor is down. |
normal | Answers, reads the greeting out, and puts the caller through
to transfer_number. No AI anywhere in it. | An overflow line, a holiday notice, the number on the back of a card — or the thing you point a number at on the day the clever line has to be switched off. |
webrtcsuperseded | Two people, introduced. Still accepted on an agent, and still works — but there is now a better way to ask for it that involves no agent at all. See below. | Nothing new. Use
POST /v1/calls/rendezvous. |
Person to person needs no agent
Introducing two browsers to each other used to mean creating an agent in webrtc
mode, and that was a mistake this platform made rather than a requirement of the problem. An
agent is a model's configuration — a prompt, a voice, a language, a set of tools, a post-call
pass — and a conversation between two people reads none of it. What the record
actually supplied was two capture switches and a ring window.
The cost was not tidiness. An agent has exactly one mode, so an account that wanted a talking line and a person-to-person one needed two agents and a branch in its own code to pick between them — and the first time somebody edited the wrong one in the portal, a customer who had been promised a colleague reached a model instead.
So a call between two people is now its own thing, with no agent to configure and no mode to get wrong:
# Place it. No agent_id, and no field for one.
curl -X POST https://voice.example.com/v1/calls/rendezvous \
-H "Authorization: Bearer $VOICE_API_KEY" -H 'Content-Type: application/json' \
-d '{"ring_seconds":30,"record":false,"transcribe":false,
"metadata":{"ticket":"ABC-123"}}'
# Hand the person who placed it their end. The caller seat claims nothing:
# the call stays ringing, which is the point of being on that end of it.
curl -X POST https://voice.example.com/v1/calls/$CALL_ID/seat \
-H "Authorization: Bearer $VOICE_API_KEY" -H 'Content-Type: application/json' \
-d '{"role":"caller"}'
# When somebody answers. Taking the responder seat IS answering: it claims the
# call under the same lock /calls/{id}/claim uses, so two people pressing
# Answer in the same second produce one connection and one 409.
curl -X POST https://voice.example.com/v1/calls/$CALL_ID/seat \
-H "Authorization: Bearer $VOICE_API_KEY" -H 'Content-Type: application/json' \
-d '{"role":"responder","participant":"member_7"}'Each seat comes back with a url that already carries the credential and the
role — hand it to the browser and nothing else. The role is not a label: the responder makes
the WebRTC offer, so a call where both ends think they are the same seat never connects.
Everything else is an ordinary call. It rings, it is claimed, it ends, it is metered, it
appears in GET /v1/calls, and it emits the same call.* webhooks in
the same order — with no agent_id in the payload, which is the one visible
difference on the wire.
Picking one
Does a human need to hear another human?
└─ yes ─▶ Is one of them an operator on your team?
├─ yes ─▶ normal (answer, then transfer)
└─ no ─▶ no agent at all
POST /v1/calls/rendezvous
└─ no ─▶ Can every reply be enumerated in advance?
├─ yes ─▶ keypad (a menu, instant, no model)
└─ no ─▶ conversationThree shapes of conversation agent
Every conversation agent is one of these, and the difference is how much structure you have imposed on it. They are not modes — you move between them by adding fields.
| Shape | How it is configured | Good for |
|---|---|---|
| Prompted | A prompt on the first language entry, and nothing else. The model decides everything. | Support, FAQ, anything open-ended. Start here. |
| Grounded | The above, plus knowledge_base_ids so
answers come from your documents rather than the model's memory. | Anything where a confidently wrong answer costs you something. See knowledge base. |
| Flowed | A flow: named nodes, each with its own
instructions, and edges the call moves along. | Conversations with an order that must hold — verify, then collect, then confirm. See flows. |
Autonomy: who holds this end of the call
autonomy is read only by a conversation agent — every other mode
never reaches a model, so there is nothing to decide. It answers a different question from the
tool list: not "which tools does this agent have" but "is there a model on this call at
all".
| Value | Who answers |
|---|---|
agenticthe default | A model, answering
by doing. Before it speaks it searches every base in knowledge_base_ids, and it may
call anything in tools. |
simple | A person, from their browser. No recognizer, model or voice is opened for the call — the audio passes between the phone and the operator and nothing here listens to it. There is no transcript, because nothing on this side heard the call. |
custom | A model, with exactly the switches
capabilities names and nothing else. This is the only value under which that block
is read. |
Under custom, capabilities takes
search_knowledge, call_functions, transfer,
end_call, max_tool_rounds and retrieval_timeout_ms. The
last two are the ones that matter under load: a model that can call tools in a loop will, and
a retrieval allowed to take as long as it likes is a silence the caller hears.
simple agent rings rather
than answers. There is nobody on this end until a person arrives, so the call is created
ringing, an operator claims it, and a call nobody claims inside
ring_seconds is recorded as missed. An outbound simple call does not
ring — the operator who placed it is already there.Changing your mind later
mode is patchable, and the change takes effect on the next call — calls already
running finish under the mode they started in. What does not survive the change is the
configuration that only one mode reads: a keypad map means nothing to a
normal agent, and a transfer_number means nothing to a
keypad one. Nothing is deleted, so switching back restores the behaviour.
/v1/agents/{id}curl -s -X PATCH https://voice.sphoro.com/v1/agents/$AGENT_ID \
-H "Authorization: Bearer $SPHORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mode": "normal", "transfer_number": "+912261234567"}'