Skip to content

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

  1. Sign in at https://dashboard.connectgain.cloud.
  2. Settings → Channels → Add Channel.
  3. Pick WhatsApp Lite.
  4. Fill in:
  5. Channel name — free text, e.g. Prod WhatsApp.
  6. 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.
  7. 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.
  8. Save. The channel row is created inactive (is_active = false) and the QR dialog opens straight away.

0.2 Scan the QR code

  1. On the phone that owns the number, open WhatsApp → Settings → Linked devices → Link a device.
  2. 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.
  3. On success the dialog shows Successfully connected and closes itself. The channel flips to is_active = true and settings.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

X-API-Key: cg_your_api_key_here

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.

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 returns 403 Table not allowed via REST API proxy.
  • limit defaults to 500 and is capped at 1000. Page with offset.

3. Send a WhatsApp Lite message

POST /functions/v1/whatsapp-lite-send
X-API-Key: {{api_key}}
Content-Type: application/json
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"
}

to is ignored on an existing thread unless you pass force_recipient: true. A WhatsApp Lite conversation is bound to the exact number that first wrote in, and that binding deliberately outranks a caller-supplied to — it is what stops a stale client from delivering a reply to the wrong person. Set force_recipient: true only when you really do mean "message this other number", and then to must 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.

POST /functions/v1/bot-control
X-API-Key: {{api_key}}
Content-Type: application/json
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


ConnectGain — omnichannel inbox, CRM & automation for WhatsApp, Messenger, Instagram, Telegram and more. Open the app · Docs home