ChatKomodo Docs Home Sign in

API reference · Account

Billing

Plans, prices, checkout and invoices.

List plans and prices

GET/v1/billing/plans

Plans with their price in every currency they are sold in. All amounts are integers in minor units (cents; IDR has no minor unit), e.g. {"IDR": 99000, "USD": 900}.

Responses

  • 200Plans
    Response fields
    • plansarray of objectrequired
    • plans[].idintegerrequired
    • plans[].namestringrequired
    • plans[].price_monthlyintegerrequired
      Price in IDR
    • plans[].max_sessionsintegerrequired
    • plans[].max_messages_per_monthintegerrequired
      0 = unlimited
    • plans[].send_per_minuteintegerrequired
      Requests per minute the plan allows on POST …messages… (see GET /v1/usage)
    • plans[].ai_replies_per_monthintegerrequired
      AI auto-replies included per month (0 = none)
    • plans[].pricesobjectrequired
    • annual_months_chargedintegerrequired
      Months charged for an annual period (12 months delivered)
Request
curl "https://api.chatkomodo.com/v1/billing/plans" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY"
const res = await fetch('https://api.chatkomodo.com/v1/billing/plans', {
  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/billing/plans",
    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/billing/plans', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "plans": [
    {
      "id": 1,
      "name": "Example",
      "price_monthly": 150000,
      "max_sessions": 1,
      "max_messages_per_month": 1,
      "send_per_minute": 1,
      "ai_replies_per_month": 1,
      "prices": {}
    }
  ],
  "annual_months_charged": 1
}

List payment methods

GET/v1/billing/options

How this tenant can pay: only payment methods that are set up are listed. default_provider is Pakasir (QRIS, bank, e-wallet; IDR) for Indonesian tenants and the cheapest international method otherwise. currency is what each provider would charge for the tenant's plan; it is null when the plan has no price in a currency that provider accepts.

Responses

  • 200Payment options
    Response fields
    • default_providerstring | nullrequired
      One of: pakasir, creem, paypal
    • tenant_currencystringrequired
      One of: IDR, USD
    • providersarray of objectrequired
    • providers[].idstringrequired
      One of: pakasir, creem, paypal
    • providers[].labelstringrequired
    • providers[].currenciesarray of stringrequired
    • providers[].currencystring | nullrequired
      ISO 4217 code, e.g. USD, IDR
Request
curl "https://api.chatkomodo.com/v1/billing/options" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY"
const res = await fetch('https://api.chatkomodo.com/v1/billing/options', {
  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/billing/options",
    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/billing/options', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "default_provider": "pakasir",
  "tenant_currency": "IDR",
  "providers": [
    {
      "id": "pakasir",
      "label": "string",
      "currencies": [
        "IDR"
      ],
      "currency": "IDR"
    }
  ]
}

Price a checkout

POST/v1/billing/quote

Price a checkout without creating anything: the same body as POST /v1/billing/checkout. Use it to show the annual saving, check a coupon (400 COUPON_INVALID with reason: not_found, inactive, not_started, expired, used_up, already_used, wrong_plan, wrong_period, wrong_currency) and show the upgrade credit before paying. provider is the method that would be used (null when none is set up).

Request body

  • plan_idinteger
    Plan to buy; omit to renew the current plan
  • providerstring
    One of: pakasir, creem, paypal
  • currencystring
    ISO 4217 code, e.g. USD, IDR
  • billing_periodstring
    Default monthly
    One of: monthly, annual
  • coupon_codestring
    Case-insensitive

Responses

  • 200Quote
    Response fields
    • plan_idintegerrequired
    • plan_namestringrequired
    • billing_periodstringrequired
      One of: monthly, annual
    • monthsintegerrequired
      Months charged (12 months are delivered for an annual period)
    • currencystringrequired
      ISO 4217 code, e.g. USD, IDR
    • changestringrequired
      upgrade starts now with a credit; downgrade starts when the current period ends; renew stacks after it
      One of: new, renew, upgrade, downgrade
    • list_amountintegerrequired
      Plan price × months charged
    • monthly_equivalentintegerrequired
      What the same months would cost month by month
    • discount_amountintegerrequired
    • coupon_codestring | nullrequired
    • proration_creditintegerrequired
      Unused part of what was paid for the current plan (upgrades only)
    • overageintegerrequired
    • amountintegerrequired
      What will be charged, minor units
    • starts_atintegerrequired
    • ends_atintegerrequired
    • providerstring | nullrequired
      One of: pakasir, creem, paypal
  • 400VALIDATION_FAILED, COUPON_INVALID, CURRENCY_NOT_SUPPORTED, or nothing to pay
Request
curl -X POST "https://api.chatkomodo.com/v1/billing/quote" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "plan_id": 2,
  "provider": "creem",
  "currency": "USD"
}'
const res = await fetch('https://api.chatkomodo.com/v1/billing/quote', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "plan_id": 2,
    "provider": "creem",
    "currency": "USD"
  }),
});
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/billing/quote",
    headers={
        "Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
    },
    json={
        "plan_id": 2,
        "provider": "creem",
        "currency": "USD",
    },
    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/billing/quote', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
    'json' => [
        'plan_id' => 2,
        'provider' => 'creem',
        'currency' => 'USD',
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "plan_id": 1,
  "plan_name": "string",
  "billing_period": "monthly",
  "months": 1,
  "currency": "IDR",
  "change": "new",
  "list_amount": 150000,
  "monthly_equivalent": 1,
  "discount_amount": 12,
  "coupon_code": "482913",
  "proration_credit": 1,
  "overage": 1,
  "amount": 150000,
  "starts_at": 1767225600000,
  "ends_at": 1767225600000,
  "provider": "pakasir"
}

