. Create an API key under API keys.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:
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));{
"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
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));- The message is logged and goes
sentat once,deliveredafter about 1 second andreadafter about 3 seconds. - Your webhooks and the live event stream get
message.sentas usual. - To test failures, send to any number ending in 0000 (for example
+447700900000): the message fails andmessage.failedis sent.POST /v1/sessions/{sessionId}/validatereports such numbers as not on WhatsApp. - There is no anti-ban pacing and no warm-up, so tests run fast. Your API key's request limit still applies.
Play the customer
Make a customer write to the test device:
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.