More sandboxes

On this page

WhatsApp sandbox

Test WhatsApp Business flows without sending a real WhatsApp message. The sandbox speaks the WhatsApp Cloud API: point your app's Graph API base URL at Testlie and every message is captured, rendered in a chat view and reported back to your webhook with Meta's status updates. Templates and the 24-hour customer-service window are enforced, so mistakes show up before production.

Create a sandbox

Open WhatsApp in the workspace and create a sandbox. It gets a fake business number and the values a real WhatsApp Business app has:

Setting Value
Base URL https://testlie.com/wa, used in place of https://graph.facebook.com
Phone number ID 15 digits, the {phone-number-id} in API paths
WhatsApp Business Account ID 15 digits, for message_templates and the webhook's entry[].id
Access token EAAT…, sent as Authorization: Bearer <token>
App secret 32 hex characters, the key of X-Hub-Signature-256
Verify token Sent in the webhook verification request

Keep these separate from your production Meta credentials. Admins can rotate the access token and app secret; the old values stop working at once.

Base URL. Paths keep their Graph API shape after the /wa prefix, with any version: https://testlie.com/wa/v21.0/<phone-number-id>/messages. Most apps keep the Graph host in one setting (for example GRAPH_API_BASE or WHATSAPP_API_URL); set it to https://testlie.com/wa.

Send a message

bash
curl -X POST "https://testlie.com/wa/v21.0/$PHONE_NUMBER_ID/messages" \
  -H "Authorization: Bearer $WHATSAPP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "messaging_product": "whatsapp",
    "to": "15551234567",
    "type": "template",
    "template": {
      "name": "order_update",
      "language": { "code": "en_US" },
      "components": [
        { "type": "body", "parameters": [ { "type": "text", "text": "Ada" }, { "type": "text", "text": "#42" } ] }
      ]
    }
  }'

The response is the Cloud API's:

json
{
  "messaging_product": "whatsapp",
  "contacts": [{ "input": "15551234567", "wa_id": "15551234567" }],
  "messages": [{ "id": "wamid.HBgL…", "message_status": "accepted" }]
}

Supported endpoints

