ChatKomodo Docs Home Sign in

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

POST/auth/registerNo authentication

Self-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

  • namestringrequired
    Business or account name
    1–64 characters
  • phonestringrequired
    Owner phone in international format +<country code><number>; also decides the initial business country, timezone and currency
    8–20 characters
  • langstring
    Language of the OTP / notice messages and the initial dashboard language (default: the request language)
    One of: en, id

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)
Request
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));
Response 200
{
  "ok": true
}

Request a sign-in code

POST/auth/request-otpNo authentication

Request a one-time password (OTP) for the given phone. Max 5 requests per 15 min per phone.

Request body

  • phonestringrequired
    International format +<country code><number>
    8–20 characters

Responses

  • 200OTP sent
    Response fields
    • ttlintegerrequired
    • viastringrequired
  • 400Validation error
  • 429Too many OTP requests
Request
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));
Response 200
{
  "ttl": 604800,
  "via": "string"
}

Sign in with a one-time code

POST/auth/verify-otpNo authentication

Verify the 6-digit OTP. Returns a JWT (2 hour expiry) and a rotating refresh token (30 days).

Request body

  • phonestringrequired
    8–20 characters
  • codestringrequired
    Exactly 6 characters

Responses

  • 200Authenticated
    Response fields
    • tokenstringrequired
      JWT to use in Authorization: Bearer header
    • refresh_tokenstring
      Refresh token (long-lived)
    • rolestringrequired
      One of: admin, agent
    • agent_idstring
    • agent_namestring
  • 401Invalid or expired OTP
Request
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));
Response 200
{
  "token": "eyJhbGciOiJIUzI1NiIs…",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs…",
  "role": "admin",
  "agent_id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
  "agent_name": "string"
}

Renew a dashboard login

POST/auth/refreshNo authentication

Exchange a valid refresh token for a fresh JWT. The old refresh token is rotated (revoked). Reuse-detection blocks compromised tokens.

Request body

  • refresh_tokenstringrequired
    At least 20 characters

Responses

  • 200Refreshed
    Response fields
    • tokenstringrequired
      JWT to use in Authorization: Bearer header
    • refresh_tokenstring
      Refresh token (long-lived)
    • rolestringrequired
      One of: admin, agent
    • agent_idstring
    • agent_namestring
  • 401Token revoked or compromised
Request
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));
Response 200
{
  "token": "eyJhbGciOiJIUzI1NiIs…",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs…",
  "role": "admin",
  "agent_id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
  "agent_name": "string"
}

Who am I

GET/v1/me

Identity 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_idstringrequired
      Tenant id (__super__ for the platform admin secret)
    • namestringrequired
      Business / account name
    • phonestring | nullrequired
      Owner login number, or the agent's own number for role agent
    • rolestringrequired
      One of: admin, agent, apikey
    • is_superboolean
      true only for the platform admin secret
    • agent_idstring | null
      Role agent only
    • agent_namestring | null
      Role agent only
    • key_idstring
      Role apikey only: id of the key used for this request (a child key has its own id)
    • planobject | nullrequired
    • plan.idintegerrequired
    • plan.namestringrequired
    • plan.active_untilinteger | nullrequired
      Unix ms until which the paid plan is active, or null
    • plan.pendingobject | null
      A paid downgrade that takes over when the current period ends
    • plan.pending.idintegerrequired
    • plan.pending.namestringrequired
    • plan.pending.starts_atintegerrequired
    • plan.pending.untilintegerrequired
    • profileobject
      Business profile; same data as GET /v1/profile. Absent for the platform admin secret
    • profile.business_namestring | nullrequired
    • profile.countrystringrequired
      ISO 3166-1 alpha-2 code
    • profile.timezonestringrequired
      IANA timezone
    • profile.localestringrequired
      en or id
    • profile.industrystring | nullrequired
    • profile.currencystringrequired
      Billing currency, IDR or USD
    • profile.configuredbooleanrequired
  • 401Missing, invalid or expired credentials
  • 404Tenant not found (NOT_FOUND)
Request
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));
Response 200
{
  "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

POST/auth/loginNo authentication

Dashboard 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

  • codestringrequired
    Access code
    1–128 characters

Responses

  • 200Signed in
    Response fields
    • tokenstringrequired
      JWT for the Authorization: Bearer header
    • rolestringrequired
      One of: admin, agent
    • agent_idstring
      Role agent only
    • agent_namestring
      Role agent only
  • 400code is missing or longer than 128 characters (no code)
  • 401Invalid access code (no code)
  • 429Too many attempts from this IP address (no code)
Request
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));
Response 200
{
  "token": "eyJhbGciOiJIUzI1NiIs…",
  "role": "admin",
  "agent_id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
  "agent_name": "string"
}

Sign out

POST/auth/logout

Ends 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_tokenstring
    Refresh token issued at sign-in
    Up to 256 characters

Responses

  • 200Signed out
    Response fields
    • okbooleanrequired
  • 400Neither a refresh token nor a valid access token was supplied (no code)
Request
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));
Response 200
{
  "ok": true
}

Sign in with an authenticator code

POST/auth/verify-totpNo authentication

Fallback 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

  • phonestringrequired
    Owner login number, international format +<country code><number>
  • codestringrequired
    6-digit authenticator code

Responses

  • 200Signed in
    Response fields
    • tokenstringrequired
      JWT for the Authorization: Bearer header
    • rolestringrequired
      One of: admin
    • refresh_tokenstringrequired
      Rotating refresh token, valid 30 days. Exchange it at /auth/refresh
  • 400phone or code missing (no code)
  • 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)
Request
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));
Response 200
{
  "token": "eyJhbGciOiJIUzI1NiIs…",
  "role": "admin",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs…"
}