Idempotency
Retry without duplicating sends. Read →
kirim.dev applies per-organisation, per-endpoint-class token-bucket rate limits. Limits are enforced atomically by a Redis Lua script — no race conditions across replicas, no surprise bursts.
| Class | HTTP methods | Examples |
|---|---|---|
write | POST, PUT, PATCH, DELETE | Send a message, attach a label, replay a webhook delivery |
read | GET, HEAD | List conversations, fetch a message, introspect via /v1/me |
Each class has its own bucket. Sending 60 messages does not eat into your read budget, and vice versa.
| Plan | Write / min | Read / min |
|---|---|---|
| Default (free) | 60 | 600 |
| Pro | 600 | 6 000 |
| Enterprise | configurable | configurable |
Upgrade your plan in the dashboard to lift limits. Enterprise plans get per-key overrides on request — contact support.
Every successful response and every 4xx error carries the same three headers so client code can self-throttle without parsing the body:
X-RateLimit-Limit: 60X-RateLimit-Remaining: 47X-RateLimit-Reset: 1716480000| Header | Meaning |
|---|---|
X-RateLimit-Limit | Bucket capacity for the class you just hit, in requests per minute. |
X-RateLimit-Remaining | Tokens currently available (floored to integer). |
X-RateLimit-Reset | Unix epoch seconds when the bucket would be full at the current refill rate. |
On a 429 there is one extra header:
Retry-After: 12Retry-After is the number of seconds until at least one token
becomes available. Sleep at least that long before retrying.
import { Kirim, RateLimitError } from '@kirimdev/sdk'
// The SDK retries 429 automatically with jittered exponential backoff.// Configure the budget if your workload needs more headroom.const kirim = new Kirim({ apiKey: process.env.KIRIM_KEY!, maxRetries: 5, // default 3 retryBaseDelayMs: 500, // default 250})
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 RateLimitError) { // Budget exhausted — persist for offline processing. await queue.enqueue({ retryAfterMs: err.retryAfterMs }) } throw err}send() { curl -sS -w "\n%{http_code}" \ -X POST "https://api.kirim.dev/v1/106540352242922/messages" \ -H "Authorization: Bearer $KIRIM_KEY" \ -H "Content-Type: application/json" \ -D /tmp/headers \ -d '{ "messaging_product": "whatsapp", "to": "+628111222333", "type": "text", "text": { "body": "Halo!" } }'}
for attempt in 1 2 3 4 5; do RESP=$(send) STATUS=$(echo "$RESP" | tail -n1) if [ "$STATUS" -ne 429 ]; then echo "$RESP" | sed '$d' break fi DELAY=$(awk 'tolower($1)=="retry-after:" { print $2 }' /tmp/headers | tr -d '\r') # Add 0.75x–1.25x jitter so retries don't synchronise. sleep $(awk -v d="$DELAY" 'BEGIN { srand(); print d * (0.75 + rand()*0.5) }')doneimport random, time, httpx
def send_with_backoff(client: httpx.Client, payload: dict, max_attempts: int = 5): for attempt in range(max_attempts): resp = client.post( "https://api.kirim.dev/v1/106540352242922/messages", json=payload, ) if resp.status_code != 429: resp.raise_for_status() return resp.json()
retry_after = int(resp.headers.get("retry-after", "1")) jitter = 0.75 + random.random() * 0.5 # 0.75x–1.25x time.sleep(retry_after * jitter)
raise RuntimeError("Rate-limit retry budget exhausted")Remaining allows. The bucket
refills continuously, but bursting it to zero and immediately
retrying just trades a 429 for another 429.Retry-After. It’s accurate. Retrying earlier
guarantees a second 429 and consumes attempts you’ll need later.Retry-After by 0.75–1.25 before sleeping.