Create an invoice and payment page

POST/v1/billing/checkout

Create an invoice and a hosted payment page. Redirect the browser to pay_url; the provider sends the buyer back to /billing?invoice=<order_id> (or ?cancelled=<order_id>). Omit plan_id to pay the current plan plus any overage for the month; send plan_id to buy or switch plans. billing_period: "annual" buys 12 months for the price of 10 (set by the operator). A coupon_code takes a percent or a fixed amount off the plan price (never off overage). Switching to a pricier plan starts it now and credits the unused part of what was paid for the current plan; a cheaper plan starts when the current period ends. Price the same body first with POST /v1/billing/quote. provider and currency default to the best match for the tenant. The plan is activated only after the provider confirms the payment (webhook or /invoices/{order_id}/confirm).

Request body

  • plan_idinteger
    Plan to buy; omit to renew the current plan
  • providerstring
    One of: pakasir, creem, paypal
  • currencystring
    ISO 4217 code, e.g. USD, IDR
  • billing_periodstring
    Default monthly
    One of: monthly, annual
  • coupon_codestring
    Case-insensitive

Responses

  • 200Payment created. With amount 0 (a 100 % coupon, or an upgrade credit that covers everything) nothing is collected: status is paid, pay_url is null and the plan is already active
    Response fields
    • pay_urlstring (uri) | nullrequired
    • order_idstringrequired
    • amountintegerrequired
      Minor units of currency
    • currencystringrequired
      ISO 4217 code, e.g. USD, IDR
    • providerstringrequired
      One of: pakasir, creem, paypal, none
    • statusstringrequired
      One of: pending, paid
    • overageintegerrequired
      Part of amount that is usage beyond the quota (minor units)
    • quoteobjectrequired
    • quote.plan_idintegerrequired
    • quote.plan_namestringrequired
    • quote.billing_periodstringrequired
      One of: monthly, annual
    • quote.monthsintegerrequired
      Months charged (12 months are delivered for an annual period)
    • quote.currencystringrequired
      ISO 4217 code, e.g. USD, IDR
    • quote.changestringrequired
      upgrade starts now with a credit; downgrade starts when the current period ends; renew stacks after it
      One of: new, renew, upgrade, downgrade
    • quote.list_amountintegerrequired
      Plan price × months charged
    • quote.monthly_equivalentintegerrequired
      What the same months would cost month by month
    • quote.discount_amountintegerrequired
    • quote.coupon_codestring | nullrequired
    • quote.proration_creditintegerrequired
      Unused part of what was paid for the current plan (upgrades only)
    • quote.overageintegerrequired
    • quote.amountintegerrequired
      What will be charged, minor units
    • quote.starts_atintegerrequired
    • quote.ends_atintegerrequired
  • 400VALIDATION_FAILED; COUPON_INVALID (with reason); CURRENCY_NOT_SUPPORTED when the plan has no price in a currency that provider accepts; or nothing to pay (free plan)
  • 502The payment service is unavailable; nothing was stored, retry shortly
  • 503No payment provider is configured
Request
curl -X POST "https://api.chatkomodo.com/v1/billing/checkout" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "plan_id": 2,
  "provider": "creem",
  "currency": "USD"
}'
const res = await fetch('https://api.chatkomodo.com/v1/billing/checkout', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "plan_id": 2,
    "provider": "creem",
    "currency": "USD"
  }),
});
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/billing/checkout",
    headers={
        "Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
    },
    json={
        "plan_id": 2,
        "provider": "creem",
        "currency": "USD",
    },
    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/billing/checkout', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
    'json' => [
        'plan_id' => 2,
        'provider' => 'creem',
        'currency' => 'USD',
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "pay_url": "https://example.com/file.jpg",
  "order_id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
  "amount": 150000,
  "currency": "IDR",
  "provider": "pakasir",
  "status": "pending",
  "overage": 1,
  "quote": {
    "plan_id": 1,
    "plan_name": "string",
    "billing_period": "monthly",
    "months": 1,
    "currency": "IDR",
    "change": "new",
    "list_amount": 150000,
    "monthly_equivalent": 1,
    "discount_amount": 12,
    "coupon_code": "482913",
    "proration_credit": 1,
    "overage": 1,
    "amount": 150000,
    "starts_at": 1767225600000,
    "ends_at": 1767225600000
  }
}

List invoices

GET/v1/billing/invoices

The 24 most recent invoices of the tenant.

