Skip to content
TypeScript SDK

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.

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)

Every KirimError instance carries:

PropertyTypeMeaning
codestringStable identifier from the error catalogue. Branch on this.
typeKirimErrorTypeBroad category — matches the subclass.
statusnumberHTTP status (0 for ConnectionError).
messagestringHuman-readable. May evolve; do not branch on text.
requestIdstring | nullThe X-Request-Id from the response. Quote in support tickets.
paramstring | undefinedField hint when validation failed (e.g. 'to', 'template.name').
rawunknownDecoded body for debugging.

RateLimitError additionally exposes retryAfter: number | null (seconds the server asked you to wait).

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
}

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.

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 classAuto-retried?Notes
InvalidRequestErrorNoDeterministic — your payload is wrong.
AuthenticationErrorNoFix the key, don’t burn the budget.
PermissionErrorNoWon’t change on retry.
NotFoundErrorNoWon’t change on retry.
ConflictErrorNoYou control the conflict (e.g. idempotency key).
RateLimitErrorYesHonors Retry-After.
ApiServerErrorYesExponential backoff with jitter.
ConnectionErrorYesExponential backoff with jitter.

Need stricter control?

// Disable retries entirely on the client
const kirim = new Kirim({ apiKey: process.env.KIRIM_KEY!, maxRetries: 0 })
// Or per-call:
await phone.messages.send(payload, { maxRetries: 0 })

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 here
try {
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.

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.