Skip to content

Send a notification

A notification is a templated WhatsApp message sent from a MauKirim-managed number. It is the right tool for “your order shipped”, “your invoice is ready”, “your appointment is tomorrow” — messages you initiate, where the content is known in advance.

Notifications are template-bound on purpose: an operator approves a template for your account, and every send names it. That is what keeps a compromised key from broadcasting arbitrary text from MauKirim’s number.

Terminal window
curl -X POST https://app.maukirim.com/api/v1/messages/send \
-H "Authorization: Bearer mk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-8821-shipped" \
-d '{
"phone": "+6281234567890",
"templateId": "tpl_…",
"variables": { "name": "Rizky", "order": "8821" }
}'
{ "ok": true, "batchId": "gwb_…" }
Field Required Notes
phone yes E.164, separators stripped
templateId yes A template that exists, is approved, and belongs to your account
variables yes An object (not an array) whose keys equal the template’s placeholder set exactly. Each value is a string, or an array of objects for a list section.
attachmentUploadId only for per-send templates The media.id from a private upload to /api/v1/messages/media. Omit it for text-only and fixed-attachment templates.

The body must be application/json and at most 8 KB. A missing Content-Type is a 400, not a guess.

If the template is Hi {{name}}, order {{order}} shipped. then variables must have exactly name and order — same names, same count. A missing key, an extra key, or a typo is refused with 400 invalid_input before anything is sent, so you cannot half-render a message.

Each scalar value is a string of at most 64 characters matching letters, digits, and _ .,:!{}@+/-()&. Invoice numbers such as 3260/VII/FDA/2026, common email addresses such as billing+ap@example.com, names such as Rina (PIC billing), and companies such as PT A & B render unchanged. These limits bound inserted text; they do not sanitize URLs that fit the allowed characters. Supply trusted values only. If you need arbitrary text, send it from your own rented number — see Send a message from your number.

Repeat a list of line items inside one message

Section titled “Repeat a list of line items inside one message”