Responses

  • 200Invoices
    Response fields
    • invoicesarray of objectrequired
    • invoices[].order_idstringrequired
    • invoices[].periodstringrequired
      YYYY-MM
    • invoices[].plan_idinteger | nullrequired
    • invoices[].amountintegerrequired
      Minor units of currency
    • invoices[].currencystringrequired
      ISO 4217 code, e.g. USD, IDR
    • invoices[].providerstringrequired
      none = nothing was collected (100 % coupon or credit)
      One of: pakasir, creem, paypal, none
    • invoices[].billing_periodstringrequired
      One of: monthly, annual
    • invoices[].monthsintegerrequired
    • invoices[].list_amountinteger | nullrequired
    • invoices[].discount_amountintegerrequired
    • invoices[].coupon_codestring | nullrequired
    • invoices[].proration_creditintegerrequired
    • invoices[].overage_amountintegerrequired
    • invoices[].plan_changestring | nullrequired
      One of: new, renew, upgrade, downgrade
    • invoices[].statusstringrequired
      pending, paid, expired, cancelled or failed
    • invoices[].created_atintegerrequired
    • invoices[].paid_atinteger | nullrequired
Request
curl "https://api.chatkomodo.com/v1/billing/invoices" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY"
const res = await fetch('https://api.chatkomodo.com/v1/billing/invoices', {
  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/billing/invoices",
    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/billing/invoices', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "invoices": [
    {
      "order_id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
      "period": "string",
      "plan_id": 1,
      "amount": 150000,
      "currency": "IDR",
      "provider": "pakasir",
      "billing_period": "monthly",
      "months": 1,
      "list_amount": 150000,
      "discount_amount": 12,
      "coupon_code": "482913",
      "proration_credit": 1,
      "overage_amount": 150000,
      "plan_change": "new",
      "status": "active",
      "created_at": 1767225600000,
      "paid_at": 1767225600000
    }
  ]
}

Check the payment of an invoice

POST/v1/billing/invoices/{order_id}/confirm

Ask the payment provider about one invoice (call it when the buyer returns from checkout, or when the webhook is late). Rate-limited to one real check per 4 seconds per invoice; faster calls answer pending with retry_after_s. A paid result activates the plan.

Path parameters

  • order_idstringrequired

Responses

  • 200Current state
    Response fields
    • statusstringrequired
      rejected = the payment does not match the invoice (amount, order, test mode) and was not activated; unverified = it cannot be verified automatically, contact support
      One of: paid, pending, rejected, unverified
    • retry_after_snumber
    • provider_statusstring
    • notestring
  • 404Invoice not found (NOT_FOUND)
Request
curl -X POST "https://api.chatkomodo.com/v1/billing/invoices/INV-20260101-0001/confirm" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY"
const res = await fetch('https://api.chatkomodo.com/v1/billing/invoices/INV-20260101-0001/confirm', {
  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/billing/invoices/INV-20260101-0001/confirm",
    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/billing/invoices/INV-20260101-0001/confirm', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "status": "paid",
  "retry_after_s": 0.5,
  "provider_status": "active",
  "note": "Prefers delivery after 5 pm"
}

Get the billing email settings

GET/v1/billing/notifications

Billing email of the tenant: invoices, payment receipts, "plan ends in 3 days" and "plan ended" are emailed there (as well as the WhatsApp reminder), in the language of the business profile.

Responses

  • 200Preferences
    Response fields
    • emailstring | nullrequired
      Where invoices, receipts and plan reminders go; null = none
    • enabledbooleanrequired
    • availablebooleanrequired
      Whether the platform has email set up at all
Request
curl "https://api.chatkomodo.com/v1/billing/notifications" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY"
const res = await fetch('https://api.chatkomodo.com/v1/billing/notifications', {
  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/billing/notifications",
    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/billing/notifications', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "email": "[email protected]",
  "enabled": true,
  "available": true
}

Update the billing email settings

PUT/v1/billing/notifications

Set the billing email (null or empty removes it) and/or switch billing emails on or off. Owner login only (403 for agents and API keys). Audited as billing.notifications.

Request body

  • emailstring | null
  • enabledboolean

Responses

  • 200Saved
    Response fields
    • emailstring | nullrequired
      Where invoices, receipts and plan reminders go; null = none
    • enabledbooleanrequired
    • availablebooleanrequired
      Whether the platform has email set up at all
  • 400VALIDATION_FAILED
  • 403Not the owner
Request
curl -X PUT "https://api.chatkomodo.com/v1/billing/notifications" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "[email protected]",
  "enabled": true
}'
const res = await fetch('https://api.chatkomodo.com/v1/billing/notifications', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "email": "[email protected]",
    "enabled": 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.put(
    "https://api.chatkomodo.com/v1/billing/notifications",
    headers={
        "Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
    },
    json={
        "email": "[email protected]",
        "enabled": True,
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())
<?php
require 'vendor/autoload.php';

$client = new GuzzleHttp\Client();
$res = $client->request('PUT', 'https://api.chatkomodo.com/v1/billing/notifications', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
    'json' => [
        'email' => '[email protected]',
        'enabled' => true,
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "email": "[email protected]",
  "enabled": true,
  "available": true
}