. Create an API key under API keys.API reference · Customers
Chat links & widget
Click-to-WhatsApp short links (for ads, bios, QR codes) and a chat button for your website. Visits are counted with their campaign tags (UTM); when a visitor sends the ready message, the chat is matched to the visit through a short code (#K7Q2MX) and the person gets the link as their lead source, plus the link's labels and stage.
List links
/v1/linksYour click-to-WhatsApp links with visits and chats (all time and last 30 days) and the leads and won deals that first came through each. Account owner or API key; not for support agents.
Responses
- 200Links
Response fields
linksarray of objectrequiredlinks[].idstringrequiredlinks[].codestringrequiredlinks[].namestringrequiredlinks[].urlstringrequiredThe short link to share (your agency's domain for its clients)links[].session_idstringrequiredThe device the chat opens withlinks[].messagestringrequiredThe message the customer's WhatsApp has ready to sendlinks[].add_refbooleanrequiredAdd a short reference (#K7Q2MX) to the message so the chat can be matched to the visit. Without it visits are counted but chats are notlinks[].utm_sourcestring | nullrequiredlinks[].utm_mediumstring | nullrequiredlinks[].utm_campaignstring | nullrequiredlinks[].labelsarray of stringrequiredPut on people who write through the linklinks[].stagestring | nullrequiredLead stage for people who write through the link and are not on the board yetlinks[].widgetobject | nullrequiredlinks[].widget.enabledbooleanrequiredlinks[].widget.colorstringrequired#rrggbblinks[].widget.positionstringrequiredlinks[].widget.titlestringrequiredGreeting title; empty = your business namelinks[].widget.textstringrequiredGreeting text; empty = a default greeting. When both are empty the button opens the chat directlylinks[].widget.buttonstringrequiredButton text; empty = "Start chat"links[].widget.delay_sintegerrequiredOpen the greeting by itself after this many seconds (0 = only on click)links[].widget_snippetstringrequiredThe<script>tag to paste into a websitelinks[].clicksintegerrequiredVisits, all time (link-preview bots not counted)links[].chatsintegerrequiredVisits that became a chat, all timelinks[].clicks_30dintegerrequiredlinks[].chats_30dintegerrequiredlinks[].leadsintegerrequiredPeople who first came through this link and are on the leads boardlinks[].wonintegerrequired…of them in a won stagelinks[].disabledbooleanrequiredlinks[].created_atintegerrequiredUnix time in millisecondslinks[].updated_atintegerrequiredUnix time in millisecondsbase_urlstringrequired
curl "https://api.chatkomodo.com/v1/links" \
-H "Authorization: Bearer $CHATKOMODO_API_KEY"const res = await fetch('https://api.chatkomodo.com/v1/links', {
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/links",
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/links', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
],
]);
print_r(json_decode((string) $res->getBody(), true));{
"links": [
{
"id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
"code": "RECIPIENT_OPTED_OUT",
"name": "Example",
"url": "https://example.com/file.jpg",
"session_id": "YOUR_DEVICE_ID",
"message": "Hi Sam, your order has shipped.",
"add_ref": true,
"utm_source": "string",
"utm_medium": "string",
"utm_campaign": "string",
"labels": [
"string"
],
"stage": "vip",
"widget": {
"enabled": true,
"color": "#0a7350",
"position": "right",
"title": "string",
"text": "Hi Sam, your order has shipped.",
"button": "string",
"delay_s": 1
},
"widget_snippet": "string",
"clicks": 1,
"chats": 1,
"clicks_30d": 1,
"chats_30d": 1
}
],
"base_url": "https://example.com/file.jpg"
}Create a link
/v1/linksA short link that opens a WhatsApp chat with one of your devices with message ready to send. Share it in ads, bios, emails or as a QR code; add ?utm_source=…&utm_medium=…&utm_campaign=… to a shared copy to tag that placement. When someone writes through it, their contact gets source (the link name) and the campaign tags as attributes, the link's labels, and stage if they are not on the board yet. At most 200 links. Recorded as link.create.
Request body
namestringrequiredsession_idstringrequiredmessagestringrequiredadd_refbooleanutm_sourcestring | nullutm_mediumstring | nullutm_campaignstring | nulllabelsarray of stringstagestring | nullwidgetobject | nullwidget.enabledbooleanrequiredwidget.colorstringrequired#rrggbbwidget.positionstringrequiredwidget.titlestringrequiredGreeting title; empty = your business namewidget.textstringrequiredGreeting text; empty = a default greeting. When both are empty the button opens the chat directlywidget.buttonstringrequiredButton text; empty = "Start chat"widget.delay_sintegerrequiredOpen the greeting by itself after this many seconds (0 = only on click)disabledboolean
Responses
- 201The link
Response fields
idstringrequiredcodestringrequirednamestringrequiredurlstringrequiredThe short link to share (your agency's domain for its clients)session_idstringrequiredThe device the chat opens withmessagestringrequiredThe message the customer's WhatsApp has ready to sendadd_refbooleanrequiredAdd a short reference (#K7Q2MX) to the message so the chat can be matched to the visit. Without it visits are counted but chats are notutm_sourcestring | nullrequiredutm_mediumstring | nullrequiredutm_campaignstring | nullrequiredlabelsarray of stringrequiredPut on people who write through the linkstagestring | nullrequiredLead stage for people who write through the link and are not on the board yetwidgetobject | nullrequiredwidget.enabledbooleanrequiredwidget.colorstringrequired#rrggbbwidget.positionstringrequiredwidget.titlestringrequiredGreeting title; empty = your business namewidget.textstringrequiredGreeting text; empty = a default greeting. When both are empty the button opens the chat directlywidget.buttonstringrequiredButton text; empty = "Start chat"widget.delay_sintegerrequiredOpen the greeting by itself after this many seconds (0 = only on click)widget_snippetstringrequiredThe<script>tag to paste into a websiteclicksintegerrequiredVisits, all time (link-preview bots not counted)chatsintegerrequiredVisits that became a chat, all timeclicks_30dintegerrequiredchats_30dintegerrequiredleadsintegerrequiredPeople who first came through this link and are on the leads boardwonintegerrequired…of them in a won stagedisabledbooleanrequiredcreated_atintegerrequiredUnix time in millisecondsupdated_atintegerrequiredUnix time in milliseconds
- 400
VALIDATION_FAILED - 409
LIMIT_REACHED
curl -X POST "https://api.chatkomodo.com/v1/links" \
-H "Authorization: Bearer $CHATKOMODO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Instagram bio",
"session_id": "YOUR_DEVICE_ID",
"message": "Hello, I'\''d like to know more.",
"utm_source": "instagram",
"utm_medium": "social",
"labels": [
"instagram"
],
"stage": "new"
}'const res = await fetch('https://api.chatkomodo.com/v1/links', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Instagram bio",
"session_id": "YOUR_DEVICE_ID",
"message": "Hello, I'd like to know more.",
"utm_source": "instagram",
"utm_medium": "social",
"labels": [
"instagram"
],
"stage": "new"
}),
});
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/links",
headers={
"Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
},
json={
"name": "Instagram bio",
"session_id": "YOUR_DEVICE_ID",
"message": "Hello, I'd like to know more.",
"utm_source": "instagram",
"utm_medium": "social",
"labels": [
"instagram",
],
"stage": "new",
},
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/links', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
],
'json' => [
'name' => 'Instagram bio',
'session_id' => 'YOUR_DEVICE_ID',
'message' => 'Hello, I\'d like to know more.',
'utm_source' => 'instagram',
'utm_medium' => 'social',
'labels' => [
'instagram',
],
'stage' => 'new',
],
]);
print_r(json_decode((string) $res->getBody(), true));{
"id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
"code": "RECIPIENT_OPTED_OUT",
"name": "Example",
"url": "https://example.com/file.jpg",
"session_id": "YOUR_DEVICE_ID",
"message": "Hi Sam, your order has shipped.",
"add_ref": true,
"utm_source": "string",
"utm_medium": "string",
"utm_campaign": "string",
"labels": [
"string"
],
"stage": "vip",
"widget": {
"enabled": true,
"color": "#0a7350",
"position": "right",
"title": "string",
"text": "Hi Sam, your order has shipped.",
"button": "string",
"delay_s": 1
},
"widget_snippet": "string",
"clicks": 1,
"chats": 1,
"clicks_30d": 1,
"chats_30d": 1
}Get a link
/v1/links/{id}One link with its short address, widget settings, visits, chats, leads and won deals.
Path parameters
idstringrequired
Responses
- 200The link
Response fields
idstringrequiredcodestringrequirednamestringrequiredurlstringrequiredThe short link to share (your agency's domain for its clients)session_idstringrequiredThe device the chat opens withmessagestringrequiredThe message the customer's WhatsApp has ready to sendadd_refbooleanrequiredAdd a short reference (#K7Q2MX) to the message so the chat can be matched to the visit. Without it visits are counted but chats are notutm_sourcestring | nullrequiredutm_mediumstring | nullrequiredutm_campaignstring | nullrequiredlabelsarray of stringrequiredPut on people who write through the linkstagestring | nullrequiredLead stage for people who write through the link and are not on the board yetwidgetobject | nullrequiredwidget.enabledbooleanrequiredwidget.colorstringrequired#rrggbbwidget.positionstringrequiredwidget.titlestringrequiredGreeting title; empty = your business namewidget.textstringrequiredGreeting text; empty = a default greeting. When both are empty the button opens the chat directlywidget.buttonstringrequiredButton text; empty = "Start chat"widget.delay_sintegerrequiredOpen the greeting by itself after this many seconds (0 = only on click)widget_snippetstringrequiredThe<script>tag to paste into a websiteclicksintegerrequiredVisits, all time (link-preview bots not counted)chatsintegerrequiredVisits that became a chat, all timeclicks_30dintegerrequiredchats_30dintegerrequiredleadsintegerrequiredPeople who first came through this link and are on the leads boardwonintegerrequired…of them in a won stagedisabledbooleanrequiredcreated_atintegerrequiredUnix time in millisecondsupdated_atintegerrequiredUnix time in milliseconds
- 404
NOT_FOUND
curl "https://api.chatkomodo.com/v1/links/ID" \
-H "Authorization: Bearer $CHATKOMODO_API_KEY"const res = await fetch('https://api.chatkomodo.com/v1/links/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/links/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/links/ID', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
],
]);
print_r(json_decode((string) $res->getBody(), true));{
"id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
"code": "RECIPIENT_OPTED_OUT",
"name": "Example",
"url": "https://example.com/file.jpg",
"session_id": "YOUR_DEVICE_ID",
"message": "Hi Sam, your order has shipped.",
"add_ref": true,
"utm_source": "string",
"utm_medium": "string",
"utm_campaign": "string",
"labels": [
"string"
],
"stage": "vip",
"widget": {
"enabled": true,
"color": "#0a7350",
"position": "right",
"title": "string",
"text": "Hi Sam, your order has shipped.",
"button": "string",
"delay_s": 1
},
"widget_snippet": "string",
"clicks": 1,
"chats": 1,
"clicks_30d": 1,
"chats_30d": 1
}Change a link
/v1/links/{id}Send only the fields to change; widget fields are merged. The short address stays the same. Recorded as link.update.
Path parameters
idstringrequired
Request body
namestringsession_idstringmessagestringadd_refbooleanutm_sourcestring | nullutm_mediumstring | nullutm_campaignstring | nulllabelsarray of stringstagestring | nullwidgetobject | nullwidget.enabledbooleanrequiredwidget.colorstringrequired#rrggbbwidget.positionstringrequiredwidget.titlestringrequiredGreeting title; empty = your business namewidget.textstringrequiredGreeting text; empty = a default greeting. When both are empty the button opens the chat directlywidget.buttonstringrequiredButton text; empty = "Start chat"widget.delay_sintegerrequiredOpen the greeting by itself after this many seconds (0 = only on click)disabledboolean
Responses
- 200The link
Response fields
idstringrequiredcodestringrequirednamestringrequiredurlstringrequiredThe short link to share (your agency's domain for its clients)session_idstringrequiredThe device the chat opens withmessagestringrequiredThe message the customer's WhatsApp has ready to sendadd_refbooleanrequiredAdd a short reference (#K7Q2MX) to the message so the chat can be matched to the visit. Without it visits are counted but chats are notutm_sourcestring | nullrequiredutm_mediumstring | nullrequiredutm_campaignstring | nullrequiredlabelsarray of stringrequiredPut on people who write through the linkstagestring | nullrequiredLead stage for people who write through the link and are not on the board yetwidgetobject | nullrequiredwidget.enabledbooleanrequiredwidget.colorstringrequired#rrggbbwidget.positionstringrequiredwidget.titlestringrequiredGreeting title; empty = your business namewidget.textstringrequiredGreeting text; empty = a default greeting. When both are empty the button opens the chat directlywidget.buttonstringrequiredButton text; empty = "Start chat"widget.delay_sintegerrequiredOpen the greeting by itself after this many seconds (0 = only on click)widget_snippetstringrequiredThe<script>tag to paste into a websiteclicksintegerrequiredVisits, all time (link-preview bots not counted)chatsintegerrequiredVisits that became a chat, all timeclicks_30dintegerrequiredchats_30dintegerrequiredleadsintegerrequiredPeople who first came through this link and are on the leads boardwonintegerrequired…of them in a won stagedisabledbooleanrequiredcreated_atintegerrequiredUnix time in millisecondsupdated_atintegerrequiredUnix time in milliseconds
- 400
VALIDATION_FAILED - 404
NOT_FOUND
curl -X PATCH "https://api.chatkomodo.com/v1/links/ID" \
-H "Authorization: Bearer $CHATKOMODO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example",
"session_id": "YOUR_DEVICE_ID",
"message": "Hi Sam, your order has shipped.",
"add_ref": true
}'const res = await fetch('https://api.chatkomodo.com/v1/links/ID', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Example",
"session_id": "YOUR_DEVICE_ID",
"message": "Hi Sam, your order has shipped.",
"add_ref": 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.patch(
"https://api.chatkomodo.com/v1/links/ID",
headers={
"Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
},
json={
"name": "Example",
"session_id": "YOUR_DEVICE_ID",
"message": "Hi Sam, your order has shipped.",
"add_ref": True,
},
timeout=30,
)
res.raise_for_status()
print(res.json())<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$res = $client->request('PATCH', 'https://api.chatkomodo.com/v1/links/ID', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
],
'json' => [
'name' => 'Example',
'session_id' => 'YOUR_DEVICE_ID',
'message' => 'Hi Sam, your order has shipped.',
'add_ref' => true,
],
]);
print_r(json_decode((string) $res->getBody(), true));{
"id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
"code": "RECIPIENT_OPTED_OUT",
"name": "Example",
"url": "https://example.com/file.jpg",
"session_id": "YOUR_DEVICE_ID",
"message": "Hi Sam, your order has shipped.",
"add_ref": true,
"utm_source": "string",
"utm_medium": "string",
"utm_campaign": "string",
"labels": [
"string"
],
"stage": "vip",
"widget": {
"enabled": true,
"color": "#0a7350",
"position": "right",
"title": "string",
"text": "Hi Sam, your order has shipped.",
"button": "string",
"delay_s": 1
},
"widget_snippet": "string",
"clicks": 1,
"chats": 1,
"clicks_30d": 1,
"chats_30d": 1
}Delete a link
/v1/links/{id}The short address stops working; people already matched keep their source. Recorded as link.delete.
Path parameters
idstringrequired
Responses
- 200Deleted
Response fields
okbooleanrequired
- 404
NOT_FOUND
curl -X DELETE "https://api.chatkomodo.com/v1/links/ID" \
-H "Authorization: Bearer $CHATKOMODO_API_KEY"const res = await fetch('https://api.chatkomodo.com/v1/links/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/links/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/links/ID', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
],
]);
print_r(json_decode((string) $res->getBody(), true));{
"ok": true
}Link statistics
/v1/links/{id}/statsVisits and chats per day over the last days (7, 30 or 90), by campaign source, referring site, device and link vs widget, plus the leads and won deals that came through it.
Path parameters
idstringrequired
Query parameters
daysstring
Responses
- 200Statistics
Response fields
daysintegerrequiredclicksintegerrequiredchatsintegerrequiredleadsintegerrequiredwonintegerrequireddailyarray of objectrequireddaily[].datestringrequiredYYYY-MM-DD in the business time zonedaily[].clicksintegerrequireddaily[].chatsintegerrequiredsourcesarray of objectrequiredByutm_source(the click's, else the link's; empty = none)sources[].namestringrequiredsources[].clicksintegerrequiredsources[].chatsintegerrequiredreferrersarray of objectrequiredBy the site the visit came fromreferrers[].namestringrequiredreferrers[].clicksintegerrequiredreferrers[].chatsintegerrequireddevicesarray of objectrequiredmobileordesktopdevices[].namestringrequireddevices[].clicksintegerrequireddevices[].chatsintegerrequiredviaarray of objectrequiredlinkorwidgetvia[].namestringrequiredvia[].clicksintegerrequiredvia[].chatsintegerrequired
- 404
NOT_FOUND
curl "https://api.chatkomodo.com/v1/links/ID/stats" \
-H "Authorization: Bearer $CHATKOMODO_API_KEY"const res = await fetch('https://api.chatkomodo.com/v1/links/ID/stats', {
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/links/ID/stats",
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/links/ID/stats', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
],
]);
print_r(json_decode((string) $res->getBody(), true));{
"days": 1,
"clicks": 1,
"chats": 1,
"leads": 1,
"won": 1,
"daily": [
{
"date": "string",
"clicks": 1,
"chats": 1
}
],
"sources": [
{
"name": "Example",
"clicks": 1,
"chats": 1
}
],
"referrers": [
{
"name": "Example",
"clicks": 1,
"chats": 1
}
],
"devices": [
{
"name": "Example",
"clicks": 1,
"chats": 1
}
],
"via": [
{
"name": "Example",
"clicks": 1,
"chats": 1
}
]
}Open a link (visitor)
/public/go/{code}No authenticationWhat the short link does: counts the visit (with utm_* from the query, overriding the link's) and redirects to the WhatsApp chat. Link-preview bots are redirected without being counted. https://chatkomodo.com/go/{code} forwards here. No authentication.
Path parameters
codestringrequired
Query parameters
utm_sourcestringutm_mediumstringutm_campaignstringutm_contentstringutm_termstring
Responses
- 302To
https://wa.me/<number>?text=… - 404Unknown link (an HTML page)
- 503The link is off or its device has no number yet (an HTML page)
curl "https://api.chatkomodo.com/public/go/CODE"const res = await fetch('https://api.chatkomodo.com/public/go/CODE', {
method: 'GET',
});
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/public/go/CODE",
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/public/go/CODE', [
]);
print_r(json_decode((string) $res->getBody(), true));Website widget script
/public/widget/{file}No authenticationThe chat button for a website: <script src="https://api.chatkomodo.com/public/widget/{code}.js" async></script>. It opens the link with the page's own utm_* tags and address. Cached for 5 minutes; a link with the widget off serves an empty script. No authentication.
Path parameters
filestringrequired{code}.js
Responses
- 200JavaScript
curl "https://api.chatkomodo.com/public/widget/FILE"const res = await fetch('https://api.chatkomodo.com/public/widget/FILE', {
method: 'GET',
});
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/public/widget/FILE",
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/public/widget/FILE', [
]);
print_r(json_decode((string) $res->getBody(), true));application/javascript — the file itself.