SelanimDocs

WhatsApp

Send WhatsApp messages, and pull the ones your customers send you.

WhatsApp is not SMS with a different transport. Two rules of the channel decide what your integration can do, and both come from Meta rather than from us. Read these two paragraphs before writing any code — almost every WhatsApp integration that fails in a confusing way fails on one of them.

The 24-hour window

When a customer sends your WhatsApp number a message, a 24-hour window opens. Inside it you may reply with anything — text, media, whatever you like. Outside it, WhatsApp will carry only a template Meta has already approved, and nothing else.

Every message the customer sends pushes the window out again. Messages you send do not: a business cannot keep a window open by talking to itself.

Sending text outside the window returns 409

We refuse it rather than passing it to Meta. This is deliberate: a message Meta rejects still counts against your number’s quality rating, and a number Meta has rated poorly reaches fewer people — permanently, and with nothing in any log to say so. Check windowOpen on the conversation and send a template when it is false.

Templates are approved by Meta

A template is created in the Selanim portal and submitted to Meta, which approves or rejects it on its own schedule — usually minutes, sometimes several days. Nothing in this API can hurry that, and until you have at least one approved template you cannot start a conversation with anybody: you can only answer people who wrote to you first.

When you send one, templateParams must contain exactly as many values as the approved body has {{1}} placeholders. Meta rejects the whole message when the count differs — it does not ignore the extras.

Send a message

POSThttps://bulksmsapi.selanim.com/v1/whatsapp/messages

Requires the whatsapp:send scope. Answers 202: the message is accepted and queued, not yet delivered.

FieldTypeDescription
torequiredstringE.164, with or without the leading plus.
textstringA free-form message. Only deliverable inside an open 24-hour window; outside one this returns 409.
templatestringThe name of a template Meta has approved. The only thing WhatsApp carries outside an open window.
templateLanguagestring
templateParamsstring[]Values for the template's {{1}}, {{2}} … placeholders, in order. The count must match the approved template EXACTLY — Meta rejects the whole message otherwise rather than ignoring extras.
mediaUrlstring (url)A publicly reachable URL. Meta fetches it directly, so a link only your own network can resolve will fail.
mediaTypestringOne of: image, video, audio, document.
mediaCaptionstring
referencestringYour own identifier, returned on every event about this message.

Inside an open window — a plain reply:

bash
curl -X POST "https://bulksmsapi.selanim.com/v1/whatsapp/messages" \
  -H "Authorization: Bearer $SBS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+255712345678",
    "text": "Asante kwa ujumbe wako. Oda yako iko tayari kuchukuliwa."
  }'

Needs: curl, on every machine already.

Outside the window — an approved template, with its values in order:

bash
curl -X POST "https://bulksmsapi.selanim.com/v1/whatsapp/messages" \
  -H "Authorization: Bearer $SBS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+255712345678",
    "template": "order_ready",
    "templateLanguage": "sw",
    "templateParams": [
      "Rehema",
      "BF-2291"
    ]
  }'
json
{
  "id": "9f1c2a44-1f7e-4d0b-9d1e-2c7a3b5e8f10",
  "to": "+255712345678",
  "status": "queued",
  "cost": 12
}

Pull the messages your customers send

GEThttps://bulksmsapi.selanim.com/v1/whatsapp/messages

Why this endpoint exists

Meta has no pull API. WhatsApp Cloud API pushes every inbound message at a webhook and keeps nothing — a delivery you miss is gone. Selanim receives those webhooks and stores them, and this endpoint serves that stored copy. That is what makes it pollable, re-readable, and resumable after an outage.

The whole contract is three steps:

  1. Call it with no cursor.
  2. Process data.
  3. Keep calling with nextCursor while hasMore is true.

Store the last cursor you received. That single value is your entire replication state — if your server is down for two days, you resume from it and get every message you missed, in order, exactly once.

