Answer an inbound call with eight lines of XML
Point a phone number at your server, reply to one webhook with a verb document, and the number starts doing something useful — greet the caller, then forward to a real phone.
A phone number on its own does nothing. It becomes useful the moment something answers it.
This is the smallest complete version of that: one HTTP endpoint, one XML document, and a number that greets a caller and forwards them to a mobile. It takes about ten minutes.
How it works
There are only two moving parts.
We call you. A call arrives on your number, and we POST the call’s details to a URL you
choose.
You answer with a document. Your server replies with a Response element containing verbs.
We run them in order, top to bottom.
That is the whole model. No state to keep, no connection to hold open. Every decision is one request and one response.
Step 1 — the smallest possible handler
Your endpoint receives JSON and returns XML. Here it is in Express:
app.post("/handle-call", (req, res) => { res.type("application/xml").send(` <Response> <Say voice="woman">Karibu. Please hold while we connect you.</Say> <Dial timeout="30" callerId="+254709080010"> <Number>+254722000000</Number> </Dial> </Response> `);});Eight lines, and the number now answers, speaks, and forwards. Say reads text aloud. Dial
bridges the caller to another number, and callerId is the number the mobile will see ringing —
use one of your own.
The request that arrived looks like this:
{ "CallSid": "call_01J...", "AccountSid": "your-account-uuid", "Direction": "inbound", "From": "+254711111111", "To": "+254709080010", "CallStatus": "ringing"}From is the caller. To is your number that was dialled. CallSid is the id you use to
correlate this call with everything that happens later.
Step 2 — point the number at it
In the dashboard, set the voice_inbound webhook to your URL. There are three webhook purposes
and you can point them at three different endpoints or at one:
| Purpose | Fires when | You return |
|---|---|---|
voice_inbound |
A call arrives on your number | A verb document |
voice_outbound |
An end-user or the API places a call | A verb document |
status |
Any lifecycle event — ringing, answered, completed | 200 OK |
If you use one endpoint for all three, branch on Direction and the X-Voice-Event header.
Step 3 — verify it is really us
Every webhook is signed. Two headers arrive with the body:
X-Voice-Signature: <base64 HMAC-SHA256>X-Voice-Timestamp: 1719900000Check the signature before you act on the body. An unverified call-control endpoint is a stranger deciding who your phone number dials, at your expense.
Step 4 — make it branch
Once one document works, the rest is composition. Record the conversation by wrapping the bridge:
<Response> <RecordSession> <Say>This call is recorded for quality.</Say> <Dial timeout="30"> <Number>+254722000000</Number> </Dial> </RecordSession></Response>List several numbers inside one Dial and they ring together — first to answer wins:
<Dial timeout="25"> <Number>+254722000000</Number> <Number>+254733000000</Number></Dial>Because the document is generated by your code, “which numbers” is an ordinary database query.
Business hours, on-call rota, language by caller prefix — all of it is just an if in your
handler.
The two limits worth knowing now
A call follows at most 10 redirects, and a single Record captures at most 3600 seconds.
Both are generous, and both are much easier to design around than to discover in production.
When your server is down
You have ten seconds to reply with 200 OK and valid XML. A timeout, an error status, or a
malformed document triggers the fallback instead — so the caller hears something rather than
silence.
Set the fallback deliberately. It is the only part of this that runs on the worst day.
Next
The call-control reference lists every verb and its attributes, and the webhooks guide covers signature verification, status callbacks and the fallback policy in full.