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
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"
curl https://api.sondahub.com/2010-04-01/Accounts/AC0123456789abcdef0123456789abcdef/Messages/SMb337ff2db88ca176ca5a3e57a1e2c7c1.json \ -u AC0123456789abcdef0123456789abcdef:sandbox_auth_token -H "X-Sondahub-Session: $TOKEN"
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"
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"
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"
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
| From | Answer |
|---|---|
| +15005550001 | 21212 The 'From' number +15005550001 is not a valid phone number, shortcode, or alphanumeric sender ID. |
| +15005550007 | 21606 The 'From' phone number provided (+15005550007) is not a valid message-capable Twilio phone number for this destination/account. |
| +15005550008 | 21611 This 'From' number has exceeded the maximum number of queued messages. |
| +15005550006 | Accepted (the account owns it) |
| any number the account does not own | 21606 |
| To | Answer |
|---|---|
| +15005550001 | 21211 Invalid 'To' Phone Number: +15005550001 |
| +15005550002 | 21612 The 'To' phone number: +15005550002, is not currently reachable using the 'From' phone number via SMS. |
| +15005550003 | 21408 Permission to send an SMS has not been enabled for the region indicated by the 'To' number: +15005550003. |
| +15005550004 | 21610 Attempt to send to unsubscribed recipient |
| +15005550009 | 21614 'To' number +15005550009 is not a valid mobile number |
Calls
| From | Answer |
|---|---|
| +15005550001 | 21212 Invalid From Number (caller ID): +15005550001 |
| +15005550006 | Accepted |
| any number the account does not own | 21210 |
| To | Answer |
|---|---|
| +15005550001 | 21217 Phone number does not appear to be valid |
| +15005550002 | 21214 'To' phone number cannot be reached |
| +15005550003 | 21215 Account not authorized to call +15005550003. Perhaps you need to enable some international permissions. |
| +15005550004 | 21216 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.
| To | An SMS ends | A call ends |
|---|---|---|
| +15550100003 | undelivered, 30003 Unreachable destination handset | no-answer |
| +15550100005 | undelivered, 30005 Unknown destination handset | failed |
| +15550100006 | undelivered, 30006 Landline or unreachable carrier | busy |
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.
{"url", "from", "to", "body", "auth_token", "account_sid"} as JSON or form fields — the same from a test suite.What it answers
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.