Query parameterTypeDescription
cursorstringThe nextCursor from your previous call. Opaque: send it back verbatim and do not parse it. Omit to start from the newest message.
directionstringinbound (the default) is what customers sent you, outbound is what you sent, all returns both in one stream. Defaults to "inbound". One of: inbound, outbound, all.
sincestring (ISO 8601)RFC3339 timestamp. Only messages at or after this.
limitintegerDefaults to 50. Maximum 200.
bash
curl -X GET "https://bulksmsapi.selanim.com/v1/whatsapp/messages?direction=inbound&limit=50" \
  -H "Authorization: Bearer $SBS_API_KEY"
json
{
  "data": [
    {
      "id": "6b2e0f19-2c44-4a67-9a1f-0d3c5f7b8e21",
      "conversationId": "1d8a4c60-9f21-4b3e-8c7a-5e2f1b9d0a34",
      "whatsappId": "wamid.HBgMMjU1NzEyMzQ1Njc4FQIAEhgg",
      "direction": "inbound",
      "from": "+255712345678",
      "type": "text",
      "body": "Habari, bei ya bundle ni ngapi?",
      "status": "received",
      "receivedAt": "2026-08-26T09:12:44Z"
    }
  ],
  "nextCursor": "eyJ0IjoiMjAyNi0wOC0yNlQwOToxMjo0NFoiLCJpZCI6IjZiMmUwZjE5In0",
  "hasMore": true
}

Cursors, not page numbers

An inbox gains rows at the top constantly. With an offset, page 2 shifts under you between polls — you would see some messages twice and silently miss others. A cursor names a position in the data, so repeating a request returns the same rows forever.

Prefer push? Subscribe to the whatsapp.received event on your delivery webhook. Both read the same stored rows, so doing both misses nothing and duplicates nothing.

Fetch one message

GEThttps://bulksmsapi.selanim.com/v1/whatsapp/messages/{messageId}

An unrecognised message type arrives as unsupported rather than being dropped, so a customer’s turn in the conversation is never lost. Branch on the types you handle and keep the rest.

Conversations

GEThttps://bulksmsapi.selanim.com/v1/whatsapp/conversations

One thread per customer. The two fields to build on are windowOpen and windowRemainingSeconds: they tell you whether you may reply freely or must use a template. Both are computed against the server clock on every read, because the window closes by the passage of time — a value you cached ten minutes ago may already be wrong.

Query parameterTypeDescription
statestringOne of: open, closed, archived.
unreadbooleanOnly threads with unread messages.
pageintegerDefaults to 1.
pageSizeintegerDefaults to 25. Maximum 200.
bash
curl -X GET "https://bulksmsapi.selanim.com/v1/whatsapp/conversations?state=open&unread=true" \
  -H "Authorization: Bearer $SBS_API_KEY"
json
{
  "data": [
    {
      "id": "1d8a4c60-9f21-4b3e-8c7a-5e2f1b9d0a34",
      "contact": "+255712345678",
      "contactName": "Asha Mwinyi",
      "lastMessage": "Habari, bei ya bundle ni ngapi?",
      "lastMessageAt": "2026-08-26T09:12:44Z",
      "unreadCount": 1,
      "state": "open",
      "windowOpen": true,
      "windowRemainingSeconds": 78436
    }
  ],
  "meta": { "page": 1, "pageSize": 20, "total": 1 }
}

Delivery states

WhatsApp reports one state SMS has no equivalent of: read. It is the receipt most clients end up judging a campaign on.

deliveryStatusTypeDescription
sentstringMeta accepted it.
deliveredstringIt reached the customer's device.
readstringThe customer opened it.
failedstringIt did not arrive. Credits for a message Meta accepted and then failed to deliver are returned automatically.

Opt-outs

A customer replying STOP, ACHA, SITAKI or a similar single word is opted out automatically, and further messages to them are refused with 403. You do not need to implement this yourself, and you should not try to work around it: repeatedly messaging somebody who asked you to stop is the fastest way to have Meta downgrade your number’s quality rating.

Scopes

ScopeTypeDescription
whatsapp:sendstringSend messages and replies.
whatsapp:readstringPull messages and read conversations. Separate from send on purpose — on this channel, reading means your customers' conversations, so a key that only replicates an inbox should not be able to message anybody.