Guides
Test with the sandbox
A separate, free and unlimited environment, with a simulator to test your webhooks without a WhatsApp number.
Updated November 12, 2026View as Markdown
Each application has a sandbox environment, strictly separate from production: its own keys (pk_test_…), businesses, numbers, webhooks and data.
| Sandbox | Production | |
|---|---|---|
| Keys | pk_test_… | pk_live_… |
| Quota and business count limits | None | Those of your plan |
| Bref Point Inbox billing | None | Depends on your plan |
| Simulator | Yes | Rejected (403 sandbox_only) |
Simulate a received message#
Ideal for testing your receiving flow end to end: the message is stored, the 24-hour window opens for this sender, and your message.received webhook fires exactly as in production (the data carries "simulated": true).
curl -X POST "$INBOX_API/sandbox/simulate/inbound-message" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"from": "+237611111111",
"text": "Hello, I'\''d like to reschedule my appointment"
}'const res = await fetch(`${process.env.INBOX_API}/sandbox/simulate/inbound-message`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INBOX_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"from": "+237611111111",
"text": "Hello, I'd like to reschedule my appointment"
}),
});
const data = await res.json();import os
import requests
res = requests.post(
f"{os.environ['INBOX_API']}/sandbox/simulate/inbound-message",
headers={
"Authorization": f"Bearer {os.environ['INBOX_KEY']}",
},
json={
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"from": "+237611111111",
"text": "Hello, I'd like to reschedule my appointment",
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/sandbox/simulate/inbound-message');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'phoneNumberId' => '4d8e654e-d744-4bf3-8211-9ceffcccfe87',
'from' => '+237611111111',
'text' => 'Hello, I\'d like to reschedule my appointment',
]),
]);
$data = json_decode(curl_exec($ch), true);{
"messageId": "3f3baf5a-f93e-42e8-8263-ce62dc4aadd6",
"from": "237611111111",
"status": "received",
"simulated": true
}Advance a message's status#
After a send (POST /messages), move the message along as Meta would:
curl -X POST "$INBOX_API/sandbox/simulate/message-status" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"messageId": "d5ad1e3e-6ab9-4f7f-8e4e-b17104a7bd00",
"status": "delivered"
}'const res = await fetch(`${process.env.INBOX_API}/sandbox/simulate/message-status`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INBOX_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"messageId": "d5ad1e3e-6ab9-4f7f-8e4e-b17104a7bd00",
"status": "delivered"
}),
});
const data = await res.json();import os
import requests
res = requests.post(
f"{os.environ['INBOX_API']}/sandbox/simulate/message-status",
headers={
"Authorization": f"Bearer {os.environ['INBOX_KEY']}",
},
json={
"messageId": "d5ad1e3e-6ab9-4f7f-8e4e-b17104a7bd00",
"status": "delivered",
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/sandbox/simulate/message-status');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'messageId' => 'd5ad1e3e-6ab9-4f7f-8e4e-b17104a7bd00',
'status' => 'delivered',
]),
]);
$data = json_decode(curl_exec($ch), true);{
"messageId": "d5ad1e3e-6ab9-4f7f-8e4e-b17104a7bd00",
"status": "delivered",
"simulated": true
}The same rules as in production apply: a status never goes backward, and a failed doesn't replace a message that is already read. A transition with no effect simply responds with the current state.
A complete scenario#
- Connect a number in the sandbox and declare a webhook on
*(Quickstart). POST /sandbox/simulate/inbound-message: your server receivesmessage.received.- Reply with
POST /messages(free-form message, the window is open). POST /sandbox/simulate/message-statuswithdelivered, thenread: you receive the corresponding events.- Cause a failure on your side (a
500response) and watch the automatic retries inGET /webhook-deliveries. - When everything works, create a production key and reuse the same calls.