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 a deliver_sm to 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.

  1. Under Inbound texts to your app, set the inbox number (the To of 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.
  2. 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:

js
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_coding 0, one septet per octet (packed when the deployment's default alphabet is gsm7-packed).
  • Other text uses UCS-2, data_coding 8.
  • Long texts are split like a carrier would (153 septets or 67 UCS-2 units per part) and sent as several deliver_sm with esm_class 0x40 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.

  1. Set the inbound target: the format your production provider uses, your endpoint URL, and for Mailgun the HTTP webhook signing key.
  2. Compose a message (with attachments), replay a message captured in the inbox, or upload an .eml file. The envelope recipient defaults to the message's To and Cc; set it to address a specific route, for example reply+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:

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

js
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=%2B15551234567

Limits

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.