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
Your backend starts the session
POST /v1/channels/whatsapp/connect with mode: "qr". Returns session_id + stream_url (relative SSE path).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.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.Start a QR session
POST /v1/channels/whatsapp/connect
Required scope: channels:connect
Request
| Field | Type | Required | Description |
|---|---|---|---|
mode | "qr" | yes | Selects the QR flow. |
name | string | no | Channel 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
| Field | Type | Description |
|---|---|---|
session_id | string | QR session id. Use it in the stream_url path. |
stream_url | string | Relative path of the SSE stream (prepend the API host). |
expires_in | number | Seconds 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
| Event | When | data |
|---|---|---|
qr | A QR code is ready to scan. Re-emitted when the code expires and refreshes. | string (QR payload) or { "data": "<qr-string>", "expires_at": "<iso>" } |
connecting | Mobile scanned the QR; WhatsApp is establishing the session. | empty object or state |
connected | Pairing complete, channel created. | { "channel_id": "ch_...", "phone_number": "+56...", "push_name": "Lucio" } |
failed | Pairing error (timeout, invalid code, linked-device limit, etc.). | { "reason": "<description>" } |
disconnected | The device was unlinked during pairing. | { "reason": "<description>" } |
connected, failed or disconnected arrives, the stream closes. The client should close the connection too.
curl example
Rendering the QR
In a terminal withqrcode-terminal (Node):
qrcode:
CLI equivalent
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 inGET /v1/whatsapp/numbers with connection_type: "qr" and are used with the same endpoint as Cloud API ones: