ChatKomodo Docs Home Sign in

API reference · Messaging

Broadcasts

Campaigns to many recipients: explicit lists or CRM audiences (segments, groups, tags), personalization, consent checks, the daily send window and progress counters.

List the campaigns of a device

GET/v1/sessions/{sessionId}/blasts

List the campaigns of a session, newest first.

Path parameters

  • sessionIdstringrequired

Responses

  • 200Campaigns
    Response fields
    • campaignsarray of objectrequired
    • campaigns[].idstringrequired
      ULID (26 characters)
    • campaigns[].session_idstringrequired
    • campaigns[].namestringrequired
    • campaigns[].statusstringrequired
      One of: draft, running, paused, completed, failed, cancelled
    • campaigns[].bodystring | nullrequired
    • campaigns[].message_typestringrequired
    • campaigns[].total_recipientsintegerrequired
    • campaigns[].sent_countintegerrequired
    • campaigns[].failed_countintegerrequired
    • campaigns[].created_atintegerrequired
    • campaigns[].started_atinteger | nullrequired
    • campaigns[].completed_atinteger | nullrequired
    • campaigns[].consent_attested_atinteger | nullrequired
      When the sender confirmed the recipients agreed to be messaged (consent_attested), if ever
Request
curl "https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/blasts" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY"
const res = await fetch('https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/blasts', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
  },
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
console.log(data);
import os
import requests

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

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

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "campaigns": [
    {
      "id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
      "session_id": "YOUR_DEVICE_ID",
      "name": "Example",
      "status": "draft",
      "body": "Hi Sam, your order has shipped.",
      "message_type": "Hi Sam, your order has shipped.",
      "total_recipients": 12,
      "sent_count": 12,
      "failed_count": 12,
      "created_at": 1767225600000,
      "started_at": 1767225600000,
      "completed_at": 1767225600000,
      "consent_attested_at": 1767225600000
    }
  ]
}

Create a campaign

POST/v1/sessions/{sessionId}/blasts

Create a blast campaign (status draft). The audience is the explicit recipients plus the contacts matched by segment_id, group_id and/or tag (CRM filters combine with AND), deduplicated, max 50 000. Nothing is sent until /start. The server applies anti-ban pacing and per-country warm-up; recipients who opted out are skipped automatically just before sending, and bulk sends are only released inside the tenant's send window (see send_window in /v1/settings/send_window).

Path parameters

  • sessionIdstringrequired

Request body

  • namestringrequired
    Campaign name (internal)
    Up to 200 characters
  • message_typestring
    One of: text, image, video, document, audioDefault: "text"
  • bodystring
    Message text. Personalization tokens: {name}, {first_name}, {phone}, {var1}…{var5}, {attr.<key>}; add a fallback with double braces: {{first_name|there}}, {{attr.plan|your plan}}. A token without a value and without a fallback becomes empty.
    Up to 65 536 characters
  • media_r2_keystring
    Media key from /v1/media/upload-url (for image/video/document/audio)
  • captionstring
    Up to 1 024 characters
  • footerstring
    Up to 200 characters
  • buttonsany
    Optional button configuration
  • delay_configobject
    Pause between recipients. Default 3 000–10 000 ms with 1 000–3 000 ms of simulated typing.
  • delay_config.min_msintegerrequired
  • delay_config.max_msintegerrequired
  • delay_config.typing_min_msintegerrequired
  • delay_config.typing_max_msintegerrequired
  • simulate_typingboolean
    Show "typing…" before each message (default true)
  • recipientsarray of object
    Explicit recipients. Combined (deduplicated) with the CRM audience below.
    Up to 50 000 items
  • recipients[].chat_idstringrequired
    Phone number (+<country code><number>; local numbers use the business country) or JID
  • recipients[].namestring
    Up to 200 characters
  • recipients[].varsarray of string
    Values for {var1}…{var5}
    Up to 5 items
  • recipients[].attrsobject
    Per-recipient values for {attr.<key>}
  • segment_idstring
    Add every contact of this saved segment
  • group_idstring
    Add every contact of this contact group
  • tagstring
    Add every contact carrying this tag
  • consent_attestedboolean
    Confirms the recipients agreed to receive these messages. Required (here or when starting) if the tenant turned on require_attestation.

Responses

  • 201Draft created
    Response fields
    • idstringrequired
      ULID (26 characters)
    • statusstringrequired
      One of: draft
    • total_recipientsintegerrequired
  • 400Validation error
  • 404Segment or group not found
Request
curl -X POST "https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/blasts" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "October promo",
  "body": "Hi {{first_name|there}}, 20% off everything this week!",
  "recipients": [
    {
      "chat_id": "+447700900123",
      "name": "Sam Carter",
      "attrs": {
        "plan": "Pro"
      }
    },
    {
      "chat_id": "+12025550123",
      "name": "Alex Kim"
    }
  ]
}'
const res = await fetch('https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/blasts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "October promo",
    "body": "Hi {{first_name|there}}, 20% off everything this week!",
    "recipients": [
      {
        "chat_id": "+447700900123",
        "name": "Sam Carter",
        "attrs": {
          "plan": "Pro"
        }
      },
      {
        "chat_id": "+12025550123",
        "name": "Alex Kim"
      }
    ]
  }),
});
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/blasts",
    headers={
        "Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
    },
    json={
        "name": "October promo",
        "body": "Hi {{first_name|there}}, 20% off everything this week!",
        "recipients": [
            {
                "chat_id": "+447700900123",
                "name": "Sam Carter",
                "attrs": {
                    "plan": "Pro",
                },
            },
            {
                "chat_id": "+12025550123",
                "name": "Alex Kim",
            },
        ],
    },
    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/blasts', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
    'json' => [
        'name' => 'October promo',
        'body' => 'Hi {{first_name|there}}, 20% off everything this week!',
        'recipients' => [
            [
                'chat_id' => '+447700900123',
                'name' => 'Sam Carter',
                'attrs' => [
                    'plan' => 'Pro',
                ],
            ],
            [
                'chat_id' => '+12025550123',
                'name' => 'Alex Kim',
            ],
        ],
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 201
{
  "id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
  "status": "draft",
  "total_recipients": 12
}

Get a campaign and its progress

GET/v1/sessions/{sessionId}/blasts/{campaignId}

Campaign detail with per-recipient status and progress counters. skipped_count counts recipients removed because they opted out.

Path parameters

  • sessionIdstringrequired
  • campaignIdstringrequired

Responses

  • 200Campaign
    Response fields
    • campaignobjectrequired
    • campaign.idstringrequired
      ULID (26 characters)
    • campaign.session_idstringrequired
    • campaign.namestringrequired
    • campaign.statusstringrequired
      One of: draft, running, paused, completed, failed, cancelled
    • campaign.bodystring | nullrequired
    • campaign.message_typestringrequired
    • campaign.total_recipientsintegerrequired
    • campaign.sent_countintegerrequired
    • campaign.failed_countintegerrequired
    • campaign.created_atintegerrequired
    • campaign.started_atinteger | nullrequired
    • campaign.completed_atinteger | nullrequired
    • campaign.consent_attested_atinteger | nullrequired
      When the sender confirmed the recipients agreed to be messaged (consent_attested), if ever
    • recipientsarray of objectrequired
    • recipients[].idstringrequired
    • recipients[].chat_idstringrequired
    • recipients[].namestring | nullrequired
    • recipients[].statusstringrequired
      pending, processing, sent, failed, skipped or cancelled
    • recipients[].errorstring | nullrequired
    • pending_countintegerrequired
    • sent_countintegerrequired
    • failed_countintegerrequired
    • skipped_countintegerrequired
  • 404Campaign not found
Request
curl "https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/blasts/CAMPAIGN_ID" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY"
const res = await fetch('https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/blasts/CAMPAIGN_ID', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
  },
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
console.log(data);
import os
import requests

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

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

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "campaign": {
    "id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
    "session_id": "YOUR_DEVICE_ID",
    "name": "Example",
    "status": "draft",
    "body": "Hi Sam, your order has shipped.",
    "message_type": "Hi Sam, your order has shipped.",
    "total_recipients": 12,
    "sent_count": 12,
    "failed_count": 12,
    "created_at": 1767225600000,
    "started_at": 1767225600000,
    "completed_at": 1767225600000,
    "consent_attested_at": 1767225600000
  },
  "recipients": [
    {
      "id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
      "chat_id": "+447700900123",
      "name": "Example",
      "status": "active",
      "error": "string"
    }
  ],
  "pending_count": 12,
  "sent_count": 12,
  "failed_count": 12,
  "skipped_count": 12
}

Delete a campaign

DEL/v1/sessions/{sessionId}/blasts/{campaignId}

Deletes the campaign and all its recipient rows, whatever its status. Messages that were already sent stay in the message history.

Path parameters

  • sessionIdstringrequired
    Session (device) id
  • campaignIdstringrequired
    Campaign id

Responses

  • 200Deleted
    Response fields
    • okbooleanrequired
  • 401Missing or invalid credentials
  • 403The API key is restricted to other sessions, or the operation is not available to CS-agent logins
  • 404Campaign not found (NOT_FOUND); unknown session, or a session of another tenant
Request
curl -X DELETE "https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/blasts/CAMPAIGN_ID" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY"
const res = await fetch('https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/blasts/CAMPAIGN_ID', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
  },
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
console.log(data);
import os
import requests

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

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

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "ok": true
}

Start or resume a campaign

POST/v1/sessions/{sessionId}/blasts/{campaignId}/start

