More sandboxes

On this page

Voice & OTP calls

Test phone-call flows (voice one-time codes, IVR menus, call notifications) without ringing a phone. The voice sandbox speaks the Twilio Voice API: your app places calls with the official Twilio SDK pointed at Testlie, Testlie plays the person who picks up, runs your TwiML and records everything that would have been said. Tests then wait for the call and read the spoken code.

Calls use the same fake Twilio account as the SMS sandbox: the SMS inbox's Account SID and auth token, and the same base URL. Open Voice calls in the workspace to see calls, their transcript and a timeline of every TwiML request, Gather input and status callback.

Place a call

Replace https://api.twilio.com with https://testlie.com/sms and authenticate with HTTP Basic AccountSid:AuthToken. Parameters are form-encoded like Twilio's:

bash
curl -u "$ACCOUNT_SID:$AUTH_TOKEN" \
  -X POST "https://testlie.com/sms/2010-04-01/Accounts/$ACCOUNT_SID/Calls.json" \
  --data-urlencode 'To=+15551234567' \
  --data-urlencode 'From=+15005550006' \
  --data-urlencode 'Url=https://your-app.example/twiml/otp' \
  --data-urlencode 'StatusCallback=https://your-app.example/twilio/status' \
  --data-urlencode 'StatusCallbackEvent=initiated ringing answered completed'

With the Node SDK:

js
const client = require("twilio")(ACCOUNT_SID, AUTH_TOKEN);
client.api.baseUrl = "https://testlie.com/sms";
await client.calls.create({ to: "+15551234567", from: "+15005550006", url: "https://your-app.example/twiml/otp" });

The other SDKs work the same way as for SMS: see the snippets on the SMS page and use calls.create instead of messages.create.

Parameter Notes
To, From E.164 numbers (To may also be client: or sip:)
Url, Method Where to fetch TwiML when the call is answered; POST (default) or GET
Twiml Inline TwiML instead of Url, up to 4,000 characters
FallbackUrl, FallbackMethod Fetched with ErrorCode and ErrorUrl when Url fails
StatusCallback, StatusCallbackMethod, StatusCallbackEvent Events initiated, ringing, answered, completed (default: completed only)
MachineDetection Enable or DetectMessageEnd: adds AnsweredBy
Timeout, TimeLimit Ring timeout and maximum call length in seconds

ApplicationSid, SendDigits, Record on the call and other parameters are not supported or ignored.

Method and path
POST /sms/2010-04-01/Accounts/{AccountSid}/Calls.json Place a call. 201 with Twilio's Call resource (status: "queued")
GET /sms/2010-04-01/Accounts/{AccountSid}/Calls.json List, newest first: To, From, Status, StartTime (<, >), PageSize, Page
GET /sms/2010-04-01/Accounts/{AccountSid}/Calls/{Sid}.json Fetch one call
POST /sms/2010-04-01/Accounts/{AccountSid}/Calls/{Sid}.json Modify a live call: Status=canceled (queued or ringing), Status=completed (any live state), or Url/Method or Twiml to redirect it
DELETE /sms/2010-04-01/Accounts/{AccountSid}/Calls/{Sid}.json Delete a finished call

Errors use Twilio's JSON (code, message, more_info, status):

Code HTTP When
20003 401 Missing or wrong credentials
20404 404 Unknown call SID or path
20429 429 Over the monthly quota, or more than 50 requests per second
21201 / 21211 400 Missing / invalid To
21213 / 21212 400 Missing / invalid From
21205 400 No Url or Twiml, or an invalid URL
12100 400 Inline Twiml is not valid TwiML
21609 400 Invalid StatusCallback URL
21220 400 Modifying (or deleting) a call in the wrong state
20001 400 Another invalid parameter (method, event name, machine detection)

What happens during a call

  1. The call is queued, then ringing (about a second). Status callbacks for initiated and ringing go out if you asked for them.
  2. The simulated callee reacts (see below). A busy, unanswered or failed line ends the call with that status; no TwiML is fetched.
  3. When answered, the call is in-progress, the answered callback goes out, and Testlie requests your TwiML with Twilio's parameters (CallSid, AccountSid, From, To, CallStatus, Direction, ApiVersion, Called, Caller, and AnsweredBy with machine detection), signed with X-Twilio-Signature, so twilio.validateRequest and Twilio's webhook middleware accept it.
  4. The verbs run in order. When the document ends, or on <Hangup>, the call is completed, the completed callback reports CallDuration, and team webhooks get call.completed.

Nothing waits for real audio: durations are simulated (speech at about 150 words a minute, <Pause> lengths, Gather timeouts). A <Pause> does wait a tenth of its length in real time, so a test can modify the live call.

TwiML

