Skip to main content
The messaging surface is how you send from your own backend: one endpoint for every message type on every connected channel, plus the contacts, templates, Flows and channels that back them.

What you can do

Manage contacts

List, read, create, update, and delete the people on the other side of your channels.

Send messages

One endpoint for text, media, location, contacts, reactions, approved templates, interactive blocks and Flows — on WhatsApp, Instagram or Messenger.

Broadcast

Schedule a template to many recipients in one call and poll the campaign’s status.

Manage templates

List the approved catalog, create new templates, and submit them to Meta for approval.

Build WhatsApp Flows

Create, publish and send in-chat forms — bookings, surveys, lead capture — without leaving WhatsApp.

Connect channels

List connected channels and onboard a new WhatsApp Business number without the portal.

Drive it from an agent

Every send and every contact operation is also an MCP tool.

Scopes

messages:send and messages:bulk are deliberately separate: bulk is the higher-risk one, since it costs money at Meta and can damage sender reputation. Keep it in a key the marketing tool owns, not the one your transactional backend uses.

Contacts are the identity layer

A contact is a person on one of your channels. Keebai creates one automatically the first time someone writes to you, and the Contacts endpoints let you read and manage them from your own backend. Two identifiers matter, and confusing them is the most common integration bug: Contacts are also how the messaging and CRM surfaces meet: a CRM ticket hangs off a contact’s _id, not off a CRM customer.

Picking the sending channel

Every send declares a channelwhatsapp, whatsapp_qr, instagram or messenger. Which concrete channel it uses is resolved in this order:
  1. channel_id, the Keebai id of the channel. Authoritative. List them with GET /v1/channels.
  2. phone_number_id, a WhatsApp-only alias: the numeric identifier Meta issues for the sending number, like 109876543210987. It is not a Keebai ObjectId, and it is not the phone number itself. List yours with GET /v1/whatsapp/numbers.
  3. Neither — then the project must have exactly one active channel of that type, and it is used. With more than one, the request fails with 400 CHANNEL_AMBIGUOUS and lists the candidates.
Cache whichever identifier you use. Channels change when someone connects or disconnects one, which is a human-timescale event, not a per-request one. Not every channel carries every message type: template, location, contacts, catalog and flow are WhatsApp-only, and an unsupported pair fails with 422 MESSAGE_TYPE_NOT_SUPPORTED_FOR_CHANNEL. The full matrix is on the send endpoint.

The 24-hour window

WhatsApp only allows free-form messages inside 24 hours of the recipient’s last inbound message. Outside that window Meta rejects the send.
Keebai does not reject a free-form send when the window is closed. The request is forwarded to Meta, and the rejection comes back as a 502 with Meta’s error passed through. A 202 means Meta accepted the message, never that it reached the device — for that, watch an outbound webhook.

Single sends and broadcasts return different things

The two paths are not the same shape, and mixing them up is a common mistake.
A single send through POST /v1/messages gives you a provider message_id — keep it to correlate the delivery and read webhooks. Only POST /v1/messages/bulk returns a broadcast_id, and it is the only thing GET /v1/messages/bulk/:broadcastId accepts. This also makes the cost model clear: one broadcast to 5,000 recipients is one request against your daily quota; a loop over 5,000 single sends is 5,000.

Errors

Branch on error.code, not on the message text.

Next steps

Send your first message

One endpoint, sixteen message types, four channels.

Build a WhatsApp Flow

Collect structured data inside the chat instead of linking out to a form.