More sandboxes
On this page
Inbound SMS & email to your app
Capturing what your app sends is half of a conversation. Testlie can also play the other side: a phone texting your app's number, or a customer emailing your app's inbound address. The message is delivered to your app the way the real provider would deliver it, so the code path you test is the one that runs in production.
- Inbound SMS (mobile-originated): a Twilio "A message comes in" webhook, signed with
X-Twilio-Signature, and adeliver_smto your SMPP client. TwiML replies are captured as outgoing texts in the same conversation. - Inbound email: a POST in the format of SendGrid Inbound Parse, Mailgun Routes, Postmark inbound webhooks, or the raw MIME message.
Every delivery goes through a queue: it is retried on network errors, timeouts, 5xx, 408 and 429, and each attempt is logged with the status code and response body your app returned.
Texts from a phone
Open SMS, pick the inbox and go to Conversations.
- Under Inbound texts to your app, set the inbox number (the
Toof incoming texts; default+15005550006) and your app's webhook URL, the one you would configure as "A message comes in" on a Twilio number. Leave SMPP deliver_sm on to also deliver to bound SMPP clients. - Type the simulated phone's number, open the conversation and send a text, or use the STOP, START, HELP and YES buttons.
The thread shows texts from the phone on the right and what your app sent to that number on the left, including replies captured from TwiML and messages your app sends later through the Twilio-compatible API or SMPP.
What your webhook receives
A POST with Content-Type: application/x-www-form-urlencoded, Twilio's parameters and Twilio's signature:
| Parameter | Value |
|---|---|
MessageSid, SmsSid, SmsMessageSid |
SM + 32 hex characters |
AccountSid |
The SMS inbox's account SID |
From, To |
The phone, the inbox number |
Body |
The text |
NumMedia, MediaUrl0…, MediaContentType0… |
Media URLs, when sent with mediaUrls |
NumSegments |
Segments as a carrier counts them (GSM-7 160/153, UCS-2 70/67) |
FromCountry, ToCountry |
ISO country from the number; FromCity, FromState, FromZip, To… are empty |
SmsStatus, ApiVersion |
received, 2010-04-01 |
X-Twilio-Signature is computed exactly like Twilio: base64 HMAC-SHA1, keyed with the inbox's auth token, of the full webhook URL (query string included) followed by every parameter name and value sorted by name. The official helpers accept it:
const twilio = require("twilio");
app.post("/twilio/sms", express.urlencoded({ extended: false }), (req, res) => {
const ok = twilio.validateRequest(AUTH_TOKEN, req.header("X-Twilio-Signature"), "https://your-app.example.com/twilio/sms", req.body);
if (!ok) return res.status(403).end();
res.type("text/xml").send("<Response><Message>Thanks, you are unsubscribed.</Message></Response>");
});Use the SMS inbox's auth token in place of your Twilio auth token in the test environment.
Replies
Answer with TwiML (text/xml): every <Message> in <Response> becomes a captured outgoing SMS from the inbox number to the phone, with <Body>, <Media> and the to, from and statusCallback/action attributes honoured. The legacy <Sms> verb works too. A text/plain answer is sent back as one reply; an empty answer or <Response/> sends nothing. Replies go through the same failure rules, quota and status progression as messages sent through the API.
SMPP clients
When your application is bound as a receiver or transceiver (see SMS over SMPP), each text arrives as a deliver_sm with esm_class 0, the phone as source_addr (TON 1 / NPI 1 for E.164 numbers) and the inbox number as destination_addr:
- GSM 03.38 text uses
data_coding0, one septet per octet (packed when the deployment's default alphabet isgsm7-packed). - Other text uses UCS-2,
data_coding8. - Long texts are split like a carrier would (153 septets or 67 UCS-2 units per part) and sent as several
deliver_smwithesm_class0x40 and an 8-bit concatenation UDH (05 00 03 ref total seq). Extension characters and surrogate pairs are never split.
Answer each with deliver_sm_resp and status 0. If nothing is bound, the attempt is logged as failed and retried; the message is also delivered as soon as a receiver binds.
Email to your app
Open an inbox and go to Send to app.
- Set the inbound target: the format your production provider uses, your endpoint URL, and for Mailgun the HTTP webhook signing key.
- Compose a message (with attachments), replay a message captured in the inbox, or upload an
.emlfile. The envelope recipient defaults to the message's To and Cc; set it to address a specific route, for examplereply+1234@parse.your-app.com.
| Format | Request |
|---|---|
| SendGrid Inbound Parse | multipart/form-data: headers, to, from, cc, subject, text, html, envelope (JSON), attachments (count), attachment-info (JSON), content-ids, charsets, SPF, dkim, sender_ip, and files attachment1…N |
| Mailgun Routes | recipient, sender, from, subject, body-plain, stripped-text, stripped-signature, body-html, stripped-html, message-headers (JSON), content-id-map, attachment-count, files attachment-1…N, plus timestamp, token, signature. multipart/form-data with attachments, form-urlencoded without |
| Postmark inbound | JSON: From, FromName, FromFull, To, ToFull, Cc, CcFull, OriginalRecipient, MailboxHash, Subject, MessageID, Date, TextBody, HtmlBody, StrippedTextReply, Headers, Attachments (base64 Content) |
| Raw MIME | The message itself, Content-Type: message/rfc822, with X-Envelope-From and X-Envelope-To |
Mailgun signature: signature is the hex HMAC-SHA256 of timestamp followed by token, keyed with the signing key you set, the same check Mailgun documents:
import hashlib, hmac
def verify(signing_key, token, timestamp, signature):
expected = hmac.new(signing_key.encode(), (timestamp + token).encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)SendGrid, Postmark and raw MIME have no signature of their own (production setups use secret URLs or basic auth). When you set a signing secret for them, Testlie adds X-Testlie-Timestamp and X-Testlie-Signature: sha256=<hex HMAC-SHA256 of "<timestamp>.<body>">, the same scheme as its webhooks.
Delivery log
Each message lists its deliveries: the HTTP status (or SMPP acknowledgement), the number of attempts and, for Twilio, how many replies were captured. Open one to see every attempt with the request headers, the response headers and the first 16 KB of the response body. Failed deliveries can be sent again with Deliver again.
Retries follow the webhook schedule: after 30 seconds, 2, 10 and 30 minutes, then 1, 2 and 4 hours (eight attempts in all). A 4xx answer other than 408 and 429 is final. Redirects are not followed. Targets that resolve to private or loopback addresses are refused on the hosted service.
API
| Method and path | Description |
|---|---|
GET, PUT /api/sms-inboxes/:id/inbound |
Inbound target of an SMS inbox: { phoneNumber?, url?, smpp? } (admins change it) |
POST /api/sms-inboxes/:id/inbound/messages |
Send a text from a phone: { from, body, mediaUrls?, to?, source? }. 201 with the message and its deliveries |
GET /api/sms-inboxes/:id/inbound/messages |
Texts sent to the inbox, newest first, with deliveries (from, page, limit) |
GET /api/sms-inboxes/:id/conversations |
One row per phone number; ?with=<number> returns that thread, oldest first, both directions |
GET, PUT /api/inboxes/:id/inbound |
Inbound target of an email inbox: { format: "sendgrid" | "mailgun" | "postmark" | "raw", url, secret? } |
POST /api/inboxes/:id/inbound/messages |
Send an email: { from, to, subject, text?, html?, cc?, attachments?: [{ filename, contentType, content (base64), contentId? }] }, { replayMessageId, to? }, { raw (base64), to? }, or a message/rfc822 body (?to=) |
GET /api/inboxes/:id/inbound/messages |
Emails sent to the app, newest first, with deliveries |
GET, DELETE /api/inbound/:id |
One message (id or SID) with every attempt; ?raw=1 downloads an email as .eml |
POST /api/inbound/:id/redeliver |
Deliver again (all deliveries, or { deliveryId }) once the previous ones finished |
From a test:
const res = await fetch(`${TESTLIE}/api/sms-inboxes/${smsInboxId}/inbound/messages`, {
method: "POST",
headers: { Authorization: `Bearer ${TESTLIE_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ from: "+15551234567", body: "STOP" }),
});
const { message } = await res.json();
// ...then assert your app unsubscribed +15551234567, or wait for its reply:
// GET /api/sms-inboxes/<id>/messages/wait?to=%2B15551234567Limits
A text from a phone counts toward the plan's SMS per month, and an email toward messages per month and the largest message size (at most 20 MB). Each inbox keeps its newest messages up to the plan's per-inbox limit, for the team's retention period (30 days at most). Senders are limited to 60 simulated messages per team per minute.