Skip to content

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.

  1. 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 parameters rather 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.

  2. 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}`)
    }
  3. The response is pending. Subscribe to message.delivered and message.failed and write the result against the order. See Receive messages.

{
"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.

{
"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.

{ "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.

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.

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.