Verb Sandbox behaviour
<Say> Text (with SSML such as <say-as>, <sub alias>, <break>) goes into the transcript with voice, language and loop (loop="0" counts as 10)
<Play> The URL, or digits, goes into the transcript; loop honoured
<Pause> Adds length seconds
<Gather> Plays nested <Say>, <Play>, <Pause>, then takes the callee's next scripted input. Honours input (dtmf, speech), numDigits, finishOnKey, timeout, action, method and actionOnEmptyResult. Input is posted as Digits (and FinishedOnKey) or SpeechResult and Confidence, and the call continues with the TwiML you return. Without input it falls through to the next verb. Without action it posts to the current document's URL, like Twilio
<Redirect> Fetches the URL (relative URLs resolve against the current document)
<Hangup> Ends the call
<Reject> Ends the call; as the first verb the call is busy
<Dial> Captured, never connected. Each <Number> (or text, <Client>, <Sip>) gets its outcome from the simulated callee rules; action receives DialCallStatus, DialCallSid and DialCallDuration
<Record> Simulated: a 5-second recording (or maxLength), posted to action as RecordingUrl, RecordingDuration, RecordingSid and Digits. The recording URL is a placeholder; there is no audio
<Sms> Captured in the account's SMS inbox like a Messages API call (to defaults to the called number)

Other Twilio verbs (<Connect>, <Enqueue>, <Pay>, ...) are recorded as skipped. TwiML can also be text/plain (said) or an audio/* URL (played).

Like Twilio, invalid TwiML ends the call with "We are sorry, an application error has occurred": a parse failure is error 12100, an unknown verb or a verb nested where it is not allowed (only <Say>, <Play> and <Pause> inside <Gather>; only nouns inside <Dial>) is 12200, an HTTP error 11200, an unreachable app 11205, a document over 64 KB 11750, and an unexpected content type 12300. The error shows on the timeline and in the call's errorCode.

Loop guards. One call makes at most 25 TwiML requests (redirects, actions) and runs at most 250 verbs; past that it ends with the application error and a "loop guard" message. TimeLimit caps the simulated length.

Simulated callees

Per account, rules decide how the number you call behaves. Open Voice calls → Simulated callees, or PUT /api/sms-inboxes/<id>/voice:

json
{ "rules": [
  { "pattern": "+1555000*", "behavior": "busy" },
  { "pattern": "+1555001*", "behavior": "no-answer" },
  { "pattern": "+1555002*", "behavior": "machine" },
  { "pattern": "+15551234567", "behavior": "answer", "inputs": ["1", "4719#"] }
] }
Behaviour Result
answer A person answers (AnsweredBy=human with machine detection) and gives inputs to successive <Gather>s
machine An answering machine: AnsweredBy=machine_start (machine_end_beep with DetectMessageEnd); it never presses keys
busy, no-answer, failed The call ends with that status

Inputs are DTMF digits (0-9, *, #, and w for a pause; "1234#" types 1234 then pound) or speech:<words>. "1" then "4719#" means: press 1 at the menu, then enter 4719 and pound. A numDigits Gather takes only that many digits; a speech-only Gather ignores digits and vice versa. Unmatched numbers answer and press nothing. Patterns use * and ? like SMS failure rules; the first match wins.

Read the code in a test

Wait for the finished call with a team API key. By default only finished calls match, so the transcript is complete:

bash
curl -f -H "Authorization: Bearer $TESTLIE_API_KEY" \
  "https://testlie.com/api/sms-inboxes/$SMS_INBOX_ID/calls/wait?to=%2B15551234567&since=$SINCE&timeout=30"
js
const since = new Date().toISOString();
await app.requestVoiceCode("+15551234567");
const res = await fetch(`${TESTLIE}/api/sms-inboxes/${id}/calls/wait?to=%2B15551234567&since=${since}`,
  { headers: { Authorization: `Bearer ${TESTLIE_KEY}` } });
const { call } = await res.json();
expect(call.status).toBe("completed");
const code = call.extracted.codes[0]; // "4719" from "Your code is 4. 7. 1. 9."

The response has call.spokenText, the ordered transcript, the steps timeline and extracted.codes. Codes are found whether they are spoken digit by digit ("4. 7. 1. 9.", "4, 7, 1, 9", "four seven one nine") or grouped ("471 925"), with codes next to words like "code" ranked first.

Method and path
GET /api/sms-inboxes/:id/calls List calls: to, status, q (spoken text contains), page, limit
DELETE /api/sms-inboxes/:id/calls Delete every finished call
GET /api/sms-inboxes/:id/calls/wait Long-poll: to, status (final by default, a status, or any), contains, since, timeout (1-120 s). 200 with { call } or 408
GET, PUT /api/sms-inboxes/:id/voice The Calls URL and the simulated callee rules (PUT for admins)
GET, PATCH, DELETE /api/calls/:sid One call with timeline and transcript; mark read; delete
GET /api/calls/:sid/extract { sid, status, spokenText, codes, links }

Webhooks

Team-wide webhooks subscribed to call.completed get a signed JSON POST when a call ends (any final status):

json
{ "event": "call.completed",
  "smsInbox": { "id": "…", "name": "App SMS" },
  "call": { "sid": "CA…", "to": "+15551234567", "from": "+15005550006", "status": "completed", "answeredBy": null,
            "duration": 14, "spokenText": "…", "codes": ["471925"], "errorCode": null, "createdAt": "…", "endTime": "…", "url": "…" } }

Limits and retention

Calls count against the plan's SMS per month limit (metered separately from texts), each account keeps its newest calls up to the plan's per-inbox limit, and calls are deleted with the team's retention like captured SMS. See Plans & limits. Outgoing requests to your app are SSRF-guarded like webhooks: private addresses are refused.

Calls ring for about a second, and <Pause> is shortened (a tenth of a second per second, at most 30 seconds of real waiting per call) so tests stay fast.