Credits & billing
Prepaid credits, 402 when credit runs out, balance and ledger endpoints
K-OTP uses prepaid credits. You top up your organization's wallet in the console, and each send spends one credit. Credits are a quantity, not money; currency in API responses is always CREDIT.
- Wallets are per organization. All apps of an organization (and all of their keys) share one wallet; usage and history stay per app.
- Top-ups happen in the console and are made by organization owners and admins. The public API cannot add credits.
One credit per send
Every OTP send costs exactly 1 credit: one issue's delivery, whatever the channel. A KakaoTalk AlimTalk send costs 1 credit whether AlimTalk delivers the code or the SMS fallback does; the fallback never costs a second credit, and turning it off with smsFallback: false does not lower the price. See Channels and SMS fallback.
Credit packs
| Pack | Credits |
|---|---|
| Mini | 250 (no bonus) |
| Starter | 1,250 (1,000 + 250 bonus) |
| Scale | 6,250 (5,000 + 1,250 bonus) |
Starter and Scale come with 25% bonus credits on top of their base credits. Credits bought earlier keep the pack they were sold with. There is no subscription.
Current prices are on the pricing page. USD prices exclude applicable taxes (such as VAT, GST or sales tax): Creem, the merchant of record, calculates them for your location and shows the total at checkout. The Korean console also offers KRW card payment (processed by Creb); see the Korean pricing page for KRW prices, which exclude the 10% VAT added at checkout.
How debits work
- At
POST /v1/issue, 1 credit is reserved. If the balance is insufficient, the request is rejected here. - Billing then settles asynchronously on the messaging provider's answer. A send is accepted when the provider takes the message for delivery, and refused when the provider rejects the request, so nothing goes out.
- accepted: the reservation is debited;
- refused, or the send could not be queued: the reservation is released and nothing is debited.
- If, after accepting a send, the provider confirms that delivery failed, or no delivery result arrives within the tracking window, the debited credit is returned (a
refundentry in the ledger;billingStatusbecomesreleased). The exception is an issue whose code was verified: verifying proves the message arrived, so it counts as delivered and stays charged even without a delivery result (since API 2.1.1). - The issue response contains no balance or debit amount. Use the endpoints below when you need them.
Per-issue billing state (billingStatus) is available from GET /v1/issues/{issueId}.
billingStatus | Meaning |
|---|---|
reserved | Credit reserved at issue |
debit_pending → debited | Debit in progress → debited |
release_pending → released | Release in progress → released (nothing charged; a debited credit is returned with a refund entry) |
Out of credit: 402 PAYMENT_REQUIRED
If the balance is insufficient, the issue is rejected with 402 and nothing is sent. data.code tells you why.
{
"defined": true,
"code": "PAYMENT_REQUIRED",
"status": 402,
"message": "PAYMENT_REQUIRED",
"data": { "code": "INSUFFICIENT_CREDIT" }
}data.code | Meaning |
|---|---|
INSUFFICIENT_CREDIT | Not enough credit. |
OVERDRAFT_LIMIT_EXCEEDED | The organization's allowed overdraft limit was exceeded. |
Top up, then retry. Show end users a generic "please try again later" message and alert your operators about the low balance.
Balance
GET /v1/balance (sk_ key, otp:balance) returns the balance of the organization's wallet, the same for every app of the organization. appId is the calling app; walletId is org:<organizationId> and walletScope is organization for the shared wallet (organizationId is then present too). A legacy per-app wallet, kept only until the transition to organization wallets completes, reports app:<appId> and app.
curl "https://api.k-otp.dev/v1/balance" \
-H "Authorization: Bearer $KOTP_SECRET_KEY"{
"appId": "app_...",
"walletId": "org:...",
"walletScope": "organization",
"organizationId": "...",
"balance": 1200,
"currency": "CREDIT",
"updatedAt": "2026-09-28T03:00:05.000Z"
}Ledger
GET /v1/credit-ledger (sk_ key, otp:ledger:read) returns changes of the organization's wallet, newest first: the wallet's top-ups and clawbacks (refund withdrawals) plus the charges and refunds of the calling app. Other apps' charges and refunds are not listed (the console shows the whole wallet).
entryType | Meaning | amountDelta |
|---|---|---|
credit | Top-up | positive |
debit | Issue charge | negative |
refund | Refund | positive |
clawback | Clawback of a top-up (refund of a purchase) | negative |
balanceAfteris the wallet's balance after the entry. Because other apps' charges are not listed, the difference between two entries can be larger than theiramountDelta. It can be negative for organizations allowed to overdraft.appIdis the attribution app: always the calling app fordebitandrefund, and present on walletcredit/clawbackentries only when the purchase was attributed to an app.issueIdis present only on issue-linked entries (debit,refund).- Pagination:
limit(1–100, default 50); pass the response'snextCursorunchanged as the nextcursor. NonextCursormeans the last page. Keep the same filters while paging. - Filters:
entryType,createdFrom(inclusive),createdTo(exclusive), both RFC 3339.
const API = "https://api.k-otp.dev/v1";
export async function* listLedger(createdFrom: string) {
let cursor: string | undefined;
do {
const params = new URLSearchParams({ limit: "100", createdFrom });
if (cursor) params.set("cursor", cursor);
const res = await fetch(`${API}/credit-ledger?${params}`, {
headers: { Authorization: `Bearer ${process.env.KOTP_SECRET_KEY}` },
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.code}: ${body.message}`);
yield* body.items;
cursor = body.nextCursor;
} while (cursor);
}Refunds
Unused credits can be withdrawn within 7 days of purchase. A partly used pack is refunded in proportion to its remaining credits excluding the bonus credits: amount paid × (remaining credits − bonus credits) ÷ base credits; bonus credits are not refundable. Remaining credits are counted on the organization's wallet, which all of its apps share. See the refund policy.