SDK Errors
Every failed call from @kirimdev/sdk throws a typed subclass of
KirimError. Branch with instanceof for coarse categorization,
inspect code for the specific stable identifier, and quote
requestId when opening a support ticket. Network failures (DNS, TCP,
timeout, abort) throw ConnectionError — same base class.
Class hierarchy
Section titled “Class hierarchy”KirimError (base — every thrown error extends this)├─ InvalidRequestError 400, 422 validation / malformed payload├─ AuthenticationError 401 missing / invalid / revoked key├─ PermissionError 403 plan limit / scope / suspended org├─ NotFoundError 404 resource id does not exist├─ ConflictError 409 idempotency conflict / state clash├─ RateLimitError 429 carries `retryAfter` in seconds├─ ApiServerError 5xx upstream or kirim.dev internal└─ ConnectionError — no response (DNS / TCP / timeout / abort)Common properties
Section titled “Common properties”Every KirimError instance carries:
| Property | Type | Meaning |
|---|---|---|
code | string | Stable identifier from the error catalogue. Branch on this. |
type | KirimErrorType | Broad category — matches the subclass. |
status | number | HTTP status (0 for ConnectionError). |
message | string | Human-readable. May evolve; do not branch on text. |
requestId | string | null | The X-Request-Id from the response. Quote in support tickets. |
param | string | undefined | Field hint when validation failed (e.g. 'to', 'template.name'). |
raw | unknown | Decoded body for debugging. |
RateLimitError additionally exposes retryAfter: number | null
(seconds the server asked you to wait).
Recipe — branch by class
Section titled “Recipe — branch by class”import { Kirim, KirimError, InvalidRequestError, AuthenticationError, PermissionError, NotFoundError, ConflictError, RateLimitError, ApiServerError, ConnectionError,} from '@kirimdev/sdk'
const kirim = new Kirim({ apiKey: process.env.KIRIM_KEY! })const phone = kirim.phoneNumbers('106540352242922')
try { await phone.messages.send({ messaging_product: 'whatsapp', to: '+628111222333', type: 'text', text: { body: 'Halo' }, })} catch (err) { if (err instanceof InvalidRequestError) { // Bad payload. err.param points at the offending field. logger.warn({ code: err.code, param: err.param }, err.message) return showFormError(err.param ?? 'unknown', err.message) }
if (err instanceof AuthenticationError) { // Key is missing / invalid / revoked / expired. return rotateApiKey() }
if (err instanceof PermissionError) { // Plan does not include API access, or org is suspended. return promptUpgrade() }
if (err instanceof NotFoundError) { // The resource id does not exist (or you can't see it). return null }
if (err instanceof ConflictError) { // e.g. idempotency_in_progress — retry the same key later. await sleep(1_000) return retry() }
if (err instanceof RateLimitError) { // The SDK already auto-retried; you saw this only after retries // were exhausted. Honor retryAfter or back off further. await sleep((err.retryAfter ?? 5) * 1_000) return retry() }
if (err instanceof ApiServerError) { // 5xx. Already auto-retried. Alert ops. alerts.fire('kirim_api_error', { requestId: err.requestId, code: err.code }) throw err }
if (err instanceof ConnectionError) { // Network failure with no response. Already auto-retried. metrics.increment('kirim.connection_error') throw err }
// Defensive fallback — future subclasses still extend KirimError. if (err instanceof KirimError) { logger.error({ err }, 'unhandled KirimError subclass') } throw err}Recipe — branch by stable code
Section titled “Recipe — branch by stable code”For finer-grained handling (e.g. distinguish invalid_phone_number
from invalid_template_name), branch on err.code:
try { await phone.messages.send(payload)} catch (err) { if (!(err instanceof KirimError)) throw err
switch (err.code) { case 'invalid_phone_number': return showValidation('to', 'Phone must be E.164 (e.g. +628…).') case 'invalid_template_name': return showValidation('template', 'Template not found in your org.') case 'whatsapp_number_not_verified': return promptConnectWhatsApp() case 'whatsapp_upstream_error': // Meta hiccup — already retried by the SDK; alert if persistent. return scheduleRetry(err.requestId) case 'rate_limit_exceeded': // The SDK exhausted its retries. Back off further. return sleep(((err as RateLimitError).retryAfter ?? 5) * 1_000) default: logger.error({ err }, 'unhandled kirim error') throw err }}The full catalogue lives in the Errors concept page — codes are stable forever; message text may evolve.
What the SDK retries for you
Section titled “What the SDK retries for you”You won’t normally see RateLimitError / ApiServerError /
ConnectionError on the first failure — the SDK transparently retries
each up to maxRetries times (default 2) with exponential backoff
plus full jitter, capped at 8 seconds per attempt. Retry-After
(seconds or HTTP-date) is honored for 429.
These errors only surface after the retry budget is exhausted.
| Error class | Auto-retried? | Notes |
|---|---|---|
InvalidRequestError | No | Deterministic — your payload is wrong. |
AuthenticationError | No | Fix the key, don’t burn the budget. |
PermissionError | No | Won’t change on retry. |
NotFoundError | No | Won’t change on retry. |
ConflictError | No | You control the conflict (e.g. idempotency key). |
RateLimitError | Yes | Honors Retry-After. |
ApiServerError | Yes | Exponential backoff with jitter. |
ConnectionError | Yes | Exponential backoff with jitter. |
Need stricter control?
// Disable retries entirely on the clientconst kirim = new Kirim({ apiKey: process.env.KIRIM_KEY!, maxRetries: 0 })
// Or per-call:await phone.messages.send(payload, { maxRetries: 0 })HTTP error ≠ delivery failure
Section titled “HTTP error ≠ delivery failure”A successful messages.send call only means kirim.dev accepted the
request and queued it for Meta. The message can still fail to deliver
later — that surfaces as a message.status_updated webhook with
status: 'failed', not an exception in your try/catch.
// HTTP-level errors: caught heretry { const msg = await phone.messages.send(payload) // msg.status === 'queued' — Meta hasn't seen it yet} catch (err) { // ... 4xx / 5xx / network}
// Delivery-level failures: arrive via webhook// See /guides/failed-sends/ and /sdks/typescript/webhooks/Read Handling Failed Sends for the full story on async delivery failures.
Logging for support
Section titled “Logging for support”For support tickets, log requestId and code:
catch (err) { if (err instanceof KirimError) { logger.error( { code: err.code, type: err.type, status: err.status, requestId: err.requestId, param: err.param, }, err.message, ) } throw err}requestId matches the X-Request-Id response header and is the
fastest way for kirim.dev support to locate your call in our logs.