| name | whatsapp-uazapi |
| description | Implement or maintain a full WhatsApp integration through the Uazapi provider on a React + TypeScript + Supabase stack — instances and QR connection, the public webhook that receives messages, sending text/media/reactions/polls, a realtime inbox, conversation assignment, labels, quick replies, notes, reminders, AI (realtime analysis, copilot, configurable agent), voice calls via API4Com, group management and notification triggers. Use when working on WhatsApp, Uazapi, QR pairing, chat inbox, message webhooks, WhatsApp media, or when a message is not arriving or not sending. |
| license | MIT |
| compatibility | Requires React 18+ with TypeScript and a Supabase project (Postgres with RLS, Deno Edge Functions, Realtime). Needs a Uazapi account with an admin token. Optional extras - API4Com for voice calls, OpenRouter or a compatible gateway for the AI layer. |
| metadata | {"version":"1.0.0","provider":"uazapi","body-language":"en","reference-language":"pt-BR","source":"extracted from a production multi-tenant CRM with a live WhatsApp module"} |
WhatsApp integration via Uazapi
A complete, production-extracted WhatsApp module: 39 tables, 34 edge
functions, 21 hooks, 56 components, plus a sprint-by-sprint implementation
plan with acceptance gates. Everything here is real code that ran in
production — not scaffolding.
Reference docs are in Brazilian Portuguese (references/), UI strings in
the components are pt-BR. Code identifiers are English. Translate the strings
for your product; the structure is language-agnostic.
0. Orient yourself before touching anything
Three questions decide what you do next:
-
Is the module already installed here?
ls supabase/functions | grep -E "uazapi|whatsapp"
grep -rl "whatsapp_configs" supabase/migrations | head
- Nothing → this is a fresh implementation: use the
wa-implement skill,
which walks the sprints with gates. Do not improvise the order.
- Partially → find the last completed sprint gate (§6) and resume there.
- Fully → you are maintaining. Read the relevant sprint doc, then the asset.
-
Something is broken? → use the wa-debug skill. WhatsApp failures are
almost always one of six known causes, and guessing wastes hours.
-
Just answering a question? → the contract below usually suffices; open a
reference only when you need the detail.
1. Architecture in one picture
FRONTEND (React)
Inbox drawer (global) ─ ConversationList · ChatWindow · MessageInput
Settings ─ connection card · QR modal · instances list
Hooks: useWhatsAppConfig / …Instances / …Conversations / …Messages /
…Realtime / …Assignments / AI hooks
│ supabase-js (RLS) │ Realtime (postgres_changes)
▼ ▼
SUPABASE (Postgres)
whatsapp_configs · whatsapp_instances · whatsapp_instance_users
whatsapp_conversations · whatsapp_messages · …assignments · …notes
…labels · …quick_replies · …ai_agent_config · …realtime_analysis
…notification_* · groupops_* · voice_calls
RLS everywhere, through one SECURITY DEFINER tenancy function
│ supabase.functions.invoke()
▼
EDGE FUNCTIONS (Deno)
create-uazapi-instance · get-uazapi-qrcode · test-uazapi-connection
manage-whatsapp-instance · configure-uazapi-webhook
whatsapp-webhook ◄──── PUBLIC: Uazapi POSTs every event here
send-whatsapp-notification · send-whatsapp-media · send-whatsapp-reaction
sync-whatsapp-chats · fetch-whatsapp-messages · download-whatsapp-media
archive / mute / pin / mark-read / block / labels / quick-replies
whatsapp-ai-assistant · initiate-voice-call · groupops-* · notifications
│ fetch() with token / admintoken headers
▼
🌐 Uazapi API
The end-to-end flow, in seven steps:
- Tenant saves Uazapi credentials in
whatsapp_configs (endpoint + admintoken).
create-uazapi-instance → POST /instance/init (admintoken) → stores the
instance + its own token → registers the webhook.
get-uazapi-qrcode (/instance/connect) shows the QR; test-uazapi-connection
(/instance/status) confirms connected.
- Inbound message → Uazapi POSTs to the public
whatsapp-webhook → upsert
whatsapp_conversations + insert whatsapp_messages.
- Realtime pushes the row to the browser; the inbox renders it. No polling.
- Outbound:
send-whatsapp-notification (/send/text) and
send-whatsapp-media (/send/media), header token.
- AI (
whatsapp-ai-assistant) analyses and suggests; the configurable agent
lives in whatsapp_ai_agent_config.
2. Uazapi contract — the part you must get right
Base URL: the tenant's whatsapp_configs.uazapi_endpoint, always
.replace(/\/$/, "") before concatenating. Content-Type: application/json.
Authentication — two different headers, and mixing them is the #1 mistake:
| Header | Value | Use for |
|---|
admintoken | whatsapp_configs.uazapi_admintoken | Only creating an instance (/instance/init) |
token | whatsapp_instances.uazapi_token | Everything else: connect, status, send, message ops |
Each instance gets its own token at creation. Using the admintoken to send, or
the instance token to create, fails with a confusing 401.
Endpoints in use:
| Method | Endpoint | Purpose |
|---|
| POST | /instance/init | Create instance (admintoken) |
| POST | /instance/connect | Generate QR / pair code |
| POST | /instance/disconnect | Disconnect |
| GET | /instance/status | Connection status |
| DELETE | /instance | Delete instance |
| PUT/POST | /webhook (+ /webhook/{id}) | Register the webhook URL |
| POST | /send/text · /send/media · /send/menu | Send text · media · poll/menu |
| POST | /message/react · /edit · /delete · /download · /find | Message ops |
| POST | /chat/find · /chat/labels | Sync chats · chat labels |
| GET/POST | /labels · /label/edit | Label CRUD |
| GET/POST | /group/info · /metadata · /participants | Groups (GroupOps) |
Full request/response samples: assets/examples/uazapi-instance-requests.md
and assets/examples/uazapi-send-requests.md.
Phone and JID normalization deserves its own read before you write any send
path — it is where integrations break silently:
assets/examples/formatacao-telefone.md. The short version: contacts are
digits with a 55 prefix; groups keep the whole @g.us JID and must never
pass through replace(/\D/g, "").
3. The webhook is the crux
whatsapp-webhook is the only public function (verify_jwt = false). It
has no user session: it runs with the service role key and resolves the tenant
from the instance, by uazapi_token → instance name/session → legacy config.
Non-negotiables:
- Always answer 2xx. Uazapi re-queues on non-2xx. An unknown payload is
ignored with a
200, not a 400.
- Route special events first, in this order: reaction → message edit → poll
vote → normal message. They arrive on the same endpoint with different shapes.
contact_name comes from the chat object, never from
message.senderName (which is the sender — in a group that is a member, not
the chat).
- Groups only reach the main inbox when the instance has
receive_group_messages = true.
- Upsert the conversation, insert the message. Match by
(tenant, phone, instance_id), falling back to (tenant, phone, instance_id IS NULL)
for rows created before instances existed.
Payload samples for every event type:
assets/examples/uazapi-webhook-payloads.md.
Full walkthrough: references/sprint-2/fase-1-webhook.md.
4. Adapter points — what "any application" means
The artifacts came from a multi-tenant CRM. Five integration points are
host-app specific; everything else is self-contained. Map them before copying
code, and be consistent across all 34 functions.
| Artifact expects | What it is | How to adapt |
|---|
company_id column + get_user_company_ids(uuid) | Tenancy. A SECURITY DEFINER function returning the caller's tenant ids, used by every RLS policy | Rename to your tenant column (org_id, workspace_id, account_id) and point the function at your membership table. Single-tenant? Keep one fixed tenant row rather than stripping the column — removing it touches all 39 tables and every policy. |
@/contexts/CompanyContext → activeCompany.company_id | Active tenant in the browser | Your equivalent context/provider |
@/hooks/useAuth | Current user | Your auth hook |
@/hooks/use-toast | Toasts (shadcn) | Your toast |
@/lib/utils → cn() | Tailwind class merge | twMerge(clsx(...)) |
Optional, only if you install the modules that use them:
useClients, useEmployees, useTasks, useClientContacts,
useClientTouchpoints, useClientNotificationSettings, useAIApiKey. These are
CRM domain hooks — replace with your own entities or drop the feature.
The UI components assume shadcn/ui primitives and Tailwind. If your project
uses another UI kit, the hooks and edge functions still port unchanged — only
the components need rewriting.
5. Conventions that keep the module working
- Credentials live in the database, per tenant (
whatsapp_configs,
whatsapp_instances) — not in environment secrets. Different tenants have
different Uazapi accounts.
- RLS through the
SECURITY DEFINER function only. A direct subquery
against an RLS-protected table inside a policy fails silently (empty array,
no error).
GRANT SELECT, INSERT, UPDATE, DELETE ON <table> TO authenticated on
every new table. Without it the frontend gets an empty array and no error —
the single most confusing failure in this stack.
- CORS + an
OPTIONS handler in every edge function, no exceptions.
- Response shape:
{ success: true, data } / { success: false, error }.
config.toml per function. verify_jwt = false only for
whatsapp-webhook and the API4Com recording webhook.
- Realtime must be enabled explicitly:
ALTER PUBLICATION supabase_realtime ADD TABLE <table> — otherwise the inbox
silently never updates.
- Migrations are append-only. Never edit an applied one.
- Never log tokens or full message bodies. Log ids and event types.
6. Implementation roadmap (six sprints, each with a gate)
Run in order. A gate that fails blocks the next sprint — the failure modes
compound, and debugging sprint 4 on a broken sprint 2 is misery.
| Sprint | Delivers | Gate |
|---|
| 0 Foundation | Core tables, tenancy function, RLS, GRANTs, Realtime | Migrations apply; SELECT returns empty (not a permission error) |
| 1 Instances | create/qrcode/status/manage/configure-webhook + hooks + connection UI | Create instance → QR renders → status becomes connected |
| 2 Messaging | Public webhook, send text/media/reaction/edit/delete, sync/fetch/download, inbox UI, chat actions | Test message arrives on the phone; inbound message shows in the inbox live |
| 3 Organization | Assignment + squad + transfer, labels, quick replies, notes, reminders | Assign, label and annotate a conversation; all persist under RLS |
| 4 AI | _shared/ai-gateway.ts, realtime analysis, copilot, configurable agent | Analysis card fills; copilot suggests; agent answers in the playground |
| 5 Adjacent | API4Com calls, GroupOps, notification triggers | Place a call; manage a group; fire a trigger |
Each sprint has one document per phase in references/sprint-0 … sprint-5, with tasks,
artifacts and its own gate. references/ORQUESTRADOR.md is the master plan.
To execute it, use the wa-implement skill — it drives the sequence and
enforces the gates.
7. Failure modes worth memorizing
| Symptom | Almost always | Fix |
|---|
Frontend gets [], no error | Missing GRANT or RLS policy | Grant to authenticated; check the tenancy function |
| Inbox never updates live | Table not in the Realtime publication | ALTER PUBLICATION supabase_realtime ADD TABLE … |
| Webhook never fires | URL not registered, or verify_jwt left true | configure-uazapi-webhook; set verify_jwt = false |
| Uazapi retries the same event forever | Function answered non-2xx | Always return 200, even on ignored payloads |
| "Sent" but never delivered | Phone normalization | See formatacao-telefone.md — group JID stripped, or double 55 |
| 401 on send | Wrong header | token (instance) to send; admintoken only to create |
| Group name shows a member's name | Read message.senderName | Take contact_name from the chat object |
| Media fails to load in the browser | Uazapi media URLs expire | download-whatsapp-media + the whatsapp-media-cache bucket |
Use the wa-debug skill to work through these systematically.
8. File map
SKILL.md this file
references/
ORQUESTRADOR.md master plan: scope, architecture, conventions, gates
sprint-0…5/ one document per phase — tasks, artifacts, gate
assets/
migrations/ 14 SQL files, numbered in apply order (+ INDEX.md)
edge-functions/ 34 Deno functions (+ per-sprint INDEX, config.toml snippets)
frontend/hooks/ 21 React hooks
frontend/components/ 56 components (inbox, settings, AI, notifications)
frontend/lib/ invokeWithRetry, whatsappFormatting
examples/ webhook payloads, API requests, phone/JID rules,
AI analysis JSON, .env.example
Companion skills in this package:
wa-implement — drives the six sprints with their gates.
wa-debug — diagnoses a broken WhatsApp integration.
9. Security rules for this module
- Uazapi tokens are credentials. Never log them, never return them to the
browser, never commit one. The frontend never sees
uazapi_admintoken.
- The public webhook is an unauthenticated internet endpoint. It must validate
that the payload resolves to a known instance before writing anything, and
must never echo its input back in an error message.
- Message content is personal data. Keep it out of logs and out of error
payloads; log ids and event types instead.
- Sending to a phone number the tenant has no relationship with is what gets
WhatsApp numbers banned. If you add bulk sending, add rate limiting and an
opt-out path — the origin system's notification triggers show one way.