WhatsApp Lite External API¶
Everything in this guide is authenticated with a single ConnectGain API key
(cg_…) sent in the X-API-Key header. No Supabase JWT, no apikey header.
The key is organization-scoped: a key issued for org A cannot read or write
anything belonging to org B — cross-org calls come back 404 Not Found, never
someone else's data.
Base URL: https://txpaxbxhnvnhsjwwaeoy.supabase.co
| Purpose | Method + path |
|---|---|
| Find contact / conversation / messages | GET /functions/v1/rest-api-proxy/<table> |
| Send a WhatsApp Lite message | POST /functions/v1/whatsapp-lite-send |
| Pause / resume the bot on a thread | POST /functions/v1/bot-control |
0. Before any API call — create the channel and scan the QR¶
The APIs below need a live, paired WhatsApp Lite channel. Pairing is a dashboard flow, not an API: WhatsApp Lite links a real WhatsApp account as a companion device, and only a phone camera can complete that handshake.
0.1 Create the channel¶
- Sign in at https://dashboard.connectgain.cloud.
- Settings → Channels → Add Channel.
- Pick WhatsApp Lite.
- Fill in:
- Channel name — free text, e.g.
Prod WhatsApp. - Phone number — full E.164 with country code, e.g.
+201119985526. This must be the number whose WhatsApp app you are about to scan with. - Owner email + password — the credentials of the organization's OWNER user. They are used once, server-side, to provision the Appgain suit that backs this number. A non-OWNER team member cannot complete this step.
- Save. The channel row is created inactive (
is_active = false) and the QR dialog opens straight away.
0.2 Scan the QR code¶
- On the phone that owns the number, open WhatsApp → Settings → Linked devices → Link a device.
- Point it at the QR code in the dialog. The code refreshes on its own; if it expires, wait for the next one rather than reloading the page.
- On success the dialog shows Successfully connected and closes itself.
The channel flips to
is_active = trueandsettings.authenticated = true.
0.3 Keep it paired¶
- The linked phone must stay online and have WhatsApp installed. Unlinking the device in WhatsApp, or the phone being offline for a long stretch, breaks the channel and every send starts failing until it is re-scanned.
- Re-scanning later is done from Settings → Channels → (the channel) → Edit, which shows the same QR panel.
0.4 How many numbers you get¶
The number of WhatsApp Lite channels is capped per organization by the plan
(whatsapp_lite_numbers) and enforced in the database by the
enforce_channel_limits trigger. Over the cap, the insert is rejected with
CHANNEL_LIMIT_REACHED; if WhatsApp Lite is not in the plan's
allowed_channel_types at all, it is rejected with CHANNEL_NOT_ALLOWED.
Both are raised before anything is provisioned, so nothing is left half-created.
1. Authentication¶
The full key is shown once, at creation. Only a SHA-256 hash is stored, so a lost key cannot be recovered — it can only be replaced.
| Situation | Response |
|---|---|
| Header missing | 401 {"error":"Missing authentication. Provide either Authorization header or X-API-Key header"} |
| Key unknown / deactivated | 401 {"error":"Invalid or expired API key"} |
Key past expires_at |
401 {"error":"API key has expired"} |
2. Get a conversation by phone number¶
Phones are stored E.164-normalized in the contacts.phones array column,
so the lookup uses the PostgREST cs (contains) operator. Always include the
leading +; --data-urlencode handles the escaping.
2.1 Recommended — two fast calls¶
Step 1 — find the contact by phone
curl -G 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/rest-api-proxy/contacts' \
-H 'X-API-Key: {{api_key}}' \
--data-urlencode 'phones=cs.{"+201100917041"}' \
--data-urlencode 'select=id,first_name,last_name,phones,emails' \
--data-urlencode 'limit=1'
[{
"id": "0a8c4c94-5991-4399-9bc6-5c6f496945a5",
"first_name": "Menna AbdElsalam",
"last_name": null,
"phones": ["+201100917041"],
"emails": []
}]
An empty [] means no contact carries that number — check that you sent the
+ and the country code.
Step 2 — list that contact's conversations
curl -G 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/rest-api-proxy/conversations' \
-H 'X-API-Key: {{api_key}}' \
--data-urlencode 'contact_id=eq.0a8c4c94-5991-4399-9bc6-5c6f496945a5' \
--data-urlencode 'select=id,status,channel_account_id,contact_id,last_message_at,assignee_id,ai_agent_active' \
--data-urlencode 'order=last_message_at.desc' \
--data-urlencode 'limit=10'
[{
"id": "1ce3c07f-eeba-4f7a-9645-89881e7ebb72",
"status": "OPEN",
"channel_account_id": "36534e3f-233f-4a13-97c4-305211b3becf",
"contact_id": "0a8c4c94-5991-4399-9bc6-5c6f496945a5",
"last_message_at": "2026-09-08T12:48:29.913579+00:00",
"assignee_id": "1b9fc68f-a77f-41b7-9e89-0e0c0e136f4e",
"ai_agent_active": false
}]
One contact can hold several conversations — one per channel they wrote in on.
Filter to a single channel by adding
--data-urlencode 'channel_account_id=eq.<uuid>', and take the first row of
the last_message_at.desc order as the current thread.
2.2 Alternative — one call with an embedded join¶
Same answer in a single request, filtering on the embedded contact:
curl -G 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/rest-api-proxy/conversations' \
-H 'X-API-Key: {{api_key}}' \
--data-urlencode 'select=id,status,last_message_at,channel_account_id,contact:contacts!inner(id,first_name,phones)' \
--data-urlencode 'contact.phones=cs.{"+201100917041"}' \
--data-urlencode 'limit=5'
Cost warning. The embedded array filter cannot use an index, so on an organization with a large conversation history this call can run for well over a minute — it was measured above 100 s on a busy account. Use §2.1 in anything latency-sensitive, and raise the Postman/client timeout before trying this one.
2.3 Read the thread's messages¶
curl -G 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/rest-api-proxy/messages' \
-H 'X-API-Key: {{api_key}}' \
--data-urlencode 'conversation_id=eq.1ce3c07f-eeba-4f7a-9645-89881e7ebb72' \
--data-urlencode 'select=id,direction,content,message_type,status,created_at' \
--data-urlencode 'order=created_at.desc' \
--data-urlencode 'limit=50'
direction is INBOUND (from the customer) or OUTBOUND (from you).
2.4 Proxy rules worth knowing¶
- Every query is force-filtered to your organization; you cannot widen it.
- Only these tables are reachable:
contacts,conversations,messages,deals,companies,channel_accounts,templates,projects,tasks, and a few more. Anything else returns403 Table not allowed via REST API proxy. limitdefaults to 500 and is capped at 1000. Page withoffset.
3. Send a WhatsApp Lite message¶
| Field | Required | Notes |
|---|---|---|
organizationId |
yes | Or suitId. Required even with an API key — it selects which paired Lite number sends. |
to |
yes* | Recipient in E.164, e.g. +201100917041. Optional if conversationId is given. |
conversationId |
yes* | Reply into an existing thread. Optional if to is given. |
message |
yes** | Max 4096 characters. Optional if media_urls is set. |
media_urls |
no | Up to 10 publicly reachable URLs. Type (image/video/audio/document) is inferred from the extension. |
contactName |
no | Name used if the contact has to be created. |
from |
no | Sender label for the inbox UI only. |
force_recipient |
no | See the warning below. |
reply_to_external_id |
no | Quote a specific WhatsApp message. |
* at least one of to / conversationId.
** at least one of message / media_urls.
Reply into an existing conversation (safest — the recipient comes from the thread):
curl -X POST 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/whatsapp-lite-send' \
-H 'X-API-Key: {{api_key}}' \
-H 'Content-Type: application/json' \
-d '{
"organizationId": "{{org_id}}",
"conversationId": "1ce3c07f-eeba-4f7a-9645-89881e7ebb72",
"message": "Hello from the ConnectGain API"
}'
Start a new conversation with a number:
curl -X POST 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/whatsapp-lite-send' \
-H 'X-API-Key: {{api_key}}' \
-H 'Content-Type: application/json' \
-d '{
"organizationId": "{{org_id}}",
"to": "+201100917041",
"contactName": "Menna",
"message": "Hello from the ConnectGain API",
"force_recipient": true
}'
With media:
curl -X POST 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/whatsapp-lite-send' \
-H 'X-API-Key: {{api_key}}' \
-H 'Content-Type: application/json' \
-d '{
"organizationId": "{{org_id}}",
"to": "+201100917041",
"message": "Your invoice",
"media_urls": ["https://cdn.example.com/invoice-1024.pdf"]
}'
Success:
{
"success": true,
"result": { "messageId": "3EB0…", "id": "3EB0…" },
"conversationId": "1ce3c07f-eeba-4f7a-9645-89881e7ebb72",
"contactId": "0a8c4c94-5991-4399-9bc6-5c6f496945a5"
}
tois ignored on an existing thread unless you passforce_recipient: true. A WhatsApp Lite conversation is bound to the exact number that first wrote in, and that binding deliberately outranks a caller-suppliedto— it is what stops a stale client from delivering a reply to the wrong person. Setforce_recipient: trueonly when you really do mean "message this other number", and thentomust be full E.164 with a country code; a local number without+is rejected rather than guessed at.
4. Pause / resume the bot on a conversation¶
Hands a thread from the AI agent to a human, and back. Same API key.
| Field | Required | Values |
|---|---|---|
conversation_id |
yes | UUID |
action |
yes | pause or resume |
curl -X POST 'https://txpaxbxhnvnhsjwwaeoy.supabase.co/functions/v1/bot-control' \
-H 'X-API-Key: {{api_key}}' \
-H 'Content-Type: application/json' \
-d '{"conversation_id":"1ce3c07f-eeba-4f7a-9645-89881e7ebb72","action":"pause"}'
{
"success": true,
"conversation_id": "1ce3c07f-eeba-4f7a-9645-89881e7ebb72",
"bot_active": false,
"action": "pause"
}
pause sets ai_agent_active = false, stamps ai_handoff_at, deactivates any
running bot_sessions on the thread, and fires the channel's
human_takeover_webhook_url if one is configured. resume sets
ai_agent_active = true and clears the handoff fields.
Read the current state any time from §2.1 step 2 — the ai_agent_active
column.
5. Error reference¶
| Status | Body | Meaning |
|---|---|---|
400 |
Either "suitId" or "organizationId" is required |
Send call is missing the org selector. |
400 |
Either "message" or "media_urls" is required |
Nothing to send. |
400 |
Message too long (max 4096 characters) |
Trim the text. |
400 |
Invalid recipient phone number |
force_recipient with a non-E.164 to. |
401 |
Missing authentication… |
No X-API-Key header. |
401 |
Invalid or expired API key |
Wrong, revoked or deleted key. |
403 |
Table not allowed via REST API proxy |
Table outside the proxy allow-list. |
404 |
Channel not found |
No active, paired WhatsApp Lite channel on the org — go back to §0. |
404 |
Conversation not found |
Bad id, or the conversation belongs to another organization. |
6. Postman setup¶
Create a collection-level variable set and reference it everywhere:
| Variable | Value |
|---|---|
base_url |
https://txpaxbxhnvnhsjwwaeoy.supabase.co |
api_key |
your cg_… key |
org_id |
your organization UUID |
Put X-API-Key: {{api_key}} on the collection (Authorization → API Key →
Header) so every request inherits it. Raise the request timeout above 120 s if
you keep §2.2 in the collection.
See also¶
- API key authentication — how
cg_keys are issued and validated. - Array column search — the
cs/ovoperators in depth. - Bot Control API — the full bot-control surface.
- API overview — every endpoint in one place.
ConnectGain — omnichannel inbox, CRM & automation for WhatsApp, Messenger, Instagram, Telegram and more. Open the app · Docs home