ChatKomodo Docs Home Sign in

Guide

Test device (sandbox): try the API without WhatsApp

A test device behaves like a linked WhatsApp number, except that nothing ever reaches WhatsApp. Use it to build and test your integration before you link a real number, and in automated tests.

Create a test device

In the dashboard: Devices → Add a test device. Or with the API:

Request
curl -X POST "https://api.chatkomodo.com/v1/sessions" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "sandbox": true
}'
const res = await fetch('https://api.chatkomodo.com/v1/sessions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "sandbox": true
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
console.log(data);
import os
import requests

res = requests.post(
    "https://api.chatkomodo.com/v1/sessions",
    headers={
        "Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
    },
    json={
        "sandbox": True,
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())
<?php
require 'vendor/autoload.php';

$client = new GuzzleHttp\Client();
$res = $client->request('POST', 'https://api.chatkomodo.com/v1/sessions', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
    'json' => [
        'sandbox' => true,
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response
{
  "id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
  "status": "connected",
  "sandbox": true,
  "phone_number": "12025550142"
}

It is connected at once (no QR code) and has a fictional number. Each business can have one test device. It does not count toward your plan's devices, and its messages do not count toward the monthly quota. In GET /v1/sessions it has "sandbox": true.

Send to it like to any device

Request
curl -X POST "https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/messages" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "chat_id": "+447700900123",
  "type": "text",
  "body": "Your order #1042 has shipped"
}'
const res = await fetch('https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/messages', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "chat_id": "+447700900123",
    "type": "text",
    "body": "Your order #1042 has shipped"
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
console.log(data);
import os
import requests

res = requests.post(
    "https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/messages",
    headers={
        "Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
    },
    json={
        "chat_id": "+447700900123",
        "type": "text",
        "body": "Your order #1042 has shipped",
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())
<?php
require 'vendor/autoload.php';

$client = new GuzzleHttp\Client();
$res = $client->request('POST', 'https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/messages', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
    'json' => [
        'chat_id' => '+447700900123',
        'type' => 'text',
        'body' => 'Your order #1042 has shipped',
    ],
]);

print_r(json_decode((string) $res->getBody(), true));

Play the customer

Make a customer write to the test device:

Request
curl -X POST "https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/sandbox/inbound" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "from": "+447700900123",
  "text": "What time do you open on Saturday?",
  "name": "Sam Carter"
}'
const res = await fetch('https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/sandbox/inbound', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "from": "+447700900123",
    "text": "What time do you open on Saturday?",
    "name": "Sam Carter"
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
console.log(data);
import os
import requests

res = requests.post(
    "https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/sandbox/inbound",
    headers={
        "Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
    },
    json={
        "from": "+447700900123",
        "text": "What time do you open on Saturday?",
        "name": "Sam Carter",
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())
<?php
require 'vendor/autoload.php';

$client = new GuzzleHttp\Client();
$res = $client->request('POST', 'https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/sandbox/inbound', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
    'json' => [
        'from' => '+447700900123',
        'text' => 'What time do you open on Saturday?',
        'name' => 'Sam Carter',
    ],
]);

print_r(json_decode((string) $res->getBody(), true));

The message is handled exactly like a real one: message.received reaches your webhooks, the chat appears in the inbox, and welcome messages, auto-reply rules, chatbot flows and AI replies answer it (their answers go back through the test device, so you can read them in the message log). The dashboard device page has the same form: Simulate a customer message.

What a test device cannot do

Groups, communities, channels, status updates, the catalog, labels and pairing codes need a real WhatsApp connection. On a test device they answer 501 with the code SANDBOX_UNSUPPORTED. Media messages are accepted; the file is checked but not delivered.

Going live

Link a real number (Quickstart, step 1) and use its device id instead. Nothing else in your code changes. You can keep the test device for automated tests, or delete it from the dashboard.