Skip to main content
The qr mode of POST /v1/channels/whatsapp/connect pairs a channel without Embedded Signup: the client receives a QR, scans it with WhatsApp mobile (Settings -> Linked Devices -> Link a device) and when pairing completes receives the channel_id of the newly created channel. No browser step, no official WABA, no Cloud API. The integration uses WhatsApp multi-device under the hood. This is what keebai whatsapp connect --qr does in the CLI.

General flow

1

Your backend starts the session

POST /v1/channels/whatsapp/connect with mode: "qr". Returns session_id + stream_url (relative SSE path).
2

Subscribe to the SSE stream

GET {stream_url} with Accept: text/event-stream and Authorization: Bearer <PAT>. You’ll receive real-time events: qr, connecting, connected, failed, disconnected.
3

Show the QR to the user

Each qr event carries a string payload. Render it as a QR (in the terminal with qrcode-terminal, on web with qrcode, etc.). If the code expires before it’s scanned, a new qr arrives automatically.
4

Receive the created channel

Once scanned successfully, the stream closes with a connected event whose data includes channel_id, phone_number and push_name.

Start a QR session

POST /v1/channels/whatsapp/connect Required scope: channels:connect

Request

curl -X POST https://api.keebai.com/v1/channels/whatsapp/connect \
  -H "Authorization: Bearer kbai_pk_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "qr",
    "name": "WhatsApp Soporte"
  }'
FieldTypeRequiredDescription
mode"qr"yesSelects the QR flow.
namestringnoChannel label. Default "WhatsApp QR".
pipeline_id and coexistence_enabled don’t apply to QR mode. If you send them, the API responds 400 QR_MODE_CANNOT_USE_COEXISTENCE_OR_PIPELINE.

Response 201

{
  "session_id": "cs_01HXYZ...",
  "provider": "whatsapp_qr",
  "mode": "qr",
  "stream_url": "/v1/channels/whatsapp/connect/cs_01HXYZ.../qr-stream",
  "expires_in": 300,
  "poll_interval": 5
}
FieldTypeDescription
session_idstringQR session id. Use it in the stream_url path.
stream_urlstringRelative path of the SSE stream (prepend the API host).
expires_innumberSeconds until pairing expires if scanning isn’t completed (5 minutes). The stream emits fresh QRs while the window is open.

SSE event stream

GET /v1/channels/whatsapp/connect/:session_id/qr-stream Required scope: channels:connect Headers: Accept: text/event-stream, Authorization: Bearer kbai_pk_xxx.

Events

EventWhendata
qrA QR code is ready to scan. Re-emitted when the code expires and refreshes.string (QR payload) or { "data": "<qr-string>", "expires_at": "<iso>" }
connectingMobile scanned the QR; WhatsApp is establishing the session.empty object or state
connectedPairing complete, channel created.{ "channel_id": "ch_...", "phone_number": "+56...", "push_name": "Lucio" }
failedPairing error (timeout, invalid code, linked-device limit, etc.).{ "reason": "<description>" }
disconnectedThe device was unlinked during pairing.{ "reason": "<description>" }
When connected, failed or disconnected arrives, the stream closes. The client should close the connection too.

curl example

curl -N \
  -H "Authorization: Bearer kbai_pk_xxx" \
  -H "Accept: text/event-stream" \
  https://api.keebai.com/v1/channels/whatsapp/connect/cs_01HXYZ.../qr-stream
event: qr
data: 2@AbcDe…/long-string

event: qr
data: 2@FghIj…/refresh

event: connecting
data: {}

event: connected
data: {"channel_id":"ch_01HXYZ...","phone_number":"+56987654321","push_name":"Lucio"}

Rendering the QR

In a terminal with qrcode-terminal (Node):
import qrcode from "qrcode-terminal";
qrcode.generate(payload, { small: true });
On the web, with qrcode:
import QRCode from "qrcode";
QRCode.toCanvas(canvasEl, payload);

CLI equivalent

keebai whatsapp connect --qr [--name "WhatsApp Soporte"]
The CLI creates the session, opens the stream, draws the QR in the terminal, refreshes it automatically, and on scan prints Channel ch_xxx connected (phone +56…, as Lucio).

Troubleshooting

Expired QR

Pairing emits fresh QRs automatically every ~20 seconds. Don’t restart the session: just wait for the next qr event.

4 linked-device limit

WhatsApp allows up to 4 devices per account. If the session ends with failed mentioning device limit, free a slot on the mobile (Settings -> Linked Devices) and call POST /v1/channels/whatsapp/connect again.

Expired session (5 min)

If nobody scanned, the stream closes and the session_id stops working. Request a new session.

Don't use Cloud API against this channel

QR channels don’t support templates or bulk sends (Meta doesn’t expose Cloud API on multi-device). POST /v1/messages/template and POST /v1/messages/bulk return 400 QR_NOT_SUPPORTED_FOR_TEMPLATE_OR_BULK. POST /v1/messages/text works as usual.

Sending messages

Once connected, QR channels show up in GET /v1/whatsapp/numbers with connection_type: "qr" and are used with the same endpoint as Cloud API ones:
curl -X POST https://api.keebai.com/v1/messages/text \
  -H "Authorization: Bearer kbai_pk_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_id": "ch_01HXYZ...",
    "to": "+5491155555555",
    "text": "Hello from the QR channel"
  }'