sondahub

sondahub / Twilio sandbox

A Twilio mock API with delivery receipts and signed callbacks

Twilio’s API, answered by sondahub: Messages, Calls, phone numbers, Verify and Lookups, with Basic auth, Twilio’s SIDs, error codes and its test credentials’ magic numbers. Messages are delivered a few seconds after they are queued, calls ring and hang up, and your status callbacks arrive signed so twilio’s own validator accepts them.

An independent imitation for testing. Not affiliated with, or endorsed by, Twilio Inc. No message is sent and no call is placed.

Connect

api.twilio.com
https://api.sondahub.com
verify.twilio.com
https://api.sondahub.com/verify
lookups.twilio.com
https://api.sondahub.com/lookups
Account SID
any AC + 32 hex — AC0123456789abcdef0123456789abcdef
Auth token
any — it signs your callbacks
Also at
https://api.sondahub.com/sandbox/twilio/…
OpenAPI 3
https://api.sondahub.com/sandbox/twilio/openapi.json

Import it. In Sonda: Import → From a URL, paste the OpenAPI address. You get a folder per resource and a request per operation, with example bodies and ids from the seed account, so each request works as it is. Then set the auth: Basic auth, user AC0123456789abcdef0123456789abcdef, any password (it signs your callbacks). Any client that imports OpenAPI 3 takes the same address.

Twilio’s SDKs know nothing about sondahub’s sessions, so give them the few lines that carry X-Sondahub-Session — otherwise each call starts from the seed account.

Python (twilio-python)

from twilio.rest import Client
from twilio.http.http_client import TwilioHttpClient

# carry the sandbox session from each answer to the next request
http = TwilioHttpClient()
def keep(r, *args, **kwargs):
    if 'X-Sondahub-Session' in r.headers:
        http.session.headers['X-Sondahub-Session'] = r.headers['X-Sondahub-Session']
http.session.hooks['response'].append(keep)

client = Client('AC0123456789abcdef0123456789abcdef', 'sandbox_auth_token', http_client=http)
client.api.base_url = 'https://api.sondahub.com'
client.verify.base_url = 'https://api.sondahub.com/verify'
client.lookups.base_url = 'https://api.sondahub.com/lookups'

msg = client.messages.create(to='+14155550100', from_='+15005550006', body='Hi', status_callback='https://your-app.example/twilio/status')

Node (twilio)

import twilio from 'twilio'

const client = twilio('AC0123456789abcdef0123456789abcdef', 'sandbox_auth_token')
client.api.baseUrl = 'https://api.sondahub.com'
client.verify.baseUrl = 'https://api.sondahub.com/verify'
client.lookups.baseUrl = 'https://api.sondahub.com/lookups'

// carry the sandbox session from each answer to the next request
let session = null
client.httpClient.axios.interceptors.request.use((config) => {
  if (session) config.headers['X-Sondahub-Session'] = session
  return config
})
client.httpClient.axios.interceptors.response.use((response) => {
  session = response.headers['x-sondahub-session'] ?? session
  return response
})

Try it here

These examples share one session: run them in order and each sees what the one before it did.
Send an SMS from Twilio’s magic number
curl -X POST https://api.sondahub.com/2010-04-01/Accounts/AC0123456789abcdef0123456789abcdef/Messages.json \
  -u AC0123456789abcdef0123456789abcdef:sandbox_auth_token \
  --data-urlencode "To=+14155550100" \
  --data-urlencode "From=+15005550006" \
  --data-urlencode "Body=Hello from the sandbox"
Fetch it again after three seconds: delivered
curl https://api.sondahub.com/2010-04-01/Accounts/AC0123456789abcdef0123456789abcdef/Messages/SMb337ff2db88ca176ca5a3e57a1e2c7c1.json \
  -u AC0123456789abcdef0123456789abcdef:sandbox_auth_token -H "X-Sondahub-Session: $TOKEN"
An invalid To number: 21211
curl -X POST https://api.sondahub.com/2010-04-01/Accounts/AC0123456789abcdef0123456789abcdef/Messages.json \
  -u AC0123456789abcdef0123456789abcdef:sandbox_auth_token \
  --data-urlencode "To=+15005550001" \
  --data-urlencode "From=+15005550006" \
  --data-urlencode "Body=Hello from the sandbox"
Start a phone verification
curl -X POST https://api.sondahub.com/verify/v2/Services/VA9e3779b1a66a4f2521a9b986b06b5d9e/Verifications \
  -u AC0123456789abcdef0123456789abcdef:sandbox_auth_token \
  --data-urlencode "To=+14155550100" \
  --data-urlencode "Channel=sms"
Check it with the code 123456: approved
curl -X POST https://api.sondahub.com/verify/v2/Services/VA9e3779b1a66a4f2521a9b986b06b5d9e/VerificationCheck \
  -u AC0123456789abcdef0123456789abcdef:sandbox_auth_token \
  --data-urlencode "To=+14155550100" \
  --data-urlencode "Code=123456"
Look a number up
curl "https://api.sondahub.com/lookups/v2/PhoneNumbers/+14155550100?Fields=line_type_intelligence" \
  -u AC0123456789abcdef0123456789abcdef:sandbox_auth_token

The seed account’s Verify service is VA9e3779b1a66a4f2521a9b986b06b5d9e; create your own with POST /verify/v2/Services. The code that approves is the first CodeLength digits of 1234567890 — 123456 by default.

Magic numbers

Twilio’s test-credential numbers, doing what Twilio documents them to do — refused when the API is called, with Twilio’s error code.

Messages