Start or resume a campaign. If the tenant turned on require_attestation (consent config) and the campaign was not attested at creation, the body must contain {"consent_attested": true}, otherwise the call fails with 400 CONSENT_ATTESTATION_REQUIRED. Opted-out recipients are removed (status skipped) before the first message.

Path parameters

  • sessionIdstringrequired
  • campaignIdstringrequired

Request body

  • consent_attestedboolean

Responses

  • 200Started
    Response fields
    • okbooleanrequired
    • statusstringrequired
      One of: running
  • 400CONSENT_ATTESTATION_REQUIRED
  • 404Campaign not found
Request
curl -X POST "https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/blasts/CAMPAIGN_ID/start" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "consent_attested": true
}'
const res = await fetch('https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/blasts/CAMPAIGN_ID/start', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "consent_attested": 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/YOUR_DEVICE_ID/blasts/CAMPAIGN_ID/start",
    headers={
        "Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
    },
    json={
        "consent_attested": 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/YOUR_DEVICE_ID/blasts/CAMPAIGN_ID/start', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
    'json' => [
        'consent_attested' => true,
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "ok": true,
  "status": "running"
}

Pause a campaign

POST/v1/sessions/{sessionId}/blasts/{campaignId}/pause

Pause a running campaign (resume with /start).

Path parameters

  • sessionIdstringrequired
  • campaignIdstringrequired

Responses

  • 200Updated
    Response fields
    • okbooleanrequired
    • statusstringrequired
  • 404Campaign not found
Request
curl -X POST "https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/blasts/CAMPAIGN_ID/pause" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY"
const res = await fetch('https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/blasts/CAMPAIGN_ID/pause', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
  },
});
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/blasts/CAMPAIGN_ID/pause",
    headers={
        "Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
    },
    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/blasts/CAMPAIGN_ID/pause', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "ok": true,
  "status": "active"
}

Cancel a campaign

POST/v1/sessions/{sessionId}/blasts/{campaignId}/cancel

Cancel a campaign; recipients that were still pending are cancelled.

Path parameters

  • sessionIdstringrequired
  • campaignIdstringrequired

Responses

  • 200Updated
    Response fields
    • okbooleanrequired
    • statusstringrequired
  • 404Campaign not found
Request
curl -X POST "https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/blasts/CAMPAIGN_ID/cancel" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY"
const res = await fetch('https://api.chatkomodo.com/v1/sessions/YOUR_DEVICE_ID/blasts/CAMPAIGN_ID/cancel', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
  },
});
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/blasts/CAMPAIGN_ID/cancel",
    headers={
        "Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
    },
    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/blasts/CAMPAIGN_ID/cancel', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "ok": true,
  "status": "active"
}

List blast campaigns of all devices

GET/v1/blasts

Every blast campaign of the tenant across devices, newest first, without pagination. Campaigns are created per device with POST /v1/sessions/{sessionId}/blasts.

Responses

  • 200Campaigns
    Response fields
    • campaignsarray of objectrequired
    • campaigns[].idstringrequired
    • campaigns[].session_idstringrequired
    • campaigns[].api_key_idstringrequired
      Owning tenant
    • campaigns[].namestringrequired
    • campaigns[].statusstringrequired
      draft, running, paused, completed, failed or cancelled
    • campaigns[].message_typestringrequired
    • campaigns[].bodystring | nullrequired
    • campaigns[].total_recipientsinteger | nullrequired
    • campaigns[].sent_countinteger | nullrequired
    • campaigns[].failed_countinteger | nullrequired
    • campaigns[].skipped_countinteger | nullrequired
      Recipients skipped, e.g. because they opted out
    • campaigns[].created_atintegerrequired
      Unix time in milliseconds
    • campaigns[].started_atinteger | nullrequired
      Unix time in milliseconds, or null
    • campaigns[].completed_atinteger | nullrequired
      Unix time in milliseconds, or null
    • campaigns[].consent_attested_atinteger | nullrequired
      When the sender attested consent, or null
  • 401Missing, invalid or expired credentials
Request
curl "https://api.chatkomodo.com/v1/blasts" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY"
const res = await fetch('https://api.chatkomodo.com/v1/blasts', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
  },
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
console.log(data);
import os
import requests

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

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

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "campaigns": [
    {
      "id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
      "session_id": "YOUR_DEVICE_ID",
      "api_key_id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
      "name": "Example",
      "status": "active",
      "message_type": "Hi Sam, your order has shipped.",
      "body": "Hi Sam, your order has shipped.",
      "total_recipients": 12,
      "sent_count": 12,
      "failed_count": 12,
      "skipped_count": 12,
      "created_at": 1767225600000,
      "started_at": 1767225600000,
      "completed_at": 1767225600000,
      "consent_attested_at": 1767225600000
    }
  ]
}