Send a template message
WhatsApp lets a business send free text only within 24 hours of the customer’s last message. Outside that window, which is where almost every notification lives, you need an approved template.
-
Find the template and what it needs
Section titled “Find the template and what it needs”Terminal window curl "https://k-message.kerneltics.com/v1/templates?search=order_shipped&status=approved" \-H "Authorization: Bearer km_live_YOUR_KEY"{"data": [{"name": "order_shipped","language": "ar","status": "APPROVED","body": "مرحبًا {{name}}، طلبك في الطريق. تتبعه برقم {{tracking}}.","parameters": ["name", "tracking"],"header_parameters": []}]}Read
parametersrather than assuming. A template edited in the dashboard changes what it expects, and code with hard-coded variable names starts sending messages with gaps in them without failing. -
Send it
Section titled “Send it”const res = await fetch('https://k-message.kerneltics.com/v1/messages', {method: 'POST',headers: {Authorization: `Bearer ${process.env.KM_API_KEY}`,'Content-Type': 'application/json','Idempotency-Key': `order-${order.id}-shipped`,},body: JSON.stringify({to: order.customerPhone,type: 'template',template: {name: 'order_shipped',parameters: { name: order.customerName, tracking: order.trackingNumber },},}),})if (!res.ok) {const { error } = await res.json()// Branch on code, never on the message text.throw new Error(`${error.code}: ${error.message}`)} -
Record the outcome
Section titled “Record the outcome”The response is
pending. Subscribe tomessage.deliveredandmessage.failedand write the result against the order. See Receive messages.
The three ways this goes wrong
Section titled “The three ways this goes wrong”A missing variable
Section titled “A missing variable”{ "error": { "code": "missing_template_parameter", "message": "Template \"order_shipped\" is missing the parameter(s): tracking. It expects: name, tracking.", "param": "parameters" }}We check this because WhatsApp does not. Meta substitutes a missing variable with nothing, delivers the message with a gap in the sentence, and reports success. This is the only place it can be caught.
The template is not approved
Section titled “The template is not approved”{ "error": { "code": "template_not_approved", "message": "Template \"order_shipped\" is in status PENDING and cannot be sent until Meta approves it." }}PENDING means wait. REJECTED means fix it in the dashboard and resubmit.
Subscribe to template.approved if you want to know the moment it clears.
The contact opted out
Section titled “The contact opted out”{ "error": { "code": "marketing_opt_out", "message": "This contact has opted out of marketing messages." } }Only for templates whose category is MARKETING. Utility and authentication
templates still reach them. This is refused as a matter of law, not preference.
Header media
Section titled “Header media”For a template whose header is an image, video or document:
{ "to": "+966500000000", "type": "template", "template": { "name": "invoice_ready", "parameters": { "name": "Sara", "amount": "450 SAR" }, "header_media": { "link": "https://example.com/invoices/1001.pdf", "filename": "Invoice-1001.pdf" } }}We fetch the link and upload it to WhatsApp. If you already have a Meta media
id, pass { "id": "..." } instead and skip the upload.
filename matters for documents: WhatsApp shows it to the recipient, so
Invoice-1001.pdf reads better than tmp7f3a.pdf.
Authentication templates
Section titled “Authentication templates”For a one-time code with a copy-code button, pass the code as parameter "1"
and the button fills itself:
{ "to": "+966500000000", "type": "template", "template": { "name": "verification_code", "parameters": { "1": "482913" } }}Meta requires the code as both a body parameter and a button parameter, which callers reliably forget because from the outside there is only one code. It is derived for you.