Errors
Error envelope, HTTP status codes, and which errors to retry
Every non-2xx response uses the same JSON envelope.
{
"defined": true,
"code": "CONFLICT",
"status": 409,
"message": "Conflict",
"data": {}
}| Field | Description |
|---|---|
defined | true for errors defined in the spec, false for unexpected ones. The shape is the same either way. |
code | Error code string (table below). Branch on code, not on message. |
status | Same as the HTTP status code. |
message | Human-readable description. Wording may change. |
data | Optional details. Defined for 402 (data.code) and 429 (data.limit, data.policy, data.retryAfterMs). |
Failed verifications on POST /v1/verify (wrong code, expired, …) are not errors: they return 200 with verified: false. See Verification results.
Status codes
| Status | code | Typical causes | Retry? |
|---|---|---|---|
| 400 | BAD_REQUEST | Invalid payload, missing/blank idempotency key, header/body key mismatch, unknown template or template variables, expiresInSec/maxAttempts/cost out of range, code not 6 digits, invalid limit/cursor/date range | No. Fix the request. |
| 401 | UNAUTHORIZED | Authorization: Bearer key missing, malformed, inactive (revoked/expired), or unrecognized | No. Check the key. |
| 402 | PAYMENT_REQUIRED | Out of credit; data.code is INSUFFICIENT_CREDIT or OVERDRAFT_LIMIT_EXCEEDED (POST /v1/issue only) | After topping up |
| 403 | FORBIDDEN | Key lacks the required scope, a pk_ key called a server-only API, or a pk_ request had no Origin / an unlisted Origin | No. Check the key settings. |
| 404 | NOT_FOUND | Unknown issueId for your app (including other apps' ids) or unknown templateId | No |
| 409 | CONFLICT | Idempotency key reused with a different body, or the key's issue has since been replaced | No |
| 429 | TOO_MANY_REQUESTS | POST /v1/issue or POST /v1/verify exceeded a per-key, per-IP or per-phone rate limit (see Rate limits); data.limit is perKey, perIp or perPhone (verify only perKey) | Yes, after Retry-After / data.retryAfterMs, with the same idempotency key |
| 500 | INTERNAL_SERVER_ERROR | Unexpected server failure | Yes, with backoff and the same idempotency key |
| 503 | SERVICE_UNAVAILABLE | Authentication or an internal dependency temporarily unavailable, the rate limiter unavailable for a pk_ key (fails closed, Retry-After set), or an earlier request with the same idempotency key is still being resolved | Yes, after Retry-After when sent, else with backoff; same idempotency key |
Test mode adds two 400 BAD_REQUEST causes: data.code TEST_NUMBER_REQUIRED (a test key sent a real number) and TEST_NUMBER_IN_LIVE_MODE (a live key sent a magic test number). See Test mode.
Issue has two more 400 BAD_REQUEST causes, in data.code: CHANNEL_NOT_SUPPORTED (channel: "sms" was requested; only alimtalk is accepted) and TEMPLATE_NOT_AVAILABLE (the template has no approved AlimTalk template yet, see Channels and SMS fallback).
Rate limits: 429 and Retry-After
A request over a rate limit is rejected with 429 TOO_MANY_REQUESTS before anything is sent, and it does not consume the limit (or a verification attempt).
{
"defined": true,
"code": "TOO_MANY_REQUESTS",
"status": 429,
"message": "Too Many Requests",
"data": { "limit": "perPhone", "policy": "key", "retryAfterMs": 1500 }
}| Field | Meaning |
|---|---|
data.limit | The rule that rejected the request: perKey, perIp or perPhone (perIp and perPhone apply to issue only). |
data.policy | key for the rule set on the API key, platform for the pk_ default or the platform ceiling. |
data.retryAfterMs | Milliseconds until a retry can be admitted. |
Retry-After | The same wait in seconds, rounded up (at least 1). It is also readable from allowed browser origins, so a pk_ browser client can honor it. |
Wait that long, then retry issue with the same idempotency key.
503 SERVICE_UNAVAILABLE carries Retry-After only when the rate limiter itself is unavailable and the request failed closed (pk_ keys); other 503 causes send no header, so retry with backoff. Both headers are listed per endpoint in the API reference.
Per-endpoint error descriptions are on each page of the API reference.
Handling example
type ApiError = {
defined: boolean;
code: string;
status: number;
message: string;
data?: { code?: "INSUFFICIENT_CREDIT" | "OVERDRAFT_LIMIT_EXCEEDED"; retryAfterMs?: number };
};
function handleIssueError(error: ApiError) {
switch (error.code) {
case "PAYMENT_REQUIRED":
// Alert operators: top-up needed (error.data?.code)
return "Please try again later.";
case "CONFLICT":
// Check whether an idempotency key was reused for a different request
return "The request could not be processed.";
case "TOO_MANY_REQUESTS":
// Wait error.data?.retryAfterMs (Retry-After header), then retry with the same key
return "Too many attempts. Please wait a moment.";
case "SERVICE_UNAVAILABLE":
case "INTERNAL_SERVER_ERROR":
// Retry with the same idempotency key (/guides/idempotency)
return "Please try again later.";
default:
return "We could not send a verification code.";
}
}Console guide
Managing apps, keys, usage, and credits in the console
SDKs
Official SDKs for K-OTP. The JavaScript and TypeScript SDK is one package with imports for browsers, servers, React, Vue, Svelte, and UI components (`npm i @k-otp/sdk`); preview server SDKs cover C#, Java, Kotlin, PHP, Go, Python, Ruby, and Dart.