. Create an API key under API keys.API reference · Account
Sign-in & account
Sign-up and dashboard sign-in (one-time code over WhatsApp, access code, authenticator) and GET /v1/me. Integrations use an API key instead.
Sign up a new business
/auth/registerNo authenticationSelf-service tenant signup. Creates a new tenant with the Free plan; the business profile (country, timezone, language, billing currency) is inferred from the phone number. Phone number must be unique across all tenants.
Request body
namestringrequiredBusiness or account namephonestringrequiredOwner phone in international format+<country code><number>; also decides the initial business country, timezone and currencylangstringLanguage of the OTP / notice messages and the initial dashboard language (default: the request language)
Responses
- 200Tenant created and a one-time code sent over WhatsApp. Sign in with
POST /auth/verify-otp.Response fields
okbooleanrequired
- 400Name or number missing or invalid
- 409Phone already registered — sign in instead
- 429Rate limited
- 502The code could not be delivered over WhatsApp
- 503Registration is temporarily unavailable (the sender device is offline)
curl -X POST "https://api.chatkomodo.com/auth/register" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Dental",
"phone": "+447700900123",
"lang": "en"
}'const res = await fetch('https://api.chatkomodo.com/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Acme Dental",
"phone": "+447700900123",
"lang": "en"
}),
});
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/auth/register",
json={
"name": "Acme Dental",
"phone": "+447700900123",
"lang": "en",
},
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/auth/register', [
'json' => [
'name' => 'Acme Dental',
'phone' => '+447700900123',
'lang' => 'en',
],
]);
print_r(json_decode((string) $res->getBody(), true));{
"ok": true
}Request a sign-in code
/auth/request-otpNo authenticationRequest a one-time password (OTP) for the given phone. Max 5 requests per 15 min per phone.
Request body
phonestringrequiredInternational format+<country code><number>
Responses
- 200OTP sent
Response fields
ttlintegerrequiredviastringrequired
- 400Validation error
- 429Too many OTP requests
curl -X POST "https://api.chatkomodo.com/auth/request-otp" \
-H "Content-Type: application/json" \
-d '{
"phone": "+447700900123"
}'const res = await fetch('https://api.chatkomodo.com/auth/request-otp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
"phone": "+447700900123"
}),
});
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/auth/request-otp",
json={
"phone": "+447700900123",
},
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/auth/request-otp', [
'json' => [
'phone' => '+447700900123',
],
]);
print_r(json_decode((string) $res->getBody(), true));{
"ttl": 604800,
"via": "string"
}Sign in with a one-time code
/auth/verify-otpNo authenticationVerify the 6-digit OTP. Returns a JWT (2 hour expiry) and a rotating refresh token (30 days).
Request body
phonestringrequiredcodestringrequired
Responses
- 200Authenticated
Response fields
tokenstringrequiredJWT to use in Authorization: Bearer headerrefresh_tokenstringRefresh token (long-lived)rolestringrequiredagent_idstringagent_namestring
- 401Invalid or expired OTP
curl -X POST "https://api.chatkomodo.com/auth/verify-otp" \
-H "Content-Type: application/json" \
-d '{
"phone": "+447700900123",
"code": "482913"
}'const res = await fetch('https://api.chatkomodo.com/auth/verify-otp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
"phone": "+447700900123",
"code": "482913"
}),
});
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/auth/verify-otp",
json={
"phone": "+447700900123",
"code": "482913",
},
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/auth/verify-otp', [
'json' => [
'phone' => '+447700900123',
'code' => '482913',
],
]);
print_r(json_decode((string) $res->getBody(), true));{
"token": "eyJhbGciOiJIUzI1NiIs…",
"refresh_token": "eyJhbGciOiJIUzI1NiIs…",
"role": "admin",
"agent_id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
"agent_name": "string"
}Renew a dashboard login
/auth/refreshNo authenticationExchange a valid refresh token for a fresh JWT. The old refresh token is rotated (revoked). Reuse-detection blocks compromised tokens.
Request body
refresh_tokenstringrequired
Responses
- 200Refreshed
Response fields
tokenstringrequiredJWT to use in Authorization: Bearer headerrefresh_tokenstringRefresh token (long-lived)rolestringrequiredagent_idstringagent_namestring
- 401Token revoked or compromised
curl -X POST "https://api.chatkomodo.com/auth/refresh" \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "eyJhbGciOiJIUzI1NiIs…"
}'const res = await fetch('https://api.chatkomodo.com/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
"refresh_token": "eyJhbGciOiJIUzI1NiIs…"
}),
});
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/auth/refresh",
json={
"refresh_token": "eyJhbGciOiJIUzI1NiIs…",
},
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/auth/refresh', [
'json' => [
'refresh_token' => 'eyJhbGciOiJIUzI1NiIs…',
],
]);
print_r(json_decode((string) $res->getBody(), true));{
"token": "eyJhbGciOiJIUzI1NiIs…",
"refresh_token": "eyJhbGciOiJIUzI1NiIs…",
"role": "admin",
"agent_id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
"agent_name": "string"
}Who am I
/v1/meDashboard loginIdentity of the caller: the tenant, its plan and business profile, and whether the credential is the owner login (admin), a customer-service agent (agent) or an API key (apikey). With the platform admin secret it returns is_super: true and no plan.
Responses
- 200Caller identity
Response fields
tenant_idstringrequiredTenant id (__super__for the platform admin secret)namestringrequiredBusiness / account namephonestring | nullrequiredOwner login number, or the agent's own number for roleagentrolestringrequiredis_superbooleantrueonly for the platform admin secretagent_idstring | nullRoleagentonlyagent_namestring | nullRoleagentonlykey_idstringRoleapikeyonly: id of the key used for this request (a child key has its own id)planobject | nullrequiredplan.idintegerrequiredplan.namestringrequiredplan.active_untilinteger | nullrequiredUnix ms until which the paid plan is active, or nullplan.pendingobject | nullA paid downgrade that takes over when the current period endsplan.pending.idintegerrequiredplan.pending.namestringrequiredplan.pending.starts_atintegerrequiredplan.pending.untilintegerrequiredprofileobjectBusiness profile; same data asGET /v1/profile. Absent for the platform admin secretprofile.business_namestring | nullrequiredprofile.countrystringrequiredISO 3166-1 alpha-2 codeprofile.timezonestringrequiredIANA timezoneprofile.localestringrequiredenoridprofile.industrystring | nullrequiredprofile.currencystringrequiredBilling currency,IDRorUSDprofile.configuredbooleanrequired
- 401Missing, invalid or expired credentials
- 404Tenant not found (
NOT_FOUND)
curl "https://api.chatkomodo.com/v1/me" \
-H "Authorization: Bearer $CHATKOMODO_API_KEY"const res = await fetch('https://api.chatkomodo.com/v1/me', {
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/me",
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/me', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
],
]);
print_r(json_decode((string) $res->getBody(), true));{
"tenant_id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
"name": "Example",
"phone": "+447700900123",
"role": "admin",
"is_super": true,
"agent_id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
"agent_name": "string",
"key_id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
"plan": {
"id": 1,
"name": "Example",
"active_until": 1767225600000,
"pending": {
"id": 1,
"name": "Example",
"starts_at": 1767225600000,
"until": 1767225600000
}
},
"profile": {
"business_name": "string",
"country": "GB",
"timezone": "Asia/Jakarta",
"locale": "en",
"industry": "string",
"currency": "IDR",
"configured": true
}
}Sign in with an access code
/auth/loginNo authenticationDashboard sign-in with the tenant owner's or a customer-service agent's access code. Returns a JWT valid for 2 hours, to be sent as Authorization: Bearer <token>. No refresh token is issued here (sign in with a one-time code via /auth/verify-otp to get one). Limited to 10 attempts per 10 minutes per IP address.
Request body
codestringrequiredAccess code
Responses
- 200Signed in
Response fields
tokenstringrequiredJWT for theAuthorization: Bearerheaderrolestringrequiredagent_idstringRoleagentonlyagent_namestringRoleagentonly
- 400
codeis missing or longer than 128 characters (nocode) - 401Invalid access code (no
code) - 429Too many attempts from this IP address (no
code)
curl -X POST "https://api.chatkomodo.com/auth/login" \
-H "Content-Type: application/json" \
-d '{
"code": "482913"
}'const res = await fetch('https://api.chatkomodo.com/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
"code": "482913"
}),
});
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/auth/login",
json={
"code": "482913",
},
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/auth/login', [
'json' => [
'code' => '482913',
],
]);
print_r(json_decode((string) $res->getBody(), true));{
"token": "eyJhbGciOiJIUzI1NiIs…",
"role": "admin",
"agent_id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
"agent_name": "string"
}Sign out
/auth/logoutEnds a dashboard login (owner or agent). Send the refresh token in the body, the access token as Authorization: Bearer, or both; at least one is required. The refresh token is deleted and the login session is revoked, so the access token stops working immediately. Idempotent. The bearer token is optional here and only a JWT counts (an API key is ignored).
Request body
refresh_tokenstringRefresh token issued at sign-in
Responses
- 200Signed out
Response fields
okbooleanrequired
- 400Neither a refresh token nor a valid access token was supplied (no
code)
curl -X POST "https://api.chatkomodo.com/auth/logout" \
-H "Authorization: Bearer $CHATKOMODO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "eyJhbGciOiJIUzI1NiIs…"
}'const res = await fetch('https://api.chatkomodo.com/auth/logout', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"refresh_token": "eyJhbGciOiJIUzI1NiIs…"
}),
});
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/auth/logout",
headers={
"Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
},
json={
"refresh_token": "eyJhbGciOiJIUzI1NiIs…",
},
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/auth/logout', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
],
'json' => [
'refresh_token' => 'eyJhbGciOiJIUzI1NiIs…',
],
]);
print_r(json_decode((string) $res->getBody(), true));{
"ok": true
}Sign in with an authenticator code
/auth/verify-totpNo authenticationFallback sign-in for a tenant owner when the WhatsApp one-time code cannot be delivered. Needs two-factor authentication to be set up on the account. Accepts the 6-digit code of the previous, current or next 30-second window. Failed codes lock the account for increasing periods (3 failures: 5 minutes, 5: 1 hour, 10: 24 hours) and each IP address is limited to 8 requests per 10 minutes (counted together with /auth/request-otp and /auth/register). Returns a 2-hour JWT and a refresh token. Customer-service agents cannot use this endpoint.
Request body
phonestringrequiredOwner login number, international format+<country code><number>codestringrequired6-digit authenticator code
Responses
- 200Signed in
Response fields
tokenstringrequiredJWT for theAuthorization: Bearerheaderrolestringrequiredrefresh_tokenstringrequiredRotating refresh token, valid 30 days. Exchange it at/auth/refresh
- 400
phoneorcodemissing (nocode) - 4012FA is not enabled for this account (also returned for an unknown number), or the code is wrong (no
code) - 429Too many requests from this IP address, or the account is locked after repeated wrong codes (no
code)
curl -X POST "https://api.chatkomodo.com/auth/verify-totp" \
-H "Content-Type: application/json" \
-d '{
"phone": "+447700900123",
"code": "482913"
}'const res = await fetch('https://api.chatkomodo.com/auth/verify-totp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
"phone": "+447700900123",
"code": "482913"
}),
});
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/auth/verify-totp",
json={
"phone": "+447700900123",
"code": "482913",
},
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/auth/verify-totp', [
'json' => [
'phone' => '+447700900123',
'code' => '482913',
],
]);
print_r(json_decode((string) $res->getBody(), true));{
"token": "eyJhbGciOiJIUzI1NiIs…",
"role": "admin",
"refresh_token": "eyJhbGciOiJIUzI1NiIs…"
}