# wazmeow — Connector: complete LLM build guide > Paste this whole file into any LLM (Claude, GPT, Gemini, …) and ask it to embed the > **wazmeow Connector** — a widget that pairs a WhatsApp number (QR, phone code, or > passkey) inside any web app, without ever exposing the real instance token to the > browser. It is self-contained: the embed, the server-side token mint (Node, Next.js, > PHP, Laravel, Go), the full endpoint reference, events, theming/i18n, the passkey > solver, security rules, and the exact wire shapes are all here. > > - **Two placeholders**: `{BASE_URL}` (your wazmeow host, e.g. `https://your-host`) > and `{REAL_TOKEN}` (the per-instance user token — server-side only, NEVER in a browser). > - **Golden rule**: the browser only ever holds an **ephemeral** token (`ct_…`). --- ## 0. Mental model (one paragraph) Your backend calls `POST {BASE_URL}/session/connector/token` with the **real** instance token and gets back a short-lived **ephemeral** token (`ct_…`, default 10 min). You send only that ephemeral token to the browser. The browser loads ` ``` `window.ZZ.connector` (alias `window.ZZGConnector`): | Method | Description | |--------|-------------| | `open(opts) → Promise` | Opens the modal; resolves `true` when paired, `false` if closed first. | | `close()` | Tear down the modal. | | `on(ev, fn)` / `off(ev, fn)` | Events: `open`, `status`, `qr`, `connected`, `error`, `close`. | **Options**: `token` (required), `baseUrl`, `theme`, `locale`, `methods{qr,phone,passkey}`, `messages` (override i18n strings), `container` (CSS selector/element; default body), `pollIntervalMs` (2000), `subscribe` (["Message"]), `autoConnect` (true — set false when the instance is already connected), `extensionInstallUrl {chrome,firefox}`, `bookmarkletUrl`. Each event `ev` also maps to an `on` callback in `open(opts)` (e.g. `onConnected`). The widget renders in a Shadow DOM (style-isolated) and is dependency-free. --- ## 3. Server-side mint — the only code you write The one integration point is a backend route that mints the ephemeral token and returns only that to the browser. Full runnable examples: `examples/connector/{node-express,nextjs,php,laravel,go}`. **Node / Express** ```js app.post("/connector-token", async (_req, res) => { const r = await fetch(`${process.env.WAZMEOW_URL}/session/connector/token`, { method: "POST", headers: { "Content-Type": "application/json", token: process.env.WAZMEOW_TOKEN }, }); const env = await r.json(); const data = env.data ?? env; res.json({ token: data.token, expiresIn: data.expiresIn }); }); ``` **Next.js (App Router)** — `app/api/connector-token/route.ts` ```ts export async function POST() { const r = await fetch(`${process.env.WAZMEOW_URL}/session/connector/token`, { method: "POST", headers: { "Content-Type": "application/json", token: process.env.WAZMEOW_TOKEN! }, cache: "no-store", }); const env = await r.json(); const data = env?.data ?? env; return Response.json({ token: data.token, expiresIn: data.expiresIn }); } ``` **PHP** ```php $ch = curl_init(getenv('WAZMEOW_URL')."/session/connector/token"); curl_setopt_array($ch, [CURLOPT_POST=>true, CURLOPT_RETURNTRANSFER=>true, CURLOPT_HTTPHEADER=>['Content-Type: application/json','token: '.getenv('WAZMEOW_TOKEN')]]); $env = json_decode(curl_exec($ch), true); $data = $env['data'] ?? $env; echo json_encode(['token'=>$data['token'],'expiresIn'=>$data['expiresIn']]); ``` **Laravel** ```php $resp = Http::withHeaders(['token' => env('WAZMEOW_TOKEN')]) ->post(env('WAZMEOW_URL')."/session/connector/token"); $data = $resp->json()['data'] ?? $resp->json(); return response()->json(['token' => $data['token'], 'expiresIn' => $data['expiresIn']]); ``` **Go (stdlib)** ```go req, _ := http.NewRequest("POST", zuckURL+"/session/connector/token", nil) req.Header.Set("token", zuckTok) resp, _ := http.DefaultClient.Do(req) body, _ := io.ReadAll(resp.Body) var env map[string]any; json.Unmarshal(body, &env) data, _ := env["data"].(map[string]any) json.NewEncoder(w).Encode(map[string]any{"token": data["token"], "expiresIn": data["expiresIn"]}) ``` --- ## 4. Passkey — "covers all cases" for free Some accounts require a **passkey** instead of QR/phone. WebAuthn binds the credential to its RP origin: WhatsApp's passkey is bound to `whatsapp.com`, and the browser only runs `navigator.credentials.get()` from a page whose origin is `https://web.whatsapp.com`. Neither the dashboard nor an embed on your origin can run it. So the connector is a bridge: 1. **Probe** — `POST /connector/passkey/challenge` forces WhatsApp to hand over the WebAuthn options on demand (deterministic; `503 passkey_unavailable` for non-passkey accounts). 2. **Bridge** — a hidden iframe (`{BASE_URL}/connector.html`) relays to the wazmeow passkey extension (content script on `web.whatsapp.com`). 3. **Solve** — the extension runs `navigator.credentials.get()` with the server's `publicKey` options **verbatim** and returns the assertion (base64url). 4. **Submit** — `POST /connector/passkey/response`, then `confirm` (or auto when `skipHandoffUX`), then the poll loop sees `loggedIn`. Because the server's options are passed straight to the browser, the device presents whatever it supports — Touch ID, Windows Hello, a security key, or a phone over hybrid/Bluetooth. No per-case handling; the universality is the WebAuthn API's. **Solvers**: (1) the `zzg-passkey-extension/` (MV3 Chrome/Edge/Brave, `.xpi` Firefox) — keep a `web.whatsapp.com` tab open; (2) the `/passkey-bookmarklet.html` bookmarklet (zero install, any browser/OS incl. Windows; accepts `ct_…` tokens; deep-linkable with `?apiUrl=…&token=…`). The widget detects the extension and, if absent, shows install links + the bookmarklet. --- ## 5. Security (do this) - **Mint server-side.** The real token authenticates the mint and stays on your backend. Only the ephemeral `ct_…` token reaches the browser; it works only on `/connector/*`. - **CORS allowlist.** For cross-origin embeds, add your app's origin to `CONNECTOR_CORS_ORIGINS` on the wazmeow host (comma-separated; `*` = dev only). Non-allow-listed origins get no CORS headers and the browser blocks them. - **Env** (wazmeow host): `CONNECTOR_CORS_ORIGINS`, `CONNECTOR_TOKEN_TTL` (default 600s), `CONNECTOR_TOKEN_RATE_PER_MIN` (default 60). - **Never** put the real token, or `NEXT_PUBLIC_*`-style secrets, in client code. --- ## 6. Operational notes - **In-memory state** (ephemeral tokens + passkey state) is per-process — lost on restart, not shared across replicas. For multi-replica, pin connector traffic to one replica or move the store to Redis (future work). - **Handoff window** ~5 min: if the passkey handoff proof lapses, reconnect for a fresh challenge. - **Passkey response** is rate-limited (1 / 5s per user); wait a moment before retrying a cancelled prompt. - Passkey server-side acceptance is validated live; the bookmarklet fallback always works. --- ## 7. Copy-paste task prompt for your LLM > Using the wazmeow Connector guide above, add "Connect WhatsApp" to my app. > Backend `{stack}` at `{BASE_URL}` with instance token in env. 1) Add a server route > `POST /connector-token` that mints an ephemeral token via > `POST {BASE_URL}/session/connector/token` (real token server-side) and returns only > `{ token }`. 2) On the page, load `{BASE_URL}/connector.js`, fetch `/connector-token`, > and call `ZZ.connector.open({ token, baseUrl: "{BASE_URL}", methods:{qr:true,phone:true,passkey:true} })`. > 3) Remind me to add my app's origin to `CONNECTOR_CORS_ORIGINS` on the wazmeow host. > Output the full files for `{stack}`. Full reference: `/docs/connector.md`. OpenAPI: `/api/spec.yml` (tag **Connector**).