Skip to content

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 IDSimulatesMessage types
ch_test_whatsappWhatsApp Businesstext, attachments, template, button, list, cta_url, location, contacts, flow, product, product_list, catalog, carousel, location_request
ch_test_telegramTelegramtext, attachments, button, location, contacts, poll
ch_test_smsSMStext, attachments
ch_test_sms_otpSMS OTPtext
ch_test_messengerMessengertext, attachments, button, carousel, product_list, receipt
ch_test_instagramInstagramtext, attachments, button, carousel, product_list
ch_test_tiktokTikToktext, 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.

Sandbox channel
{
  "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 inOutcomeEvents sent
0000The message fails with error.code: "provider_error".message.failed
0001The message reaches sent and stays there. It is never delivered.message.sent
0002The message goes sent, delivered, then read.message.sent, message.delivered, message.read
Anything elseThe 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"
    }
  }'
Message after the simulated failure
{
  "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#

BehaviourIn test mode
Authentication, scopes, IP allowlistIdentical to live mode.
Request validation and error responsesIdentical to live mode, including unsupported_message_type per channel type.
Idempotency, pagination, rate limitsIdentical to live mode.
DeliverySimulated. No provider is contacted and no recipient receives anything.
Statuses and timestampsSimulated according to the magic recipient rules.
WebhooksReal. Events for test messages are sent, signed, to webhook endpoints created with a test key.
BillingNone. billing.source is none; neither packages nor the wallet change, and 402 insufficient_balance never occurs.
Inbound messagesNot simulated. message.received is only produced by real channels.
Provider-side rulesNot 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-Key on every POST and handle 402, 422 and 429 responses. See Errors.

    Loading