A template can also repeat part of itself. A list section is the block between {{#name}} and {{/name}}, and the variable named there is an array of objects instead of a string. This repeats content inside one message to one recipient; it is not the recipient loop further down.

Say the template’s body is:

Order {{order}}:
{{#items}}- {{name}} x{{quantity}}
{{/items}}

An operator still has to approve a template before any send, and editing it — including adding a section — returns it to review. Once approved, variables holds order as a string and items as an array, one object per rendered line. Each object’s keys must be exactly the placeholders used inside the section:

Terminal window
curl -X POST https://app.maukirim.com/api/v1/messages/send \
-H "Authorization: Bearer mk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-8821" \
-d '{
"phone": "+6281234567890",
"templateId": "tpl_…",
"variables": {
"order": "8821",
"items": [
{ "name": "Kopi luwak", "quantity": "2" },
{ "name": "Teh botol", "quantity": "3" }
]
}
}'

That is one message and one charge — the header line followed by the two item lines — not two notifications. The same array shape works from the dashboard’s Notification → Integration & test form and from the MCP tools below.

The scalar rules above still apply, plus these:

  • A list holds 0–10 objects. An empty array renders the section with no lines; a text-only notification still needs some text outside that section. An attachment can be sent without a caption.
  • Every object carries every placeholder in its section and nothing else. A missing or stray key is a 400 invalid_input before anything is sent.
  • Every field is still a string matching ^[\p{L}\p{N}_ .,:!{}@+\/()&-]{1,64}$; keep quantities and numbers as text.
  • A template may declare several lists with different names, at most 8 top-level names in total; sections never nest, section names are unique, and each list binds at most 8 fields.
  • The rendered message is limited to 4096 characters, or 1024 characters as an attachment caption.

The template summary reports lists as { name, fields }[] and variables as all top-level names — every scalar placeholder and every list name — so you can build the payload without guessing.

Send different variables to several recipients

Section titled “Send different variables to several recipients”

This is a different axis from the list section above: a list section repeats content inside one message, while this loop sends one message per recipient, each with its own charge.

In the dashboard, open Notification → Integration & test → Send to multiple recipients. Choose one approved template, then add up to 10 rows. Each row has its own phone and values for that template’s placeholders. MauKirim queues and charges each row separately. It locks queued rows on the page so a retry sends only failed rows.

The REST endpoint still takes one recipient per request. For an integration, loop over the recipients and give each send its own stable idempotency key:

for (const recipient of [
{ phone: "+6281234567890", variables: { name: "Rizky", order: "8821" }, key: "shipment-8821-rizky" },
{ phone: "+6281234567891", variables: { name: "Ayu", order: "8822" }, key: "shipment-8822-ayu" },
]) {
const response = await fetch("https://app.maukirim.com/api/v1/messages/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MAUKIRIM_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": recipient.key,
},
body: JSON.stringify({
phone: recipient.phone,
templateId: "tpl_…",
variables: recipient.variables,
}),
})
const result = await response.json()
console.log({ queued: response.ok && result.ok, batchId: result.batchId, code: result.code })
}

A failure on one row does not cancel rows already queued. Keep the same key and unchanged payload when retrying a row whose result is uncertain; a successful replay returns its original batchId without another charge. The dashboard keeps keys while that page is open, not across a reload.

An MCP client with notification scope can call send_notification_loop with confirm: true, an approved templateId, and 1–10 {phone, variables, idempotencyKey} entries. The user must approve every phone and the per-message charges first. The tool returns queued, failed, and one result per recipient (status: "queued" with batchId, or status: "failed" with code). Each row’s key must be distinct and stable across retries. A recipient’s variables may hold a list array for a list-section template, exactly as in the REST payload; it still renders one message for that recipient. For a per-send attachment, supply one attachmentUploadId to use on every row. The tool validates all phones and variable sets before sending the first row, but later billing or device failures can still leave only some rows queued.

An approved template chooses exactly one attachment mode:

Mode What the recipient gets
Text only The rendered message as text. An attachmentUploadId is refused.
Fixed image or document The same private file the administrator inspected and approved. Do not include attachmentUploadId in a send.
Per-send image or document An account-owned upload supplied as attachmentUploadId with each request. The same upload may be reused for several sends while available. The rendered message becomes its caption.

To use per-send media, submit a template with Different image or file with each send in the dashboard, or call MCP create_notification_template with attachmentMode: "per_send". An administrator must approve that permission. Approval covers the template text and permission to attach later files; it does not review each file’s bytes. Only the account that owns the approved template can send with this mode.

Upload a file from your server with a notification-scoped API key:

Terminal window
curl -X POST https://app.maukirim.com/api/v1/messages/media \
-H "Authorization: Bearer mk_live_…" \
-F "file=@invoice-8821.pdf"
{ "ok": true, "media": { "id": "gwm_…", "kind": "file", "fileName": "invoice-8821.pdf", "contentType": "application/pdf", "byteSize": 12345, "expiresAt": "2026-10-06T00:00:00.000Z" } }

In MCP, upload_notification_media accepts bounded base64 bytes and returns the same private upload id. Use it with create_notification_template for a fixed attachment, or pass it on an approved per-send template’s send_sample_notification call. Neither tool returns a bucket key.

Then send with the approved per-send template and that upload id:

Terminal window
curl -X POST https://app.maukirim.com/api/v1/messages/send \
-H "Authorization: Bearer mk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-8821" \
-d '{"phone":"+6281234567890","templateId":"tpl_…","variables":{"order":"8821"},"attachmentUploadId":"gwm_…"}'

The same upload endpoint stores JPEG or PNG images up to 5 MB, or supported PDF, Office, CSV, TXT and ZIP documents up to 16 MB. The response returns an id, never a public file URL or bucket key. An unused upload expires after seven days; a queued item holds its own claim until it no longer needs the bytes. A retry with the same Idempotency-Key must use the same upload id. A different id with that key returns 409 batch_idempotency_conflict.

Fixed files can be uploaded in the dashboard, through MCP upload_notification_media, or with the private media endpoint, then submitted for review with attachmentMode: "static" and attachmentUploadId. Editing fixed bytes or changing the mode resets the template to pending review. Because the rendered message becomes the caption, both fixed and per-send media are limited to 1024 caption characters. OTP messages never carry media.

200 means the message was queued, not delivered. The response gives you a batchId; the sequence from there is:

  1. MauKirim’s worker claims the item and hands it to the WhatsApp gateway. Acceptance is not delivery.
  2. When the gateway reports delivered or read, MauKirim records the item’s new state. If your account has an active notification callback, it also queues a signed receipt for your endpoint.
  3. Seven days after the item becomes terminal, the message body is purged. The ids, states and timestamps stay in the dashboard and delivery API.

Just before it hands the message over, the worker asks the sending number to show a typing indicator to the recipient, so a greeting does not arrive out of a silent chat. That is best effort and nothing more: it lasts about 1.2 seconds, it is not a delivery signal, and a number that refuses to show it does not stop the message.

Notification callbacks are account-owned. You do not need access to the shared sending number’s webhook, and a rented number’s webhook never receives these notification receipts.

Set a callback in Notification webhook settings, or use MCP set_notification_webhook with a webhook-scoped key and confirm: true. Supply a public HTTPS URL without credentials, query parameters or a fragment. Only one callback URL can be active per account. The signing secret appears once when you create or rotate it; save it in your receiver’s secret store. get_notification_webhook and list_notification_webhook_deliveries use read scope and never reveal it.

The endpoint receives one JSON POST per observed state transition:

{
"event": "notification.delivered",
"delivery_id": "nwd_…",
"batch_id": "gwb_…",
"item_id": "gwi_…",
"status": "delivered",
"created_at": "2026-10-01T00:00:00.000Z"
}

For a read receipt, event is notification.read and status is read. batch_id matches the send response; item_id identifies its item. created_at is when MauKirim recorded the receipt, not a timestamp asserted by the recipient. The callback contains no sender device, shared phone number, recipient number, provider message id or message body.

Verify x-maukirim-signature: sha256=… as the HMAC-SHA256 of the raw POST body using your notification callback secret, before parsing the JSON. The webhook verification example uses the same signing format. x-maukirim-event repeats event, and x-maukirim-delivery repeats delivery_id. Deduplicate by delivery id and reply 2xx, including on repeats: attempts are at-least-once, time out after 10 seconds, and retry up to five times. A redirect is a failure. Delivery failures do not resend or roll back the WhatsApp message. View attempts in notification webhook settings or with MCP list_notification_webhook_deliveries.

Without a callback, MauKirim still records any receipts its gateway reports for the dashboard and delivery API; it makes no outbound POST. Pausing stops new callbacks and marks queued attempts exhausted on the worker’s next pass. Deleting removes the configuration and its delivery log. Rotating the secret invalidates the old one immediately, so update your receiver before its next delivery. An active callback requires a shared sender with a managed receipt connection: if none is ready, a new notification is refused before charging rather than silently sending without delivery tracking. This does not affect accounts without an active callback.

What you see What it means
400 invalid_input with “variables” in the log The variable set does not match the template. Read the template’s placeholders and match them exactly, including each list object’s keys.
403 forbidden The template exists but is not approved, or belongs to another account. MauKirim will not send it.
410 media_unavailable The fixed file is gone, or a per-send upload is missing, expired or owned by another account. Upload a new file; an id cannot be reused after expiry.
400 invalid_input with “attachment” The upload id is missing for per-send mode, or was supplied for a fixed or text-only template.
402 insufficient_credits The balance is below this message’s cost.
409 batch_idempotency_conflict You reused an Idempotency-Key with a different body. Use a fresh key for a new message.