Get started
Test mode
A sandbox for building and testing an integration. Requests are validated exactly as in live mode, but nothing is delivered and nothing is billed.
How test mode works#
Test mode is selected by the API key. Any request made with a key that starts with om_test_ runs in the sandbox: messages are accepted, stored and moved through simulated statuses, webhooks fire for them, and no provider is contacted. There is nothing to switch on.
Objects created in test mode carry mode: "test" and are invisible to live keys, and the reverse. Your code does not change between modes; only the key and the channel ID do.
Sandbox channels#
You do not connect channels in test mode. Every account has one built-in sandbox channel per channel type, with a fixed ID of the form ch_test_<type>. Each has the same capabilities as a real channel of that type, so unsupported message types are rejected exactly as they would be in live mode.
| Channel ID | Simulates | Message types |
|---|---|---|
ch_test_whatsapp | WhatsApp Business | text, attachments, template, button, list, cta_url, location, contacts, flow, product, product_list, catalog, carousel, location_request |
ch_test_telegram | Telegram | text, attachments, button, location, contacts, poll |
ch_test_sms | SMS | text, attachments |
ch_test_sms_otp | SMS OTP | text |
ch_test_messenger | Messenger | text, attachments, button, carousel, product_list, receipt |
ch_test_instagram | text, attachments, button, carousel, product_list | |
ch_test_tiktok | TikTok | text, attachments, button |
GET /v1/channels with a test key lists these channels. They cannot be renamed or deleted, and POST /v1/channels requires a live key.
{
"id": "ch_test_whatsapp",
"object": "channel",
"mode": "test",
"type": "whatsapp",
"name": "WhatsApp sandbox",
"identifier": "sandbox",
"status": "active",
"connection_status": "connected",
"capabilities": [
"text",
"attachments",
"template",
"button",
"list",
"cta_url",
"location",
"contacts",
"flow",
"product",
"product_list",
"catalog",
"carousel",
"location_request"
],
"created_at": "2026-10-01T08:00:00.000Z"
}Magic recipients#
The last four characters of to choose what the sandbox does with a message. Use them to exercise each branch of your status handling.
to ends in | Outcome | Events sent |
|---|---|---|
0000 | The message fails with error.code: "provider_error". | message.failed |
0001 | The message reaches sent and stays there. It is never delivered. | message.sent |
0002 | The message goes sent, delivered, then read. | message.sent, message.delivered, message.read |
| Anything else | The message goes sent, then delivered within about two seconds. | message.sent, message.delivered |
The rule applies to any recipient format. For a phone number use, for example, +15550100000; for a Telegram chat ID use a value such as 4829100001.
curl https://api.omnimessage.co/v1/messages \
-H "Authorization: Bearer om_test_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"channel": "ch_test_sms",
"to": "+15550100000",
"type": "text",
"text": {
"body": "This message will fail"
}
}'{
"id": "msg_9cV4nH7jK2mP5qR8sT1w",
"object": "message",
"mode": "test",
"channel_id": "ch_test_sms",
"channel_type": "sms",
"direction": "outbound",
"to": "+15550100000",
"from": "sandbox",
"type": "text",
"content": {
"text": {
"body": "This message will fail"
}
},
"status": "failed",
"error": {
"code": "provider_error",
"message": "Simulated failure (test mode).",
"provider_code": null
},
"reference": null,
"metadata": {},
"billing": {
"source": "none",
"amount_micros": 0,
"package_grant_id": null,
"refunded": false
},
"sender": null,
"contact_id": null,
"created_at": "2026-10-05T09:30:00.000Z",
"updated_at": "2026-10-05T09:30:02.871Z",
"sent_at": null,
"delivered_at": null,
"read_at": null,
"failed_at": "2026-10-05T09:30:03.118Z"
}What is simulated#
| Behaviour | In test mode |
|---|---|
| Authentication, scopes, IP allowlist | Identical to live mode. |
| Request validation and error responses | Identical to live mode, including unsupported_message_type per channel type. |
| Idempotency, pagination, rate limits | Identical to live mode. |
| Delivery | Simulated. No provider is contacted and no recipient receives anything. |
| Statuses and timestamps | Simulated according to the magic recipient rules. |
| Webhooks | Real. Events for test messages are sent, signed, to webhook endpoints created with a test key. |
| Billing | None. billing.source is none; neither packages nor the wallet change, and 402 insufficient_balance never occurs. |
| Inbound messages | Not simulated. message.received is only produced by real channels. |
| Provider-side rules | Not simulated. WhatsApp template approval and the 24-hour window, for example, are enforced only in live mode. |
Webhooks in test mode#
Webhook endpoints belong to a mode. Create an endpoint with a test key and it receives only test-mode events, each with mode: "test" in the body. This keeps test traffic away from your production handler. You can register the same URL in both modes; the two endpoints have different signing secrets.
Account-level events (balance.low, package.exhausted, package.expiring) describe real funds and are sent to live endpoints only. To check connectivity and signature verification at any time, call POST /v1/webhook_endpoints/{id}/test.
Going live#
- Connect at least one channel and wait for
connection_status: "connected". - Make sure the account has package credits or wallet balance:
GET /v1/balance. - Create a live key with only the scopes the service needs, and deploy it as a secret.
- Replace the sandbox channel ID with the ID of your connected channel.
- Create a live webhook endpoint and deploy its signing secret.
- Send
Idempotency-Keyon everyPOSTand handle402,422and429responses. See Errors.