Create templates
Use POST /v1/{phone_number_id}/templates to author a template
programmatically. On success Kirimdev persists the row with
status: "pending" and forwards the structure to Meta for review.
Poll GET /v1/{phone_number_id}/templates/{name} (or run
POST .../templates/sync) until Meta sets approved or
rejected.
This page covers creation. To send an already-approved template (including runtime header images), see Send templates and Header media at send time.
Body-only template
Section titled “Body-only template”curl -X POST "https://api.kirimdev.com/v1/$PHONE_ID/templates" \ -H "Authorization: Bearer $KIRIM_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "order_update", "category": "UTILITY", "language": "id", "components": [ { "type": "BODY", "text": "Pesanan {{1}} sedang diproses.", "example": { "body_text": [["A123"]] } } ] }'import { Kirim } from '@kirimdev/sdk'
const kirim = new Kirim({ apiKey: process.env.KIRIM_KEY! })
const template = await kirim .phoneNumbers(process.env.PHONE_ID!) .templates.create({ name: 'order_update', category: 'UTILITY', language: 'id', components: [ { type: 'BODY', text: 'Pesanan {{1}} sedang diproses.', example: { body_text: [['A123']] }, }, ], })
console.log(template.status) // "pending"A duplicate name + language for the account returns 409 template_already_exists before any Meta call.
Media header (IMAGE / VIDEO / DOCUMENT)
Section titled “Media header (IMAGE / VIDEO / DOCUMENT)”Meta requires a sample asset at creation time. Kirimdev accepts a
public https:// URL, downloads the file server-side, uploads it to
Meta via the Resumable Upload API, and submits the resulting asset
handle to Meta on your behalf. You only need your Kirim API key — no
direct Meta token required.
Preferred field: example.header_url
{ "name": "promo_gambar", "category": "MARKETING", "language": "id", "components": [ { "type": "HEADER", "format": "IMAGE", "example": { "header_url": ["https://cdn.example.com/banners/juni.jpg"] } }, { "type": "BODY", "text": "Halo {{1}}, ada promo spesial hari ini.", "example": { "body_text": [["Budi"]] } } ]}Backward-compatible alias: you may place the same public URL in
example.header_handle instead of header_url. Kirimdev detects
https:// values and treats them as URLs to fetch.
Requirements for sample URLs
Section titled “Requirements for sample URLs”| Constraint | Detail |
|---|---|
| Scheme | https:// (public internet; private IPs blocked) |
| Reachability | Kirimdev servers must be able to GET the URL once at create time |
| IMAGE | JPEG or PNG, ≤ 5 MB (WebP rejected by Meta for samples) |
| VIDEO | MP4 or 3GPP, ≤ 16 MB |
| DOCUMENT | PDF and common office formats, ≤ 100 MB |
| Content-Type | Served by the origin, or inferable from the file extension |
If the URL is unreachable, the wrong type, or too large, the API
returns 400 invalid_field_value with param: "components".
Already have a Meta handle?
Section titled “Already have a Meta handle?”If you uploaded the asset yourself via Meta’s Resumable Upload API, pass the handle verbatim:
"example": { "header_handle": ["4::aW1hZ2UvanBlZw==:YOUR_HANDLE"] }Kirimdev passes it through without re-uploading.
Errors
Section titled “Errors”| Situation | HTTP | error.code |
|---|---|---|
| Invalid body / Meta rejects template content | 400 | invalid_field_value |
| Bad or unreachable header media URL | 400 | invalid_field_value |
| Duplicate name + language (create) | 409 | template_already_exists |
| Editing a template still under review (edit) | 409 | template_not_editable |
| Meta unreachable / transient upstream fault | 502 | whatsapp_upstream_error |
Meta validation failures (wrong examples, format rules) surface as
400, not 502 — retrying the same payload will not succeed until the
components are fixed.
Edit a template
Section titled “Edit a template”Use PATCH /v1/{phone_number_id}/templates/{name} to change an
existing template’s content and re-submit it to Meta. The request body is
components-only — send the full desired components array. Meta
replaces all components at once, so this is a replacement, not a
partial patch: omitting a component removes it.
name, language, and category cannot change. When a name has
multiple languages, pass ?language= to pick which one to edit.
curl -X PATCH "https://api.kirimdev.com/v1/$PHONE_ID/templates/order_update?language=id" \ -H "Authorization: Bearer $KIRIM_KEY" \ -H "Content-Type: application/json" \ -d '{ "components": [ { "type": "BODY", "text": "Pesanan {{1}} sudah dikirim.", "example": { "body_text": [["A123"]] } } ] }'const template = await kirim .phoneNumbers(process.env.PHONE_ID!) .templates.update( 'order_update', { components: [ { type: 'BODY', text: 'Pesanan {{1}} sudah dikirim.', example: { body_text: [['A123']] }, }, ], }, { language: 'id' }, )
console.log(template.status) // "pending" — an edit re-enters reviewA successful edit puts the template back into Meta review, so the
returned status is pending; poll
GET /v1/{phone_number_id}/templates/{name} for the eventual
approved / rejected transition.
Media card carousels
Section titled “Media card carousels”A carousel template is a MARKETING message with 2–10 swipeable
product cards. Each card has its own image or video header, an optional
body text, and up to two buttons (quick reply, URL, or phone number). Add
a CAROUSEL component next to the message BODY:
{ "name": "rare_succulents_carousel", "language": "en_US", "category": "MARKETING", "components": [ { "type": "BODY", "text": "Rare succulents for sale!" }, { "type": "CAROUSEL", "cards": [ { "components": [ { "type": "HEADER", "format": "IMAGE", "example": { "header_url": ["https://cdn.example.com/aloe.jpg"] } }, { "type": "BUTTONS", "buttons": [ { "type": "QUICK_REPLY", "text": "Send more like this" }, { "type": "URL", "text": "Shop", "url": "https://shop.example.com/aloe" } ] } ] } // ...at least one more card, same structure ] } ]}Rules Meta enforces (validated up front):
- 2–10 cards, and the approved template always sends exactly that many.
- Every card shares the same structure — same header format and the same button types in the same order.
- All-or-nothing card body — if one card has body text, all must.
- Carousels are MARKETING-only and cannot carry a top-level
HEADER/FOOTER/BUTTONS(put those on the cards). - Provide each card header as
example.header_url(publichttps://URL); Kirimdev uploads it to Meta and keeps the URL so the template stays sendable. A dynamic card URL button ({{1}}suffix) needs anexamplevalue, just like a top-level dynamic URL button.
See Send templates for how to fill each card at send time.
Dashboard alternative
Section titled “Dashboard alternative”The Kirimdev dashboard can also create templates and accepts a media
library file via headerMediaId on the internal API. The Public API
does not expose headerMediaId; use example.header_url or create from
the dashboard UI instead.
Authentication (OTP) templates
Section titled “Authentication (OTP) templates”Category AUTHENTICATION is for one-time passwords. Meta auto-approves
these templates and generates the body text; you configure the security
disclaimer, optional expiry footer, and OTP button. Full create + send
examples live on OTP (authentication templates).
If Meta rejects creation, inspect the API error’s message. Kirimdev returns
Meta’s most specific available validation detail, which may identify an
invalid component, OTP button setting, example value, or another rejected
parameter. The generic Invalid parameter text is used only when Meta does
not provide a more precise explanation.