FromAnswer
+1500555000121212 The 'From' number +15005550001 is not a valid phone number, shortcode, or alphanumeric sender ID.
+1500555000721606 The 'From' phone number provided (+15005550007) is not a valid message-capable Twilio phone number for this destination/account.
+1500555000821611 This 'From' number has exceeded the maximum number of queued messages.
+15005550006Accepted (the account owns it)
any number the account does not own21606
ToAnswer
+1500555000121211 Invalid 'To' Phone Number: +15005550001
+1500555000221612 The 'To' phone number: +15005550002, is not currently reachable using the 'From' phone number via SMS.
+1500555000321408 Permission to send an SMS has not been enabled for the region indicated by the 'To' number: +15005550003.
+1500555000421610 Attempt to send to unsubscribed recipient
+1500555000921614 'To' number +15005550009 is not a valid mobile number

Calls

FromAnswer
+1500555000121212 Invalid From Number (caller ID): +15005550001
+15005550006Accepted
any number the account does not own21210
ToAnswer
+1500555000121217 Phone number does not appear to be valid
+1500555000221214 'To' phone number cannot be reached
+1500555000321215 Account not authorized to call +15005550003. Perhaps you need to enable some international permissions.
+1500555000421216 Call blocked by Twilio blocklist

After the answer: the sandbox’s own numbers

Twilio’s test credentials stop at the API call; these go further, ending the way a real message or call can end. They are fictional 555-01xx numbers.

ToAn SMS endsA call ends
+15550100003undelivered, 30003 Unreachable destination handsetno-answer
+15550100005undelivered, 30005 Unknown destination handsetfailed
+15550100006undelivered, 30006 Landline or unreachable carrierbusy

The account owns +15005550006, +14155550123, +12125550142, +13125550187, +16175550164. Buying with PhoneNumber=+15005550000 answers 21422, +15005550001 21421, AreaCode=533 21452; any other area code gets a number.

Status callbacks, signed

Give StatusCallback and the sandbox POSTs the status changes to it — for a message sent then delivered (or undelivered with ErrorCode); for a call the StatusCallbackEvents you ask for (initiated, ringing, answered) and always how it ended. A call’s Url is fetched when it is answered, as Twilio fetches your TwiML. Every request carries X-Twilio-Signature: HMAC-SHA1 of the URL and the sorted form fields, keyed with the auth token you called with.

from twilio.request_validator import RequestValidator

# Flask: the full URL Twilio called, the form fields, the header
ok = RequestValidator('sandbox_auth_token').validate(request.url, request.form, request.headers.get('X-Twilio-Signature'))

Simulate an incoming SMS

Twilio calls your app when a message arrives. This sends your webhook exactly that request — the form fields Twilio sends, signed with your auth token — and shows what your app answered, TwiML <Message> replies picked out.

The signature is made with it, as Twilio makes it with your account’s token.
POST/sandbox/twilio/simulate/sms{"url", "from", "to", "body", "auth_token", "account_sid"} as JSON or form fields — the same from a test suite.

What it answers

POST/2010-04-01/Accounts/{AccountSid}/Messages.jsonTo, From or MessagingServiceSid, Body and/or MediaUrl, StatusCallback; list (To, From, DateSent), fetch, redact (Body=), DELETE.
POST/2010-04-01/Accounts/{AccountSid}/Calls.jsonTo, From, Url or Twiml, StatusCallback and StatusCallbackEvent; list (To, From, Status), fetch, Status=completed / canceled, DELETE.
GET/2010-04-01/Accounts/{AccountSid}/IncomingPhoneNumbers.jsonThe account’s numbers; buy (PhoneNumber or AreaCode), update SmsUrl / VoiceUrl, DELETE.
GET/2010-04-01/Accounts/{AccountSid}.jsonThe account.
POST/verify/v2/ServicesAnd list, fetch, update, DELETE; /Verifications (sms, call, email, whatsapp), fetch and cancel; /VerificationCheck.
GET/lookups/v2/PhoneNumbers/{PhoneNumber}valid, country, national format; Fields=line_type_intelligence,caller_name.

Lists page with PageSize and Page and answer Twilio’s envelopes (next_page_uri, Verify’s meta); errors are {"code", "message", "more_info", "status"} with Twilio’s codes. Verify allows five sends and five checks per verification and expires it after ten minutes.

Questions

Is this Twilio?

No — an independent imitation of Twilio’s API for testing, not affiliated with or endorsed by Twilio Inc. No message is sent and no call is placed; the phone numbers are Twilio’s magic test numbers and fictional 555-01xx ones.

Which credentials do I use?

Any Account SID (AC followed by 32 hex digits — AC0123456789abcdef0123456789abcdef works) and any auth token. The token matters in one way: callbacks are signed with it, as Twilio signs them with yours, so your validator needs the same one.

Will my Twilio SDK work against it?

Yes, by changing each domain’s base URL — client.api, client.verify, client.lookups. The calls on this page were run with the official twilio-python SDK against the sandbox, every callback checked with its RequestValidator; the Node set-up uses twilio-node’s own domain base URL and its axios instance. Add the session hook, or a message you send is not found on the next fetch.

Why does a message say queued, then delivered?

Because a real one does. Status follows the clock from the moment it was sent: queued, sending, sent within about a second and a half, delivered (or undelivered) at three seconds — with status callbacks at sent and at the end. A call rings for two seconds, is answered at three and completes at thirteen.

What is not modelled?

Conversations, Studio, TaskRouter, Video, recordings and media, Messaging Services beyond taking their SID, and anything else outside the list above; those paths — sub-resources like …/Messages/{Sid}/Media included, and other products under /sandbox/twilio/ — answer a Twilio-shaped 404 saying so.