API Reference
WhatsApp Messaging

Send a WhatsApp message

POST
/v1/whatsapp/messages

Sends a text or template message from one of your connected WhatsApp numbers.

Pick the sending number with EITHER number_id (a Sautikit number id — the same one the rest of this API uses; we map it to Meta's phone_number_id for you) OR connection_id (a WhatsApp connection, whose WhatsApp number is used). Exactly one is required.

A number_id that is not yours, or that no connected WhatsApp account carries, is rejected before anything reaches Meta.

Returns 202 with Meta's wamid. Delivery is asynchronous — subscribe to the whatsapp.event.received workspace webhook, which relays Meta's delivery-status payloads to you verbatim.

Sautikit does not store messages. The wamid is your handle for correlating the status events that follow.

WhatsApp only permits free-form text inside a 24-hour customer service window opened by the contact messaging you. Outside it, use type: template with a Meta-approved template.

Message types

type names the message, and an object of the SAME NAME carries its contents. That object is forwarded to WhatsApp untouched, so its shape is WhatsApp's — including any type added after this page was written.

Text

{  "number_id": "9d2b1f53-8c0e-4f1d-9a6b-5d3a8c47e9f0",  "to": "254712345678",  "type": "text",  "text": {    "body": "Habari Wanjiru — your order from Duka la Vitabu is ready for pickup on Kimathi Street until 6pm.",    "preview_url": false  }}

Set preview_url: true to render a preview card for the first link in the body.

Image, video, audio, document, sticker

Media takes EITHER id (from POST /v1/whatsapp/media) or link (a public URL WhatsApp fetches). Prefer id: a link is re-fetched on every send and cached for only 10 minutes.

Image with a caption — an M-PESA receipt:

{  "number_id": "9d2b1f53-8c0e-4f1d-9a6b-5d3a8c47e9f0",  "to": "254712345678",  "type": "image",  "image": {    "id": "1234567890123456",    "caption": "Receipt for KES 2,400 — Sarit Centre branch"  }}

Document — an invoice a customer can download:

{  "type": "document",  "document": {    "id": "1234567890123456",    "filename": "Invoice-INV-2291.pdf",    "caption": "Your invoice for July deliveries"  }}

Audio has no caption (WhatsApp renders it as a voice note); sticker takes only id or link and must be WebP.

Location

Send a pickup point:

{  "type": "location",  "location": {    "latitude": -1.2833,    "longitude": 36.8172,    "name": "Mama Ngina Street pickup point",    "address": "Mama Ngina St, Nairobi CBD"  }}

Contacts

Hand over a rider's details:

{  "type": "contacts",  "contacts": [    {      "name": { "formatted_name": "Otieno Odhiambo", "first_name": "Otieno" },      "phones": [{ "phone": "+254733000111", "type": "CELL", "wa_id": "254733000111" }],      "org": { "company": "Nairobi Swift Deliveries", "title": "Rider" }    }  ]}

Interactive: reply buttons

Up to three buttons. Confirming a delivery slot:

{  "type": "interactive",  "interactive": {    "type": "button",    "body": { "text": "Your parcel arrives in Westlands tomorrow. Which slot suits you?" },    "action": {      "buttons": [        { "type": "reply", "reply": { "id": "slot_morning", "title": "8am - 12pm" } },        { "type": "reply", "reply": { "id": "slot_afternoon", "title": "12pm - 5pm" } }      ]    }  }}

The tap arrives on your webhook as an inbound interactive message carrying the id you chose.

Interactive: list

Up to ten rows, grouped in sections — good for a menu or a catalogue the customer scrolls:

{  "type": "interactive",  "interactive": {    "type": "list",    "header": { "type": "text", "text": "Today's menu" },    "body": { "text": "Choose a dish and we'll deliver within 45 minutes." },    "action": {      "button": "View menu",      "sections": [        {          "title": "Mains",          "rows": [            { "id": "nyama_choma", "title": "Nyama choma", "description": "Half kilo, KES 750" },            { "id": "pilau", "title": "Pilau ya nyama", "description": "KES 450" }          ]        }      ]    }  }}

Interactive: CTA URL button

Puts a long or ugly link behind a button:

{  "type": "interactive",  "interactive": {    "type": "cta_url",    "body": { "text": "Track your parcel from our Mombasa Road depot." },    "action": {      "name": "cta_url",      "parameters": { "display_text": "Track parcel", "url": "https://track.example.co.ke/p/9F2K" }    }  }}

Interactive: location request

Ask a customer where to deliver, instead of parsing a typed address:

{  "type": "interactive",  "interactive": {    "type": "location_request_message",    "body": { "text": "Where should the boda drop your order?" },    "action": { "name": "send_location" }  }}

Interactive: Flow

A multi-step form inside the chat — bookings, KYC, surveys. The answers arrive on your webhook as structured data:

{  "type": "interactive",  "interactive": {    "type": "flow",    "body": { "text": "Book your service appointment at our Thika Road garage." },    "action": {      "name": "flow",      "parameters": {        "flow_message_version": "3",        "flow_id": "1234567890",        "flow_cta": "Book a slot",        "flow_action": "navigate"      }    }  }}

Reaction

React to a message the customer sent you:

{  "type": "reaction",  "reaction": { "message_id": "wamid.HBgMMjU0NzEyMzQ1Njc4...", "emoji": "👍" }}

Send an empty emoji to remove a reaction.

Replying in context

Any message can quote one the customer sent, which is worth doing when a conversation has several threads running:

{  "type": "text",  "context": { "message_id": "wamid.HBgMMjU0NzEyMzQ1Njc4..." },  "text": { "body": "Yes — that size is in stock at our Nakuru branch." }}

Templates

Outside the 24-hour window you may only send a template Meta has approved. Sautikit does not create templates — that happens in WhatsApp Manager or through Meta's Message Templates API — here you send one by name.

template.components is forwarded untouched, so its shape is Meta's. Body parameters fill {{1}}, {{2}} in order, and the count must match the approved template exactly or Meta rejects the send.

Utility template

Transactional, tied to something the customer did — an order, a payment, a delivery:

{  "number_id": "9d2b1f53-8c0e-4f1d-9a6b-5d3a8c47e9f0",  "to": "254712345678",  "type": "template",  "template": {    "name": "order_dispatched",    "language_code": "en",    "components": [      {        "type": "body",        "parameters": [          { "type": "text", "text": "Wanjiru" },          { "type": "text", "text": "SK-4821" },          { "type": "text", "text": "Tuesday" }        ]      }    ]  }}

Marketing template

Promotional. Subject to per-user marketing limits and the recipient's marketing opt-out, so a send can be accepted here and still not delivered:

{  "type": "template",  "template": {    "name": "back_to_school_offer",    "language_code": "en",    "components": [      {        "type": "header",        "parameters": [          { "type": "image", "image": { "id": "1234567890123456" } }        ]      },      {        "type": "body",        "parameters": [{ "type": "text", "text": "20%" }]      },      {        "type": "button",        "sub_type": "url",        "index": "0",        "parameters": [{ "type": "text", "text": "back-to-school" }]      }    ]  }}

Authentication template

One-time passwords. The body text is fixed by Meta — you supply only the code — and the same code repeats as the button parameter so the customer can copy or autofill it:

{  "type": "template",  "template": {    "name": "verification_code",    "language_code": "en",    "components": [      {        "type": "body",        "parameters": [{ "type": "text", "text": "418902" }]      },      {        "type": "button",        "sub_type": "url",        "index": "0",        "parameters": [{ "type": "text", "text": "418902" }]      }    ]  }}

Authentication templates have a 10-minute time-to-live (everything else is 30 days) — an undelivered code is dropped rather than arriving long after it expired. They are also delivered only to the customer's primary WhatsApp device.

Phone number format

Include the + and country code. Without a +, WhatsApp prepends YOUR number's country code — for a Kenyan number 0712345678 becomes +2540712345678, which is not a real number. Send +254712345678 or 254712345678.

What a 202 means

A 202 means WhatsApp ACCEPTED the request, not that the message arrived. Delivery outcomes reach you on the whatsapp.event.received webhook; correlate them by the wamid returned here.

API-key scope: whatsapp.messages.

Authorization

bearerAuth
AuthorizationBearer <token>

Long-lived ES256 JWT minted from the dashboard (https://app.sautikit.com/developers/api-keys). Signed by the platform keyring. Carries workspace_id and scopes claims; revoked via the platform deny-list.

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

application/json

curl -X POST "https://example.com/v1/whatsapp/messages" \  -H "Content-Type: application/json" \  -d '{    "number_id": "9d2b1f53-8c0e-4f1d-9a6b-5d3a8c47e9f0",    "to": "254700000001",    "type": "text",    "text": {      "body": "Your order is ready for pickup.",      "preview_url": false    }  }'
{  "wamid": "wamid.HBgMMjU0NzAwMDAwMDAxFQIAERgSQjBBMEEwMDAwMDAwMDAwMDAA",  "status": "accepted"}
{  "error": {    "code": "validation.bad_request",    "message": "string",    "request_id": "string",    "details": [      "string"    ],    "resolution": "string",    "reason": "invalid_characters",    "suggested_e164": "+254727524723"  }}
{  "error": {    "code": "validation.bad_request",    "message": "string",    "request_id": "string",    "details": [      "string"    ],    "resolution": "string",    "reason": "invalid_characters",    "suggested_e164": "+254727524723"  }}
{  "error": {    "code": "validation.bad_request",    "message": "string",    "request_id": "string",    "details": [      "string"    ],    "resolution": "string",    "reason": "invalid_characters",    "suggested_e164": "+254727524723"  }}