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: 1719900000

Check 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.