Send Your First Message
A practical guide to POST /v1/messages covering every supported
content type and the gotchas you’ll hit in production.
Pre-requisites
Section titled “Pre-requisites”- A Kirim API key (see Quickstart).
- At least one connected WhatsApp Business account in your organisation.
- For free-form messages: an open 24-hour conversation window with the recipient (i.e. they’ve messaged you in the last 24 h).
- For template messages: the template name + language, approved by
Meta. Browse yours via
GET /v1/templates.
Multi-account orgs: pass from
Section titled “Multi-account orgs: pass from”If your organisation has more than one connected WhatsApp number, you must specify which one to send from:
{ "from": "+628511760008029", "to": "+628111111111", "type": "text", "text": { "body": "..." }}Single-account orgs may omit from; Kirim auto-resolves the only
connected number. If you have zero connected numbers, the call
returns 422 whatsapp_number_not_verified.
{ "to": "+628111111111", "type": "text", "text": { "body": "Halo dari Kirim" }}| Field | Notes |
|---|---|
text.body | 1-4096 characters. Plain text only — Meta strips most formatting except *bold*, _italic_, ~strike~, and `code`. |
Template
Section titled “Template”For sending outside the 24-hour window or for transactional notifications (OTP, order updates), use a template:
{ "to": "+628111111111", "type": "template", "template": { "name": "order_confirmation", "language": "id_ID", "components": [ { "type": "body", "parameters": [ { "type": "text", "text": "John" }, { "type": "text", "text": "ORD-12345" } ] } ] }}components follows Meta’s exact shape — Kirim forwards verbatim. See
Meta’s template message
docs
for the full schema.
Media (image, document, video, audio)
Section titled “Media (image, document, video, audio)”{ "to": "+628111111111", "type": "image", "image": { "url": "https://your-cdn.example.com/screenshot.jpg", "caption": "Lihat tangkapan layar" }}| Field | Notes |
|---|---|
image.url | Public HTTPS URL Meta can fetch from. Must be served as the correct Content-Type. |
image.caption | Optional, max 1024 characters. |
Same shape for document (allows extra filename field), video,
audio. Maximum sizes per Meta:
| Type | Max size |
|---|---|
image | 5 MB (JPEG, PNG) |
document | 100 MB (PDF, DOCX, XLSX, …) |
video | 16 MB (MP4, 3GPP) |
audio | 16 MB (AAC, M4A, MP3, OPUS, AMR) |
Interactive (cta_url)
Section titled “Interactive (cta_url)”{ "to": "+628111111111", "type": "interactive", "interactive": { "type": "cta_url", "body": { "text": "Klik untuk melacak pesanan Anda." }, "action": { "name": "cta_url", "parameters": { "display_text": "Lacak Pesanan", "url": "https://your-app.example.com/orders/12345" } } }}Interactive (list)
Section titled “Interactive (list)”{ "to": "+628111111111", "type": "interactive", "interactive": { "type": "list", "header": { "type": "text", "text": "Pilih layanan" }, "body": { "text": "Layanan apa yang Anda butuhkan?" }, "footer": { "text": "Pilih satu" }, "action": { "button": "Lihat opsi", "sections": [ { "title": "Bantuan", "rows": [ { "id": "billing", "title": "Tagihan", "description": "Tanya soal tagihan" }, { "id": "tech", "title": "Teknis", "description": "Bug, error, kesulitan" } ] } ] } }}The customer’s selection arrives back as a message.received event
with type: "interactive" containing the chosen id.
Response shape
Section titled “Response shape”Every successful send returns:
{ "data": { "id": "msg_01HXYZABCDEFGHJKMNPQRSTVWX", "object": "message", "to": "+628111111111", "type": "text", "status": "pending", "created_at": "2026-05-24T08:00:00.000Z" }, "request_id": "req_…"}status starts at pending and transitions via webhook callbacks
(message.status events) through sent → delivered → read. Set
up webhooks rather than polling.
Handle failures
Section titled “Handle failures”If the send hits a fatal upstream error (recipient blocked, template
disabled, outside 24 h window for text), the message persists with
status: "failed" and an error block:
{ "data": { "id": "msg_…", "status": "failed", "error": { "code": "outside_24h_window", "message": "Message failed to send because more than 24 hours...", "provider_code": 131047 } }}The structured error.code is one of the stable codes from the
error catalogue. Use it to drive retry or
fallback logic — e.g. on outside_24h_window, fall back to a
template:
async function sendWithFallback(payload: TextSendBody) { const res = await send(payload) if (res.data.status === 'failed' && res.data.error?.code === 'outside_24h_window') { return send({ to: payload.to, type: 'template', template: { name: 'reengagement', language: 'id_ID' }, }) } return res}Idempotent retries
Section titled “Idempotent retries”Every POST /v1/messages should carry an Idempotency-Key so a
network blip doesn’t send the message twice:
curl https://api-kckit.kirim.chat/v1/messages \ -H "Authorization: Bearer $KIRIM_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8e1a2c30-f0a4-4c70-9c2d-7b5e3aef9201" \ -d '{"to":"+628111111111","type":"text","text":{"body":"Halo"}}'See Idempotency for the full contract.