Method and path (after https://testlie.com/wa) Description
POST /{version}/{phone-number-id}/messages Send any message type below, or mark an incoming message as read with { "messaging_product": "whatsapp", "status": "read", "message_id": "wamid…" } (answers { "success": true })
POST /{version}/{phone-number-id}/media Upload media (multipart: file, type, messaging_product=whatsapp); answers { "id" }
GET /{version}/{media-id} { messaging_product, url, mime_type, sha256, file_size, id }
GET /media/{media-id} The url above: the file's bytes (bearer token required)
DELETE /{version}/{media-id} Delete uploaded media
GET /{version}/{phone-number-id} The phone number (display_phone_number, verified_name, quality_rating)
GET /{version}/{waba-id}/phone_numbers The account's phone number
GET, POST, DELETE /{version}/{waba-id}/message_templates List, create (Meta's components format) or delete (?name=) templates

Message types

type Notes
text body up to 4096 characters, preview_url
template name, language.code, components with header, body and button parameters (text, currency, date_time, image, video, document, location, payload, coupon_code)
image, video, audio, document, sticker By link (http/https) or uploaded media id; caption (not audio/sticker); filename for documents
interactive button (1-3 reply buttons, titles up to 20 characters), list (up to 10 sections and 10 rows), cta_url
reaction message_id and emoji (an empty emoji removes the reaction)
location latitude, longitude, optional name and address
contacts At least name.formatted_name per contact

A context.message_id makes the message a reply. Media ids must belong to the sandbox (131053 otherwise). JSON bodies are expected; form-encoded bodies with JSON values are accepted too, like the Graph API.

Templates

Each sandbox has a template registry (Templates tab, the app API, or POST /{waba-id}/message_templates). A template has a name, language, category (MARKETING, UTILITY, AUTHENTICATION), approval status, optional header (TEXT with up to one placeholder, IMAGE, VIDEO, DOCUMENT, LOCATION), a body with {{1}}, {{2}}… placeholders (or named ones such as {{first_name}}), an optional footer and buttons (QUICK_REPLY, URL with an optional {{1}} suffix, PHONE_NUMBER, COPY_CODE).

Template sends are checked like Meta checks them:

  • Unknown name, or no translation for language.code: 132001.
  • Status PENDING or REJECTED: 132001; PAUSED: 132015; DISABLED: 132016. Change the status on the Templates tab to test how your app handles it.
  • Body, header or button parameter count differs from the placeholders: 132000, with the component and both counts in error_data.details.
  • A parameter of the wrong type (a video for an image header, a body image, a quick reply without payload): 132012. Empty values: 131008; values with more than four consecutive spaces: 131009.

Templates created through the Graph API are approved immediately.

The 24-hour window

Free-form messages (everything except templates) are only allowed within 24 hours of the test user's last message. Outside the window the message is accepted and then reported as failed with 131047 (re-engagement message) on your webhook, as the Cloud API does. Simulate a user message in the chat view (or with the app API) to open the window. The chat header shows whether the window is open and until when.

Outcomes per number

By default every message goes sent → delivered → read, one step per second. Outcome rules (on the Outcomes tab) match the recipient with a glob (* any run of digits, ? one) and change that:

Outcome Statuses
Deliver and read sent, delivered, read (the default)
Deliver, never read sent, delivered
No WhatsApp account failed with 131026 (message undeliverable)
Fail failed with the code you pick: 131000, 131026, 131049, 131021 or 131053
json
{ "outcomeRules": [
  { "pattern": "1555000*", "outcome": "no_whatsapp" },
  { "pattern": "1555111*", "outcome": "failed", "errorCode": 131049 }
] }

Set error mode to sync to get window and outcome failures as an HTTP 400 error on the send itself instead of a failed status.

Your webhook

Set the callback URL and verify token on the Credentials & webhook tab. Saving sends Meta's verification request:

GET <callback>?hub.mode=subscribe&hub.challenge=1158201444&hub.verify_token=<verify token>

Your endpoint must answer 200 with the hub.challenge value as the body. (Tick skip the verification request to save a URL that cannot answer yet.)

Then every status change and every simulated user message is POSTed as JSON in Meta's format, signed with X-Hub-Signature-256: sha256=<hex HMAC-SHA256 of the raw body, keyed with the app secret>:

json
{
  "object": "whatsapp_business_account",
  "entry": [{
    "id": "<waba-id>",
    "changes": [{
      "field": "messages",
      "value": {
        "messaging_product": "whatsapp",
        "metadata": { "display_phone_number": "15550101234", "phone_number_id": "<phone-number-id>" },
        "statuses": [{
          "id": "wamid.HBgL…",
          "status": "failed",
          "timestamp": "1792238400",
          "recipient_id": "15551234567",
          "errors": [{ "code": 131047, "title": "Re-engagement message", "message": "Re-engagement message",
                       "error_data": { "details": "Message failed to send because more than 24 hours have passed since the customer last replied to this number." } }]
        }]
      }
    }]
  }]
}

Successful statuses carry conversation and pricing (pricing_model: "PMP", category from the template, service for free-form replies). User messages arrive as value.contacts and value.messages: text, button (template quick reply: payload and text), interactive (button_reply or list_reply with id and title), location and reaction, with context when they reply to one of your messages.

Deliveries are SSRF-guarded like Testlie's own webhooks and retried on network errors, timeouts, 5xx, 408 and 429. The tab shows the last delivery's result.

js
// Express: verification handshake and signature check
app.get("/webhooks/whatsapp", (req, res) =>
  req.query["hub.verify_token"] === process.env.WHATSAPP_VERIFY_TOKEN ? res.send(req.query["hub.challenge"]) : res.sendStatus(403));

app.post("/webhooks/whatsapp", express.raw({ type: "application/json" }), (req, res) => {
  const expected = "sha256=" + crypto.createHmac("sha256", process.env.WHATSAPP_APP_SECRET).update(req.body).digest("hex");
  if (req.get("X-Hub-Signature-256") !== expected) return res.sendStatus(401);
  // handle JSON.parse(req.body).entry[0].changes[0].value.statuses / .messages
  res.sendStatus(200);
});

Simulate the user

The Chats tab shows one conversation per test user, as their phone would: templates rendered with header, footer and buttons, media, interactive messages and delivery ticks. From there you can write as the user, tap quick-reply, reply and list buttons, share a location, react, reply to a message, and mark a message read. Each action reaches your webhook like a real user's would.

From tests, use the app API with a team API key:

bash
curl -X POST -H "Authorization: Bearer $TESTLIE_API_KEY" -H "Content-Type: application/json" \
  "https://testlie.com/api/whatsapp-sandboxes/$SANDBOX_ID/simulate" \
  -d '{ "type": "text", "from": "15551234567", "profileName": "Ada", "text": "Hi, where is my order?" }'

type is text, button (contextWamid, index or text of the quick reply), button_reply or list_reply (contextWamid, id), location (latitude, longitude), reaction (messageId, emoji) or read (wamid).

Assert on the capture

bash
curl -f -H "Authorization: Bearer $TESTLIE_API_KEY" \
  "https://testlie.com/api/whatsapp-sandboxes/$SANDBOX_ID/messages/wait?to=15551234567&template=otp_code&timeout=30"

A match returns 200 with { message }, including the rendered template and extracted.codes (a copy-code button's value first) and extracted.links; a timeout returns 408. Filters: to, contains, template, type, direction (outbound by default, or inbound), since (ISO time) and timeout (1-120 s).

Method and path Description
GET, POST /api/whatsapp-sandboxes List or create sandboxes ({ "name", "projectId"?, "verifiedName"?, "outcomeRules"?, "errorMode"? }); responses include the credentials
GET, PATCH, DELETE /api/whatsapp-sandboxes/:id Read, update (also webhookUrl, verifyToken, skipVerification) or delete
POST /api/whatsapp-sandboxes/:id/rotate { "secret": "accessToken" | "appSecret" | "verifyToken" }
POST /api/whatsapp-sandboxes/:id/verify-webhook Run the verification request again: { ok, status, error }
GET, POST /api/whatsapp-sandboxes/:id/templates List or add templates
PATCH, DELETE /api/whatsapp-sandboxes/:id/templates/:templateId Change (for example the status) or delete a template
GET, DELETE /api/whatsapp-sandboxes/:id/messages List (waId, direction, q, page, limit, plus each user's last message time) or clear
GET /api/whatsapp-sandboxes/:id/messages/wait Long-poll, see above
POST /api/whatsapp-sandboxes/:id/simulate Act as a test user, see above
GET /api/whatsapp-sandboxes/:id/media/:mediaId Uploaded media bytes
GET, DELETE /api/whatsapp/messages/:wamid One message with the request as sent; delete
GET /api/whatsapp/messages/:wamid/extract { wamid, codes, links }

Team-wide webhooks subscribed to whatsapp.received get a signed JSON POST (same signing as message.received) for every captured message: { "event": "whatsapp.received", "whatsappSandbox": { id, name }, "message": { wamid, to, type, templateName, text, status, receivedAt, url } }.

Errors

Errors use the Graph API envelope:

json
{ "error": { "message": "(#132000) Number of parameters does not match the expected number of params", "type": "OAuthException", "code": 132000,
             "error_data": { "messaging_product": "whatsapp", "details": "body: number of localizable_params (1) does not match the expected number of params (2)" },
             "fbtrace_id": "…" } }
Code HTTP When
190 401 Missing, wrong or rotated access token
100 400 Invalid parameter (details name it); subcode 33 when the phone number, account or media id is not the token's
130429 429 More than 50 requests per second for one number, or the plan's monthly message quota is used up
131021 400 Sending to the business's own number
131026 400 / webhook No WhatsApp account (outcome rule)
131047 400 / webhook Free-form message outside the 24-hour window
131053 400 Unknown media id, or an upload that is empty or too large
132000, 132001, 132012, 132015, 132016 400 / 404 Template errors, see above

Limits

WhatsApp shares the SMS limits: sandboxes count toward the plan's SMS inboxes, and each captured message toward its monthly SMS messages (see plans and limits). Simulated user messages do not count. A sandbox keeps the plan's newest messages per inbox and its newest 200 uploads of up to 16 MB each; uploads expire after 30 days, like Meta's. Messages are deleted after the team's retention period.

Simulated statuses arrive about a second apart. Webhook retries follow the same schedule as Testlie's own webhooks, and callback URLs must be reachable from the internet (no private addresses).

Not supported: real delivery, calling, flows, catalogs and product messages, typing indicators beyond accepting them, business profile and phone registration endpoints, template review (templates are approved at once unless you set another status), inbound media from simulated users, and webhook fields other than messages.