# v-chat — WhatsApp Gateway + CRM API > v-chat is a multi-session WhatsApp gateway with a built-in CRM. It connects through > WhatsApp Multi-Device (Linked Devices), the same mechanism as WhatsApp Web — no Meta > Business API approval and no per-message fees. This document is the machine-readable > reference for the public REST API, outbound webhooks, and Socket.io realtime events. > It is intended to be read in full by an AI agent or developer to integrate against the API. - Base URL (production): `https://vchat.ae/api` - Content type: `application/json` for all requests and responses - Human docs: https://vchat.ae/developers - Error shape (all non-2xx): `{ "error": "", "message"?: "", "issues"?: [] }` ## Core concepts - **Organization** — a company/tenant. The first registrant is the owner (`admin`). Members are `admin` or `member`. - **Session** — one paired WhatsApp number. Belongs to an org. Identified by a `sessionId` (Mongo id string). - **JID** — a WhatsApp address. Direct: `@s.whatsapp.net` (e.g. `971501234567@s.whatsapp.net`). Group: `@g.us`. In URL paths the `@` must be URL-encoded as `%40`. - **Phone number format** — international, digits only, no `+` (e.g. `971501234567`). ## Authentication Every protected endpoint accepts EITHER a user JWT OR an API key. Both resolve to the permissions of the user who owns them. ### API key (recommended for server integrations) - Create in the dashboard: Settings → API Keys. Plaintext token is shown ONCE; store it securely. - Token prefix: `wk_` (format `wk_`). - Send via header `x-api-key: wk_...` OR `Authorization: Bearer wk_...`. - Two scopes: - **Account key** (created with no session) — inherits the owner's full access: read all chats, send, manage sessions, mint keys. - **Session-scoped key** (bound to one session) — required for `POST /messages/send`, which derives the session from the key. ### User JWT - `POST /api/auth/login` with `{ "email": "...", "password": "..." }` → `{ token, expiresIn, refreshToken, refreshExpiresAt, user }`. - Send via header `Authorization: Bearer `. Default access-token lifetime 7d; refresh token 30d, rotating. ## Quickstart — send a text message (session-scoped key) ``` POST https://vchat.ae/api/messages/send x-api-key: wk_xxxxxxxxxxxxxxxxxxxxxxxx Content-Type: application/json { "to": "971501234567", "type": "text", "text": "Hello from v-chat" } ``` Response: `{ "id": "", "to": "971501234567@s.whatsapp.net", "status": "sent" }` ## Sending messages ### A) One-shot — `POST /api/messages/send` (session-scoped API key only) Body is a discriminated union on `type`. `to` is a phone number or full JID. Media is supplied as a public `mediaUrl` OR inline `mediaBase64`. - text: `{ "to", "type": "text", "text": "<=4096 chars" }` - image: `{ "to", "type": "image", "mediaUrl"|"mediaBase64", "caption"? }` - document: `{ "to", "type": "document", "filename", "mimetype"?, "mediaUrl"|"mediaBase64", "caption"? }` Returns `{ id, to, status: "sent" }`. Errors: 409 if the session is not connected. ### B) Full send — `POST /api/sessions/:id/chats/:jid/send` (any key or JWT) `:jid` is URL-encoded. Returns `{ "ok": true, "id": "" }`. Inline media goes in `mediaBase64` (a `data:...;base64,` prefix is allowed and stripped). Add `"quotedId": ""` to any send to reply to a message. Supported `type` values and their fields: - `text` — `{ "type":"text", "text":"..." }` (WhatsApp markdown: `*bold*`, `_italic_`, `~strike~`, ```` ```mono``` ````) - `image` — `{ "type":"image", "mediaBase64":"...", "caption"? }` - `video` — `{ "type":"video", "mediaBase64":"...", "caption"?, "gifPlayback"? }` - `audio` — `{ "type":"audio", "mediaBase64":"...", "ptt"? }` (`ptt:true` = voice note) - `document` — `{ "type":"document", "mediaBase64":"...", "filename", "mimetype"? }` - `sticker` — `{ "type":"sticker", "mediaBase64":"..." }` (webp) - `location` — `{ "type":"location", "lat":, "lng":, "locationName"? }` - `poll` — `{ "type":"poll", "question":"...", "options":["A","B",...], "selectableCount"? }` (2–12 options) - `contact` — `{ "type":"contact", "contacts":[{ "name":"...", "phone":"..." }] }` (or `{ "vcard", "displayName" }`) - `react` — `{ "type":"react", "targetMsgId":"", "emoji":"👍" }` (empty `emoji` removes) - `edit` — `{ "type":"edit", "targetMsgId":"", "text":"..." }` - `delete` — `{ "type":"delete", "targetMsgId":"" }` - `forward` — `{ "type":"forward", "sourceJid":"", "sourceMsgId":"" }` ## Reading data - `GET /api/sessions` — list your sessions (each row has `id, name, status, phoneNumber`, effective access flags). - `GET /api/sessions/:id` — session detail + connection status. - `GET /api/sessions/:id/stats` — per-session aggregate (chats, messages, scheduled, campaigns, webhooks, contacts). - `GET /api/sessions/stats` — account-wide aggregate across all sessions. - `GET /api/sessions/:id/chats` — list chats (paginated sidebar rows). - `GET /api/sessions/:id/chats/:jid/messages?before=&limit=50` — page through messages (newest first). - `GET /api/sessions/:id/chats/:jid/messages/:msgId/media` — stream a message's media bytes. - `POST /api/sessions/:id/chats/:jid/read` — mark a chat as read. - `POST /api/sessions/:id/chats/:jid/typing` — `{ "state": "composing"|"paused"|"recording" }`. - `GET /api/sessions/:id/contacts/:jid` — resolved contact info (name, phone, avatar, status, group meta). - `PUT /api/sessions/:id/contacts/:jid/rename` — `{ "customName": "..." }` CRM override (empty clears). - `GET /api/sessions/:id/search?q=&jid=` — search messages (session-wide if no `jid`). - `GET /api/sessions/:id/chats/:jid/meta` / `PUT` — CRM `{ tags, notes }` per chat. ### Scheduling - `POST /api/sessions/:id/chats/:jid/schedule` — `{ "runAt": , "payload": }`. - `GET /api/sessions/:id/scheduled?jid=` — list pending/sent jobs. - `DELETE /api/sessions/:id/scheduled/:jobId` — cancel a pending job. ### Other resource groups (see Postman collection for full detail) - Sessions lifecycle: `POST /api/sessions` (create), `POST /:id/restart|logout`, `POST /:id/pairing-code`, `DELETE /:id`. - Groups: `/api/sessions/:id/groups` (create, metadata, invite, participants add/remove/promote/demote, subject/description/settings). - Channels/Newsletters: `/api/sessions/:id/newsletters` (list, follow/unfollow, react, create). - Status: `/api/sessions/:id/status` (post, received). - Bulk campaigns: `/api/sessions/:id/bulk` (templates, campaigns, recipients, funnel). - Team/RBAC: `/api/org` (members), `/api/sessions/:id/grants` (per-session access). - API keys: `/api/keys` (list, create, revoke). Devices/push: `/api/devices`. Billing: `/api/billing`. ## Webhooks Register a per-session HTTP endpoint to receive signed events. This is the way to RECEIVE inbound messages and delivery receipts server-side (e.g. on an API-only number). ### Register ``` POST https://vchat.ae/api/sessions/:sessionId/webhooks { "url": "https://your-server.example.com/wa-hook", "events": ["message.received", "message.status"], "secret": "your-shared-secret", "headers": { "X-Custom": "optional" } } ``` Use `"events": ["*"]` to subscribe to everything. Manage: `GET|POST|PATCH|DELETE /api/sessions/:sessionId/webhooks[/:id]`, `GET /:id/deliveries`, `POST /:id/deliveries/:deliveryId/retry`. ### Event types - `message.received` — inbound message arrived - `message.sent` — outbound message confirmed by WhatsApp - `message.status` — delivery receipt (delivered / read / played) - `message.deleted` — a message was remote-deleted - `message.edited` — a message was remote-edited - `message.reaction` — reaction added or removed - `session.connected` — WhatsApp connection came up - `session.disconnected` — WhatsApp connection went down - `session.qr` — a new QR code is available for pairing - `chat.update` — chat metadata changed (pin / read / mute / archive …) - `presence` — a contact's presence changed (typing / online) - `group.update` — group subject / participants / settings changed - `*` — all events ### Delivery format POST to your URL with headers: ``` Content-Type: application/json User-Agent: wacrm-webhooks/1 X-Webhook-Id: X-Webhook-Event: message.received X-Idempotency-Key: # stable across retries — dedupe on this X-Signature: t=, v1= ``` Body: ``` { "id": "", "event": "message.received", "sessionId": "", "timestamp": , "payload": { } } ``` ### Signature verification (HMAC-SHA256) - Signed string is `` `${timestamp}.${rawBody}` `` (the raw request body, before JSON parse). - `X-Signature` header: `t=, v1=`. Reject if the timestamp is older than 5 minutes. - Compute `HMAC-SHA256(secret, ".")` and compare to `v1` with a constant-time check. ```js import { createHmac } from 'node:crypto'; function verify(rawBody, header, secret, toleranceSec = 300) { const p = Object.fromEntries(header.split(',').map(s => s.trim().split('='))); const ts = Number(p.t); if (Math.abs(Date.now()/1000 - ts/1000) > toleranceSec) return false; const expected = createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex'); return expected.length === p.v1.length && Buffer.from(expected,'hex').equals(Buffer.from(p.v1,'hex')); } ``` ### Retries - Respond `2xx` fast to ACK; process asynchronously. Non-2xx / network error / timeout (10s) → retry. - Up to 6 attempts, backoff: 30s → 1m → 2m → 5m → 15m → 30m (±25% jitter). - After 50 consecutive failures the webhook auto-disables; re-enable with `PATCH /webhooks/:id`. - Retries reuse `X-Idempotency-Key` — dedupe on it so re-delivery is a safe no-op. ## Realtime (Socket.io) For live clients (not server-to-server — use webhooks for that). Socket.io is served at the server root (path `/socket.io`, same origin as the REST API, NOT under `/api`). ```js import { io } from 'socket.io-client'; const socket = io('https://vchat.ae', { auth: { token: '' } }); socket.emit('join', '', (ack) => { /* { ok: true } | { error: 'forbidden' } */ }); socket.on('message:received', (m) => {}); // { sessionId, jid, msgId, fromMe:false, timestamp, preview, msgType } socket.on('receipt', (r) => {}); // { sessionId, jid, msgId, status: 'sent'|'delivered'|'read' } socket.on('qr', (q) => {}); // { sessionId, qr } during pairing socket.on('status', (s) => {}); // { sessionId, status } ``` - Every payload carries its own `sessionId`. Leave a room with `socket.emit('leave', '')`. - On reconnect, re-emit `join` and re-fetch the chat list — the server does not auto-re-push state. ## Errors & limits - `400 validation_error` — request body failed schema; details in `issues`. - `401 unauthorized` — missing or invalid credentials. - `402 subscription_required` — plan/quota required (trial expired, seat or broadcast limit). - `404 not_found` — missing OR no access (existence is not leaked). - `409 session_not_connected` — the WhatsApp session is not ready to send. - WhatsApp rate-limits aggressive sending. For bulk, use the built-in campaign engine (paced sends, daily caps, business hours, STOP opt-outs) rather than looping `/send`. ## Full reference The complete REST surface plus the Socket.io event reference ship as a Postman collection. Human-readable docs with copyable examples: https://vchat.ae/developers