๐ฑ WhatsApp Multi-Driver API v1.0
Complete Developer Guide - Multi-Driver WhatsApp Integration
Welcome to the WhatsApp Multi-Driver API documentation. This API provides WhatsApp integration through two drivers: WWebJS (WhatsApp Web.js) and Cloud API (Meta's official Business API). Choose the driver that best fits your use case.
- REST API - Traditional HTTP endpoints for all operations
- MCP (Model Context Protocol) - AI-friendly interface for Claude and other AI agents
๐ Quick Start (5 minutes)
Get up and running with the WhatsApp Multi-Driver API in 5 minutes.
Step 1: Get API Key
Contact your system administrator to obtain an API key. The API key is required for all API calls and should be stored securely.
Step 2: Test Connection
// Test API connection (public liveness probe โ no authentication required)
const response = await fetch('https://apiwts.top/health/live');
const data = await response.json();
console.log('API Status:', data.status); // Should be "alive"
// Response: { "status": "alive", "timestamp": "2026-06-01T10:30:00.000Z" }
GET /health/live is public (no key) and returns
{ "status": "alive" } โ use it for load balancers and uptime monitors. The
detailed GET /monitoring/health endpoint (returns "healthy")
requires authentication.
Step 3: Create WhatsApp Session
// Create a new WhatsApp session
const response = await fetch('https://apiwts.top/api/v1/sessions', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key-here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-first-session'
})
});
const session = await response.json();
console.log('Session created:', session);
Step 4: Get QR Code
// Get QR code to scan with WhatsApp
const response = await fetch('https://apiwts.top/api/v1/sessions/my-first-session/qr', {
headers: {
'X-API-Key': 'your-api-key-here'
}
});
const blob = await response.blob();
const qrUrl = URL.createObjectURL(blob);
// Display QR code in an img tag
document.getElementById('qr-code').src = qrUrl;
Step 5: Send Your First Message
// Send WhatsApp message
const response = await fetch('https://apiwts.top/api/v1/messages/text', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key-here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-first-session',
to: '+5511999999999', // International format
text: 'Hello from WhatsApp API! ๐'
})
});
const result = await response.json();
console.log('Message sent:', result.id);
โ ๏ธ CORS Setup - Critical Configuration
Why CORS Matters
- Browser Security: Browsers block direct requests to external domains
- API Protection: External APIs often don't allow all origins
- Development vs Production: Different solutions needed for each environment
Development Solution: Vite Proxy
For development with Vite, use proxy configuration:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'https://apiwts.top',
changeOrigin: true,
secure: false
},
'/health': {
target: 'https://apiwts.top',
changeOrigin: true,
secure: false
}
}
}
});
Code Implementation
// โ
CORRECT - Use relative paths (works with proxy)
const response = await fetch('/api/v1/sessions/my-session/qr', {
headers: { 'X-API-Key': 'your-key' }
});
// โ WRONG - Direct external URL (CORS error)
const response = await fetch('https://apiwts.top/api/v1/sessions/my-session/qr', {
headers: { 'X-API-Key': 'your-key' }
});
๐ Authentication
All API requests require authentication using an API Key. Provide it in the
X-API-Key header (recommended), or equivalently as a Bearer token in the
Authorization header โ both are accepted:
// Either of these works (X-API-Key is checked first):
'X-API-Key': 'sk_live_...'
'Authorization': 'Bearer sk_live_...'
Getting Your API Key
Contact your system administrator to obtain an API key for your application. Each API key is unique and provides access to the API resources.
- Production:
sk_live_{64-character-hex} - Test:
sk_test_{64-character-hex} - Total Length: 72 characters
-
Example:
sk_live_a1b2c3d4e5f6789...(shortened for display)
Security Note: API keys are generated with 256 bits of cryptographic randomness and stored as SHA-256 hashes in the database.
Using the API Key
// Include API Key in all requests
const headers = {
'X-API-Key': 'your-api-key-here',
'Content-Type': 'application/json'
};
const response = await fetch('https://apiwts.top/api/v1/sessions', {
headers
});
๐ Driver Comparison
This API supports two WhatsApp integration drivers. Choose based on your requirements:
Available Drivers
| Driver | Type | Best For |
|---|---|---|
| WWebJS WWebJS | WhatsApp Web.js (Puppeteer) | Full feature access, personal/business accounts, no Meta approval needed |
| Cloud API Cloud API | Meta Official Business API | Enterprise scale, template messages, official support, requires Meta approval |
| Instagram Instagram | Meta Graph API (Instagram Messaging) | Instagram DM automation, business accounts, requires Meta App approval (v3.9.0+) |
Feature Compatibility Matrix
| Feature | WWebJS | Cloud API | Notes |
|---|---|---|---|
| Text Messages | โ | โ | Full support on both |
| Media Messages (Image, Video, Audio, Document) | โ | โ | Full support on both |
| Template Messages | โ | โ | Requires Meta-approved templates |
| Message Reactions | โ | โ | Emoji reactions to messages |
| Message Revoke/Delete | โ | โ | ~48 hour window |
| Forward Messages | โ | โ | Forward existing messages |
| Edit Messages | โ | โ | ~15 min window (v3.4+) |
| Download Media | โ | โ | Base64 from messages (v3.4+) |
| Send Location | โ | โ | Geographic coordinates (v3.4+) |
| Get Quoted Message | โ | โ | Retrieve replied-to message (v3.4+) |
| Star/Unstar Messages | โ | โ | Favorite/unfavorite messages (v3.4+) |
| Pin/Unpin Messages | โ | โ | Pin messages in chat for duration (v3.4+) |
| Get Message Mentions | โ | โ | Get @mentions in a message (v3.4+) |
| Get Poll Votes | โ | โ | Get votes from poll messages (v3.4+) |
| Message Info (Receipts) | โ | โ | Detailed delivery/read info |
| Contact Management | โ | โ | List, block, unblock contacts |
| Chat Management | โ | โ | Archive, mute, pin, typing indicators |
| Groups (Basic) | โ | โ | Create, info, add/remove participants |
| Groups (Admin) | โ | โ | Promote/demote admins, settings |
| Scheduled Messages | โ | โ | Future message scheduling |
| Webhooks | โ | โ | Real-time notifications |
| QR Code Authentication | โ | โ | Cloud API uses phone number verification |
When to Use Each Driver
- You need full feature access (reactions, revoke, contacts, chats)
- You want to use personal or business WhatsApp accounts
- You don't want to go through Meta's approval process
- You need group admin operations
- You need enterprise-scale messaging
- You want to use pre-approved message templates
- You require official Meta support
- Your use case is approved by Meta
Specifying Driver in Requests
When creating a session, specify the driver in the configuration:
// Create session with WWebJS driver (default)
POST /api/v1/sessions
{
"sessionId": "my-session",
"config": {
"driver": "wwebjs"
}
}
// Create session with Cloud API driver
POST /api/v1/sessions
{
"sessionId": "my-cloud-session",
"config": {
"driver": "cloud-api",
"phoneNumberId": "your-phone-number-id",
"accessToken": "your-meta-access-token"
}
}
Provisioning a Cloud API Tenant (Admin runbook)
โจ NEW in v3.100.1
Inbound Cloud API webhooks are routed to a tenant by matching the webhook's
phone_number_id against tenants.cloud_phone_number_id. To
enable the end-to-end flow (inbound + outbound) for a Cloud number, provision the
tenant and its session in two steps:
-
Create the Cloud tenant (Admin โ Tenants โ New, driver =
Cloud API): set Phone Number ID (the Metaphone_number_id, stored plaintext as the indexed resolver key), Access Token and optional App Secret. The token and secret are encrypted at rest (AES-256-GCM) and returned masked (***redacted***) byGET /admin/api/tenants/:idโ never in plaintext. Editing the tenant without retyping the token preserves the stored value (no-wipe). -
Create a Cloud session for that tenant (driver =
cloud) with awebhookUrl(where inbound messages are forwarded) anddriverConfig.phoneNumberIdset to the samephone_number_id(used to disambiguate the resolver).
***redacted***). Outbound sends decrypt the token transparently at the
persistence boundary before calling Meta.
- WWebJS - Available only with WWebJS driver
- Cloud API - Available only with Cloud API driver
- Instagram - Available only with Instagram driver (v3.9.0+)
- Both - Available with both WhatsApp drivers
๐ REST API Reference
Health Check
Updated in v3.22.1
GET /health/live
Public liveness probe - verify API is responding. Use this for load balancers and uptime monitors.
// Request (no authentication required)
const response = await fetch('https://apiwts.top/health/live');
// Response 200 - Alive
{
"status": "alive",
"timestamp": "2025-01-15T10:30:00.000Z"
}
Additional monitoring endpoints (
/monitoring/health,
/monitoring/ready, /monitoring/metrics) are available for
infrastructure monitoring but require authentication (X-API-Key header or admin
session). Contact support for integration details.
Admin Diagnostics (Restricted)
โจ NEW in v3.8.13
GET /admin/api/system/diagnostics
Returns system health diagnostics for troubleshooting infrastructure issues.
Responses
- 200 OK: Returns diagnostics summary with overall system status
- 401 Unauthorized: Admin authentication required
- 403 Forbidden: Insufficient permissions
- 503 Service Unavailable: Diagnostics check failed
๐ Internal Documentation: Detailed schema, response examples, and
troubleshooting guide available in docs/progress/v3.8.13-progress.md.
Tenant Information
GET /api/v1/tenant/me
Get information about the authenticated tenant. Returns tier, limits, and account status.
๐ก Enterprise Pattern: This endpoint follows the industry-standard
/me pattern (like /users/me) for self-identification without
exposing internal IDs in the URL.
// Request
const response = await fetch('https://apiwts.top/api/v1/tenant/me', {
headers: { 'X-API-Key': 'your-api-key' }
});
// Response 200
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"name": "My Company",
"tier": "professional",
"isActive": true,
"createdAt": "2025-01-01T00:00:00.000Z",
"limits": {
"maxSessions": 10,
"maxActiveSessions": 5,
"maxMessagesPerDay": 10000,
"requestsPerMinute": 60
}
}
Use Cases:
- Display account information in dashboards
- Check tier/limits before operations
- Validate API key and get tenant UUID for OAuth flows
Session Management
POST /api/v1/sessions
Create a new WhatsApp session.
// Request
const response = await fetch('https://apiwts.top/api/v1/sessions', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session-001'
})
});
// Response 201
{
"sessionId": "my-session-001",
"tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"state": "INITIALIZING",
"driver": "wwebjs",
"driverVersion": "1.26.0",
"phoneNumber": null,
"pushname": null,
"platform": null,
"qualityRating": null,
"createdAt": "2025-01-15T10:35:00.000Z",
"authenticatedAt": null,
"readyAt": null,
"lastActivity": null
}
// ๐ก TIP: Save the "tenantId" from this response - you'll need it for OAuth flows
// Example: Use tenantId to build OAuth URL for Instagram:
// https://apiwts.top/admin/api/instagram/oauth/start?tenantId={tenantId}&sessionId={sessionId}
GET /api/v1/sessions/:sessionId/qr
Get QR code for WhatsApp authentication.
// Request
const response = await fetch('https://apiwts.top/api/v1/sessions/my-session-001/qr', {
headers: { 'X-API-Key': 'your-api-key' }
});
// Response 200 - Binary image (PNG)
const blob = await response.blob();
const qrUrl = URL.createObjectURL(blob);
GET /api/v1/sessions/:sessionId v3.33.0+
Get detailed session information including webhook configuration.
// Request
const response = await fetch('https://apiwts.top/api/v1/sessions/my-session-001', {
headers: { 'X-API-Key': 'your-api-key' }
});
// Response 200
{
"sessionId": "my-session-001",
"tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"state": "READY",
"driver": "wwebjs",
"driverVersion": "1.26.0",
"phoneNumber": "+5511999999999",
"pushname": "My Business",
"platform": "smba",
"qualityRating": "GREEN",
"createdAt": "2025-01-15T10:35:00.000Z",
"authenticatedAt": "2025-01-15T10:36:00.000Z",
"readyAt": "2025-01-15T10:36:30.000Z",
"lastActivity": "2025-01-15T12:00:00.000Z",
// v3.33.0: Webhook configuration fields
"webhookUrl": "https://example.com/webhook", // string or null
"webhookAuthToken": "***redacted***", // "***redacted***" if set, null if not
"selfSentWebhookEnabled": true, // boolean
"deduplicationWindowSeconds": 60 // integer or null
}
webhookAuthToken is always returned as
"***redacted***" when set (never exposes the actual token). Use PATCH to
update it.
GET /api/v1/sessions
List all sessions for your tenant.
// Request
const response = await fetch('https://apiwts.top/api/v1/sessions', {
headers: { 'X-API-Key': 'your-api-key' }
});
// Response 200
{
"sessions": [
{
"sessionId": "my-session-001",
"state": "READY",
"phoneNumber": "+5511999999999",
"createdAt": "2025-01-15T10:35:00.000Z"
}
]
}
DELETE /api/v1/sessions/:sessionId
Permanently delete a WhatsApp session. This will:
- Remove the session record from the database
- Delete all session authentication files (requires new QR scan)
- Cancel any pending scheduled messages for this session
POST /sessions/:sessionId/restart instead - it disconnects the WhatsApp
connection but preserves the session for reconnection.
// Request - CAUTION: This permanently deletes the session!
const response = await fetch('https://apiwts.top/api/v1/sessions/my-session-001', {
method: 'DELETE',
headers: { 'X-API-Key': 'your-api-key' }
});
// Response 204 - No Content (session permanently deleted)
// Response 404 - Session not found
Session Lifecycle & Operations
Understanding the difference between session operations is crucial:
| Operation | Endpoint | Effect | Recoverable? |
|---|---|---|---|
| Create | POST /sessions |
Creates new session record | - |
| Connect | POST /sessions/:id/connect |
Initializes WhatsApp connection | Yes |
| Restart | POST /sessions/:id/restart |
Disconnects + reconnects (keeps session) | Yes |
| Disconnect v3.7.5+ | POST /sessions/:id/disconnect |
Temporarily disconnects (preserves auth files) | Yes |
| Reconnect v3.7.5+ | POST /sessions/:id/reconnect |
Reconnects a disconnected session | Yes |
| Delete | DELETE /sessions/:id |
PERMANENTLY removes session | NO |
/disconnect for temporary
disconnections (e.g., "Disconnect WhatsApp" buttons), then /reconnect
OR /restart when ready to reconnect โ both perform the
full shutdown โ init cycle (equivalent behavior since v3.81.0 FASE 1B). Allow 20-30s for
full transition to READY. Only use DELETE when you truly want to remove the
session permanently.
Dangerous Operations IRREVERSIBLE
The following operations are IRREVERSIBLE:
-
DELETE /api/v1/sessions/:sessionId- Permanently deletes session and all authentication files -
DELETE /api/v1/messages/scheduled/:id- Permanently cancels scheduled message
Always implement a confirmation dialog in your UI before calling these
endpoints.
For temporary disconnection, use
POST /sessions/:sessionId/disconnect instead of DELETE.
Example: Temporarily Disconnect WhatsApp
To disconnect a WhatsApp session without deleting it (e.g., for a "Disconnect" button in your UI):
// โ
CORRECT: Disconnect temporarily (session preserved)
const disconnectWhatsApp = async (sessionId, apiKey) => {
const response = await fetch(`https://apiwts.top/api/v1/sessions/${sessionId}/disconnect`, {
method: 'POST',
headers: { 'X-API-Key': apiKey }
});
if (response.ok) {
const data = await response.json();
console.log(`Session ${data.sessionId} disconnected`);
// User can reconnect later using /reconnect endpoint
}
};
// To reconnect later:
const reconnectWhatsApp = async (sessionId, apiKey) => {
const response = await fetch(`https://apiwts.top/api/v1/sessions/${sessionId}/reconnect`, {
method: 'POST',
headers: { 'X-API-Key': apiKey }
});
// Check /qr endpoint if new QR code is needed
};
// โ WRONG: This DELETES the session permanently!
// await fetch(`/api/v1/sessions/${sessionId}`, { method: 'DELETE' });
POST /api/v1/sessions/:sessionId/connect
Connect/Initialize a WhatsApp session to start generating QR codes.
// Request
const response = await fetch('https://apiwts.top/api/v1/sessions/my-session-001/connect', {
method: 'POST',
headers: { 'X-API-Key': 'your-api-key' }
});
// Response 201 - Connection initiated
{
"sessionId": "my-session-001",
"state": "INITIALIZING",
"message": "Session my-session-001 connection initiated",
"timestamp": "2025-01-15T10:35:00.000Z"
}
// Response 200 - Already connected
{
"sessionId": "my-session-001",
"state": "READY",
"message": "Session my-session-001 already connected",
"timestamp": "2025-01-15T10:35:00.000Z"
}
GET /api/v1/sessions/:sessionId/status
Get session status and QR code availability.
// Request
const response = await fetch('https://apiwts.top/api/v1/sessions/my-session-001/status', {
headers: { 'X-API-Key': 'your-api-key' }
});
// Response 200
{
"sessionId": "my-session-001",
"state": "READY",
"qrAvailable": false,
"lastQrAt": "2025-01-15T10:30:00.000Z",
"retries": 0,
"metadata": {}
}
Session States (canonical)
The state field returns one of the following values:
| State | Meaning |
|---|---|
INITIALIZING |
Session is starting up / driver is initializing. |
QR_PENDING |
Waiting for the QR code to be scanned (check /qr). |
AUTHENTICATED |
QR scanned; finishing the handshake before becoming READY. |
READY |
Connected and able to send/receive messages. |
DISCONNECTED |
Not connected (may be transient during internal recovery โ see note below). |
FAILED |
Initialization or authentication failed permanently; needs
/reconnect or re-pairing.
|
DISCONNECTED during internal recovery:
During the first AUTHENTICATED โ READY transition the session may briefly
drop to DISCONNECTED and auto-recover on its own (typically ~18-27s, p99
~27s, up to ~90s). GET /status reports the real underlying state, so it
can return DISCONNECTED during this window even though no
action is needed. The v3.90.0+ webhook debounce only suppresses
the session.disconnected webhook for this case โ it does
not mask the /status value. Recommendation: when polling
/status and you see DISCONNECTED, keep re-polling
for ~30-45 seconds before reacting (e.g. before calling
/reconnect or surfacing an error). Only treat it as a real disconnect if it
persists past that grace window.
POST /api/v1/sessions/:sessionId/restart v3.81.0+
Restart a WhatsApp session โ performs full
shutdown โ re-initialization cycle. Equivalent in behavior to
POST /sessions/:sessionId/reconnect since v3.81.0 FASE 1B.
- Response time: ~5 seconds (handler returns once driver enters INITIALIZING phase).
- Full transition to READY: 20-30 seconds additional (asynchronous).
-
Recommended: Poll
GET /sessions/:id/statusevery 2-5s untilstate: 'READY'before resuming traffic. - Client HTTP timeout: set to โฅ 30 seconds to safely cover handler response window.
- Do NOT expect synchronous READY โ sending messages immediately after this call will fail.
-
Rate limit (v3.103.0): max 5 requests/min per session.
Excess returns
429 Too Many Requestswith aRetry-Afterheader โ do NOT retry-storm; back off and pollGET /sessions/:id/statusinstead.
// Request
const response = await fetch('https://apiwts.top/api/v1/sessions/my-session-001/restart', {
method: 'POST',
headers: { 'X-API-Key': 'your-api-key' }
});
// Response 200 (~5s)
{
"sessionId": "my-session-001",
"state": "INITIALIZING",
"message": "Session my-session-001 restart initiated",
"timestamp": "2026-05-08T10:35:00.000Z"
}
// Then poll until READY:
const pollUntilReady = async (sessionId, apiKey) => {
for (let i = 0; i < 15; i++) {
const r = await fetch(`https://apiwts.top/api/v1/sessions/${sessionId}/status`, {
headers: { 'X-API-Key': apiKey }
});
const { state } = await r.json();
if (state === 'READY') return true;
await new Promise(res => setTimeout(res, 2000));
}
throw new Error('Session did not reach READY within 30s');
};
// Possible error responses:
// 404: session not found (or belongs to other tenant)
// 503: tenant inactive (Service Unavailable)
// 500: internal error
GET /api/v1/sessions/:sessionId/health
Get session health and connectivity information.
// Request
const response = await fetch('https://apiwts.top/api/v1/sessions/my-session-001/health', {
headers: { 'X-API-Key': 'your-api-key' }
});
// Response 200
{
"sessionId": "my-session-001",
"status": "connected",
"connected": true,
"qrAvailable": false,
"lastSeen": "2025-01-15T10:35:00.000Z",
"driverType": "wwebjs",
"metadata": {},
"timestamp": "2025-01-15T10:35:00.000Z"
}
POST /api/v1/sessions/:sessionId/disconnect v3.7.5+
Temporarily disconnect a WhatsApp session without deleting it. Use this instead of DELETE when you want to disconnect temporarily (e.g., for a "Disconnect" button).
/reconnect to
reconnect later.
// Request
const response = await fetch('https://apiwts.top/api/v1/sessions/my-session-001/disconnect', {
method: 'POST',
headers: { 'X-API-Key': 'your-api-key' }
});
// Response 200 - Disconnected successfully
{
"sessionId": "my-session-001",
"state": "DISCONNECTED",
"message": "Session has been disconnected. Use /reconnect to reconnect.",
"timestamp": "2025-01-15T10:35:00.000Z"
}
// Response 200 - Already disconnected (idempotent)
{
"sessionId": "my-session-001",
"state": "DISCONNECTED",
"message": "Session is already disconnected",
"timestamp": "2025-01-15T10:35:00.000Z"
}
POST /api/v1/sessions/:sessionId/reconnect v3.7.5+
Reconnect a previously disconnected session. If authentication has expired, a new QR code will be generated.
// Request
const response = await fetch('https://apiwts.top/api/v1/sessions/my-session-001/reconnect', {
method: 'POST',
headers: { 'X-API-Key': 'your-api-key' }
});
// Response 200 - Reconnect initiated
{
"sessionId": "my-session-001",
"state": "INITIALIZING",
"message": "Session reconnect initiated. Check /qr for QR code if needed.",
"timestamp": "2025-01-15T10:35:00.000Z"
}
// Possible error responses:
// 404: session not found (or belongs to other tenant)
// 429: rate limit exceeded (v3.103.0 โ max 5/min per session; Retry-After header)
// 500: internal error
// NOTE: unlike /restart, /reconnect returns 500 (not 503) when the tenant is inactive.
PATCH /api/v1/sessions/:sessionId v3.15.0+
Update session configuration including webhook settings and deduplication window.
Supports PATCH semantics: omit a field to keep current value, send null to
remove/reset to default.
Available Fields v3.16.1+
| Field | Type | Description |
|---|---|---|
webhookUrl |
string | null | URL for receiving webhook callbacks |
webhookAuthToken |
string | null | Bearer token for webhook authentication (encrypted at rest) |
webhookSecret v3.84.1+ |
string | null |
Session-level HMAC secret. When set, each dispatched event carries an
X-Signature (HMAC-SHA256, base64) over the body.
null removes it (no signing). See
Session-Level HMAC Signing.
|
deduplicationWindowSeconds |
integer | null |
Anti-duplication window: 0 = disabled, 1-3600 = window
in seconds, null = use tenant default
|
selfSentWebhookEnabled v3.31.0+
|
boolean |
Enable message.self_sent webhook for messages sent from phone. WWebJS
only. Default: false
|
Security Features (Enterprise)
- Anti-SSRF: Webhook URLs are validated with DNS resolution and IP blocklist (private ranges, localhost, cloud metadata)
- Token Encryption: Auth tokens are encrypted at rest (AES-256-GCM with versioned prefix)
- Rate Limiting: 10 requests per minute per session
- Audit Trail: All changes logged with before/after values (tokens redacted)
-
PII Redaction: Tokens never returned in responses, only
***redacted***
// Request - Update webhook URL only (keep existing token)
const response = await fetch('https://apiwts.top/api/v1/sessions/my-session-001', {
method: 'PATCH',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
webhookUrl: 'https://example.com/webhook/whatsapp'
})
});
// Request - Update both URL and auth token
const response = await fetch('https://apiwts.top/api/v1/sessions/my-session-001', {
method: 'PATCH',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
webhookUrl: 'https://example.com/webhook/whatsapp',
webhookAuthToken: 'Bearer my-secret-token'
})
});
// Request - Remove webhook URL (set to null)
const response = await fetch('https://apiwts.top/api/v1/sessions/my-session-001', {
method: 'PATCH',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
webhookUrl: null
})
});
// Request - Disable deduplication for chatbot session (v3.16.1+)
const response = await fetch('https://apiwts.top/api/v1/sessions/chatbot-session', {
method: 'PATCH',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
deduplicationWindowSeconds: 0 // Disable - allow rapid identical messages
})
});
// Request - Set 5 second deduplication window (v3.16.1+)
const response = await fetch('https://apiwts.top/api/v1/sessions/my-session-001', {
method: 'PATCH',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
deduplicationWindowSeconds: 5 // 5 second window
})
});
// Request - Enable self-sent webhook (v3.31.0+ - WWebJS only)
const response = await fetch('https://apiwts.top/api/v1/sessions/my-session-001', {
method: 'PATCH',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
selfSentWebhookEnabled: true // Receive message.self_sent webhooks
})
});
// Response 200 - Success
{
"success": true,
"session": {
"sessionId": "my-session-001",
"webhookUrl": "https://example.com/webhook/whatsapp",
"webhookAuthToken": "***redacted***", // "***redacted***" if set, null if not
"webhookSecret": "***redacted***", // v3.84.1+: same mask as GET โ never the real value
"deduplicationWindowSeconds": 5,
"selfSentWebhookEnabled": true,
"updatedAt": "2026-01-16T12:00:00.000Z"
}
}
// Response 400 - Invalid URL (SSRF blocked)
{
"statusCode": 400,
"error": "Bad Request",
"message": "Invalid webhook URL: Hostname resolves to private IPv4 address",
"timestamp": "2026-01-16T12:00:00.000Z"
}
// Response 429 - Rate limit exceeded
{
"statusCode": 429,
"error": "Too Many Requests",
"message": "Rate limit exceeded for session_webhook_update",
"timestamp": "2026-01-16T12:00:00.000Z"
}
PATCH Semantics
| Field Value | Behavior | Example |
|---|---|---|
undefined (omitted) |
Keep current value | {} - no changes |
null |
Remove value (set to NULL in database) | {"webhookUrl": null} |
| String value | Update to new value | {"webhookUrl": "https://..."} |
Proteรงรฃo Anti-Duplicaรงรฃo v3.16.1+
Sistema de proteรงรฃo que previne o envio de mensagens duplicadas dentro de uma janela de tempo configurรกvel. Uma mensagem รฉ considerada duplicada quando tem:
- Mesmo destinatรกrio (nรบmero de telefone)
- Mesmo conteรบdo (tipo + payload exatamente iguais - hash SHA-256)
- Mesma sessรฃo
- Dentro da janela de tempo configurada
Hierarquia de Configuraรงรฃo
| Nรญvel | Endpoint | Prioridade |
|---|---|---|
| Sessรฃo | PATCH /api/v1/sessions/:id |
Alta (override) |
| Tenant | Via Admin ou Database | Mรฉdia (default para sessรตes) |
| Global | - | Baixa (60 segundos) |
Valores Possรญveis
| Valor | Comportamento |
|---|---|
null |
Usa configuraรงรฃo do nรญvel acima (heranรงa) |
0 |
Desativado - sem proteรงรฃo anti-duplicaรงรฃo |
1-3600 |
Janela em segundos (ex: 5, 30, 60) |
Casos de Uso Recomendados
| Cenรกrio | Configuraรงรฃo | Motivo |
|---|---|---|
| Notificaรงรตes transacionais | 60 (default) |
Evita spam por retry de sistemas externos |
| Chatbot conversacional | 5 ou 0 |
Respostas rรกpidas podem repetir texto intencionalmente |
| Envio em massa (marketing) | 30 |
Proteรงรฃo moderada contra duplicaรงรฃo acidental |
| Confirmaรงรตes de pedido | 10 |
Mesmo pedido pode ser confirmado rapidamente |
๐ก Dica: ID รnico no Payload
Para garantir unicidade absoluta (mesmo com deduplicaรงรฃo ativa), vocรช pode incluir um ID รบnico no payload da mensagem. Isso garante que cada mensagem tenha um hash diferente:
// Exemplo de mensagem com ID รบnico
{
"to": "5547912345678",
"type": "text",
"content": {
"body": "Seu pedido #12345 foi confirmado!",
"_messageId": "uuid-unico-gerado-pelo-seu-sistema"
}
}
Session Reliability & Persistence (v2.87)
โ Enhanced Session Reliability: Version 2.87 introduces automatic recovery mechanisms and improved session persistence during deployments, ensuring 99%+ uptime for all sessions.
๐ง Detached Frame Auto-Recovery
WhatsApp sessions can occasionally experience Puppeteer/Chromium corruption (detached frame errors) after QR code timeouts or authentication failures. The system now automatically detects and recovers from these errors:
- Detection: Health checks monitor for "detached Frame" errors every 60 seconds
- Recovery: Corrupted clients are automatically destroyed and cleaned up
- State Update: Session marked as DISCONNECTED in database (ready for reconnection)
- User Action: Click "Generate QR Code" in Admin Dashboard to reconnect
๐ก Note: This eliminates the need for manual container restarts when sessions become unresponsive. The system self-heals automatically within 1-2 minutes.
๐ Blue-Green Session Sync
During zero-downtime deployments, the system now synchronizes session files between the active and target environments:
- Pre-Deployment Sync: All session authentication files copied from active (blue/green) to target environment
- Lock Exclusion: Chromium locks excluded (SingletonLock, SingletonSocket) to prevent conflicts
- Orphan Recovery: Target environment successfully recovers sessions with valid authentication
- Zero QR Generation: No QR codes required during traffic switch (authentication already valid)
๐ก Note: Sessions remain READY (connected) during deployments. No manual intervention required.
โจ๏ธ Session Warm-up
After session sync, the system performs a 30-second warm-up period to initialize orphan recovery before switching traffic:
- Health Check Trigger: Validates all sessions in target environment
- Orphan Recovery: WWebJS detects and reconnects existing session files
- Smoke Tests: Run AFTER warm-up to validate sessions are READY
- Traffic Switch: Only proceeds if all validations pass
๐ Expected Uptime
With v2.87 improvements:
- Before v2.87: 40% uptime during deployments (sessions disconnect, require manual QR scan)
- After v2.87 (Phase 1): 95%+ uptime (automatic recovery + session sync)
- Future (Phase 2): 99%+ uptime (shared volume + enhanced lock cleanup)
๐จ Troubleshooting Session Issues
If a session becomes DISCONNECTED:
- Check logs: Look for "[v2.87] Detached frame detected" messages
- Wait 2 minutes: Auto-recovery runs every 60 seconds (may take up to 2 health check cycles)
- Manual reconnection: If still DISCONNECTED, click "Generate QR Code" in Admin Dashboard
- Fresh QR scan: Scan QR code with WhatsApp mobile app
๐ก Note: If reconnection fails repeatedly, check if your WhatsApp account has been banned or restricted by Meta. Contact support@apiwts.top for assistance.
Messaging
๐ Idempotency (Duplicate Prevention) - v3.2+ (Enhanced v3.7.9)
To prevent duplicate messages during retries or network failures,
ALWAYS include the X-Idempotency-Key header in message
sending requests.
| Header | Format | TTL | Description |
|---|---|---|---|
X-Idempotency-Key |
Alphanumeric, hyphens, underscores (max 255 chars) | 24 hours | Unique identifier for this request |
Behavior:
- โ First request: Message created and queued (HTTP 200)
-
โ
Retry with SAME key: Returns cached response (HTTP 200, header
X-Idempotency-Replay: true) - ๐ Concurrent request with SAME key: Returns HTTP 409 Conflict (v3.7.9+)
- โ ๏ธ Retry with DIFFERENT key: NEW message created (potential duplicate)
- โ ๏ธ Request WITHOUT key: No idempotency protection (each request creates new message)
HTTP 409 Conflict Response (v3.7.9+):
When a request with the same idempotency key is already being processed, the API returns HTTP 409 to prevent race conditions:
// HTTP 409 Conflict
{
"statusCode": 409,
"error": "Conflict",
"message": "Request with idempotency key \"msg-xxx\" is already being processed",
"retryAfter": 5
}
// Headers:
// Retry-After: 5
Client handling: Wait for retryAfter seconds and retry.
The original request will have completed by then.
Recommended format: msg-{uuid-v4} or
{client-request-id}
// Example with Idempotency Key (RECOMMENDED)
const response = await fetch('https://apiwts.top/api/v1/messages/text', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json',
'X-Idempotency-Key': 'msg-550e8400-e29b-41d4-a716-446655440000' // โ CRITICAL
},
body: JSON.stringify({
sessionId: 'my-session-001',
to: '+5511999999999',
text: 'Hello from WhatsApp API!'
})
});
// Response (first request)
// HTTP 200
{
"id": "msg_1234567890",
"status": "queued"
}
// Response (retry with same key)
// HTTP 200 + Header: X-Idempotency-Replay: true
{
"id": "msg_1234567890", // Same ID - no duplicate created
"status": "queued"
}
POST /api/v1/messages/text
Send text message via WhatsApp.
// Request
const response = await fetch('https://apiwts.top/api/v1/messages/text', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json',
'X-Idempotency-Key': 'msg-' + crypto.randomUUID() // โ Recommended
},
body: JSON.stringify({
sessionId: 'my-session-001',
to: '+5511999999999',
text: 'Hello from WhatsApp API!'
})
});
// Response 200
{
"id": "msg_1234567890",
"sessionId": "my-session-001",
"to": "+5511999999999",
"status": "queued",
"timestamp": "2025-01-15T10:35:00.000Z",
"clientMessageId": "msg-550e8400-e29b-41d4-a716-446655440000" // v3.93.0+: echoes X-Idempotency-Key โ your correlation key
}
clientMessageId echoes the
X-Idempotency-Key you sent, so you can correlate this send-response with the
later message.sent / message.self_sent / delivery-status
webhooks by a single stable key. See
Message correlation contract.
queued vs sent. Text and media sends are
asynchronous: the API enqueues the message and returns
"status": "queued"
immediately โ actual delivery happens in the background (track it via the
message.sent/delivered/read webhooks). Location,
contact and poll sends are synchronous and return
"status": "sent" once the driver has handed the message off.
| Field | Type | Required | Description |
|---|---|---|---|
sessionId |
string | Yes | WhatsApp session identifier |
to |
string | Yes | Recipient phone (E.164 format) or WhatsApp JID (@c.us, @lid, @g.us) |
text |
string | Yes | Message content (max 4096 characters) |
quotedMessageId |
string | No | ID of the message to quote/reply to. The sent message will appear as a reply to this message. WWebJS Only |
linkPreview |
boolean | No | Generate link preview for URLs in the message (default: true) |
To send a message as a reply to another message, include the
quotedMessageId parameter with the serialized ID of the
message you want to quote. The recipient will see your message with the quoted message
displayed above it.
// Reply to a message
{
"sessionId": "my-session",
"to": "5547991246688",
"text": "Yes, I agree with that!",
"quotedMessageId": "false_5547991246688@c.us_3EB0A0B0C1D2E3F4"
}
Important: The quotedMessageId must be in the
serialized format: {fromMe}_{remote}_{id}
-
fromMe: "true" for messages you sent, "false" for received messages remote: The chat ID (e.g., "5547991246688@c.us")id: The message hash (e.g., "3EB0A0B0C1D2E3F4")
How to get the serializedId: Use the serializedId field
from webhook payloads (available since v3.8.18). Example webhook payload:
{
"event": "message.received.text",
"message": {
"id": "3EB0A0B0C1D2E3F4",
"serializedId": "false_5547991246688@c.us_3EB0A0B0C1D2E3F4" // Use this for quotedMessageId
}
}
Note: The message being quoted must be in WhatsApp Web's cache (approximately the last 500 messages in the conversation).
Graceful Degradation (v3.8.20+): If the quoted message is not found in the cache, the API will log a warning and send the message without the quote (the message will still be delivered, just not as a reply). This ensures message delivery is never blocked by cache limitations.
POST /api/v1/messages/media
Send media messages (images, videos, audio, documents).
// Send image by URL
const response = await fetch('https://apiwts.top/api/v1/messages/media', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session-001',
to: '+5511999999999',
mediaUrl: 'https://example.com/image.jpg',
mimetype: 'image/jpeg',
caption: 'Check out this image!'
})
});
// Send document
const response = await fetch('https://apiwts.top/api/v1/messages/media', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session-001',
to: '+5511999999999',
mediaUrl: 'https://example.com/document.pdf',
mimetype: 'application/pdf',
filename: 'invoice.pdf',
caption: 'Your invoice is attached'
})
});
// Response 200 (media is queued like text โ track delivery via webhooks)
{
"id": "msg_1234567890",
"sessionId": "my-session-001",
"to": "+5511999999999",
"status": "queued",
"type": "media",
"timestamp": "2025-01-15T10:35:00.000Z"
}
| Field | Type | Required | Description |
|---|---|---|---|
sessionId |
string | โ | WhatsApp session identifier |
to |
string | โ | Recipient phone (international format) |
mediaUrl |
string | * | URL to media file |
mediaBase64 |
string | * | Base64 encoded media |
mimetype |
string | โ |
Canonical MIME type. See
Supported Media Types below. Voice notes (PTT): use
audio/ogg; codecs=opus. audio/webm is
not supported โ see warning below.
|
caption |
string | โ | Media caption (max 1024 chars) |
filename |
string | โ | Custom filename for documents |
Supported Media Types (WWebJS / WhatsApp Web)
| Category | Canonical MIME type | Notes |
|---|---|---|
| Image | image/jpeg, image/png, image/webp |
Max 16MB* (*Cloud API guideline โ see size note below). Animated WebP supported. |
| Video | video/mp4 (H.264 + AAC) |
Max 16MB* (*Cloud API guideline โ see size note below). video/webm,
video/avi, video/mov accepted but transcoded by
WhatsApp.
|
| Voice note (PTT) | audio/ogg; codecs=opus |
โญ Preferred for voice messages. Renders with WhatsApp's native voice-note player. |
| Audio file |
audio/mpeg (MP3), audio/mp4 /
audio/aac (M4A/AAC)
|
Renders as audio file (different player than voice note). Use Opus above if intent is voice message. |
| Document |
application/pdf, application/msword,
application/vnd.openxmlformats-officedocument.*,
text/plain, ...
|
Max 100MB. Set filename to preserve the original name on delivery.
|
-
The gateway enforces a
single request body limit of 100 MB (Fastify
bodyLimit), shared across all media types โ there is no per-type size enforcement in the gateway. The16 MBfigures above are WhatsApp Cloud API (Meta) guidelines, not WWebJS limits. -
Two transports, two ceilings:
-
Base64 in the request body (
mediaBase64field) โ capped by the 100 MB body limit (โ75 MB of raw media after base64 decoding). -
URL fetch (
mediaUrl) โ the gateway downloads the file, so the 100 MB body limit does not apply; the practical ceiling is higher (bounded by the gateway download + WhatsApp Web). PrefermediaUrlfor large files.
-
Base64 in the request body (
-
Document types are illustrative, not a whitelist. The gateway is
permissive: it accepts
any MIME type not on the blocklist (currently only
audio/webm, since v3.80.12) up to the body limit โ sendapplication/zip,text/csv, or any other type as a document.
mediaBase64 now supported (v3.100.3):
-
Cloud sessions accept both
mediaUrlandmediaBase64(previously URL-only). When you passmediaBase64(and nomediaUrl), the gateway uploads the bytes to Meta (POST /{phone_number_id}/media) and sends the message by the returnedmedia_idโ no public URL needed. -
url-first precedence: if
mediaUrlis present it wins (Meta fetches the URL bylink; nothing is uploaded).mediaBase64is used only whenmediaUrlis absent โ mirroring the WWebJS behavior. -
Per-type size caps (Cloud base64 upload): image
5 MB, audio 16 MB, video
16 MB, document 100 MB. Oversize
mediaBase64is rejected before any upload (fast-fail) per Meta's documented limits. -
Known gap: stickers (
image/webp) sent to a Cloud session are delivered as documents, not native stickers (the Cloud driver maps WebP todocument). For native stickers use a WWebJS session.
-
audio/webm(the default output ofMediaRecorderin Chromium-based browsers). WhatsApp Web rejects this container even when the inner codec is Opus. Repackage toaudio/ogg; codecs=opusbefore sending โ for example withffmpeg -i input.webm -c:a libopus -b:a 32k -ac 1 -ar 48000 output.ogg. Sendingaudio/webmreturns HTTP 422 immediately (since v3.80.12). -
Raw
audio/wavโ passes but the file size is impractical. Prefer Opus. -
Opus inside a WebM container (
audio/webm; codecs=opus) โ must be in an Ogg container.
// Send a voice note (PTT โ preferred)
const response = await fetch('https://apiwts.top/api/v1/messages/media', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session-001',
to: '+5511999999999',
mediaUrl: 'https://example.com/voice-note.ogg',
mimetype: 'audio/ogg; codecs=opus'
// caption is not displayed for voice notes
})
});
// Send an audio file (non-PTT)
{
sessionId: 'my-session-001',
to: '+5511999999999',
mediaUrl: 'https://example.com/song.mp3',
mimetype: 'audio/mpeg',
caption: 'New track'
}
// HTTP 422 response example (audio/webm rejected)
{
"statusCode": 422,
"error": "Unsupported Media Type",
"message": "WhatsApp Web does not support 'audio/webm'. Repackage to Ogg/Opus container before sending.",
"mimetype": "audio/webm",
"suggestedMimetypes": ["audio/ogg; codecs=opus", "audio/mpeg", "audio/mp4"],
"documentationUrl": "https://apiwts.top/reference#audio-mimetypes"
}
Session not ready โ 400 SESSION_NOT_READY (retryable)
Every send endpoint (/messages/text, /media,
/location, /contact, /poll,
/template) returns this 400 when the target session is not in
the READY state. Since v3.92.0 the body carries two
machine-readable signals so clients can re-queue and resend automatically instead of
failing the operator.
// 400 โ session still coming up (recoverable: retry, do NOT reconnect)
{
"statusCode": 400,
"error": "Bad Request",
"message": "Session mysession is not ready (current state: INITIALIZING).",
"code": "SESSION_NOT_READY",
"sessionStatus": "INITIALIZING",
"needsReconnect": false,
"retryAfterSeconds": 25
}
// 400 โ session needs an explicit reconnect / QR scan (no retry hint)
{
"statusCode": 400,
"error": "Bad Request",
"message": "Session mysession is not ready (current state: FAILED).",
"code": "SESSION_NOT_READY",
"sessionStatus": "FAILED",
"needsReconnect": true
}
| Field | Type | Meaning |
|---|---|---|
code |
string |
Always "SESSION_NOT_READY" for this case โ branch on this, not on the
message text.
|
sessionStatus |
string |
The session's current non-READY state: INITIALIZING,
AUTHENTICATED, QR_PENDING, DISCONNECTED, or
FAILED.
|
needsReconnect |
boolean |
true when an explicit reconnect / QR scan is the reliable path to
READY (QR_PENDING, FAILED,
DISCONNECTED).
|
retryAfterSeconds |
number โ integer, seconds (optional) |
Present when READY is expected via auto-recovery
(INITIALIZING, AUTHENTICATED,
DISCONNECTED). A
hint/estimate (~25s), not a guarantee โ treat it as a floor for
backoff.
|
-
If
retryAfterSecondsis present: wait that many seconds and re-queue / resend (idempotency viaX-Idempotency-Keyprotects against duplicates). -
Cap the retries (e.g. 3โ5 attempts with backoff). If the session is
still not
READYandneedsReconnectistrue, stop retrying and triggerPOST /api/v1/sessions/:id/reconnect(or a QR re-scan if the device is unpaired). This prevents an unbounded retry loop on a persistently disconnected session. -
When
needsReconnectistruewith noretryAfterSeconds(e.g.QR_PENDING/FAILED): do not retry blindly โ surface a reconnect / QR action.
GET /api/v1/messages/:messageId
Get the status of a sent message.
// Request
const response = await fetch('https://apiwts.top/api/v1/messages/msg_abc123', {
headers: { 'X-API-Key': 'your-api-key' }
});
// Response 200
{
"id": "msg_abc123",
"sessionId": "my-session-001",
"to": "+5511999999999",
"status": "sent",
"type": "text",
"timestamp": "2025-01-15T10:35:00.000Z",
"driverMessageId": "3EB0XXXXXXXXXXXXX"
}
GET /api/v1/messages
List all messages for the authenticated tenant with pagination.
// Request with filters
const response = await fetch('https://apiwts.top/api/v1/messages?page=1&limit=20&sessionId=my-session-001&status=sent', {
headers: { 'X-API-Key': 'your-api-key' }
});
// Response 200
{
"messages": [
{
"id": "msg_abc123",
"sessionId": "my-session-001",
"to": "+5511999999999",
"status": "sent",
"type": "text",
"timestamp": "2025-01-15T10:35:00.000Z",
"driverMessageId": "3EB0XXXXXXXXXXXXX"
}
],
"total": 150,
"page": 1,
"limit": 20
}
| Query Parameter | Type | Default | Description |
|---|---|---|---|
page |
number | 1 | Page number (min: 1) |
limit |
number | 20 | Messages per page (min: 1, max: 100) |
sessionId |
string | - | Filter by session ID (optional) |
status |
string | - | Filter by status: queued, sent, delivered, read, failed (optional) |
โฐ Scheduled Messages (v2.46)
Schedule WhatsApp messages to be sent at a future date and time. No maximum scheduling limit - you can schedule messages years in advance for use cases like insurance renewals, warranty expirations, annual contracts, etc.
-
โ
Real-time Stats API: Get pending/sent/failed/total counters via
/messages/scheduled/stats - โ Advanced Filters: Filter by tenant, status, date range via query parameters
- โ Enhanced Security: HTML sanitization on all user-generated content
- โ Enhanced Buffer: Increased from 1min to 2min (120s) to account for network latency + clock skew
- โ Improved Error Messages: Detailed timing context with actionable suggestions
- โ Better UX: Errors now include exact timestamps, buffer requirements, and suggested valid time
POST /api/v1/messages/scheduled
Schedule a message for future delivery.
// Schedule message 1 year from now (insurance renewal)
const response = await fetch('https://apiwts.top/api/v1/messages/scheduled', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session-001',
to: '+5511999999999',
text: 'Your car insurance will expire in 30 days. Renew now to avoid coverage gaps!',
scheduledAt: '2026-01-15T10:00:00Z'
})
});
// Response
{
"id": "msg_abc123",
"sessionId": "my-session-001",
"to": "+5511999999999",
"text": "Your car insurance will expire in 30 days...",
"scheduledAt": "2026-01-15T10:00:00.000Z",
"status": "pending",
"createdAt": "2025-01-15T10:00:00.000Z"
}
Error Response (400 Bad Request - Scheduling Too Soon)
v2.55: Enhanced error messages with detailed timing context and actionable suggestions.
// Attempt to schedule message less than 2 minutes in the future
{
"statusCode": 400,
"error": "SchedulingTooSoon",
"message": "Message scheduled too close to current time",
"details": {
"scheduledAt": "2025-10-10T10:01:00Z", // Your requested time
"currentTime": "2025-10-10T10:00:00Z", // Server current time
"minimumScheduleTime": "2025-10-10T10:02:00Z", // Minimum valid time
"minimumBufferSeconds": 120, // Required buffer (2 minutes)
"timeUntilScheduled": "60s", // How early you are
"suggestion": "Schedule at least 120s in the future. Try: 2025-10-10T10:02:00Z"
}
}
// How to fix: Use the suggested timestamp from error.details.minimumScheduleTime
const errorResponse = await response.json();
if (errorResponse.error === 'SchedulingTooSoon') {
// Retry with suggested time
const suggestedTime = errorResponse.details.minimumScheduleTime;
console.log(`Retrying with suggested time: ${suggestedTime}`);
}
| Field | Type | Required | Description |
|---|---|---|---|
sessionId |
string | โ | WhatsApp session identifier |
to |
string | โ | Recipient phone (E.164 format: +5511999999999) |
text |
string | โ | Message content (1-4096 characters) |
scheduledAt |
string (ISO 8601) | โ | Future timestamp (min: +2 minutes, no max limit) |
- Minimum: 2 minutes from now (120 seconds)
- Maximum: No limit (can schedule years ahead)
- Processing: Checked automatically every minute
- Session Validation: Relaxed - can schedule even if session disconnected (will auto-retry when reconnected)
- Buffer Reason: Accounts for network latency, clock skew, and processing time
GET /api/v1/messages/scheduled
List scheduled messages with optional filters.
// List all pending scheduled messages for a session
const response = await fetch('https://apiwts.top/api/v1/messages/scheduled?sessionId=my-session-001&status=pending', {
headers: {
'X-API-Key': 'your-api-key'
}
});
// Response
{
"messages": [
{
"id": "msg_abc123",
"sessionId": "my-session-001",
"to": "+5511999999999",
"text": "Insurance renewal reminder",
"scheduledAt": "2026-01-15T10:00:00.000Z",
"status": "pending",
"createdAt": "2025-01-15T10:00:00.000Z"
}
],
"total": 1,
"page": 1,
"limit": 50
}
| Query Parameter | Type | Description |
|---|---|---|
sessionId |
string | Filter by session ID |
status |
string | Filter by status (pending, queued, sent, failed) |
startDate |
ISO 8601 | Filter messages scheduled after this date |
endDate |
ISO 8601 | Filter messages scheduled before this date |
page |
number | Page number (default: 1) |
limit |
number | Results per page (default: 50) |
DELETE /api/v1/messages/scheduled/:id
Cancel a scheduled message (only if status is pending).
// Cancel scheduled message
const response = await fetch('https://apiwts.top/api/v1/messages/scheduled/msg_abc123', {
method: 'DELETE',
headers: {
'X-API-Key': 'your-api-key'
}
});
// Response (200 OK)
{
"success": true,
"message": "Scheduled message cancelled successfully"
}
// Response (404 Not Found - if already sent or not pending)
{
"statusCode": 404,
"error": "Not Found",
"message": "Message not found or cannot be cancelled (already sent or not scheduled)"
}
DELETE /api/v1/messages/:messageId
v2.84+ Revoke/delete a sent message. Supports "delete for me" or "delete for everyone" (within ~48 hours).
Important: Only supported on wwebjs driver. Cloud API
driver will return 422 error.
Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
type |
string | No | everyone |
Revoke type: me (delete for yourself only) or
everyone (delete for all participants)
|
Message Status Requirements
Only messages with status sent, delivered, or
read can be revoked. Messages with status pending,
queued, failed, or revoked will return 400 error.
Time Constraint
WhatsApp allows deleting messages for everyone only within approximately
48 hours of sending. After this period, you can only delete for
yourself (type=me).
// Delete message for everyone (default)
const response = await fetch('https://apiwts.top/api/v1/messages/550e8400-e29b-41d4-a716-446655440000', {
method: 'DELETE',
headers: {
'X-API-Key': 'your-api-key'
}
});
// Delete message for yourself only
const response = await fetch('https://apiwts.top/api/v1/messages/550e8400-e29b-41d4-a716-446655440000?type=me', {
method: 'DELETE',
headers: {
'X-API-Key': 'your-api-key'
}
});
// Response (200 OK - Success)
{
"success": true,
"message": "Message revoked successfully (everyone)",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"status": "revoked",
"revokedAt": "2025-11-23T06:00:00.000Z",
"revokeType": "everyone",
"revokeReason": "User-initiated via API (DELETE /messages/...?type=everyone)"
}
}
// Response (200 OK - Already Revoked - Idempotent)
{
"success": true,
"message": "Message already revoked",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"status": "revoked",
"revokedAt": "2025-11-23T05:55:00.000Z",
"revokeType": "everyone"
}
}
// Response (400 Bad Request - Invalid Status)
{
"statusCode": 400,
"error": "Bad Request",
"message": "Cannot revoke message with status 'pending'. Only 'sent', 'delivered', or 'read' messages can be revoked."
}
// Response (403 Forbidden - Unauthorized)
{
"statusCode": 403,
"error": "Forbidden",
"message": "You are not authorized to revoke this message"
}
// Response (404 Not Found)
{
"statusCode": 404,
"error": "Not Found",
"message": "Message not found"
}
// Response (410 Gone - Time Expired)
{
"statusCode": 410,
"error": "Gone",
"message": "Message revocation time limit expired (can only delete for yourself now)"
}
// Response (422 Unprocessable Entity - Driver Not Supported)
{
"statusCode": 422,
"error": "Unprocessable Entity",
"message": "Message revocation is not supported by this driver. Only WWebJS driver supports deleting sent messages.",
"driver": "cloud-api",
"supportedDrivers": ["wwebjs"]
}
POST /api/v1/messages/:messageId/react
v2.85+ React to a message with an emoji (WWebJS only).
Important: Only supported on wwebjs driver. Cloud API
driver will return 422 error.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
emoji |
string | Yes |
Any valid Unicode emoji to add reaction, or empty string "" to remove
existing reaction (v3.8.32+). Common examples: ๐ ๐ โค๏ธ ๐ ๐ฎ ๐ข ๐ ๐ฅ โ
โ ๐
๐ฏ.
|
// React with thumbs up emoji
const response = await fetch('https://apiwts.top/api/v1/messages/550e8400-e29b-41d4-a716-446655440000/react', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
emoji: '๐'
})
});
// Response (200 OK - Success)
{
"success": true,
"message": "Reaction \"๐\" added successfully",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"emoji": "๐",
"reactedAt": "2025-11-23T08:00:00.000Z"
}
}
// Remove reaction (v3.8.32+) - Send empty string
body: JSON.stringify({
emoji: '' // Empty string removes existing reaction
})
// Response (200 OK - Reaction Removed)
{
"success": true,
"message": "Reaction removed successfully",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"emoji": "",
"reactedAt": "2026-01-05T10:30:00.000Z"
}
}
// Response (400 Bad Request - Invalid Status)
{
"statusCode": 400,
"error": "Bad Request",
"message": "Cannot react to message with status 'queued'. Only 'sent', 'delivered', or 'read' messages can receive reactions."
}
// Response (422 Unprocessable Entity - Driver Not Supported)
{
"statusCode": 422,
"error": "Unprocessable Entity",
"message": "Message reactions are not supported by this driver",
"driver": "cloud-api",
"supportedDrivers": ["wwebjs"]
}
POST /api/v1/sessions/:sessionId/messages/:driverMessageId/react
v3.8.17+ WWebJS Only
Add an emoji reaction to a received (inbound) message using the
driverMessageId from the webhook payload.
-
POST /messages/:messageId/react- For sent messages (you have the UUID from the database) -
POST /sessions/:sessionId/messages/:driverMessageId/react- For received messages (you have the driverMessageId from webhook)
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Yes | Session ID (path parameter) |
| driverMessageId | string | Yes | Driver message ID from webhook payload (path parameter) |
| emoji | string | Yes |
Emoji to react with, or empty string "" to remove existing reaction
(v3.8.32+)
|
Finding the driverMessageId
The webhook payload contains two ID formats.
Use serializedId for the /react endpoint:
-
id- Short format (e.g., "3EB0ABC123DEF456789") - for display/logging only -
serializedId- Full format (e.g., "false_55...@c.us_3EB0...") - required for /react endpoint
// Webhook payload example (message.received.text) - v3.8.18+
{
"event": "message.received.text",
"sessionId": "mysession",
"message": {
"id": "3EB0ABC123DEF456789", // Short format (display only)
"serializedId": "false_5547991246688@c.us_3EB0ABC123DEF456789", // <-- USE THIS for /react
"from": "5547991246688@c.us",
"to": "5511999887766@c.us",
"body": "Hello!",
"timestamp": "2026-01-02T10:30:00.000Z"
}
}
// React to an inbound message (use serializedId from webhook)
const response = await fetch('https://apiwts.top/api/v1/sessions/mysession/messages/false_5547991246688@c.us_3EB0ABC123DEF456789/react', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
emoji: "thumbs up"
})
});
// Response (200 OK)
{
"success": true,
"message": "Reaction \"thumbs up\" added successfully",
"data": {
"driverMessageId": "false_5547991246688@c.us_3EB0ABC123DEF456789",
"emoji": "thumbs up",
"reactedAt": "2026-01-02T10:31:00.000Z"
}
}
// Response (404 Not Found - Session)
{
"statusCode": 404,
"error": "Not Found",
"message": "Session mysession not found",
"code": "SESSION_NOT_FOUND"
}
// Response (400 Bad Request - Session Not Active)
{
"statusCode": 400,
"error": "Bad Request",
"message": "Session mysession is not active",
"code": "SESSION_NOT_ACTIVE"
}
// Response (404 Not Found - Message Not in Cache)
{
"statusCode": 404,
"error": "Not Found",
"message": "Message not found. It may have been deleted or is no longer in the WhatsApp cache (~500 recent messages per chat).",
"code": "MESSAGE_NOT_FOUND"
}
// Response (422 Unprocessable Entity - Driver Not Supported)
{
"statusCode": 422,
"error": "Unprocessable Entity",
"message": "Message reactions are not supported by this driver",
"driver": "cloud-api",
"supportedDrivers": ["wwebjs"]
}
GET /api/v1/messages/:messageId/info
v2.85+ Get detailed delivery information about a message, including ACK level and group delivery details (WWebJS only).
Important: Only supported on wwebjs driver. Cloud API
driver will return 422 error.
ACK Levels
- -1: Error (message failed to send)
- 0: Pending (message queued on client)
- 1: Server (message sent to WhatsApp server)
- 2: Device (message delivered to recipient device)
- 3: Read (message read by recipient)
- 4: Played (voice/video message played by recipient)
// Get message delivery info
const response = await fetch('https://apiwts.top/api/v1/messages/550e8400-e29b-41d4-a716-446655440000/info', {
headers: {
'X-API-Key': 'your-api-key'
}
});
// Response (200 OK - Individual Chat)
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"ack": 3,
"timestamp": "2025-11-23T08:00:00.000Z"
}
}
// Response (200 OK - Group Chat with Delivery Details)
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"ack": 3,
"timestamp": "2025-11-23T08:00:00.000Z",
"deliveryDetails": {
"deliveredTo": [
{
"contact": "5511999999999@c.us",
"timestamp": "2025-11-23T08:00:05.000Z"
}
],
"readBy": [
{
"contact": "5511999999999@c.us",
"timestamp": "2025-11-23T08:01:00.000Z"
}
]
}
}
}
// Response (422 Unprocessable Entity - Driver Not Supported)
{
"statusCode": 422,
"error": "Unprocessable Entity",
"message": "Message delivery info is not supported by this driver",
"driver": "cloud-api",
"supportedDrivers": ["wwebjs"]
}
POST /api/v1/messages/:messageId/forward
v2.85+ Forward a message to another chat (WWebJS only).
Important: Only supported on wwebjs driver. Cloud API
driver will return 422 error.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
targetChatId |
string | Yes | WhatsApp chat ID to forward to (e.g., "5511999999999@c.us") |
// Forward message to another chat
const response = await fetch('https://apiwts.top/api/v1/messages/550e8400-e29b-41d4-a716-446655440000/forward', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
targetChatId: '5511888888888@c.us'
})
});
// Response (200 OK - Success)
{
"success": true,
"message": "Message forwarded successfully",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"targetChatId": "5511888888888@c.us",
"forwardedAt": "2025-11-23T08:00:00.000Z"
}
}
// Response (400 Bad Request - Invalid Status)
{
"statusCode": 400,
"error": "Bad Request",
"message": "Cannot forward message with status 'failed'. Only 'sent', 'delivered', or 'read' messages can be forwarded."
}
// Response (422 Unprocessable Entity - Driver Not Supported)
{
"statusCode": 422,
"error": "Unprocessable Entity",
"message": "Message forwarding is not supported by this driver",
"driver": "cloud-api",
"supportedDrivers": ["wwebjs"]
}
PATCH /api/v1/messages/:messageId/content
v3.30.0+ Edit a sent text message (WWebJS only).
Important: Only supported on wwebjs driver. Cloud API
driver will return 422 error. WhatsApp allows editing messages within ~15 minutes of
sending.
Note (v3.30.2): Only messages sent after v3.30.2 can be edited. Messages sent before this version have an incompatible ID format stored in the database.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
newContent |
string | Yes | New text content for the message (max 4096 characters) |
// Edit a sent text message
// Note: Use the UUID returned from POST /api/v1/messages/send (not the WWebJS serialized ID)
const response = await fetch('https://apiwts.top/api/v1/messages/550e8400-e29b-41d4-a716-446655440000/content', {
method: 'PATCH',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
newContent: 'Updated message content'
})
});
// Response (200 OK - Success)
{
"success": true,
"message": "Message edited successfully",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"newContent": "Updated message content",
"editedAt": "2025-11-28T10:00:00.000Z"
}
}
// Response (400 Bad Request - Invalid Message Type)
{
"statusCode": 400,
"error": "Bad Request",
"message": "Cannot edit message of type 'media'. Only text messages can be edited.",
"code": "INVALID_MESSAGE_TYPE"
}
// Response (400 Bad Request - Invalid Status)
{
"statusCode": 400,
"error": "Bad Request",
"message": "Cannot edit message with status 'failed'. Only sent/delivered/read messages can be edited.",
"code": "INVALID_MESSAGE_STATE"
}
// Response (410 Gone - Edit Window Expired)
{
"statusCode": 410,
"error": "Gone",
"message": "Message edit window has expired. Messages can only be edited within 15 minutes of sending.",
"code": "MESSAGE_EDIT_WINDOW_EXPIRED"
}
// Response (422 Unprocessable Entity - Driver Not Supported)
{
"statusCode": 422,
"error": "Unprocessable Entity",
"message": "Message editing is not supported by this driver",
"code": "MESSAGE_EDIT_NOT_SUPPORTED",
"driver": "cloud-api",
"supportedDrivers": ["wwebjs"]
}
GET /api/v1/messages/:messageId/media
Download media from a message (WWebJS only).
v3.7.12+ Webhook Integration
Starting in v3.7.12, message.received.media webhooks include a
mediaUrl field that points directly to this endpoint. The URL uses a
driverMessageId format that is cached for a configurable TTL (default:
5 minutes).
Flow:
- Receive
message.received.mediawebhook withmediaUrl - Call GET on the
mediaUrlwithin TTL window - Receive media as binary response (Content-Type: image/jpeg, audio/ogg, etc.)
TTL Configuration (v3.7.12+)
Media references are cached with configurable TTL:
MEDIA_DOWNLOAD_TTL_SECONDSenv var (default: 300 = 5 minutes)- Range: 60-1800 seconds (1-30 minutes)
- After TTL expires, returns 404 "Media reference expired"
Supported messageId Formats
-
UUID:
550e8400-e29b-41d4-a716-446655440000(database lookup) -
driverMessageId:
AC99FE4B684B8EE1AF5A6940ED188444(cache lookup from webhook)
Important: Only supported on wwebjs driver. Cloud API
provides media URLs directly in webhooks with pre-signed URLs instead.
Authentication: Both X-API-Key: sk_live_... and
Authorization: Bearer sk_live_... headers are supported. Use whichever
matches your application's standard pattern.
// v3.7.12+: Download media using mediaUrl from webhook
// Webhook payload includes: "mediaUrl": "https://apiwts.top/api/v1/messages/AC99FE4B.../media"
const response = await fetch(webhook.message.mediaUrl, {
headers: {
'X-API-Key': 'sk_live_your_api_key'
// OR: 'Authorization': 'Bearer sk_live_your_api_key'
}
});
// Response (200 OK - Binary media data)
// Headers:
// Content-Type: image/jpeg (or audio/ogg, video/mp4, application/pdf, etc.)
// Content-Disposition: attachment; filename="photo.jpg"
// Content-Length: 245678
// Body: Binary media data
// Alternative: Download using database UUID (original method)
const response = await fetch('https://apiwts.top/api/v1/messages/550e8400-e29b-41d4-a716-446655440000/media', {
headers: {
'X-API-Key': 'sk_live_your_api_key'
// OR: 'Authorization': 'Bearer sk_live_your_api_key'
}
});
// Response (200 OK - JSON with base64)
{
"success": true,
"message": "Media downloaded successfully",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"base64": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQ...",
"mimetype": "image/jpeg",
"filename": "photo.jpg",
"filesize": 245678
}
}
// Response (400 Bad Request - No Media)
{
"statusCode": 400,
"error": "Bad Request",
"message": "Message does not contain media",
"code": "MESSAGE_HAS_NO_MEDIA"
}
// Response (404 Not Found - Message not in database)
{
"statusCode": 404,
"error": "Not Found",
"message": "Message 550e8400-e29b-41d4-a716-446655440000 not found",
"code": "MESSAGE_NOT_FOUND"
}
// Response (404 Not Found - v3.7.12+ TTL Expired)
{
"statusCode": 404,
"error": "Not Found",
"message": "Media reference expired or not found. The download link has a limited TTL.",
"code": "MEDIA_REFERENCE_EXPIRED"
}
// Response (403 Forbidden - Tenant mismatch)
{
"statusCode": 403,
"error": "Forbidden",
"message": "You do not have permission to access this media",
"code": "FORBIDDEN"
}
// Response (422 Unprocessable Entity - Driver Not Supported)
{
"statusCode": 422,
"error": "Unprocessable Entity",
"message": "Media download is not supported by this driver. Only WWebJS driver supports downloading media from messages. Cloud API provides media URLs directly in webhooks.",
"code": "MEDIA_DOWNLOAD_NOT_SUPPORTED",
"driver": "cloud-api",
"supportedDrivers": ["wwebjs"]
}
POST /api/v1/messages/location
v3.4+ Send a location message with geographic coordinates.
Supported drivers: wwebjs, cloud-api
// Send location message
const response = await fetch('https://apiwts.top/api/v1/messages/location', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session-001',
to: '5511999999999',
latitude: -23.5505199,
longitude: -46.6333094,
description: 'Sรฃo Paulo, Brazil',
address: 'Av. Paulista, 1578 - Bela Vista'
})
});
// Response (200 OK - Success) โ wrapped in { success, message, data }
{
"success": true,
"message": "Location message sent successfully",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"driverMessageId": "true_5511999999999@c.us_ABCDEF123456",
"status": "sent",
"timestamp": "2025-11-28T14:00:00.000Z"
}
}
// Response (400 Bad Request - Invalid Coordinates)
{
"statusCode": 400,
"error": "Bad Request",
"message": "Latitude must be between -90 and 90"
}
// Response (404 Not Found - Session Not Found)
{
"statusCode": 404,
"error": "Not Found",
"message": "Session my-session-001 not found"
}
// Response (422 Unprocessable Entity - Driver Not Supported)
{
"statusCode": 422,
"error": "Unprocessable Entity",
"message": "Location sending is not supported by this driver.",
"code": "LOCATION_NOT_SUPPORTED",
"driver": "cloud-api",
"supportedDrivers": ["wwebjs", "cloud"]
}
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
sessionId |
string | Yes | WhatsApp session ID |
to |
string | Yes | Recipient phone number or WhatsApp JID |
latitude |
number | Yes | Latitude (-90 to 90) |
longitude |
number | Yes | Longitude (-180 to 180) |
description |
string | No | Location name/description (max 256 chars). Canonical field. |
name |
string | No |
v3.76.0+ Alias of
description. Cannot be combined with
description (returns 400
LOCATION_AMBIGUOUS_DESCRIPTION).
|
address |
string | No | Address text (max 256 chars) |
/messages/location natively (was WWebJS-only in v3.4-v3.75.x). Field
name is accepted as an alias of description for cross-SDK
compatibility. Providing both returns 400 LOCATION_AMBIGUOUS_DESCRIPTION.
POST /api/v1/messages/contact
Send a contact card (vCard) via WhatsApp. Supports both drivers. Fields
name, organization, and email reject CR, LF and
null-byte characters at the schema layer to prevent vCard field injection.
// Send contact card
const response = await fetch('https://apiwts.top/api/v1/messages/contact', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session-001',
to: '5511999999999',
contact: {
name: 'Joรฃo Silva',
phone: '+5511888887777',
organization: 'Empresa XYZ', // optional
email: 'joao@empresa.xyz' // optional
}
})
});
// Response (200 OK)
{
"success": true,
"message": "Contact message sent successfully",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"driverMessageId": "true_5511999999999@c.us_ABC123",
"status": "sent",
"timestamp": "2026-04-18T14:00:00.000Z"
}
}
// Response (400 Bad Request โ CRLF injection attempt)
{
"statusCode": 400,
"error": "Bad Request",
"message": "body/contact/name must match pattern \"^[^\\r\\n\\x00]+$\""
}
// Response (422 โ driver unsupported)
{
"statusCode": 422,
"error": "Unprocessable Entity",
"message": "Contact sending is not supported by this driver.",
"code": "CONTACT_NOT_SUPPORTED",
"driver": "instagram",
"supportedDrivers": ["wwebjs", "cloud"]
}
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
sessionId |
string | Yes | WhatsApp session ID |
to |
string | Yes | Recipient phone number or WhatsApp JID |
contact.name |
string | Yes | Display name (1-256 chars). CR/LF/null-byte rejected. |
contact.phone |
string | Yes | E.164 (digits only, optional leading +). 5-20 chars. |
contact.organization |
string | No | Organization (max 256). CR/LF rejected. |
contact.email |
string | No | Email (RFC 5322, max 256). |
selfSentWebhookEnabled: true for the
session, the message.self_sent webhook fires with
type: "vcard" and vCards: [{ vcard: "BEGIN:VCARD..." }] โ same
shape as an inbound vCard message.
POST /api/v1/messages/poll
Create an interactive poll message. WWebJS only โ Cloud API does not
support polls natively and returns 422 POLL_NOT_SUPPORTED. Schema enforces
WhatsApp limits: question max 255 chars, 2-12 options, each option max 100 chars.
// Create interactive poll
const response = await fetch('https://apiwts.top/api/v1/messages/poll', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session-001',
to: '5511999999999',
question: 'Qual produto prefere?',
options: ['Produto A', 'Produto B', 'Produto C'],
allowMultipleAnswers: false // default false
})
});
// Response (200 OK)
{
"success": true,
"message": "Poll message sent successfully",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"driverMessageId": "true_5511999999999@c.us_POLL123",
"status": "sent",
"timestamp": "2026-04-18T14:00:00.000Z"
}
}
// Response (400 โ option limits exceeded)
{
"statusCode": 400,
"error": "Bad Request",
"message": "body/options must NOT have more than 12 items"
}
// Response (422 โ Cloud API driver)
{
"statusCode": 422,
"error": "Unprocessable Entity",
"message": "Poll sending is not supported by this driver. Use WWebJS driver.",
"code": "POLL_NOT_SUPPORTED",
"driver": "cloud",
"supportedDrivers": ["wwebjs"]
}
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
sessionId |
string | Yes | WhatsApp session ID (WWebJS driver required) |
to |
string | Yes | Recipient phone number or JID |
question |
string | Yes | Poll question (1-255 chars) |
options |
string[] | Yes | 2-12 items, each 1-100 chars |
allowMultipleAnswers |
boolean | No | Default false. When true, voters may select multiple options. |
GET /api/v1/messages/:driverMessageId/poll-votes to retrieve vote results
(see Get Poll Votes).
GET /api/v1/messages/:messageId/quoted
Get the original message that was quoted/replied to in a message. Returns the quoted message content or null if the message is not a reply. Useful for building conversation threads.
// Get quoted message from a reply
const response = await fetch('https://apiwts.top/api/v1/messages/msg-uuid-here/quoted', {
headers: {
'X-API-Key': 'your-api-key'
}
});
// Response (when message is a reply)
{
"messageId": "msg-uuid-here",
"quotedMessage": {
"id": "true_5511999999999@c.us_AAABB...",
"from": "5511888888888@c.us",
"to": "5511999999999@c.us",
"body": "Original message text",
"type": "chat",
"timestamp": "2024-01-15T10:30:00.000Z",
"hasMedia": false
}
}
// Response (when message is NOT a reply)
{
"messageId": "msg-uuid-here",
"quotedMessage": null
}
Response Fields
| Field | Type | Description |
|---|---|---|
quotedMessage.id |
string | WhatsApp message ID of the quoted message |
quotedMessage.from |
string | Sender of the quoted message (WhatsApp JID) |
quotedMessage.to |
string | Recipient of the quoted message (WhatsApp JID) |
quotedMessage.body |
string | Text content of the quoted message |
quotedMessage.type |
string | Message type (chat, image, video, etc.) |
quotedMessage.timestamp |
string | ISO 8601 timestamp when the quoted message was sent |
quotedMessage.hasMedia |
boolean | Whether the quoted message contains media |
POST /api/v1/messages/:messageId/star
Star (favorite) a message. Starred messages appear in the "Starred Messages" section of the chat.
// Star a message
const response = await fetch('https://apiwts.top/api/v1/messages/550e8400-e29b-41d4-a716-446655440000/star', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key'
}
});
// Response
{
"success": true,
"message": "Message starred successfully",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"starredAt": "2024-01-15T10:30:00.000Z"
}
}
DELETE /api/v1/messages/:messageId/star
Remove star (unfavorite) from a message.
// Unstar a message
const response = await fetch('https://apiwts.top/api/v1/messages/550e8400-e29b-41d4-a716-446655440000/star', {
method: 'DELETE',
headers: {
'X-API-Key': 'your-api-key'
}
});
// Response
{
"success": true,
"message": "Message unstarred successfully",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"unstarredAt": "2024-01-15T10:30:00.000Z"
}
}
GET /api/v1/messages/:messageId/reactions
Get all emoji reactions on a message. Returns list of reactions with emoji, sender ID, and timestamp. Useful for analyzing engagement and user feedback.
// Get reactions on a message
const response = await fetch('https://apiwts.top/api/v1/messages/550e8400-e29b-41d4-a716-446655440000/reactions', {
headers: {
'X-API-Key': 'your-api-key'
}
});
// Response
{
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"reactions": [
{
"emoji": "๐",
"senderId": "5511999999999@c.us",
"timestamp": "2024-01-15T10:30:00.000Z"
},
{
"emoji": "โค๏ธ",
"senderId": "5511888888888@c.us",
"timestamp": "2024-01-15T10:31:00.000Z"
}
],
"totalCount": 2
}
POST /api/v1/messages/:messageId/pin
Pin a message in chat for a specified duration. The message will be pinned at the top of the chat for the specified time. Useful for highlighting important announcements or information in groups.
// Pin a message for 1 day (86400 seconds)
const response = await fetch('https://apiwts.top/api/v1/messages/550e8400-e29b-41d4-a716-446655440000/pin', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
durationSeconds: 86400 // 1 day (min: 60s, max: 2592000s = 30 days)
})
});
// Response
{
"success": true,
"message": "Message pinned successfully",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"pinned": true,
"durationSeconds": 86400
}
}
DELETE /api/v1/messages/:messageId/pin
Unpin a pinned message in chat. Removes the message from the pinned position in the chat.
// Unpin a message
const response = await fetch('https://apiwts.top/api/v1/messages/550e8400-e29b-41d4-a716-446655440000/pin', {
method: 'DELETE',
headers: {
'X-API-Key': 'your-api-key'
}
});
// Response
{
"success": true,
"message": "Message unpinned successfully",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"pinned": false
}
}
GET /api/v1/messages/:messageId/mentions
Get all @mentions in a message. Returns list of mentioned contacts with their IDs, names (if available), phone numbers, and whether they are groups. Useful for processing messages that mention users or groups.
// Get mentions in a message
const response = await fetch('https://apiwts.top/api/v1/messages/550e8400-e29b-41d4-a716-446655440000/mentions', {
headers: {
'X-API-Key': 'your-api-key'
}
});
// Response
{
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"mentions": [
{
"id": "5511999999999@c.us",
"name": "John Doe",
"phoneNumber": "5511999999999",
"isGroup": false
},
{
"id": "5511888888888@c.us",
"phoneNumber": "5511888888888",
"isGroup": false
}
],
"totalCount": 2
}
GET /api/v1/messages/:messageId/poll-votes
Get all votes from a poll message. Returns list of voters with their selected options and vote timestamps. Only works on poll_creation type messages.
// Get votes from a poll message
const response = await fetch('https://apiwts.top/api/v1/messages/550e8400-e29b-41d4-a716-446655440000/poll-votes', {
headers: {
'X-API-Key': 'your-api-key'
}
});
// Response
{
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"votes": [
{
"voter": "5511999999999@c.us",
"selectedOptions": ["Option A", "Option C"],
"interactedAt": "2025-01-15T10:30:00.000Z"
},
{
"voter": "5511888888888@c.us",
"selectedOptions": ["Option B"],
"interactedAt": "2025-01-15T10:35:00.000Z"
}
],
"totalVoters": 2
}
GET /api/v1/messages/scheduled/stats
Get count of scheduled messages by status.
// Get scheduled messages statistics
const response = await fetch('https://apiwts.top/api/v1/messages/scheduled/stats', {
headers: {
'X-API-Key': 'your-api-key'
}
});
// Response
{
"pending": 45,
"queued": 3,
"sent": 1200,
"failed": 8
}
Status Lifecycle
- pending: Scheduled, waiting for execution time
- queued: Execution time reached, enqueued in send-message queue
- sent: Successfully sent via WhatsApp driver
- failed: Failed to send (session deleted, network error, etc.)
Use Cases
- Short-term: Appointment reminders (24h), follow-ups (3 days), marketing campaigns (1 week)
- Long-term: Insurance renewals (1 year), warranty expirations (11 months), annual contracts
- Business: Birthday messages, subscription renewals, seasonal campaigns
- All rate limiting and retry logic applies to scheduled messages
- If session is disconnected at send time, message will be retried automatically when session reconnects
-
If session is deleted before send time, message will be marked as
failed - Messages are checked every 1 minute - scheduling accuracy is ยฑ60 seconds
๐ Webhooks - Real-time Notifications
Configure webhooks to receive real-time notifications when messages arrive, status updates occur, and other events happen.
๐ฏ Webhook Configuration Guide v3.25.0+
APIWTS supports webhooks at two levels. Understanding the difference is crucial for multi-tenant architectures.
Quick Comparison
| Aspect | Per-Session Webhook | Per-Tenant Webhook |
|---|---|---|
| Endpoint | PATCH /api/v1/sessions/:sessionId |
POST /api/v1/webhooks/configure |
| Scope | Single session only | All sessions under your API Key |
| Priority | โฌ๏ธ HIGH (overrides tenant) | โฌ๏ธ FALLBACK (used if no session webhook) |
| Event Filtering | โ Receives all events | โ Can filter by event type |
| Auth Token | โ
Supports webhookAuthToken |
โ
Supports secret (HMAC) |
| Best For | Multi-tenant SaaS platforms | Single-tenant or global webhook handler |
๐ Decision Tree: Which Method Should I Use?
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Do you need different webhook URLs for different sessions? โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโ
โ โ
YES NO
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Use Per-Session โ โ Do you need event filtering โ
โ PATCH /sessions/:id โ โ (only certain event types)? โ
โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโ
โ โ
YES NO
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ
โ Use Per-Tenant โ โ Use either: โ
โ POST /webhooks/ โ โ - Per-Session for โ
โ configure โ โ simplicity โ
โโโโโโโโโโโโโโโโโโโโโ โ - Per-Tenant for โ
โ event filtering โ
โโโโโโโโโโโโโโโโโโโโโ
โก Priority Hierarchy
When dispatching webhooks, the system checks in this order:
- โ Session webhook configured? โ Use session webhook, STOP
- โฉ๏ธ No session webhook? โ Check tenant webhook
- โ Tenant webhook configured & active? โ Use tenant webhook
- โ Neither configured? โ No webhook delivery
๐ข Multi-Tenant SaaS Example
For SaaS platforms where each customer needs their own webhook endpoint:
// Scenario: You have 3 customers, each with their own WhatsApp session
// Customer 1: Acme Corp - webhook to their server
await fetch('https://apiwts.top/api/v1/sessions/acme-whatsapp', {
method: 'PATCH',
headers: {
'X-API-Key': 'sk_live_your_api_key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
webhookUrl: 'https://acme-corp.com/webhooks/whatsapp',
webhookAuthToken: 'acme-secret-token'
})
});
// Customer 2: Beta Inc - webhook to their API
await fetch('https://apiwts.top/api/v1/sessions/beta-whatsapp', {
method: 'PATCH',
headers: {
'X-API-Key': 'sk_live_your_api_key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
webhookUrl: 'https://beta-inc.com/api/whatsapp',
webhookAuthToken: 'beta-secret-token'
})
});
// Customer 3: Gamma Ltd - webhook to your central orchestrator
await fetch('https://apiwts.top/api/v1/sessions/gamma-whatsapp', {
method: 'PATCH',
headers: {
'X-API-Key': 'sk_live_your_api_key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
webhookUrl: 'https://your-orchestrator.com/webhooks/gamma'
})
});
// Result: Each session delivers webhooks to its own URL
// - acme-whatsapp โ acme-corp.com
// - beta-whatsapp โ beta-inc.com
// - gamma-whatsapp โ your-orchestrator.com
Method 1: Per-Session Webhook (Recommended for Multi-Tenant)
Configure webhook URL per session using PATCH /api/v1/sessions/:sessionId.
See Session Management - PATCH for full documentation.
// Quick example - configure webhook for a specific session
const response = await fetch('https://apiwts.top/api/v1/sessions/my-session', {
method: 'PATCH',
headers: {
'X-API-Key': 'sk_live_your_api_key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
webhookUrl: 'https://your-server.com/webhook',
webhookAuthToken: 'your-bearer-token' // Optional
})
});
Method 2: Per-Tenant Webhook (Global for all sessions)
POST /api/v1/webhooks/configure
Set up webhook URL and events.
// Request
const response = await fetch('https://apiwts.top/api/v1/webhooks/configure', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://your-app.com/webhooks/whatsapp',
events: [
'message.received.text',
'message.received.media',
'message.sent',
'message.delivered',
'message.read'
],
secret: 'your-webhook-secret'
})
});
Available Events
message.received.text- Text message receivedmessage.received.media- Media message receivedmessage.received.location- Location message receivedmessage.received.contact- Contact card receivedmessage.sent- Message successfully sent-
message.self_sent- Message sent from phone (v3.31.0+, WWebJS only, opt-in) message.delivered- Message delivered to recipientmessage.read- Message read by recipient (blue ticks)message.failed- Message failed to send-
message.reaction- A reaction was added/removed on a message (v2.90+) -
lid_pn_linked- A@lidwas linked to a phone number (v3.83.0+, when LIDโPN enrichment is active) โ seelid_pn_linked session.ready- WhatsApp session ready-
session.disconnected- WhatsApp session disconnected (debounced for fast internal recovery โ see note below, v3.90.0+)
message.status_update is no longer emitted (since
v2.90).
Delivery-status changes now arrive as granular events:
message.sent, message.delivered, message.read and
message.failed. Subscribe to those instead.
session.disconnected debounce (anti-flap)
To avoid noisy false disconnects, the API debounces the
session.disconnected webhook when it originates from a fast
internal recovery cycle (a transient store-stream stall during the first
AUTHENTICATED โ READY transition, which the API auto-recovers on its
own). Behavior:
-
Recovery completes within ~90s (the common case): the webhook is
held and cancelled โ you receive no
session.disconnectedand no positive follow-up event. The session simply stays/returnsREADYwith no flap. This is the correct state to observe: silence. -
Recovery does NOT complete within ~90s (rare): the
session.disconnectedwebhook is delivered as usual (real, durable disconnect). - Genuine disconnects (unpair / device unlinked / admin shutdown) are never debounced โ they are delivered immediately.
Residual (status polling): the underlying session DB state still
transitions DISCONNECTED โ READY during the debounce window. A consumer
that polls GET /sessions/:id/status during that ~90s window may
briefly observe DISCONNECTED even though no webhook is sent.
Webhook-driven consumers are unaffected. The feature can be disabled via the
WWEBJS_DISCONNECT_DEBOUNCE_ENABLED kill switch (default on).
WhatsApp Cloud API Webhooks (Inbound)
For tenants on the Meta WhatsApp Business Cloud API driver
(driver: "cloud"), Meta delivers inbound messages and delivery receipts to a
dedicated public endpoint. Configure the Callback URL and
Verify Token in the Meta App Dashboard (WhatsApp โ Configuration โ
Webhooks) and subscribe to the messages field.
GET /webhooks/cloud โ Verification handshake
Meta sends hub.mode=subscribe, hub.verify_token and
hub.challenge. When the token matches
CLOUD_WEBHOOK_VERIFY_TOKEN the endpoint echoes the
hub.challenge back as text/plain (HTTP 200); otherwise it
returns 403. This must succeed before Meta will activate the subscription.
POST /webhooks/cloud โ Events
-
Validated via HMAC-SHA256 over the raw body
(
X-Hub-Signature-256header, signed with the Cloud App SecretCLOUD_APP_SECRET). The check is fail-closed: when the app secret is configured (production), an invalid, missing, or unverifiable signature โ 401. Only an unconfigured secret (dev) downgrades to a logged warning. -
ACK-first: the endpoint replies
200 EVENT_RECEIVEDimmediately and processes the event asynchronously (Meta expects 200 within ~20s).
Inbound message โ consumer webhook
Inbound Cloud messages are forwarded to your configured webhook using the
same envelope as the WWebJS driver, with
platform: "cloud". The sender phone is normalized to
<E.164>@c.us so fromType is "phone". The
granular event type is determined by the message content:
textโmessage.received.text(body= text)-
locationโmessage.received.locationโ the payload carries an inlinelocation: { latitude, longitude, name?, address? }object;bodyis a derived label (name/address/coords). -
contactsโmessage.received.contactโ the payload carries the inlinecontactsarray (vCard);bodyis the first contact's formatted name. -
interactive/buttonreply โmessage.received.text(the choice title asbody). A reply with no resolvable title is skipped (not forwarded as an empty event). -
image/audio/video/document/stickerโmessage.received.mediawithhasMedia: true. v3.100.4 (Fase A.2):mediaUrlis now POPULATED for Cloud media (same contract as WWebJS). The gateway fetches the binary on receipt (Meta's 2-step media_id โ CDN download) and exposes it atGET /api/v1/messages/{id}/media(returns{ base64, mimetype, filename?, filesize? }) โ identical to WWebJS, so your consumer stays driver-agnostic.-
TTL window: the
mediaUrlis backed by a TTL-cached reference (default ~300s). Download within the window. After expiry the endpoint returns404 MEDIA_REFERENCE_EXPIREDโ re-fetch is no longer possible for that message. -
Large files (>10MB): the binary is not cached as base64; the
gateway re-fetches it on demand when you call the
mediaUrl(still within the Cloud media retention window). -
Graceful fallback: if the binary cannot be fetched (e.g. Meta
error 131052 "media download error", or the feature flag is off), the event is
still delivered with
mediaPending: trueandmediaUrl: nullโ no 404-bound URL is ever emitted. A Meta re-delivery of the same message re-attempts the fetch.
-
TTL window: the
-
Other inbound types (
reaction,order,system,unknown, โฆ) are not forwarded in A.1 (skipped + counted); they are not classified as text.
Delivery status โ consumer webhook
Cloud delivery receipts are forwarded as the granular events
message.delivered, message.read and
message.failed (the Cloud sent receipt is suppressed โ the
on-send message.sent already covers it). A
message.failed payload additionally carries the Meta error code
(errorCode, e.g. 131014) and title so you can act on the cause
(invalid number, outside the 24h window, etc.).
v2.81+: The API now automatically tracks message delivery status via WhatsApp ACK (acknowledgment) events. Status updates happen in real-time without requiring API polling:
- pending โ Message queued, waiting to be sent (gray clock icon)
- sent โ Message reached WhatsApp servers (single gray checkmark)
- delivered โ Message reached recipient's device (double gray checkmarks)
- read โ Message opened by recipient (double blue checkmarks)
- failed โ Message delivery failed (red exclamation)
How it works: WWebJS driver listens for message_ack events
from WhatsApp and automatically updates the database. Subscribe to the granular events
message.sent, message.delivered, message.read and
message.failed to receive real-time notifications whenever status changes
occur.
(The legacy message.status_update event was replaced by these granular
events in v2.90 and is no longer emitted.)
Webhook Payload Example
v2.70.2+: Payload fields are now
flattened to top-level for easier consumption. The full nested data is
still available in the data field for backward compatibility.
event, eventId,
sessionId, from, to, body,
type, hasMedia, mediaUrl) and the full original
object is nested under data (so the rich fields shown further down live at
data.message.*). timestamp at the top level is a
Unix epoch in milliseconds (number); the ISO string timestamps appear
inside data.
{
"event": "message.received.text",
"eventId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", // v3.87.0+ (dedup key, == X-Event-Id)
"timestamp": 1736937323000, // Unix ms (number)
"messageId": "msg_987654321",
"tenantId": "tenant_abc",
"sessionId": "my-session-001",
"from": "5511999999999@c.us", // top-level flat fields
"to": "554791246688@c.us",
"body": "Hello!",
"type": "text",
"hasMedia": false,
"mediaUrl": null,
"data": { // full original payload nested here
"event": "message.received.text",
"sessionId": "my-session-001",
"message": { "id": "msg_987654321", "from": "5511999999999@c.us", "fromType": "phone", "body": "Hello!", "...": "all rich fields shown below live under data.message.*" }
}
}
The nested examples that follow describe the contents of data.message (i.e.
paths like data.message.from, data.message.metadata.chatName).
v3.0.3+: Added fromType field to indicate identifier
format. See Identifier Formats section below.
v3.4+: Added 11 contextual fields for richer webhook payloads:
-
isForwarded,forwardingScore- Forwarded message info (score max 127) links- URLs extracted from message bodyauthor- Sender in group messagesisEphemeral- Disappearing message flagduration- Voice/video message duration in secondsvCards- Contact cards array for contact messages-
isStarred,isGif,isStatus,hasReaction,broadcast- Message flags -
deviceType- Device that sent the message (web, android, iphone, etc.)
v3.8.21+: Added hasQuotedMsg and
quotedMsg fields for quote/reply support.
v3.8.25+: Added quotedMsgId field + improved reliability
with fallback mechanism.
-
hasQuotedMsg- Boolean indicating if message is a reply to another message -
quotedMsgId- v3.8.25+ Direct access to quoted message serialized ID (null if not a reply) -
quotedMsg- Object with quoted message data (null if not a reply or data unavailable):id- Serialized ID of the quoted message-
body- Text content of the quoted message (may be null if unavailable) -
from- Sender JID of the quoted message (may be null if unavailable) type- Message type (chat, image, video, etc.)-
timestamp- ISO 8601 timestamp of the quoted message (may be null if unavailable)
// Text message received (v3.4+ format with all contextual fields)
{
"event": "message.received.text",
"timestamp": "2025-01-15T10:35:23.000Z",
"sessionId": "my-session-001",
"message": {
"id": "msg_987654321",
"from": "5511999999999@c.us", // WhatsApp JID format (see Identifier Formats)
"fromType": "phone", // v3.0.3+: 'phone' | 'lid' | 'group' | 'unknown'
"to": "554791246688@c.us",
"timestamp": "2025-01-15T10:35:23.000Z",
"type": "chat",
"body": "Check out this link: https://example.com",
"hasMedia": false,
"isForwarded": false, // v3.4+: Whether message was forwarded
"forwardingScore": 0, // v3.4+: How many times forwarded (0-127)
"links": ["https://example.com"], // v3.4+: URLs extracted from message (optional)
"isGroup": false, // Whether message is from a group
"isEphemeral": false, // v3.4+: Disappearing message
"isStarred": false, // v3.4+: Whether message is starred
"isGif": false, // v3.4+: Whether message is a GIF
"isStatus": false, // v3.4+: Whether message is a status update
"hasReaction": false, // v3.4+: Whether message has reactions
"hasQuotedMsg": false, // v3.8.21+: Whether message is a reply
"broadcast": false, // v3.4+: Whether message was broadcast
"deviceType": "android", // v3.4+: Device type (web, android, iphone, etc.)
"metadata": { // Additional metadata from driver
"chatId": "5511999999999@c.us",
"chatName": "John Doe", // Contact/chat name
"contactName": "John Doe", // Contact name from address book
"hasMedia": false
}
}
}
// Message from Linked Identity (Switzerland, Business accounts, etc.)
// โ ๏ธ IMPORTANT: @lid identifiers are NOT phone numbers!
{
"event": "message.received.text",
"timestamp": "2025-01-15T10:35:23.000Z",
"sessionId": "my-session-001",
"message": {
"id": "msg_lid_example",
"from": "77442588909785@lid", // Linked Identity (NOT a phone number!)
"fromType": "lid", // v3.0.3+: Use metadata.chatName for contact name
"to": "554791246688@c.us",
"timestamp": "2025-01-15T10:35:23.000Z",
"type": "chat",
"body": "Hello from Switzerland!",
"hasMedia": false,
"metadata": {
"chatId": "77442588909785@lid",
"chatName": "Giovanni Moser", // โ Use this for contact identification with @lid
"contactName": "Giovanni Moser",
"hasMedia": false
}
}
}
// Media message received (v3.7.12+: includes mediaUrl for download)
{
"event": "message.received.media",
"timestamp": "2025-01-15T10:36:55.000Z",
"sessionId": "my-session-001",
"message": {
"id": "msg_image_12345",
"from": "5511999999999@c.us",
"fromType": "phone",
"to": "554791246688@c.us",
"timestamp": "2025-01-15T10:36:55.000Z",
"type": "image",
"body": "Check this out!",
"hasMedia": true,
"mediaUrl": "https://apiwts.top/api/v1/messages/AC99FE4B684B8EE1AF5A6940ED188444/media",
"metadata": {
"chatName": "John Doe",
"hasMedia": true
}
}
}
// Group message received (v3.4+ with author and isForwarded)
{
"event": "message.received.text",
"timestamp": "2025-01-15T10:37:00.000Z",
"sessionId": "my-session-001",
"message": {
"id": "msg_group_12345",
"from": "120363220421466993@g.us", // Group JID
"fromType": "group", // v3.0.3+: This is a group message
"to": "554791246688@c.us",
"timestamp": "2025-01-15T10:37:00.000Z",
"type": "chat",
"body": "Hello team!",
"hasMedia": false,
"isGroup": true, // v3.4+: Message is from a group
"author": "5511999999999@c.us", // v3.4+: Actual sender within the group
"isForwarded": true, // v3.4+: This message was forwarded
"forwardingScore": 3, // v3.4+: Forwarded 3 times
"metadata": {
"chatId": "120363220421466993@g.us",
"chatName": "Project Team", // Group name
"hasMedia": false
}
}
}
// Reply/quoted message received (v3.8.21+ with hasQuotedMsg and quotedMsg, v3.8.25+ with quotedMsgId)
{
"event": "message.received.text",
"timestamp": "2026-01-02T11:00:00.000Z",
"sessionId": "my-session-001",
"message": {
"id": "msg_reply_12345",
"serializedId": "false_5547991246688@c.us_3EB0ABC123",
"from": "5547991246688@c.us",
"fromType": "phone",
"to": "554791246688@c.us",
"timestamp": "2026-01-02T11:00:00.000Z",
"type": "chat",
"body": "Yes, I received it!", // The reply message text
"hasMedia": false,
"hasQuotedMsg": true, // v3.8.21+: This is a reply to another message
"quotedMsgId": "false_554791246688@c.us_3EB0DEF456", // v3.8.25+: Direct access to quoted ID
"quotedMsg": { // v3.8.21+: The original message being quoted
"id": "false_554791246688@c.us_3EB0DEF456",
"body": "Did you receive the document?",
"from": "554791246688@c.us",
"type": "chat",
"timestamp": "2026-01-02T10:55:00.000Z"
},
"metadata": {
"chatId": "5547991246688@c.us",
"chatName": "John Doe",
"contactName": "John Doe",
"hasMedia": false
}
}
}
// Voice message received (v3.4+ with duration, v3.7.12+ with mediaUrl)
{
"event": "message.received.media",
"timestamp": "2025-01-15T10:38:00.000Z",
"sessionId": "my-session-001",
"message": {
"id": "msg_voice_12345",
"from": "5511999999999@c.us",
"fromType": "phone",
"to": "554791246688@c.us",
"timestamp": "2025-01-15T10:38:00.000Z",
"type": "ptt", // ptt = push-to-talk (voice message)
"hasMedia": true,
"mediaUrl": "https://apiwts.top/api/v1/messages/B7C8D9E0F1234567890123456789ABCD/media",
"duration": 15, // v3.4+: Duration in seconds
"deviceType": "iphone", // v3.4+: Sent from iPhone
"metadata": {
"chatName": "John Doe",
"hasMedia": true
}
}
}
// Contact card message received (v3.4+ with vCards)
{
"event": "message.received.contact",
"timestamp": "2025-01-15T10:39:00.000Z",
"sessionId": "my-session-001",
"message": {
"id": "msg_contact_12345",
"from": "5511999999999@c.us",
"fromType": "phone",
"to": "554791246688@c.us",
"timestamp": "2025-01-15T10:39:00.000Z",
"type": "vcard",
"hasMedia": false,
"vCards": [ // v3.4+: Contact cards in vCard format
"BEGIN:VCARD\nVERSION:3.0\nFN:Jane Smith\nTEL:+5511888888888\nEND:VCARD"
],
"metadata": {
"chatName": "John Doe",
"hasMedia": false
}
}
}
// Message delivery status (v2.90+ โ granular events, automatic tracking)
// NOTE: the legacy "message.status_update" event was REPLACED in v2.90 and is no longer emitted.
// Each status change now arrives as its own event type: message.sent / message.delivered /
// message.read / message.failed.
{
"event": "message.delivered", // or message.sent / message.read
"timestamp": "2025-11-21T15:45:32.000Z",
"sessionId": "claudecode",
"message": {
"id": "9a0c43b4-9416-4133-ae5e-b768c8ac8839", // Internal message ID
"driverMessageId": "3EB0F0E7C8D4F1234567890ABCDEF", // WhatsApp message ID โ BARE form (message.id.id)
"serializedId": "true_41764791479@c.us_3EB0F0E7C8D4F1234567890ABCDEF", // v3.96.0+: QUALIFIED form (message.id._serialized)
"status": "delivered", // sent โ delivered โ read
"updatedAt": "2025-11-21T15:45:32.000Z" // When status changed
}
}
// v3.96.0+: status events now ALSO carry "serializedId" โ the QUALIFIED message id
// (message.id._serialized, e.g. "true_...@c.us_3EB0..." or "true_...@lid_3EB0..."), alongside the
// existing "driverMessageId" which is the BARE id (message.id.id). Match the status to the original
// message by EITHER one (this mirrors message.self_sent, which already exposes both forms).
//
// ID FORMAT GUIDE (which id is bare vs qualified, per event):
// * driverMessageId โ BARE (message.id.id) in message.self_sent, status events, and inbound
// message events; QUALIFIED in the SEND-RESPONSE only (legacy field shape).
// * serializedId โ QUALIFIED (message.id._serialized) in message.self_sent and status events.
//
// Status lifecycle (3 separate webhook events for the same message):
// 1. message.sent - Message reached WhatsApp servers
// 2. message.delivered - Message reached recipient's device
// 3. message.read - Recipient opened message
// Phone-sent status (v3.96.0+ โ WWebJS only):
// Messages typed on the PHONE (apiOriginated:false, surfaced first as message.self_sent) now ALSO
// emit status webhooks as the message's ACK changes โ so a phone-sent message no longer stays stuck
// on "sending/clock" in your UI. Correlate these status events to the original message by
// driverMessageId (BARE id == messageId of the message.self_sent) OR by serializedId (QUALIFIED id
// == data.message.serializedId of the message.self_sent โ v3.96.0+, present here too). The status
// payload carries the marker "apiOriginated": false; the clientMessageId field is ABSENT for
// phone-sent (only API sends echo X-Idempotency-Key).
//
// There are two distinct outcomes โ do NOT treat them as one rising scale:
// * PROGRESSION (forward-only): message.sent โ message.delivered โ message.read. Each step has a
// higher ACK than the previous; later steps supersede earlier ones.
// * TERMINAL FAILURE: message.failed does NOT "rise" with the ACK โ it is the LOWEST ack level
// (an error ack), delivered once as a terminal state. It can arrive as the first status, or
// after a message.delivered, and means the send was acknowledged with an error.
//
// Note: for phone-sent, the message.self_sent (v3.95.0+) usually already arrives with
// "deliveryState": "server" ("ackLevel": 1) โ a separate message.sent may NOT arrive. Use the
// deliveryState of the self_sent as "reached the server", and these v3.96.0 status events for
// delivered / read / failed.
//
// โโ Send idempotency / single-delivery (v3.98.0+, WWebJS only โ CAMADA 2) โโโโโโโโโโโโโโโโโโโโโโ
// On a cold-send/restart the API used to re-execute the same job โ the SAME message could be
// physically delivered MORE THAN ONCE. v3.98.0 adds a safety net: once WhatsApp acknowledges the
// transmission (ACK_SERVER), the retry that would re-send the same message is SUPPRESSED instead.
// The suppression is conservative by design โ it only fires with a HIGH-CONFIDENCE positive signal,
// so an ambiguous/race/Redis-error case ALWAYS re-sends (a rare duplicate is acceptable; LOSING a
// message is not). Your correlation does not change: a suppressed retry still emits the SAME
// message.sent (with the same clientMessageId / driverMessageId), so dedup by clientMessageId or
// driverMessageId as usual. The full physical-double-send cure (1 single send at the source) is a
// follow-up; this layer covers the tail + the field-preservation below.
//
// CONTRACT-001 (v3.98.0+): a message sent via the API that surfaces as message.self_sent after a
// cold-send now carries "apiOriginated": true (so you can dedup it deterministically by the echoed
// clientMessageId). Consequence: a cold-send API copy NO LONGER appears in the phone-sent status
// stream (apiOriginated:false). Status for API sends comes from message.sent/delivered/read keyed
// by driverMessageId/serializedId as before. Messages typed on the PHONE (real phone-sent) are
// UNCHANGED โ they keep apiOriginated:false and the phone-sent status forward above.
{
"event": "message.delivered", // or message.sent / message.read / message.failed
"timestamp": "2026-06-03T19:00:09.000Z",
"sessionId": "my-session",
"message": {
"id": "3EB07792E4E84D4B3285FC", // == driverMessageId for phone-sent (no internal UUID)
"driverMessageId": "3EB07792E4E84D4B3285FC", // BARE id โ correlate by this (matches the message.self_sent)
"serializedId": "true_41764791479@c.us_3EB07792E4E84D4B3285FC", // v3.96.0+: QUALIFIED id (also on message.self_sent)
"status": "delivered",
"apiOriginated": false, // v3.96.0+: marker for phone-sent status forward
"updatedAt": "2026-06-03T19:00:09.000Z"
}
}
// Message send failed (v3.7.0+ - explicit failure detection)
{
"event": "message.failed",
"timestamp": "2025-12-02T14:30:45.000Z",
"sessionId": "claudecode",
"message": {
"id": "6257fd0f-d62a-4587-86fa-90583aeb95d3",
"recipient": "41764791479@c.us",
"error": "Driver returned failed status or empty driverMessageId",
"failedAt": "2025-12-02T14:30:45.000Z",
"attempt": 5,
"maxAttempts": 5
}
}
// Message sent successfully (v3.8.12+ - includes body, from, type)
// Use case: Lead Automation - detect patterns in self-sent messages
{
"event": "message.sent",
"timestamp": 1734523456789,
"messageId": "9a0c43b4-9416-4133-ae5e-b768c8ac8839",
"sessionId": "cloudagent",
"from": "5547991246688", // v3.8.12+: Sender phone number
"to": "5511999999999",
"type": "text", // v3.8.12+: Message type (text, media, template, etc.)
"body": "๐ Nova Oportunidade\nLead: Joรฃo Silva", // v3.8.12+: Message content
"hasMedia": false, // v3.8.12+: Whether message contains media
"driverMessageId": "3EB0F0E7C8D4F1234567890ABCDEF",
"status": "sent"
}
// Media message sent (v3.8.12+)
{
"event": "message.sent",
"timestamp": 1734523500000,
"messageId": "b2c3d4e5-f678-9012-3456-789abcdef012",
"sessionId": "cloudagent",
"from": "5547991246688",
"to": "5511999999999",
"type": "media",
"body": "Photo caption here", // Caption for media, empty string if none
"hasMedia": true,
"driverMessageId": "3EB0F0E7C8D4F9876543210FEDCBA",
"status": "sent"
}
// Message reaction received (v2.90+)
// Triggered when someone reacts to a message with an emoji
{
"event": "message.reaction",
"timestamp": "2025-01-01T10:00:00.000Z",
"sessionId": "my-session",
"message": {
"messageId": "3EB0A0B0C1D2E3F4", // ID of the message that received the reaction
"from": "5547991246688@c.us", // WhatsApp ID of the person who reacted
"reaction": "๐", // The emoji used (empty string if reaction removed)
"reactedAt": "2025-01-01T10:00:00.000Z" // When the reaction was added
}
}
// Reaction removed (v2.90+)
// When a user removes their reaction, the emoji field is empty
{
"event": "message.reaction",
"timestamp": "2025-01-01T10:05:00.000Z",
"sessionId": "my-session",
"message": {
"messageId": "3EB0A0B0C1D2E3F4",
"from": "5547991246688@c.us",
"reaction": "", // Empty string = reaction removed
"reactedAt": "2025-01-01T10:05:00.000Z"
}
}
// Message sent from phone (v3.31.0+ - opt-in, WWebJS only)
// apiOriginated: false = sent from physical phone
// Requires selfSentWebhookEnabled: true on the session
{
"event": "message.self_sent",
"timestamp": 1738934400000,
"sessionId": "my-session",
"from": "5547991246688@c.us",
"to": "5511999999999@c.us",
"toPhoneJid": "5511999999999@c.us",
"type": "chat",
"body": "Hello from my phone!",
"hasMedia": false,
"fromMe": true,
"apiOriginated": false,
"ackLevel": 1, // v3.95.0+: phone-typed message already reached the server
"deliveryState": "server", // derived from ackLevel (failed/pending/server/device/read)
"driverMessageId": "true_5511999999999@c.us_3EB0F0E7C8D4F1234567890ABCDEF",
"serializedId": "true_5511999999999@c.us_3EB0F0E7C8D4F1234567890ABCDEF",
"contactName": "John Doe",
"chatName": "John Doe"
}
// Message sent via API (v3.34.0+)
// apiOriginated: true = sent via POST /messages/text or POST /messages/media
{
"event": "message.self_sent",
"timestamp": 1738934400000,
"sessionId": "my-session",
"from": "5547991246688@c.us",
"to": "258252558344206@lid",
"toPhoneJid": "258252558344206@lid",
"type": "chat",
"body": "Hello via API!",
"hasMedia": false,
"fromMe": true,
"apiOriginated": true,
"ackLevel": 0, // v3.95.0+: API send is usually pending at creation time
"deliveryState": "pending", // NOT "sent" โ wait for the message.sent STATUS event
"clientMessageId": "msg-550e8400-e29b-41d4-a716-446655440000", // v3.93.0+: echoes X-Idempotency-Key (text/media only)
"driverMessageId": "true_258252558344206@lid_3EB0F0E7C8D4F1234567890ABCDEF",
"serializedId": "true_258252558344206@lid_3EB0F0E7C8D4F1234567890ABCDEF",
"contactName": "Giovanni Moser",
"chatName": "Giovanni Moser"
}
The message.reaction webhook is triggered when someone reacts to a message
with an emoji.
| Field | Type | Description |
|---|---|---|
messageId |
string | ID of the message that received the reaction |
from |
string | WhatsApp ID of the person who reacted (@c.us or @lid format) |
reaction |
string | The emoji used. Empty string ("") when reaction is removed. |
reactedAt |
string | ISO 8601 timestamp when the reaction was added/removed |
v3.8.12+: The message.sent webhook now includes additional
fields for richer integrations:
from- Sender phone number (E.164 format without +)type- Message type: text, media, template, interactive, listbody- Message content (text for text messages, caption for media)hasMedia- Boolean indicating if message contains media
Use Case: Lead Automation
With the new body field, you can now detect patterns in self-sent messages
to trigger automation flows:
// Example: Detect "Nova Oportunidade" pattern for Lead Automation
app.post('/webhooks/whatsapp', (req, res) => {
const { event, body, from, sessionId } = req.body;
if (event === 'message.sent' && body?.includes('๐ Nova Oportunidade')) {
// Trigger Lead Automation workflow
triggerLeadWorkflow({
source: 'whatsapp',
sessionId,
senderPhone: from,
messageBody: body
});
}
res.status(200).send('OK');
});
v3.7.0+: The MessageWorker now explicitly verifies send results before marking messages as sent:
-
Before v3.7.0: If WWebJS
sendMessage()failed silently (returned emptydriverMessageId), messages were incorrectly marked as "sent" -
After v3.7.0: Messages are only marked as "sent" if they receive a
valid
driverMessageIdfrom WhatsApp
Retry Behavior: Failed messages are retried up to 5 times with
exponential backoff. After max retries, they go to the Dead Letter Queue (DLQ) and a
message.failed webhook is dispatched.
Common Failure Causes:
- Invalid recipient number (doesn't exist on WhatsApp)
- Number format mismatch (@c.us vs @lid)
- Session disconnected during send
- WhatsApp rate limiting
v3.31.0+: The message.self_sent webhook is triggered when
a message is sent from the phone or via the API. This is useful for CRM systems and
chatbot orchestrators that need complete conversation visibility.
v3.34.0+: New apiOriginated boolean field allows you to
distinguish messages sent via API from messages sent from the physical phone. When
true, the message was sent through the API (POST /messages/text
or POST /messages/media). When false, it was sent from the
phone.
Availability: WWebJS driver only. Cloud API and Instagram do not support this event.
Opt-in: Disabled by default. Enable per-session via:
- Admin Dashboard: Edit Session > "Webhook para Mensagens Enviadas pelo Celular"
-
API:
PATCH /api/v1/sessions/:sessionIdwith{"selfSentWebhookEnabled": true}
| Field | Type | Description |
|---|---|---|
from |
string | Sender WhatsApp ID (your own number, @c.us format) |
to |
string | Recipient WhatsApp ID (@c.us or @lid format from contact) |
toPhoneJid |
string |
Raw recipient JID from WhatsApp (msg.to). May be @c.us or @lid format
depending on the contact (v3.32.0+).
|
type |
string | Message type (chat, image, video, document, etc.) |
body |
string | Message text content |
fromMe |
boolean | Always true for self-sent messages |
apiOriginated |
boolean |
v3.34.0+: true if message was sent via API,
false if sent from phone. Use this to prevent duplicate processing
when your system sends messages via API and also receives
message.self_sent webhooks.
|
clientMessageId |
string |
v3.93.0+: echo of the X-Idempotency-Key you sent on
the original POST /messages/text or
POST /messages/media request. This is the
stable correlation key that lets you match this
self_sent back to the send-response (which carries the same
clientMessageId). Present only for
API-sent text/media messages that included the header; absent for
phone-sent messages and for the synchronous endpoints (location/contact/poll/
template โ those do not persist, so correlate their self_sent by the
WhatsApp id instead โ see
Message correlation contract).
|
ackLevel |
number |
v3.95.0+: The real WhatsApp ack level of the
message at the moment it was created in the local store:
-1 (error/failed), 0 (pending โ created, not yet
acknowledged by the server), 1 (server received),
2 (delivered to device), 3+ (read/played). Messages sent
via the API are usually 0 (pending) at this moment;
messages typed on the phone may already be 1 or
higher.
|
deliveryState |
string |
v3.95.0+: Human-readable derivation of ackLevel:
'failed' (ack -1), 'pending' (ack
0), 'server' (ack 1),
'device' (ack 2), 'read' (ack
≥3).
Do not treat self_sent as "sent" โ see the
delivery-truthfulness note below.
|
hasMedia |
boolean | Whether message contains media (media not cached for self-sent) |
message.self_sent means "created", NOT "delivered to the
server".
This event fires the moment WhatsApp creates the message in the local store โ
before transmission. From v3.95.0+ it carries the
real ackLevel and a derived deliveryState so
you never confuse a created message with a sent one.
The truth of the acks:
ackLevel |
deliveryState |
Meaning |
|---|---|---|
-1 |
'failed' |
WhatsApp reported an error for this message โ it will not be delivered as-is. |
0 |
'pending' |
Created in the local store, not yet acknowledged by the server. Typical for messages sent via the API at creation time. |
1 |
'server' |
Reached the WhatsApp server (same as the message.sent status event).
|
2 |
'device' |
Delivered to the recipient's device (same as message.delivered).
|
≥3 |
'read' |
Read/played by the recipient (same as message.read). |
Messages sent through the API are generally
deliveryState: 'pending' (ackLevel 0) when the
self_sent fires; messages typed on the phone may already
be 'server'/'device' (ackLevel ≥1).
So do not document or treat self_sent as "always pending"
โ read the real deliveryState on each event.
-
Use the STATUS event
message.sent(ackLevel1) as your "sent" signal โ not theself_sentevent. -
Treat a
self_sentwithdeliveryState: 'pending'as created / not yet confirmed, and surface it to the user as such. -
If a
self_sent-pending message has no matchingmessage.sentwithin ~60โ90s, treat it as a probable failure (the recipient likely never received it), even before the terminalmessage.failedarrives.
Message correlation contract (clientMessageId)
When you send a message with the X-Idempotency-Key header, the API echoes
that value back as clientMessageId across the whole message lifecycle. This
gives you one stable key to correlate the
send-response, the message.sent webhook, the
message.self_sent webhook, and the delivery-status webhooks
(message.delivered/message.read) โ without having to reconcile
the internal UUID against the WhatsApp id yourself.
POST /messages/text or /messages/media is
asynchronous: the send-response returns the API's
internal UUID (id, status: "queued"), but the
later message.self_sent / message.sent webhooks identify the
message by its WhatsApp id (a different value). With nothing linking the
two, a naive consumer writes two rows for one message. The
clientMessageId echo is the machine-readable link that closes that gap:
UPSERT by clientMessageId and you get exactly one row.
Where the key appears
| Stage | Field path | Carries clientMessageId? |
|---|---|---|
| send-response (text/media/template) | clientMessageId (top level) |
โ
when X-Idempotency-Key was sent |
| send-response (location/contact/poll) | data.clientMessageId |
โ
when X-Idempotency-Key was sent |
message.sent webhook (on-send) |
data.message.clientMessageId * |
โ text/media (the recommended UPSERT point โ see below) |
message.self_sent webhook |
data.message.clientMessageId |
โ text/media only (the 4 synchronous endpoints don't persist) |
message.delivered / message.read (status) |
data.message.clientMessageId |
โ text/media |
* Webhook envelopes are flattened โ top-level mirrors of the rich
data.message.* fields are also present (see the
Webhook Payload Example).
message.sent (the worker event).
There are two events named message.sent: the
on-send one emitted by the message worker the moment the driver hands
the message off (it carries the internal messageId [UUID], the
driverMessageId [WhatsApp id] and clientMessageId
together), and the ack-driven status updates
(delivered/read). The on-send message.sent is the
single best point to UPSERT your row: all three ids arrive in one payload. Key your row
by clientMessageId and update it as the later status events arrive.
The id semantics, per event (read this before keying on message.id)
message.id is not the same kind of value in every event โ
keying your dedup on it directly is the footgun. Prefer clientMessageId;
when it's absent (no idempotency key, or phone-sent), fall back to the WhatsApp id.
| Event | message.id |
WhatsApp id field |
|---|---|---|
| send-response (text/media) | internal UUID ("queued") |
โ (not yet assigned) |
message.self_sent |
short WhatsApp id (3EB0โฆ) |
serializedId (full format) |
message.sent (worker on-send) |
internal UUID (messageId) |
driverMessageId (full WhatsApp id) |
status (message.sent/delivered/read ack)
|
internal UUID | driverMessageId (short WhatsApp id) |
clientMessageId to their message.self_sent webhook. Their
send-response is synchronous and already returns the real WhatsApp id in
data.driverMessageId โ so correlate their self_sent by
the WhatsApp id (serializedId/driverMessageId), not by
clientMessageId. The send-responses still echo the key: location/contact/poll
echo it as data.clientMessageId, while template โ like text/media โ echoes it
at the top level as clientMessageId (see the table above).
apiOriginated โ origin discriminator
The message.self_sent webhook carries
apiOriginated: boolean โ true when the message was sent through
this API (POST /messages/*), false when it was typed on the
physical phone. Use it together with clientMessageId to avoid
double-processing: an apiOriginated: true self_sent is the echo of a message
you sent (dedup it via clientMessageId); an
apiOriginated: false self_sent is a genuinely new outbound message from the
user's phone.
Webhook Security (HMAC)
Webhooks include an X-Signature header with HMAC-SHA256 signature
(base64-encoded) for verification.
Signature Header: X-Signature: <base64-encoded-hmac>
HMAC Secret (tenant-level webhooks): The secret is the
webhook config secret โ the value you pass as secret to
POST /webhooks/configure (or the auto-generated one returned if you omit
it). It is not your API key. Store this exact value and use it to verify every
delivery.
HMAC Secret (session-level webhooks): For per-session webhooks
(configured via PATCH /api/v1/sessions/:id), signing is enabled only when
you set webhookSecret on the session. See
Session-Level HMAC Signing. If no
webhookSecret is configured, session webhooks are sent
without X-Signature (use the
webhookAuthToken Bearer header to authenticate instead).
express.raw() / Fastify raw body) and pass those bytes to the verifier.
Compare the X-Signature header (base64) against
HMAC-SHA256(rawBody, secret).
// Node.js โ verify the X-Signature over the RAW body bytes
import crypto from 'crypto';
// rawBody MUST be the exact bytes received (Buffer or string), NOT a re-stringified object
const verifyWebhookSignature = (rawBody, receivedSignature, secret) => {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('base64'); // base64-encoded (v2.8+)
const a = Buffer.from(receivedSignature || '', 'utf8');
const b = Buffer.from(expected, 'utf8');
// Length check first โ timingSafeEqual throws on length mismatch
return a.length === b.length && crypto.timingSafeEqual(a, b);
};
// Express.js โ use express.raw() so you get the untouched bytes
app.post('/webhooks/whatsapp', express.raw({ type: 'application/json' }), (req, res) => {
const receivedSignature = req.headers['x-signature'];
// req.body is a Buffer here (the raw bytes) โ perfect for HMAC
if (!verifyWebhookSignature(req.body, receivedSignature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body.toString('utf8'));
// event.event, event.from, event.to, event.data ...
res.status(200).send('OK');
});
// Fastify โ capture the raw body via the contentTypeParser (or addHook 'preParsing')
fastify.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => {
req.rawBody = body; // keep the raw bytes for verification
try { done(null, JSON.parse(body.toString('utf8'))); } catch (err) { done(err); }
});
fastify.post('/webhooks/whatsapp', async (request, reply) => {
const receivedSignature = request.headers['x-signature'];
if (!verifyWebhookSignature(request.rawBody, receivedSignature, process.env.WEBHOOK_SECRET)) {
return reply.status(401).send({ error: 'Invalid signature' });
}
const event = request.body; // already parsed by the contentTypeParser above
return { status: 'ok' };
});
WhatsApp Identifier Formats - v3.0.3+ ๐
The from field in webhooks uses WhatsApp JID (Jabber ID) format.
Understanding these formats is crucial for proper message handling.
โ ๏ธ Important: @lid Format
The @lid format (Linked Identity) is NOT a phone number.
Use metadata.chatName to identify the contact. This format is common in
Switzerland, some European countries, and WhatsApp Business accounts.
Identifier Types
| Format | fromType | Example | Description | Contains Phone? |
|---|---|---|---|---|
@c.us |
"phone" |
5547999999999@c.us |
Traditional individual chat | โ Yes - can be normalized to E.164 |
@lid |
"lid" |
77442588909785@lid |
Linked Identity (internal ID) | โ No - use metadata.chatName |
@g.us |
"group" |
120363220421466993@g.us |
Group chat | โ No - this is a group ID |
Processing by fromType (v3.0.3+)
// TypeScript example - Handle different identifier types
interface WebhookPayload {
event: string;
sessionId: string;
message: {
from: string;
fromType: 'phone' | 'lid' | 'group' | 'unknown'; // v3.0.3+
metadata?: {
chatName?: string;
contactName?: string;
};
};
}
function extractContactInfo(webhook: WebhookPayload) {
const { from, fromType, metadata } = webhook.message;
switch (fromType) {
case 'phone':
// Traditional format - normalize to E.164
const phone = from.replace('@c.us', '');
return {
type: 'phone',
identifier: '+' + phone,
displayName: metadata?.contactName || metadata?.chatName || phone
};
case 'lid':
// Linked Identity - use chatName for identification
return {
type: 'lid',
identifier: from, // Keep as-is (internal ID)
displayName: metadata?.chatName || 'Unknown Contact'
};
case 'group':
// Group message - extract author for actual sender
return {
type: 'group',
groupId: from,
displayName: metadata?.chatName || 'Unknown Group'
};
default:
return {
type: 'unknown',
identifier: from,
displayName: metadata?.chatName || from
};
}
}
Phone Number Normalization (for @c.us only)
// Only normalize @c.us format to E.164
function normalizePhoneNumber(jid: string): string | null {
if (!jid.endsWith('@c.us')) {
return null; // Cannot normalize @lid or @g.us
}
const phone = jid.replace('@c.us', '');
return '+' + phone;
}
// Examples:
normalizePhoneNumber('5547999999999@c.us') // โ '+5547999999999'
normalizePhoneNumber('77442588909785@lid') // โ null (not a phone!)
normalizePhoneNumber('120363...@g.us') // โ null (group ID)
Sending Messages to @lid Contacts - v3.0.4+ ๐
Starting from v3.0.4, you can send messages using the complete JID format in the
to field. This is essential for replying to contacts who appear with
@lid format.
๐ก Key Insight
When you receive a message from 77442588909785@lid, you
cannot simply use the numeric portion as a phone number. You must
send the reply using the complete JID including the
@lid suffix.
Accepted Formats for to Field
| Format | Example | Use Case |
|---|---|---|
| E.164 (phone) | 5547999999999 |
Traditional contacts - converted to @c.us internally |
| JID @c.us | 5547999999999@c.us |
Explicit contact JID (used as-is) |
| JID @lid | 77442588909785@lid |
Linked Identity contacts โ ๏ธ (used as-is) |
| JID @g.us | 120363220421466993@g.us |
Send to groups (used as-is) |
Example: Replying to an @lid Contact
// Step 1: You receive a webhook with @lid format
{
"event": "message.received.text",
"sessionId": "sensyxboutique",
"message": {
"from": "77442588909785@lid", // โ Note the @lid format
"fromType": "lid",
"body": "Olรก!",
"metadata": {
"chatName": "Giovanni Moser" // โ Use this to identify the contact
}
}
}
// Step 2: Reply using the COMPLETE JID
POST /api/v1/messages/text
{
"sessionId": "sensyxboutique",
"to": "77442588909785@lid", // โ Use the complete JID from webhook
"text": "Olรก! Bem-vindo ร Sensyx Boutique!"
}
โ ๏ธ Common Mistake
Wrong: "to": "77442588909785" โ Message queued but NOT
delivered (wrong format)
Correct: "to": "77442588909785@lid" โ Message delivered
successfully โ
@lid Limitations
-
No phone number on the JID itself: The
@lidJID cannot be converted to E.164 directly. (v3.83.0+: when the gateway can resolve the linked number, it is provided separately inmessage.senderPn/message.recipientPnas+E.164โ see "LID โ Phone Number Enrichment" below. A residual privacy tail remains unresolvable.) -
Identification: Use
message.senderPn/message.recipientPn(when present) or fall back tometadata.chatNameto identify the contact - Storage: Store the complete @lid JID in your database for future communication
- Regional: Common in Switzerland, some European countries, and WhatsApp Business accounts
LID โ Phone Number Enrichment (v3.83.0+)
To deterministically reconcile a @lid contact with its real phone number,
message webhooks may include the following additive fields inside
data.message.* (existing fields such as from, to,
fromType, body, chatName are unchanged):
| Field | Direction | Type | Description |
|---|---|---|---|
senderLid |
incoming | string | null |
The sender's @lid JID (stable internal identity), or
null.
|
senderPn |
incoming | string | null |
The sender's resolved phone in
E.164 with a leading + (e.g.
+41791234567), or null when WhatsApp does not expose it
(privacy tail).
|
recipientLid |
self_sent | string | null |
The recipient's @lid JID for messages you sent from the phone, or
null.
|
recipientPn |
self_sent | string | null |
The recipient's resolved phone in
E.164 with a leading +, or null.
|
pnResolved |
both | boolean |
true only when a valid +E.164 phone was resolved.
Promote LID โ phone only when true.
|
pnOutcome |
both | string |
Granular outcome: resolved | not_in_contacts |
unresolved_private_or_unknown | error. Informational
(observability) โ your logic should key off pnResolved.
|
capabilities |
both | string[] |
["lid_pn"] โ present on every enriched message (even
when unresolved), so you can distinguish "gateway does not support enrichment"
from "supports it but could not resolve this contact".
|
payloadVersion |
both | string |
Semver string (e.g. "3.83.0"). Parse it on your
side; the gateway always sends a string.
|
Notes
-
The phone is always E.164 with a leading
+(never the raw@c.usJID). - These fields are 100% additive and only appear when enrichment is enabled for the session; consumers that ignore them are unaffected.
-
During rollout, enrichment may be enabled per session (canary): a
message without these fields means enrichment is not yet enabled for
that specific session โ it does not mean the gateway lacks
support. Detect support per message via the presence of
capabilities: ["lid_pn"], not globally. -
A phone may be linked after the first message arrives โ see the
lid_pn_linkedevent below.
Event: lid_pn_linked
(v3.83.0+)
Emitted as its own webhook (session-level, signed with X-Signature when a
session HMAC secret is configured โ same as all other events) when the gateway observes
a new mapping between a @lid and a phone number. Use it to
link/merge a card that was first created from a @lid with a later-resolved
phone number.
{
"event": "lid_pn_linked",
"sessionId": "sensyxboutique",
"lid": "77442588909785@lid",
"pn": "+41791234567",
"linkedFrom": "incoming", // "incoming" | "self_sent" โ the direction that triggered the link
"timestamp": "2026-05-31T01:00:00.000Z"
}
data. There is
no message sub-object for this event โ read the fields at
data.lid, data.pn, data.linkedFrom. The top-level
event, eventId and sessionId are also present
(message-specific top-level fields like from/body will be
absent).
Best Practice: Store the JID
// When receiving a message, store the complete JID
async function handleIncomingMessage(webhook: WebhookPayload) {
const { from, fromType, metadata } = webhook.message;
// Store the complete JID for future replies
await db.contacts.upsert({
jid: from, // "77442588909785@lid" or "5547999999999@c.us"
type: fromType, // "lid" or "phone"
displayName: metadata?.chatName, // "Giovanni Moser"
sessionId: webhook.sessionId
});
}
// When sending a reply, use the stored JID
async function sendReply(contactId: string, text: string) {
const contact = await db.contacts.findById(contactId);
await api.post('/api/v1/messages/text', {
sessionId: contact.sessionId,
to: contact.jid, // Use stored JID directly
text: text
});
}
Webhook Authentication (Bearer Token) - v2.86+
Configure Bearer token authentication for secure webhook delivery to protected endpoints (Supabase Edge Functions, AWS Lambda, Azure Functions, etc.).
๐ Security Requirements
- HTTPS Required: Webhook URL must use HTTPS when auth token is configured
- Token Length: Minimum 20 characters, maximum 500 characters
- Storage: Tokens are stored encrypted in database
- Logging: Only first 8 characters logged (security)
When to Use Bearer Token Authentication
Bearer tokens are required when your webhook endpoint needs authentication:
-
Supabase Edge Functions: Requires
anonorservice_rolekey - AWS Lambda + API Gateway: JWT authorizer tokens
-
Azure Functions: Function keys (
x-functions-keyorAuthorization) -
Custom APIs: Any endpoint requiring
Authorization: Bearer <token>
Configuration via Admin Dashboard (v2.88: Token Generation UI)
Generate and configure secure webhook tokens directly in the Admin Dashboard:
โจ NEW in v2.88: Automatic Token Generation
The Admin Dashboard now includes a "Gerar Token Seguro" button that
automatically generates cryptographically secure tokens (40+ characters) with proper
format: wh_sessao_20251123_{random}
Step-by-Step: Generate Token
- Navigate to Sessions tab in Admin Dashboard
- Click Create Session or edit existing session
- Fill in Session ID (required for token generation)
- Fill in Webhook URL (must be HTTPS)
- Click "๐ Gerar Token Seguro" button - Token will be auto-generated and populated
- (Optional) Click ๐ Copy button to copy token to clipboard
- (Optional) Click ๐๏ธ Mostrar to toggle token visibility
- Save session
Token Format (v2.88)
Auto-generated tokens follow this secure format:
wh_{sessionId}_{YYYYMMDD}_{20-random-chars}
Example:
wh_sensyx_20251123_a7f3k9j2m1n4p6q8r0s2t4u6v8w0x2y4z6
โโโฌโโโ โโโโโฌโโโโ โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
โ โ โโ 20 crypto-secure random chars
โ โโ Creation date (YYYYMMDD)
โโ Session identifier (prefix: wh_)
Manual Token Entry (Alternative)
You can also manually enter tokens (e.g., Supabase anon key, custom JWT):
- Paste token into Webhook Auth Token field
- Ensure token is minimum 20 characters
- Save session
Configuration via API
// Create session with webhook authentication
const response = await fetch('https://apiwts.top/admin/api/sessions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cookie': 'session=...' // Admin authentication
},
body: JSON.stringify({
tenantId: 'your-tenant-id',
sessionId: 'my-session',
webhookUrl: 'https://your-edge-function.supabase.co/webhook',
webhookAuthToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' // Your Bearer token
})
});
// Update existing session
const updateResponse = await fetch('https://apiwts.top/admin/api/sessions/my-session', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Cookie': 'session=...'
},
body: JSON.stringify({
webhookUrl: 'https://new-endpoint.com/webhook',
webhookAuthToken: 'your-new-token' // Leave empty to keep existing
})
});
Session-Level HMAC Signing โ webhookSecret
(v3.84.1+)
Set an optional webhookSecret on a session to make every
session-level webhook delivery include an
X-Signature header โ an HMAC-SHA256 of the raw JSON payload,
base64-encoded. The consumer recomputes the HMAC with the same secret to verify
authenticity and integrity. This is independent of webhookAuthToken (Bearer
auth): set either, both, or neither. The secret is encrypted at rest and is
never returned by the API (responses show
"***redacted***" when set, null when not). Max 500 characters.
# Enable HMAC signing for a session (set the secret)
curl -X PATCH https://apiwts.top/api/v1/sessions/my-session \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"webhookSecret": "your-strong-shared-secret"}'
# Remove the secret (reverts to Bearer-only / unsigned)
curl -X PATCH https://apiwts.top/api/v1/sessions/my-session \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"webhookSecret": null}'
Verify the X-Signature on the consumer side (Node.js):
const crypto = require('crypto');
// rawBody MUST be the exact bytes apiwts.top sent (do NOT re-stringify a parsed object)
function verifySessionWebhook(rawBody, xSignatureHeader, webhookSecret) {
const expected = crypto.createHmac('sha256', webhookSecret).update(rawBody).digest('base64');
const a = Buffer.from(xSignatureHeader || '', 'utf8');
const b = Buffer.from(expected, 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b); // constant-time compare
}
webhookSecret takes effect on the next dispatched event. Webhooks already
enqueued (in flight) carry the secret captured at enqueue time, so signatures computed
during the change window may validate against the previous secret. To rotate safely,
accept both old and new secrets on the consumer for a brief overlap, or pause traffic
during the change.
How apiwts.top Sends Authenticated Webhooks
When webhookAuthToken is configured, apiwts.top includes the
Authorization header:
POST /webhook HTTP/1.1
Host: your-edge-function.supabase.co
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Webhook-Event: message.received
X-Message-ID: msg_123456789
X-Session-Id: my-session (v3.87.0+, present for session-scoped events)
X-Event-Id: 7c9e6679-7425-40de-944b-e07fc1f90ae7 (v3.87.0+, matches body.eventId)
X-Signature: abc123... (HMAC signature, if secret configured)
User-Agent: WhatsApp-Multi-Driver-API/1.0
{
"event": "message.received",
"eventId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"sessionId": "my-session",
"from": "5551234567890",
"body": "Hello World",
"timestamp": 1730000000000
}
Example: Supabase Edge Function
// Supabase Edge Function (TypeScript)
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
serve(async (req) => {
// 1. Verify Authorization header
const authHeader = req.headers.get('Authorization');
const expectedToken = 'Bearer ' + Deno.env.get('SUPABASE_ANON_KEY');
if (authHeader !== expectedToken) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
// 2. Process webhook
const webhook = await req.json();
console.log('Webhook received:', webhook.event, webhook.from);
// Your business logic here...
return new Response(JSON.stringify({ status: 'ok' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
});
Example: Express.js with Bearer Token
// Express.js endpoint (Node.js)
// Use express.raw() so HMAC can be verified over the exact bytes (see Webhook Security).
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
// 1. Verify Bearer token
const authHeader = req.headers.authorization;
const expectedToken = 'Bearer ' + process.env.WEBHOOK_TOKEN;
if (authHeader !== expectedToken) {
return res.status(401).json({ error: 'Unauthorized' });
}
// 2. Optional: Verify HMAC signature (if configured) โ over the RAW body bytes
const signature = req.headers['x-signature'];
if (signature) {
if (!verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
}
// 3. Process webhook
const event = JSON.parse(req.body.toString('utf8'));
// event.event, event.from ...
res.status(200).send('OK');
});
Example: AWS Lambda + API Gateway
// Lambda function (Node.js)
export const handler = async (event) => {
// API Gateway JWT Authorizer already validated token
// event.requestContext.authorizer.claims contains JWT claims
const webhook = JSON.parse(event.body);
console.log('Webhook received:', webhook.event, webhook.from);
// Your business logic here...
return {
statusCode: 200,
body: JSON.stringify({ status: 'ok' })
};
};
Error Handling & Retry Logic
apiwts.top implements intelligent retry logic for webhook deliveries:
- 401 Unauthorized: Invalid/expired token - NO RETRY (fix token configuration)
- 403 Forbidden: Valid token but insufficient permissions - NO RETRY (check token permissions)
- 408 Request Timeout: Retries up to 3 times (1s, 5s, 30s intervals)
- 429 Too Many Requests: Retries up to 3 times
- 5xx Server Errors: Retries up to 3 times (temporary failures)
- Other 4xx Errors: NO RETRY (client errors are permanent)
Troubleshooting
โ ๏ธ Common Issues
-
HTTP 401 "Webhook URL must use HTTPS when auth token is configured"
Solution: Change webhook URL fromhttp://tohttps:// -
Webhooks not arriving at Supabase Edge Function
Solution: ConfigurewebhookAuthTokenwith your Supabase anon key -
Webhook delivers but returns 401 Unauthorized
Solution: Verify token is correct and matches your endpoint's expected token -
Token rejected: "must be at least 20 characters"
Solution: Use a secure token with minimum 20 characters (recommended: 32+ chars)
โ Identifier Format Issues (v3.0.3+)
-
"Invalid phone format" error when processing webhooks
Cause: Code is trying to normalize@lidformat as a phone number.
Solution: CheckfromTypefield before normalizing. Only normalize whenfromType === "phone". -
Messages from some contacts not appearing in your system
Cause: Validation filters are rejecting@lidformat as invalid.
Solution: Accept all three formats (@c.us,@lid,@g.us). UsefromTypeto differentiate. -
Contact name shows as "Unknown" for @lid messages
Cause: Code is parsing thefromfield expecting a phone number.
Solution: For@lidformat, usemetadata.chatNameormetadata.contactNamefor the contact's display name. -
Swiss/European contacts always show @lid instead of phone number
Cause: This is expected behavior. Some WhatsApp accounts (especially Business accounts and certain regions) use Linked Identity.
Solution: Store both thefrom(identifier) andmetadata.chatName(display name) in your database.
Security Best Practices
-
Use HTTPS: Always use
https://webhook URLs when auth tokens are configured - Rotate Tokens: Regularly rotate Bearer tokens (recommended: every 90 days)
- Minimum Length: Use tokens with at least 32 characters for security
- Environment Variables: Store tokens in environment variables, never hardcode
- Dual Authentication: Use both Bearer token + HMAC signature for maximum security
- Monitor Failures: Check webhook delivery logs for authentication failures
Webhook Headers
apiwts.top sends these headers with every webhook delivery:
Content-Type: application/jsonX-Webhook-Event: message.received- Event typeX-Message-ID: msg_987654321- Message identifier-
X-Event-Id: <uuid>v3.87.0+ - Unique per-delivery id for dedup / anti-replay. Identical to the top-leveleventIdfield in the body, and stable across retries of the same delivery (use it as your idempotency key). -
X-Session-Id: <session-id>v3.87.0+ - Session that produced the event. Present for session-scoped events; absent for tenant-level events that have no session. Useful for resolving the session-level HMAC secret by header. X-Signature: <base64-hmac>- HMAC signature for verificationUser-Agent: WhatsApp-Multi-Driver-API/1.0
Body field eventId
v3.87.0+: every webhook body now includes a
top-level eventId (UUID) alongside
event/timestamp. It is part of the signed body and equals the
X-Event-Id header. It is the recommended idempotency/dedup key for all
event types (including session.* and lid_pn_linked events that
lack a X-Message-ID).
GET /api/v1/webhooks/config
Retrieve current webhook configuration for your tenant.
// Request
const response = await fetch('https://apiwts.top/api/v1/webhooks/config', {
method: 'GET',
headers: {
'X-API-Key': 'your-api-key'
}
});
const config = await response.json();
console.log(config);
// Response (200 OK)
{
"id": "webhook_config_abc123",
"tenantId": "tenant_xyz789",
"url": "https://your-app.com/webhooks/whatsapp",
"events": [
"message.received.text",
"message.received.media",
"message.sent"
],
"retryCount": 3,
"timeoutMs": 10000,
"active": true,
"createdAt": "2025-10-01T10:00:00.000Z",
"updatedAt": "2025-10-01T10:00:00.000Z"
}
// Response (404 Not Found) - No webhook configured
{
"statusCode": 404,
"error": "Not Found",
"message": "No webhook configuration found for this tenant"
}
DELETE /api/v1/webhooks/config
Remove webhook configuration (soft delete - sets active = false).
// Request
const response = await fetch('https://apiwts.top/api/v1/webhooks/config', {
method: 'DELETE',
headers: {
'X-API-Key': 'your-api-key'
}
});
// Response (200 OK)
{
"message": "Webhook configuration deleted successfully"
}
// Response (404 Not Found)
{
"statusCode": 404,
"error": "Not Found",
"message": "No webhook configuration found for this tenant"
}
GET /api/v1/webhooks/events
List the webhook event types returned by this endpoint. The response below is exactly what the endpoint returns (the source of truth).
GET /webhooks/events returns the 18 documented event types below. The
POST /webhooks/configure validator additionally accepts
message.edited, message.reaction,
message.revoked and poll.vote_update โ you may subscribe to
those even though they are not listed here. A few events are emitted but are
not selectable via the events allowlist (e.g.
message.self_sent is opt-in per session, and lid_pn_linked is
delivered when LIDโPN enrichment is active). When in doubt, subscribe broadly and filter
on event in your handler.
// Request
const response = await fetch('https://apiwts.top/api/v1/webhooks/events', {
headers: { 'X-API-Key': 'your-api-key' }
});
// Response 200 (18 events)
{
"events": [
// Message Events (8)
{ "type": "message.received.text", "description": "Text message received from contact", "category": "messages" },
{ "type": "message.received.media", "description": "Media message (image/video/audio/document) received", "category": "messages" },
{ "type": "message.received.location", "description": "Location message received", "category": "messages" },
{ "type": "message.received.contact", "description": "Contact card received", "category": "messages" },
{ "type": "message.sent", "description": "Message successfully sent via WhatsApp", "category": "messages" },
{ "type": "message.delivered", "description": "Message delivered to recipient", "category": "messages" },
{ "type": "message.read", "description": "Message read by recipient (blue ticks)", "category": "messages" },
{ "type": "message.failed", "description": "Message failed to send", "category": "messages" },
// Session Events (4)
{ "type": "session.ready", "description": "WhatsApp session is ready to send/receive messages", "category": "session" },
{ "type": "session.disconnected", "description": "WhatsApp session disconnected", "category": "session" },
{ "type": "session.qr_updated", "description": "New QR code generated for authentication", "category": "session" },
{ "type": "session.auth_failure", "description": "Authentication failed or expired", "category": "session" },
// Group Events (3)
{ "type": "group.participant_added", "description": "Participant added to group", "category": "groups" },
{ "type": "group.participant_removed", "description": "Participant removed from group", "category": "groups" },
{ "type": "group.info_updated", "description": "Group name, description, or picture updated", "category": "groups" },
// Contact Events (1)
{ "type": "contact.status_updated", "description": "Contact status/about message updated", "category": "contacts" },
// Call Events (2)
{ "type": "call.received", "description": "Incoming voice/video call", "category": "calls" },
{ "type": "call.ended", "description": "Call ended", "category": "calls" }
]
}
POST /api/v1/webhooks/test
Send a test webhook event to verify your endpoint configuration.
// Request
const response = await fetch('https://apiwts.top/api/v1/webhooks/test', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://your-app.com/webhooks/whatsapp',
secret: 'your-webhook-secret' // Optional
})
});
// Response 200
{
"success": true,
"message": "Test webhook delivered successfully",
"testEventId": "test_1642598523000_abc123",
"deliveryStatus": "delivered"
}
secret, the test endpoint sends the signature as
X-Webhook-Signature: sha256=<hex> (hex, with a
sha256= prefix).
Production / real deliveries use X-Signature: <base64>
(base64, no prefix โ see Webhook Security). If your verifier
only handles one form, a passing test does not guarantee production verification will
pass, and vice-versa. Validate against the X-Signature base64 form for
production traffic.
GET /api/v1/webhooks/analytics/trends
โจ NEW in v2.12
Get temporal trends (daily success/failure counts) and latency percentiles for webhook deliveries.
// Request with custom period
const response = await fetch('https://apiwts.top/api/v1/webhooks/analytics/trends?days=14', {
headers: { 'X-API-Key': 'your-api-key' }
});
// Response 200
{
"period": {
"start": "2025-01-01",
"end": "2025-01-15",
"days": 14
},
"trends": [
{
"date": "2025-01-15",
"success": 145,
"failed": 5,
"avgLatencyMs": 250.5
},
{
"date": "2025-01-14",
"success": 132,
"failed": 2,
"avgLatencyMs": 180.3
}
],
"latency": {
"p50": 180.5,
"p95": 450.2,
"p99": 890.7,
"samples": 2500
}
}
| Query Parameter | Type | Default | Description |
|---|---|---|---|
days |
number | 7 | Number of days to analyze (min: 1, max: 30) |
๐ Webhook Analytics Dashboard
โจ NEW in v2.10
Get comprehensive analytics and metrics about your webhook delivery performance, including success rates, latency, top failing events, and circuit breaker status.
GET /api/v1/webhooks/stats
Retrieve complete webhook delivery statistics and analytics.
Request Example
// JavaScript/Node.js
const response = await fetch('https://apiwts.top/api/v1/webhooks/stats', {
method: 'GET',
headers: {
'X-API-Key': 'your-api-key'
}
});
const stats = await response.json();
console.log('Webhook Analytics:', stats);
// cURL
curl -X GET https://apiwts.top/api/v1/webhooks/stats \
-H "X-API-Key: your-api-key"
Response Schema
{
"totalDeliveries": number, // Total webhook delivery attempts
"successRate": number, // Success percentage (0-100, 2 decimals)
"avgLatencyMs": number | null, // Average delivery latency in milliseconds
"failed": {
"total": number, // Total failed deliveries
"last24h": number // Failed deliveries in last 24 hours
},
"circuitBreakers": number, // Number of circuit breaker activations
"topFailingEvents": [ // Top 5 events with most failures
{
"event": string, // Event type
"failures": number, // Number of failures
"rate": number // Percentage of total failures (2 decimals)
}
]
}
Success Response (200 OK) - With Deliveries
{
"totalDeliveries": 15234,
"successRate": 97.52,
"avgLatencyMs": 245,
"failed": {
"total": 381,
"last24h": 12
},
"circuitBreakers": 3,
"topFailingEvents": [
{
"event": "message.received.media",
"failures": 145,
"rate": 38.06
},
{
"event": "session.qr_updated",
"failures": 89,
"rate": 23.36
},
{
"event": "message.sent",
"failures": 67,
"rate": 17.59
},
{
"event": "session.ready",
"failures": 45,
"rate": 11.81
},
{
"event": "message.delivered",
"failures": 35,
"rate": 9.19
}
]
}
Success Response (200 OK) - No Deliveries Yet
{
"totalDeliveries": 0,
"successRate": 0,
"avgLatencyMs": null,
"failed": {
"total": 0,
"last24h": 0
},
"circuitBreakers": 0,
"topFailingEvents": []
}
Error Response (404 Not Found)
{
"statusCode": 404,
"error": "Not Found",
"message": "No webhook configuration found for this tenant"
}
Metrics Explained
- totalDeliveries: COUNT(*) from webhook_deliveries table
- successRate: Calculated as (successCount / totalDeliveries) * 100, rounded to 2 decimals
- avgLatencyMs: Average time between created_at and delivered_at in milliseconds (only for completed deliveries)
- failed.total: Total count of deliveries with status = 'failed'
- failed.last24h: Failed deliveries created in the last 24 hours
- circuitBreakers: Number of webhooks disabled due to exceeding 10 consecutive failures
- topFailingEvents: Top 5 event types ordered by failure count, with percentage of total failures
Use Cases
- ๐ Monitor webhook health: Track success rate and latency trends
- ๐ Identify problematic events: Find which events are failing most frequently
- โ ๏ธ Circuit breaker alerts: Get notified when webhooks are auto-disabled
- ๐ Performance optimization: Use latency metrics to optimize webhook endpoints
- ๐จ Incident response: Check failed.last24h to detect recent delivery issues
Circuit Breaker Feature
The system automatically disables webhooks (active = false) when:
- ๐จ Threshold: 10 consecutive failures detected
- โฑ๏ธ Check Frequency: Checked automatically every minute
- ๐ Manual Reset: Re-configure webhook to re-enable (contact administrator if needed)
- ๐ Tracking: circuitBreakers metric shows total disabled webhooks
Example: Monitoring Dashboard
// Fetch webhook stats every 30 seconds for dashboard
setInterval(async () => {
const response = await fetch('https://apiwts.top/api/v1/webhooks/stats', {
headers: { 'X-API-Key': process.env.API_KEY }
});
const stats = await response.json();
// Alert if success rate drops below 95%
if (stats.successRate < 95) {
console.error('โ ๏ธ Webhook success rate LOW:', stats.successRate + '%');
console.error('Top failing events:', stats.topFailingEvents);
}
// Alert if circuit breaker activated
if (stats.circuitBreakers > 0) {
console.error('๐จ Circuit breaker activated! Webhooks disabled:', stats.circuitBreakers);
}
// Monitor latency
if (stats.avgLatencyMs > 1000) {
console.warn('โฑ๏ธ High webhook latency:', stats.avgLatencyMs + 'ms');
}
}, 30000);
Future Enhancements (Roadmap)
- ๐ Temporal Graphs: 7-day success/failure trends with DATE_TRUNC
- ๐ Latency Percentiles: p50, p95, p99 using PERCENTILE_CONT
- ๐ Audit Log Integration: Full circuit breaker activation history
- ๐ Real-time Alerts: WebSocket notifications for critical events
POST /api/v1/webhooks/reset-circuit-breaker
โจ NEW in v2.11
Manually reactivate a webhook that was disabled by circuit breaker after 10 consecutive failures. Resets the failed_deliveries counter and sets active = true without changing webhook configuration.
Request Example
// JavaScript/Node.js
const response = await fetch('https://apiwts.top/api/v1/webhooks/reset-circuit-breaker', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key'
}
});
const result = await response.json();
console.log(result);
// cURL
curl -X POST https://apiwts.top/api/v1/webhooks/reset-circuit-breaker \
-H "X-API-Key: your-api-key"
Success Response (200 OK)
{
"success": true,
"message": "Circuit breaker reset successfully. Webhook is now active.",
"webhookConfigId": "webhook_config_abc123"
}
Error Response (404 Not Found)
{
"statusCode": 404,
"error": "Not Found",
"message": "No webhook configuration found for this tenant"
}
Error Response (409 Conflict)
{
"statusCode": 409,
"error": "Conflict",
"message": "Webhook is already active. Circuit breaker reset is only available for disabled webhooks."
}
When to Use
- ๐ง After fixing webhook endpoint: Your server was down and circuit breaker triggered
- ๐ Debugging webhook issues: Iteratively test fixes without full reconfiguration
- โก Quick recovery: Preserves URL, events, secret - only resets counter
- ๐ Alternative to reconfigure: No need to provide all settings again
Comparison: Reset vs Reconfigure
| Feature | Reset Circuit Breaker (v2.11) | Reconfigure Webhook (v2.7) |
|---|---|---|
| Preserves URL | โ Yes | โ Must provide again |
| Preserves Events | โ Yes | โ Must provide again |
| Preserves Secret | โ Yes | โ Auto-regenerated |
| Resets Counter | โ Yes (failed_deliveries = 0) | โ Yes (new config) |
| Use Case | Quick recovery after fix | Change webhook settings |
๐ Session Backfill WWebJS Only v3.73.0
Recover messages received while a WWebJS session was disconnected. Used by clients to detect and recover "dead windows" between disconnect and reconnect.
Endpoint
GET /api/v1/sessions/:sessionId/messages/since?from={epochMs}&to={epochMs}&includeGroups=false&chatId=...
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from |
integer (epoch ms) | โ Yes | Window start timestamp in epoch milliseconds |
to |
integer (epoch ms) | โ No | Window end timestamp. Defaults to Date.now() |
includeGroups |
boolean | โ No |
Include group chats in backfill. Default false (1:1 chats only)
|
chatId |
string | โ No | Filter to a single chat (e.g., 5551234@c.us) |
Constraints
- Max window: 24 hours (86,400,000 ms). Returns 400 if exceeded.
-
Hard cap per chat: 500 messages. If a chat has more than 500 messages
in the window, response includes
truncated: true. -
Hard cap total: 500 messages across all chats. If exceeded, response
is truncated to the 500 oldest in-window and
truncated: true. - Rate limit: 10 requests per minute per session.
- Driver: WWebJS only. Cloud API and Instagram return 400.
-
Session state: Must be
READY. Disconnected/authenticated sessions return 400.
Example Request
curl -H "Authorization: Bearer sk_live_..." \
"https://apiwts.top/api/v1/sessions/my-session/messages/since?from=$(( ( $(date +%s) - 3600 ) * 1000 ))"
Example Response (200)
{
"sessionId": "my-session",
"windowStart": 1776170000000,
"windowEnd": 1776173600000,
"messages": [
{
"driverMessageId": "false_5551@c.us_3EB0...",
"serializedId": "false_5551@c.us_3EB0...",
"timestamp": "2026-04-14T13:05:12.000Z",
"from": "5551234@c.us",
"to": "5552222@c.us",
"fromMe": false,
"isGroup": false,
"type": "chat",
"body": "Message received during disconnect",
"hasMedia": false,
"hasQuotedMsg": false,
"replayed": true,
"mediaRef": null
}
],
"complete": true,
"truncated": false,
"chatsScanned": 8,
"messagesReturned": 1
}
Response Fields
| Field | Meaning |
|---|---|
complete |
true if all messages in the window were returned.
false if any chat was truncated (consider re-calling with narrower
window).
|
truncated |
true if per-chat cap (500 msgs/chat) or total cap (500 msgs) was
reached.
|
chatsScanned |
Number of chats that had messages in the window. |
messagesReturned |
Total messages in response (after sorting + cap). |
replayed: true |
Always true โ indicates the message comes from backfill, NOT live
stream. Use to distinguish from real-time webhook deliveries.
|
mediaRef.downloadMayFail |
Always true for backfill responses. The WhatsApp CDN may have expired
for old messages. Client should retry download immediately via
GET /messages/:id/media.
|
Recommended Workflow
-
Client detects session reconnect (webhook
session.readyaftersession.disconnected). -
Client records
lastEventTimestampfrom last successfully processed message. - Client calls
GET /messages/since?from=lastEventTimestampONE TIME. - Client deduplicates by
driverMessageIdagainst its own store. - If
complete: false, client may re-call with a narrower window.
Error Responses
| Status | Meaning |
|---|---|
| 400 |
Invalid from/to, window too large, session not READY, or
driver != WWebJS
|
| 404 | Session not found (also returned for cross-tenant access โ does not leak existence) |
| 429 | Rate limit exceeded (10 requests/minute/session) |
| 500 | Internal error (circuit breaker open, Puppeteer timeout, etc.) |
| 503 | Feature flag SESSION_BACKFILL_ENABLED is off (kill switch) |
mediaRef field contains only references. Clients should call
GET /messages/:id/media immediately after receiving the backfill response โ
the CDN may have already expired for messages older than ~14 days.
๐ก๏ธ Message Delivery Guarantees v3.79.1
This section documents the honest service-level expectations for inbound message delivery so you can choose the driver that matches your business risk profile.
Per-Driver Guarantees
| Driver | Inbound Guarantee | Loss Window | Recovery Mechanism |
|---|---|---|---|
| Cloud API | Zero-loss | None โ Meta retries until 200 | Meta-side persistent queue; webhook delivered until acknowledged |
| WWebJS | At-least-once with deploy-window risk | ~5โ30s during Blue-Green deploys (v3.79.1 reduction); โค 60min historical messages recoverable |
v3.79.0 Inbox Pattern (durability post-ingestion) + v3.79.1 auto-backfill on
session.ready (recovers messages from the offline window) + v3.79.1
nightly invariant reconciler (canary alert)
|
| Zero-loss | None โ Meta retries until 200 (same model as Cloud API) | Meta Graph webhooks with persistent retry |
What v3.79.1 Adds for WWebJS Tenants
-
Auto-backfill on reconnect: when a session graceful-shuts (e.g.
during deploy) and later reaches
READYagain, the system automatically calls the same Session Backfill machinery internally and reinjects every missed message through the Inbox Pattern. No client action required. Window: 60 minutes. -
Invariant metric: a nightly cron compares messages present in
WhatsApp against rows in our DB and writes any discrepancy to
undetected_loss_log. A single row in this table fires PagerDuty CRITICAL โ a contractual canary that proves the durability stack is working. -
Honest metric:
whatsapp_backfill_window_exceeded_totalincrements when a session was offline for more than 60min on reconnect. Recovery is impossible past that window (outside WhatsApp retention) and we surface the loss rather than silently swallow it.
Recommendations
WWebJS is fine when: customer-service flows where occasional reconnect-window loss is recoverable via UX (e.g. "we missed your last message, please resend") and the tenant cannot or does not want to migrate to Cloud API.
Observability
-
whatsapp_backfill_recovered_messages_total{reason}โ KPI counting how many messages auto-backfill saved that would otherwise have been lost. -
whatsapp_backfill_window_exceeded_totalโ alert when sessions exceed the 60-minute recovery window. -
whatsapp_undetected_loss_violation_total{driver}โ the canary. > 0 in 1h triggers CRITICAL PagerDuty.
๐ฅ Contact Management WWebJS Only
Manage WhatsApp contacts: list, retrieve, block, unblock, and get profile pictures.
GET /api/v1/contacts v3.42.0+
List all contacts from your WhatsApp session with pagination and filters.
WWEBJS_GET_CONTACTS_ENABLED). When disabled, this endpoint returns
403 with { "code": "FEATURE_DISABLED" }. Contact the
operator to enable it for your tenant.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| sessionId | string | No | Session ID (defaults to tenant's first session) |
| page | number | No |
Page number, 1-based (default: 1). Use with limit for pagination.
(NEW v3.42.0)
|
| limit | number | No | Maximum contacts per page (default: 500, max: 1000) (v3.42.0: increased from 500) |
| isBlocked | boolean | No |
Filter by blocked status: true = only blocked, false =
only unblocked (v3.8.26+)
|
| isMyContact | boolean | No |
Filter by saved contacts: true = only saved in phone,
false = not saved (v3.8.26+)
|
Example Request (with pagination)
// Get page 2 of saved contacts (50 per page)
const response = await fetch('https://apiwts.top/api/v1/contacts?page=2&limit=50&isMyContact=true&isBlocked=false', {
method: 'GET',
headers: {
'X-API-Key': 'your-api-key'
}
});
Example Response (200 OK)
{
"success": true,
"contacts": [
{
"id": "5511999999999@c.us",
"name": "Joรฃo Silva",
"number": "5511999999999",
"pushname": "Joรฃo",
"isBlocked": false,
"isMyContact": true,
"profilePicUrl": "https://pps.whatsapp.net/v/..."
},
{
"id": "5511888888888@c.us",
"name": "Maria Santos",
"number": "5511888888888",
"pushname": "Maria",
"isBlocked": false,
"isMyContact": true,
"profilePicUrl": null
}
],
"total": 10000,
"returned": 2,
"pagination": {
"page": 2,
"limit": 50,
"total": 10000,
"totalPages": 200,
"hasMore": true
},
"filters": {
"limit": 50,
"isBlocked": false,
"isMyContact": true
}
}
Example Response (403 Forbidden โ feature disabled, v3.101.0+)
{
"statusCode": 403,
"error": "Forbidden",
"message": "Bulk contact listing is disabled (WWEBJS_GET_CONTACTS_ENABLED). Contact the operator to enable.",
"code": "FEATURE_DISABLED"
}
Response Fields
| Field | Type | Description |
|---|---|---|
| contacts | array | List of contacts for the current page |
| contacts[].isMyContact | boolean | Whether contact is saved in phone's address book |
| total | number | Total contacts matching filters (real count) |
| returned | number | Number of contacts in this page |
| pagination | object | Pagination metadata (NEW v3.42.0) |
| pagination.page | number | Current page number |
| pagination.limit | number | Items per page |
| pagination.total | number | Total contacts matching filters |
| pagination.totalPages | number | Total number of pages |
| pagination.hasMore | boolean | Whether more pages exist after current page |
| filters | object | Applied filters summary |
To iterate through all contacts:
let page = 1;
let allContacts = [];
do {
const res = await fetch(`/api/v1/contacts?page=${page}&limit=500`, { headers });
const data = await res.json();
allContacts.push(...data.contacts);
page++;
} while (data.pagination.hasMore);
Errors
| Status | Error | Description |
|---|---|---|
| 404 | Not Found | No sessions found for this tenant |
| 400 | Bad Request | Session not READY or driver not WWebJS |
GET /api/v1/contacts/:phoneNumber
Get details of a specific contact by phone number.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| phoneNumber | string | Yes | Phone number with country code (e.g., 5511999999999, 41764791479, 14155552671) |
Example Request
const response = await fetch('https://apiwts.top/api/v1/contacts/5511999999999', {
method: 'GET',
headers: {
'X-API-Key': 'your-api-key'
}
});
Example Response (200 OK)
{
"success": true,
"contact": {
"id": "5511999999999@c.us",
"name": "Joรฃo Silva",
"number": "5511999999999",
"pushname": "Joรฃo",
"isBlocked": false,
"profilePicUrl": "https://pps.whatsapp.net/v/..."
}
}
Errors
| Status | Error | Description |
|---|---|---|
| 404 | Not Found | Contact not found in session |
| 400 | Bad Request | Invalid session state or driver |
POST /api/v1/contacts/:phoneNumber/block
Block a contact on WhatsApp. Useful for spam prevention.
Example Request
const response = await fetch('https://apiwts.top/api/v1/contacts/5511999999999/block', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key'
}
});
Example Response (200 OK)
{
"success": true,
"message": "Contact blocked successfully",
"phoneNumber": "5511999999999"
}
POST /api/v1/contacts/:phoneNumber/unblock
Unblock a previously blocked contact.
Example Request
const response = await fetch('https://apiwts.top/api/v1/contacts/5511999999999/unblock', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key'
}
});
Example Response (200 OK)
{
"success": true,
"message": "Contact unblocked successfully",
"phoneNumber": "5511999999999"
}
GET /api/v1/contacts/:phoneNumber/profile-pic
Get the profile picture URL of a contact. Returns null if contact has no
profile picture or privacy settings prevent access.
Example Request
const response = await fetch('https://apiwts.top/api/v1/contacts/5511999999999/profile-pic', {
method: 'GET',
headers: {
'X-API-Key': 'your-api-key'
}
});
Example Response (200 OK)
{
"success": true,
"profilePicUrl": "https://pps.whatsapp.net/v/t61.24694-24/...",
"phoneNumber": "5511999999999",
"reason": null
}
Example Response (No Profile Picture)
{
"success": true,
"profilePicUrl": null,
"phoneNumber": "5511999999999",
"reason": null
}
Example Response (@lid Identifier - v3.8.31+)
{
"success": true,
"profilePicUrl": null,
"phoneNumber": "170321256693940@lid",
"reason": "LID_NOT_SUPPORTED"
}
-
Privacy Settings: Contacts with private profile settings will
return
profilePicUrl: null - Contact Sync: The contact must have recent interaction with the session for reliable retrieval
- Rate Limiting: WhatsApp may rate-limit excessive profile picture requests
- Cache Delay: Newly added contacts may take a few seconds to sync before their profile picture is available
- What is @lid? Linked Identity - WhatsApp's privacy feature to hide phone numbers in groups
-
Profile Pictures: @lid contacts always return
profilePicUrl: null- this is by WhatsApp design, not a bug - Why? WhatsApp intentionally hides profile data for @lid identifiers to protect user privacy
- Workaround? None available - there is no method to convert @lid to phone number (by design)
-
API Response: When @lid is detected, response includes
"reason": "LID_NOT_SUPPORTED"to identify this case - References: GitHub Issue #3519, Issue #3834
Use Cases
| Use Case | Endpoint | Description |
|---|---|---|
| CRM Integration | GET /contacts | Sync WhatsApp contacts with external CRM system |
| Spam Prevention | POST /contacts/:phone/block | Automatically block known spam numbers |
| Profile Backup | GET /contacts/:phone/profile-pic | Download and store contact profile pictures |
| Contact Validation | GET /contacts/:phone | Check if number exists in contacts before messaging |
- WWebJS Only: Contact Management is not available with Cloud API driver (Meta API limitation)
- Session State: All endpoints require session in READY state
- Rate Limiting: WhatsApp may rate-limit excessive profile picture requests
- Privacy: Some contacts may have privacy settings preventing profile picture access
๐ฌ Chat Management WWebJS Only
Manage WhatsApp chats with archive, mute, pin, and retrieval operations.
- List all chats with filtering (archived, unread, groups)
- Get individual chat details by ID
- Archive/unarchive chats for organization
- Mute/unmute chats with optional duration
- Pin/unpin important conversations
GET /api/v1/chats
Retrieve all chats from the WhatsApp session with optional filtering.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| sessionId | string | No | Session ID (defaults to tenant's first session) |
| archived | boolean | No | Filter by archived status |
| unreadOnly | boolean | No | Show only chats with unread messages |
| groups | boolean | No | Filter by group chats (true) or individual (false) |
| limit | number | No | Maximum chats to return (1-500, default 100) |
Example Request
const response = await fetch('https://apiwts.top/api/v1/chats?unreadOnly=true&limit=50', {
method: 'GET',
headers: {
'X-API-Key': 'your-api-key'
}
});
Example Response
{
"success": true,
"chats": [
{
"id": "5511999999999@c.us",
"name": "John Doe",
"isGroup": false,
"unreadCount": 3,
"lastMessage": {
"body": "Hey, are you there?",
"timestamp": "2025-10-03T14:30:00.000Z",
"fromMe": false
},
"timestamp": 1727967000000,
"archived": false,
"muted": false,
"muteExpiration": null,
"pinned": true
}
],
"total": 1
}
GET /api/v1/chats/:chatId
Get detailed information about a specific chat.
Example Request
const response = await fetch('https://apiwts.top/api/v1/chats/5511999999999@c.us', {
method: 'GET',
headers: {
'X-API-Key': 'your-api-key'
}
});
Example Response
{
"success": true,
"chat": {
"id": "5511999999999@c.us",
"name": "John Doe",
"isGroup": false,
"unreadCount": 3,
"lastMessage": {
"body": "Hey, are you there?",
"timestamp": "2025-10-03T14:30:00.000Z",
"fromMe": false
},
"timestamp": 1727967000000,
"archived": false,
"muted": false,
"muteExpiration": null,
"pinned": true
}
}
POST /api/v1/chats/:chatId/archive
Archive a chat to hide it from the main chat list.
Example Request
const response = await fetch('https://apiwts.top/api/v1/chats/5511999999999@c.us/archive', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session-001'
})
});
Example Response
{
"success": true,
"message": "Chat archived successfully",
"chatId": "5511999999999@c.us"
}
POST /api/v1/chats/:chatId/unarchive
Unarchive a chat to restore it to the main chat list.
Example Request
const response = await fetch('https://apiwts.top/api/v1/chats/5511999999999@c.us/unarchive', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session-001'
})
});
Example Response
{
"success": true,
"message": "Chat unarchived successfully",
"chatId": "5511999999999@c.us"
}
POST /api/v1/chats/:chatId/mute
Mute chat notifications. Duration is optional (indefinite if not provided).
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| sessionId | string | No | Session ID |
| duration | number | No | Mute duration in milliseconds |
Example Request (Mute for 24 hours)
const response = await fetch('https://apiwts.top/api/v1/chats/5511999999999@c.us/mute', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session-001',
duration: 86400000 // 24 hours in milliseconds
})
});
Example Response (Temporary Mute)
{
"success": true,
"message": "Chat muted until 2025-10-04T14:30:00.000Z",
"chatId": "5511999999999@c.us"
}
Example Request (Indefinite Mute)
const response = await fetch('https://apiwts.top/api/v1/chats/5511999999999@c.us/mute', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session-001'
})
});
Example Response (Indefinite Mute)
{
"success": true,
"message": "Chat muted indefinitely",
"chatId": "5511999999999@c.us"
}
POST /api/v1/chats/:chatId/unmute
Unmute a chat to restore notifications.
Example Request
const response = await fetch('https://apiwts.top/api/v1/chats/5511999999999@c.us/unmute', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session-001'
})
});
Example Response
{
"success": true,
"message": "Chat unmuted successfully",
"chatId": "5511999999999@c.us"
}
POST /api/v1/chats/:chatId/pin
Pin a chat to keep it at the top of the chat list.
Example Request
const response = await fetch('https://apiwts.top/api/v1/chats/5511999999999@c.us/pin', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session-001'
})
});
Example Response
{
"success": true,
"message": "Chat pinned successfully",
"chatId": "5511999999999@c.us"
}
POST /api/v1/chats/:chatId/unpin
Unpin a chat to remove it from the pinned section.
Example Request
const response = await fetch('https://apiwts.top/api/v1/chats/5511999999999@c.us/unpin', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session-001'
})
});
Example Response
{
"success": true,
"message": "Chat unpinned successfully",
"chatId": "5511999999999@c.us"
}
POST /api/v1/chats/:chatId/state
v2.85+ Set chat presence state - typing/recording indicators (WWebJS only).
Important: Only supported on wwebjs driver. Cloud API
driver will return 422 error.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
sessionId |
string | Yes | Session identifier |
state |
string | Yes |
State to set: typing, recording, or stop
|
Example Request
// Simulate typing indicator
const response = await fetch('https://apiwts.top/api/v1/chats/5511999999999@c.us/state', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session-001',
state: 'typing'
})
});
// Response (200 OK - Success)
{
"success": true,
"message": "Chat state \"typing\" set successfully",
"data": {
"chatId": "5511999999999@c.us",
"state": "typing",
"appliedAt": "2025-11-23T08:00:00.000Z"
}
}
States
- typing: Shows "typing..." indicator to the recipient
- recording: Shows "recording..." indicator (for voice messages)
- stop: Clears any active presence state
Use Cases
- Human-like Bot Behavior: Simulate natural conversation by showing typing indicator before sending response
- Voice Message Indication: Show recording state before sending voice message
- Engagement: Increase user engagement by making automated responses feel more natural
Important Notes
- State is temporary and cleared automatically after ~20-30 seconds
- Recommended to send actual message within 5-10 seconds of setting state
- Only supported on WWebJS driver (Cloud API does not support presence simulation)
Use Cases
| Use Case | Endpoint | Description |
|---|---|---|
| Dashboard Inbox | GET /chats?unreadOnly=true | Display only chats with pending messages |
| Group Management | GET /chats?groups=true | List all group conversations |
| Notification Control | POST /chats/:id/mute | Silence noisy chats during business hours |
| Priority Chats | POST /chats/:id/pin | Keep important customers at the top |
| Archive Management | POST /chats/:id/archive | Hide completed support tickets |
- WWebJS Only: Chat Management is not available with Cloud API driver (Meta API limitation)
- Session State: All endpoints require session in READY state
- Chat ID Format: Use WhatsApp format (e.g., 5511999999999@c.us for individuals, xxxxx@g.us for groups)
- Client-Side Filtering: Filters are applied after fetching all chats (performance consideration for large chat lists)
- Mute Duration: Omit duration parameter for indefinite mute
๐ฅ Group Management
โจ NEW in v2.89
Manage WhatsApp groups with create, update, participant management, and administrative operations. Both WWebJS and Cloud API drivers supported.
- Create Groups: Both drivers (Cloud API limit: 8 participants, WWebJS: unlimited)
- Get Group Info: Retrieve group details, participants, and admins
- Add/Remove Participants: Manage group membership (batch operations supported)
- Update Group: Change group name and description
- Leave Group: Exit from a group programmatically
- Promote/Demote Admins: โ ๏ธ WWebJS driver only
- Invite Code Management: โ ๏ธ WWebJS driver only
- Group Settings: โ ๏ธ WWebJS driver only
- Cloud API - Participant Limit: Maximum 8 participants during group creation (WhatsApp Business API restriction)
- Cloud API - Admin Operations Not Supported: promote/demote, invite code retrieval/revoke, group settings
- WWebJS - No Limitations: Supports all group operations including admin management
- Error Handling: Unsupported operations return HTTP 400 with DriverNotSupportedError
POST /api/v1/groups
Create a new WhatsApp group with participants. Requires at least 1 participant. Returns group ID and participant count.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Yes | Session ID to use for group creation |
| name | string | Yes | Group name (max 25 characters - WhatsApp limit) |
| description | string | No | Group description (max 512 characters - WhatsApp limit) |
| participants | string[] | Yes | Array of participant phone numbers with country code (Cloud API max 8, WWebJS unlimited) |
Example Request
curl -X POST https://apiwts.top/api/v1/groups \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "mysession",
"name": "Team Project Alpha",
"description": "Project discussion and updates",
"participants": [
"5511999999999",
"5511988888888",
"5511977777777"
]
}'
Example Response
{
"success": true,
"groupId": "123456789012345678@g.us",
"inviteCode": "AbCdEfGh1234567", // WWebJS only
"participantsAdded": 3
}
GET /api/v1/groups
WWebJS Only List all groups for a session. This is a convenience wrapper for GET /api/v1/chats with groups filter. Returns a simplified list of groups with basic information.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Yes | Session ID to use |
| limit | number | No | Maximum number of groups to return (default: 100, max: 500) |
Example Request
curl -X GET "https://apiwts.top/api/v1/groups?sessionId=mysession&limit=50" \
-H "Authorization: Bearer YOUR_API_KEY"
Example Response
{
"groups": [
{
"id": "123456789012345678@g.us",
"name": "Team Project Alpha",
"participantCount": 8,
"isGroup": true
},
{
"id": "987654321098765432@g.us",
"name": "Customer Support",
"participantCount": 15,
"isGroup": true
}
],
"total": 2
}
Error Responses v2.92+
| Status | Error | Description |
|---|---|---|
| 400 | Bad Request | Session is not in READY state, or using Cloud API driver (not supported) |
| 401 | Unauthorized | Missing or invalid API key |
| 404 | Not Found | Session not found or driver not initialized |
| 503 | Service Unavailable | WhatsApp client not ready (try again later) |
Error Response Example
// 404 - Session not found
{
"statusCode": 404,
"error": "Not Found",
"message": "Session mysession not found"
}
// 400 - Session not ready
{
"statusCode": 400,
"error": "Bad Request",
"message": "Session mysession is not ready (current state: DISCONNECTED)"
}
// 503 - Client not initialized
{
"statusCode": 503,
"error": "Service Unavailable",
"message": "WhatsApp client is not ready. Please try again later."
}
GET /api/v1/groups/:groupId
Retrieve detailed information about a specific WhatsApp group including name, description, participants list, and admins.
id,
isAdmin, and isSuperAdmin. The phoneNumber
(+E.164) field is omitted unless the operator enables
WWEBJS_GROUP_MEMBER_PHONES_ENABLED.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| groupId | string | Group ID (format: 123456789-1234567890@g.us) |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Yes | Session ID to use |
Example Request
curl -X GET "https://apiwts.top/api/v1/groups/123456789012345678@g.us?sessionId=mysession" \
-H "Authorization: Bearer YOUR_API_KEY"
Example Response
{
"id": "123456789012345678@g.us",
"name": "Team Project Alpha",
"description": "Project discussion and updates",
"participants": [
{
"id": "5511999999999@c.us",
"isAdmin": true,
"isSuperAdmin": true
},
{
"id": "5511988888888@c.us",
"isAdmin": false,
"isSuperAdmin": false
},
{
"id": "5511977777777@c.us",
"isAdmin": true,
"isSuperAdmin": false
}
],
"admins": [
"5511999999999@c.us",
"5511977777777@c.us"
],
"createdAt": "2025-01-15T10:30:00Z"
}
POST /api/v1/groups/:groupId/participants
Add one or more participants to a WhatsApp group. Returns results array showing success/failure for each participant.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| groupId | string | Group ID (format: 123456789-1234567890@g.us) |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Yes | Session ID to use |
| participants | string[] | Yes | Array of participant phone numbers to add |
Example Request
curl -X POST https://apiwts.top/api/v1/groups/123456789012345678@g.us/participants \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "mysession",
"participants": [
"5511966666666",
"5511955555555"
]
}'
Example Response
{
"success": true,
"results": [
{
"participant": "5511966666666@c.us",
"success": true
},
{
"participant": "5511955555555@c.us",
"success": false,
"error": "Participant has privacy settings restricting group invites"
}
]
}
DELETE /api/v1/groups/:groupId/participants
Remove one or more participants from a WhatsApp group. Requires admin permissions. Returns results array showing success/failure for each participant.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| groupId | string | Group ID (format: 123456789-1234567890@g.us) |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Yes | Session ID to use |
| participants | string[] | Yes | Array of participant phone numbers to remove |
Example Request
curl -X DELETE https://apiwts.top/api/v1/groups/123456789012345678@g.us/participants \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "mysession",
"participants": [
"5511966666666"
]
}'
Example Response
{
"success": true,
"results": [
{
"participant": "5511966666666@c.us",
"success": true
}
]
}
PATCH /api/v1/groups/:groupId
Update group name and/or description. Requires admin permissions. At least one field (name or description) must be provided.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| groupId | string | Group ID (format: 123456789-1234567890@g.us) |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Yes | Session ID to use |
| name | string | No | New group name (max 25 characters) |
| description | string | No | New group description (max 512 characters) |
Example Request
curl -X PATCH https://apiwts.top/api/v1/groups/123456789012345678@g.us \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "mysession",
"name": "Team Alpha - Q1 2025",
"description": "Updated project scope for Q1"
}'
Example Response
{
"success": true
}
POST /api/v1/groups/:groupId/leave
Leave a WhatsApp group. This action removes the session user from the group and cannot be undone programmatically.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| groupId | string | Group ID to leave (format: 123456789-1234567890@g.us) |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Yes | Session ID to use |
Example Request
curl -X POST https://apiwts.top/api/v1/groups/123456789012345678@g.us/leave \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "mysession"
}'
Example Response
{
"success": true
}
POST /api/v1/groups/:groupId/participants/promote
โ ๏ธ WWebJS Only Promote one or more participants to group admin. Requires admin permissions. Returns results array showing success/failure for each participant.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| groupId | string | Group ID (format: 123456789-1234567890@g.us) |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Yes | Session ID to use (WWebJS driver required) |
| participants | string[] | Yes | Array of participant phone numbers to promote |
Example Request
curl -X POST https://apiwts.top/api/v1/groups/123456789012345678@g.us/participants/promote \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "mysession",
"participants": [
"5511966666666"
]
}'
Example Response (WWebJS)
{
"success": true,
"results": [
{
"participant": "5511966666666@c.us",
"success": true
}
]
}
Example Response (Cloud API - Not Supported)
{
"statusCode": 400,
"error": "Driver Not Supported",
"message": "Feature \"promoteParticipants\" is not supported by Cloud API driver",
"code": "DRIVER_NOT_SUPPORTED",
"feature": "promoteParticipants",
"driver": "Cloud API"
}
POST /api/v1/groups/:groupId/participants/demote
โ ๏ธ WWebJS Only Demote one or more group admins to regular participants. Requires admin permissions. Returns results array showing success/failure for each participant.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| groupId | string | Group ID (format: 123456789-1234567890@g.us) |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Yes | Session ID to use (WWebJS driver required) |
| participants | string[] | Yes | Array of participant phone numbers to demote |
Example Request
curl -X POST https://apiwts.top/api/v1/groups/123456789012345678@g.us/participants/demote \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "mysession",
"participants": [
"5511977777777"
]
}'
Example Response
{
"success": true,
"results": [
{
"participant": "5511977777777@c.us",
"success": true
}
]
}
GET /api/v1/groups/:groupId/invite-code
โ ๏ธ WWebJS Only Retrieve the group's invite code (invitation link). Returns a 16-character code that can be used to join the group.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| groupId | string | Group ID (format: 123456789-1234567890@g.us) |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Yes | Session ID to use (WWebJS driver required) |
Example Request
curl -X GET "https://apiwts.top/api/v1/groups/123456789012345678@g.us/invite-code?sessionId=mysession" \
-H "Authorization: Bearer YOUR_API_KEY"
Example Response
{
"inviteCode": "AbCdEfGh1234567",
"inviteLink": "https://chat.whatsapp.com/AbCdEfGh1234567"
}
POST /api/v1/groups/:groupId/invite-code/revoke
โ ๏ธ WWebJS Only Revoke the current invite code and generate a new one. Previous invite links will no longer work. Returns the new invite code.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| groupId | string | Group ID (format: 123456789-1234567890@g.us) |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Yes | Session ID to use (WWebJS driver required) |
Example Request
curl -X POST https://apiwts.top/api/v1/groups/123456789012345678@g.us/invite-code/revoke \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "mysession"
}'
Example Response
{
"success": true,
"newInviteCode": "XyZ9876543210Ab"
}
PATCH /api/v1/groups/:groupId/settings
โ ๏ธ WWebJS Only Update group settings for participant restrictions (who can send messages, edit group info).
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| groupId | string | Group ID (format: 123456789-1234567890@g.us) |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Yes | Session ID to use (WWebJS driver required) |
| messageSend | boolean | No | If false, only admins can send messages |
| groupInfoEdit | boolean | No | If false, only admins can edit group info |
Example Request
curl -X PATCH https://apiwts.top/api/v1/groups/123456789012345678@g.us/settings \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "mysession",
"messageSend": false,
"groupInfoEdit": false
}'
Example Response
{
"success": true
}
- Group ID Format: Use WhatsApp format (e.g., 123456789-1234567890@g.us)
- Admin Permissions Required: Most operations (add/remove/promote/demote/settings) require the session user to be a group admin
- Cloud API Participant Limit: Maximum 8 participants during group creation (WhatsApp Business API restriction)
- WWebJS-Only Operations: promote/demote, invite code management, and group settings return HTTP 400 with DriverNotSupportedError on Cloud API
- Batch Operations: add/remove/promote/demote support multiple participants in a single request
- Character Limits: Group name (25 chars), description (512 chars) enforced by WhatsApp
- Leave Action Permanent: Leaving a group cannot be undone via API (requires manual re-invite)
๐ธ Instagram Messaging v3.10.0
Send and receive Instagram Direct Messages via Meta's Graph API. Available as a separate driver for Instagram Business accounts.
- Driver: Uses Meta Graph API (separate from WhatsApp Cloud API)
- Requirements: Instagram Business Account + Facebook Page + Meta App
- Authentication: OAuth 2.0 with long-lived tokens (60 days, auto-refresh)
- Webhooks: Receive DMs via webhook subscriptions
- 24-Hour Window: Must respond within 24 hours of user's last message
- Instagram Business Account: Personal accounts are NOT supported
- Connected Facebook Page: Page must be linked to Instagram account
- Meta App: Create app at developers.facebook.com
-
Permissions:
instagram_basic,instagram_manage_messages,pages_manage_metadata,pages_messaging - App Review: Required for production use (Meta approval process)
Environment Configuration
Set these environment variables in your deployment:
# Meta App Configuration (Global - shared across all tenants)
META_APP_ID=your_meta_app_id
META_APP_SECRET=your_meta_app_secret
GRAPH_API_VERSION=v24.0
INSTAGRAM_WEBHOOK_VERIFY_TOKEN=your_secure_random_string
OAuth Flow (Admin Only)
Connect an Instagram Business account to your tenant:
Step 1: Start OAuth
GET /admin/api/instagram/oauth/start
// Response:
{
"authUrl": "https://www.facebook.com/v24.0/dialog/oauth?..."
}
Redirect admin user to authUrl to grant permissions.
Step 2: OAuth Callback
GET /admin/api/instagram/oauth/callback?code=xxx&state=yyy
// Response (on success):
{
"success": true,
"igUserId": "17841401234567890",
"igUsername": "mybusiness",
"pageId": "123456789012345",
"pageName": "My Business Page"
}
Public OAuth API v3.15.5+
Use
GET /api/v1/instagram/oauth/start to initiate OAuth with your API Key.
No admin session required - perfect for programmatic integrations.
GET /api/v1/instagram/oauth/start
Generate Instagram OAuth authorization URL using API Key authentication.
GET /api/v1/instagram/oauth/start?sessionId=my-instagram-session
X-API-Key: sk_live_...
// Response 200:
{
"success": true,
"authorizationUrl": "https://www.instagram.com/oauth/authorize?client_id=...&redirect_uri=...&scope=...&state=...",
"state": "abc123def456...",
"expiresIn": 600
}
// Error 400 (session belongs to different tenant):
{
"statusCode": 400,
"error": "Bad Request",
"message": "Session ID already exists for a different tenant.",
"timestamp": "2026-01-18T..."
}
// Error 500 (OAuth not configured):
{
"statusCode": 500,
"error": "Configuration Error",
"message": "INSTAGRAM_APP_ID not configured. Contact administrator.",
"timestamp": "2026-01-18T..."
}
Key Differences from Admin Endpoint:
- Uses
X-API-Keyheader instead of admin session cookie tenantIdis automatically extracted from your API Key- Perfect for server-to-server integrations
- Callback uses same endpoint as admin OAuth
Complete Example (API-Based)
async function connectInstagramViaAPI(sessionId, apiKey) {
// 1. Get OAuth URL via API
const oauthRes = await fetch(
`https://apiwts.top/api/v1/instagram/oauth/start?sessionId=${sessionId}`,
{ headers: { 'X-API-Key': apiKey } }
);
const { authorizationUrl, state, expiresIn } = await oauthRes.json();
// 2. Open OAuth in popup (or redirect)
const popup = window.open(authorizationUrl, 'instagram-oauth', 'width=600,height=700');
// 3. Poll status or use webhook to detect completion
// (same as External OAuth Integration below)
}
External OAuth Integration v3.15.1+
apiwts.top for security reasons
(prevents open redirect vulnerabilities). Use webhooks or polling to detect when
authentication completes.
/admin/api/instagram/oauth/start), you need
your tenantId. Option A (Recommended): Use
GET /api/v1/tenant/me to get
your tenant info.
Option B: Use
POST /api/v1/sessions response which
includes "tenantId". Or use the new API endpoint (v3.15.5+):
GET /api/v1/instagram/oauth/start - tenantId is extracted automatically
from your API Key!
Option 1: Webhook Notification (Recommended)
Configure a webhook URL on your session to receive session.ready event when
OAuth completes:
// Step 1: Create session with webhook (or update existing)
PATCH /api/v1/sessions/my-instagram-session
Content-Type: application/json
X-API-Key: your_api_key
{
"webhookUrl": "https://your-domain.com/api/instagram/callback",
"webhookAuthToken": "your-secret-token"
}
// Step 2: Open OAuth in popup/new tab
window.open(
'https://apiwts.top/admin/api/instagram/oauth/start?tenantId=YOUR_TENANT&sessionId=my-instagram-session',
'instagram-oauth',
'width=600,height=700'
);
// Step 3: Receive webhook when connected
POST https://your-domain.com/api/instagram/callback
Authorization: Bearer your-secret-token
Content-Type: application/json
{
"event": "session.ready",
"sessionId": "my-instagram-session",
"session": {
"id": "my-instagram-session"
},
"timestamp": "2026-01-17T10:30:00.000Z"
}
Option 2: Status Polling
Poll the session status endpoint until state becomes READY:
// Poll every 3 seconds
GET /api/v1/sessions/my-instagram-session/status
X-API-Key: your_api_key
// Response when pending:
{ "sessionId": "my-instagram-session", "state": "INITIALIZING" }
// Response when connected:
{ "sessionId": "my-instagram-session", "state": "READY" }
Complete Frontend Example
async function connectInstagram(tenantId, sessionId, apiKey) {
// 1. Configure webhook (optional but recommended)
await fetch(`https://apiwts.top/api/v1/sessions/${sessionId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey
},
body: JSON.stringify({
webhookUrl: 'https://your-domain.com/callback'
})
});
// 2. Open OAuth popup
const popup = window.open(
`https://apiwts.top/admin/api/instagram/oauth/start?tenantId=${tenantId}&sessionId=${sessionId}`,
'instagram-oauth',
'width=600,height=700'
);
// 3. Poll until connected (or wait for webhook)
const checkStatus = async () => {
const res = await fetch(`https://apiwts.top/api/v1/sessions/${sessionId}/status`, {
headers: { 'X-API-Key': apiKey }
});
const { state } = await res.json();
if (state === 'READY') {
popup?.close();
console.log('Instagram connected successfully!');
return true;
}
return false;
};
// Poll every 3 seconds, timeout after 5 minutes
let attempts = 0;
const interval = setInterval(async () => {
if (await checkStatus() || ++attempts > 100) {
clearInterval(interval);
}
}, 3000);
}
Sending Messages
Once connected, use the standard messaging endpoints with the Instagram session:
Send Text Message
POST /api/v1/messages/text
Content-Type: application/json
Authorization: Bearer your_api_key
{
"sessionId": "instagram-mybusiness",
"to": "17841409876543210", // Instagram-scoped ID (IGSID)
"message": "Hello from Instagram!"
}
Send Media
POST /api/v1/messages/media
Content-Type: application/json
Authorization: Bearer your_api_key
{
"sessionId": "instagram-mybusiness",
"to": "17841409876543210",
"mediaUrl": "https://example.com/image.jpg",
"type": "image",
"caption": "Check out this product!"
}
Send Quick Replies
POST /api/v1/messages/interactive
Content-Type: application/json
Authorization: Bearer your_api_key
{
"sessionId": "instagram-mybusiness",
"to": "17841409876543210",
"type": "quick_replies",
"body": "How can we help you today?",
"quickReplies": [
{"id": "order_status", "title": "Order Status"},
{"id": "returns", "title": "Returns"},
{"id": "support", "title": "Talk to Support"}
]
}
Receiving Messages (Webhooks)
Configure webhook URL in your Meta App dashboard:
Webhook URL: https://your-domain.com/webhooks/instagram
Verify Token: (same as INSTAGRAM_WEBHOOK_VERIFY_TOKEN env var)
Webhook Payload (message.received)
{
"event": "message.received",
"timestamp": 1704067200000,
"sessionId": "instagram-mybusiness",
"tenantId": "your-tenant-id",
"data": {
"from": "17841409876543210",
"to": "17841401234567890",
"body": "Hi, I have a question about my order",
"type": "text",
"messageId": "aWdfZAG1faW...",
"metadata": {
"platform": "instagram",
"timestamp": "2024-01-01T12:00:00.000Z"
}
}
}
Instagram-Specific Limitations
- 24-Hour Messaging Window: Can only send messages within 24 hours of user's last message (standard messaging). Human Agent tag extends to 7 days.
- No Phone Numbers: Uses Instagram-Scoped IDs (IGSID) instead of phone numbers
- No Groups: Instagram DMs are 1:1 only (group chats not supported via API)
- No Templates: Template messages not available (use WhatsApp Cloud API for templates)
- Media Limits: Images max 8MB, Videos max 25MB
- Rate Limits: Subject to Meta Graph API rate limiting
Supported Message Types
| Type | Support | Notes |
|---|---|---|
| Text | โ | Plain text messages |
| Image | โ | JPEG, PNG (max 8MB) |
| Video | โ | MP4 (max 25MB, 60 seconds) |
| Audio | โ | Voice messages |
| Sticker | โ | Static and animated |
| Quick Replies | โ | Up to 13 buttons |
| Generic Template | โ | Carousel with cards |
| Ice Breakers | โ | Conversation starters (FAQ) |
Token Management
The Instagram driver automatically manages token lifecycle:
- User Token: Long-lived (60 days), stored encrypted in database
- Page Token: Derived from user token, never expires while user token valid
- Auto-Refresh: Tokens refreshed 10 days before expiration
- Encryption: AES-256-GCM encryption for stored tokens
The system automatically refreshes tokens before expiration. If refresh fails (e.g.,
user revoked permissions), the session will be marked as
token_expired and admin will need to re-authenticate via OAuth flow.
Advanced Features v3.10.0+
Instagram-specific advanced features for enhanced messaging capabilities.
Sender Actions (Typing Indicators)
Show typing indicators or mark messages as seen:
POST /api/v1/instagram/sessions/:sessionId/typing
Content-Type: application/json
Authorization: Bearer your_api_key
{
"recipientId": "17841409876543210",
"action": "typing_on" // typing_on | typing_off | mark_seen
}
// Response:
{
"success": true,
"action": "typing_on",
"recipientId": "17841409876543210"
}
-
typing_on- Shows typing indicator (auto-clears after 20s or next message) typing_off- Hides typing indicator immediatelymark_seen- Marks conversation as read (blue checkmarks)
Message Reactions
React to messages with emoji or remove reactions:
// Add reaction
POST /api/v1/instagram/sessions/:sessionId/messages/:messageId/react
Content-Type: application/json
Authorization: Bearer your_api_key
{
"recipientId": "17841409876543210",
"reaction": "love" // love | haha | wow | sad | angry | like | or any emoji
}
// Response:
{
"success": true,
"messageId": "aWdfZAG1faW...",
"reaction": "love"
}
// Remove reaction
DELETE /api/v1/instagram/sessions/:sessionId/messages/:messageId/react
Content-Type: application/json
Authorization: Bearer your_api_key
{
"recipientId": "17841409876543210"
}
// Response:
{
"success": true,
"messageId": "aWdfZAG1faW...",
"reaction": null
}
Multi-Image Sending (BETA)
Send multiple images in a single message:
POST /api/v1/instagram/sessions/:sessionId/messages/images
Content-Type: application/json
Authorization: Bearer your_api_key
{
"recipientId": "17841409876543210",
"imageUrls": [
"https://example.com/image1.jpg",
"https://example.com/image2.jpg",
"https://example.com/image3.jpg"
]
}
// Response:
{
"success": true,
"messageId": "aWdfZAG1faW...",
"status": "sent",
"imagesCount": 3
}
- Maximum: 10 images per message
- Size Limit: Each image max 8MB
- Beta Feature: May not be available for all Instagram accounts
-
Error Code: Error subcode
2534068means feature not enabled for your app
User Moderation
Block, unblock, or mark users as spam:
// Block user
POST /api/v1/instagram/sessions/:sessionId/users/:userId/block
Authorization: Bearer your_api_key
// Response:
{
"success": true,
"userId": "17841409876543210",
"action": "block_user"
}
// Unblock user
DELETE /api/v1/instagram/sessions/:sessionId/users/:userId/block
Authorization: Bearer your_api_key
// Response:
{
"success": true,
"userId": "17841409876543210",
"action": "unblock_user"
}
// Move to spam
POST /api/v1/instagram/sessions/:sessionId/users/:userId/spam
Authorization: Bearer your_api_key
// Response:
{
"success": true,
"userId": "17841409876543210",
"action": "move_to_spam"
}
Instagram Webhook Events v3.10.0+
New webhook events for Instagram messaging:
// Reaction received webhook
{
"event": "message.reaction",
"timestamp": 1704067200000,
"sessionId": "instagram-mybusiness",
"tenantId": "your-tenant-id",
"data": {
"messageId": "aWdfZAG1faW...",
"from": "17841409876543210",
"reaction": "โค๏ธ",
"timestamp": "2024-01-01T12:00:00.000Z"
}
}
// Message edited webhook
{
"event": "message.edit",
"timestamp": 1704067200000,
"sessionId": "instagram-mybusiness",
"tenantId": "your-tenant-id",
"data": {
"messageId": "aWdfZAG1faW...",
"from": "17841409876543210",
"newBody": "Updated message text",
"timestamp": "2024-01-01T12:00:00.000Z"
}
}
Feature Flags
Instagram v3.10.0 features can be enabled/disabled via Redis-based feature flags:
| Flag | Default | Description |
|---|---|---|
INSTAGRAM_SENDER_ACTIONS_ENABLED |
โ | typing_on/typing_off/mark_seen |
INSTAGRAM_REACTIONS_ENABLED |
โ | React/unreact to messages |
INSTAGRAM_REACTION_WEBHOOKS_ENABLED |
โ | Dispatch reaction webhooks |
INSTAGRAM_REPLY_TO_ENABLED |
โ | Reply-to message parameter |
INSTAGRAM_MULTI_IMAGE_ENABLED |
โ | Multi-image sending (BETA) |
INSTAGRAM_MODERATION_ENABLED |
โ | Block/unblock/spam users |
INSTAGRAM_MESSAGE_EDIT_WEBHOOKS_ENABLED |
โ | Message edit webhooks |
๐ Message Templates Cloud API Only
Send pre-approved WhatsApp template messages with variable substitution, rich media, and interactive buttons.
- Pre-Approval Required: Templates must be created and approved in Facebook Business Manager
- Cannot Create via API: Templates are managed in Business Manager UI, not via API
- Session State: Session must be READY with Cloud API driver configured
- Components: Header (text/media), Body (with variables), Footer, Buttons (quick reply/call-to-action)
- Variable Substitution: Replace {{1}}, {{2}} placeholders at send-time for personalization
- Localization: Support for 60+ languages with language code specification
- Use Cases: Transactional notifications, marketing campaigns, OTP/authentication, customer support
- Quality Ratings: Template performance affects delivery limits (GREEN/YELLOW/RED ratings)
How to Create Templates
- Access Business Manager: Go to Facebook Business Manager
- Navigate to Templates: WhatsApp Manager โ Message Templates
-
Create Template: Click "Create Template" and configure components:
- Name: Lowercase, underscores, no spaces (e.g., order_confirmation)
- Category: Marketing, Utility, or Authentication
- Language: Select primary language (can add more later)
- Header: Optional text, image, video, document, or location
- Body: Message text with {{1}}, {{2}} variables (up to 1024 characters)
- Footer: Optional footer text (up to 60 characters)
- Buttons: Up to 3 buttons (Call to Action or Quick Reply)
- Submit for Review: Meta reviews templates (24-48 hours approval time)
- Wait for Approval: Template status: PENDING โ APPROVED/REJECTED
- Use Approved Template: Only APPROVED templates can be sent via API
POST /api/v1/messages/template
Send a pre-approved WhatsApp template message with variable substitution.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
sessionId |
string | Yes | Session identifier (must be Cloud API session in READY state) |
to |
string | Yes | Recipient WhatsApp number in E.164 format with country code (e.g., +5511999999999, +41764791479, +14155552671) |
templateName |
string | Yes | Template name from Business Manager (1-512 characters) |
languageCode |
string | No | Template language code (e.g., 'en', 'pt_BR', 'es'). Default: 'en' |
headerVariables |
array | No | Header component variables (for media headers). Array of {type, value} |
bodyVariables |
string[] | No | Body variables to replace {{1}}, {{2}} placeholders |
buttons |
array | No | Button component values. Array of {type: 'url'|'quick_reply', value} |
Example: Simple Text Template
const response = await fetch('https://apiwts.top/api/v1/messages/template', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'cloud-session-001',
to: '+5511999999999',
templateName: 'welcome_message',
languageCode: 'pt_BR',
bodyVariables: ['Joรฃo Silva']
})
});
const result = await response.json();
console.log(result);
// {
// "id": "msg_uuid_12345",
// "sessionId": "cloud-session-001",
// "to": "+5511999999999",
// "status": "sent",
// "type": "template",
// "timestamp": "2025-10-03T12:00:00.000Z",
// "driverMessageId": "wamid.HBgLNTU1MTEOTk5OTk5OTk5..."
// }
Example: Order Confirmation Template
// Template in Business Manager:
// Name: order_confirmation
// Body: Hi {{1}}, your order #{{2}} has been confirmed for ${{3}}.
// Footer: Thank you for shopping with us!
// Button: View Order (URL)
const response = await fetch('https://apiwts.top/api/v1/messages/template', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'cloud-session-001',
to: '+5511999999999',
templateName: 'order_confirmation',
languageCode: 'en',
bodyVariables: ['Joรฃo Silva', 'ORD-12345', '49.99'],
buttons: [
{ type: 'url', value: 'https://example.com/orders/12345' }
]
})
});
const result = await response.json();
Example: Template with Image Header
// Template with image header
const response = await fetch('https://apiwts.top/api/v1/messages/template', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'cloud-session-001',
to: '+5511999999999',
templateName: 'product_launch',
languageCode: 'en',
headerVariables: [
{
type: 'image',
value: 'https://example.com/images/product.jpg'
}
],
bodyVariables: ['New Product', '25% OFF'],
buttons: [
{ type: 'url', value: 'https://example.com/products/new' },
{ type: 'quick_reply', value: 'LEARN_MORE' }
]
})
});
Response Schema
| Field | Type | Description |
|---|---|---|
id |
string | Internal message ID (UUID) |
sessionId |
string | Session identifier used for sending |
to |
string | Recipient WhatsApp number |
status |
string | Message status: 'sent', 'queued', 'delivered', etc. |
type |
string | Message type: 'template' |
timestamp |
string | ISO 8601 timestamp of message creation |
driverMessageId |
string | WhatsApp message ID from Cloud API (e.g., wamid.HBgL...) |
Error Responses
| Status | Error | Description |
|---|---|---|
| 400 | Bad Request | WWebJS driver attempted (Cloud API only feature) |
| 400 | Bad Request | Session not in READY state |
| 404 | Not Found | Session not found or doesn't belong to tenant |
| 500 | Internal Server Error | Template not approved, invalid parameters, or Cloud API error |
Common Template Errors
// Error: WWebJS driver
{
"statusCode": 400,
"error": "Bad Request",
"message": "Template messages are only available for Cloud API driver (WWebJS does not support templates)"
}
// Error: Session not ready
{
"statusCode": 400,
"error": "Bad Request",
"message": "Session cloud-session-001 is not ready (current state: INITIALIZING)"
}
// Error: Template not approved
{
"statusCode": 500,
"error": "Internal Server Error",
"message": "Failed to send template: Template not found or not approved"
}
// Error: Invalid language code
{
"statusCode": 500,
"error": "Internal Server Error",
"message": "Failed to send template: Language code not supported for template"
}
Template Best Practices
- Use Descriptive Names: e.g., order_confirmation, password_reset, welcome_message
- Keep Body Concise: Max 1024 characters, avoid overly long messages
- Test Before Launch: Send test messages to verify variables and formatting
- Follow WhatsApp Guidelines: Avoid spam, excessive marketing, or misleading content
- Monitor Quality Ratings: Poor user feedback affects delivery limits (GREEN โ YELLOW โ RED)
- Localize Templates: Create language-specific versions for better engagement
- Limit Variables: Use only necessary variables to reduce complexity
- Button Clarity: Use clear call-to-action text (e.g., "View Order" not "Click Here")
Template Categories
| Category | Use Case | Examples |
|---|---|---|
| Utility | Transactional notifications | Order confirmations, shipping updates, appointment reminders, account alerts |
| Authentication | Security and verification | OTP codes, password resets, 2FA verification, login alerts |
| Marketing | Promotional messages | Product launches, seasonal sales, discount codes, event invitations |
- US Phone Numbers: Marketing templates to US numbers paused starting April 1, 2025
- Rate Limits: Per-user marketing message limits based on account tier and quality rating
- Opt-Out: Users can block or report marketing messages, affecting quality rating
- Compliance: Must comply with WhatsApp Business Policy and local regulations (GDPR, CAN-SPAM, etc.)
Driver Compatibility
| Feature | WWebJS | Cloud API |
|---|---|---|
| Template Messages | โ Not Supported | โ Full Support |
| Dynamic Templates | โ Not Supported | โ Pre-approval Required |
| Variable Substitution | โ Not Supported | โ Full Support |
| Interactive Buttons | โ Deprecated | โ Full Support |
| Media Headers | โ Not Supported | โ Image/Video/Document |
Related Resources
๐ค MCP Reference (Model Context Protocol)
The WhatsApp Multi-Driver API exposes all its functionality via Model Context Protocol (MCP), enabling AI agents like Claude Code to interact with WhatsApp using natural language.
What is MCP?
MCP is an open protocol by Anthropic that connects AI applications to external tools and data sources in a standardized way.
How It Works
Claude Code
โ JSON-RPC 2.0
MCP Server (https://apiwts.top/mcp)
โ Internal API calls
WhatsApp Multi-Driver API
โ Drivers
WhatsApp (WWebJS or Cloud API)
MCP Endpoint
POST /mcp
JSON-RPC 2.0 endpoint for AI tool integration.
// List available tools
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 1
}
// Response
{
"jsonrpc": "2.0",
"result": {
"tools": [
{
"name": "sendTextMessage",
"description": "Send a text message via WhatsApp",
"inputSchema": {
"type": "object",
"properties": {
"sessionId": { "type": "string" },
"to": { "type": "string" },
"text": { "type": "string" }
},
"required": ["sessionId", "to", "text"]
}
}
// ... 57 tools
]
},
"id": 1
}
MCP Lifecycle Methods v3.7.6+
MCP defines lifecycle methods for protocol handshake and health checking. All methods require Bearer token authentication.
initialize
Handshake method to establish protocol version and capabilities. Required as the first method call.
// Request
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "my-client", "version": "1.0"}
}
}
// Response
{
"jsonrpc": "2.0",
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
"serverInfo": {
"name": "whatsapp-multi-driver-api",
"version": "3.7.6"
},
"instructions": "WhatsApp Multi-Driver API MCP Server. Use tools/list to discover available operations."
},
"id": 1
}
2024-11-05,
2025-03-26
notifications/initialized
Notification sent by client after receiving initialize response. No response expected (JSON-RPC notification).
// Request (notification - no id field)
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
// Response: HTTP 204 No Content (empty body)
ping
Simple health check method.
// Request
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"jsonrpc": "2.0",
"method": "ping",
"id": 2
}
// Response
{
"jsonrpc": "2.0",
"result": {},
"id": 2
}
Authentication
MCP uses the same API key authentication as the REST API, but via Bearer token:
// Include API Key in Authorization header
Authorization: Bearer YOUR_API_KEY
46 Available Tools
Sessions Management (9 tools)
| Tool | Description |
|---|---|
createSession |
Create new WhatsApp session |
listSessions |
List all sessions |
getSession |
Get session details |
getSessionStatus |
Get status + QR availability |
getQRCode |
Get QR code (PNG base64) |
connectSession |
Initialize session |
restartSession |
Restart session |
deleteSession |
Delete session |
getSessionHealth |
Health metrics |
Messaging (20 tools)
| Tool | Description |
|---|---|
sendTextMessage |
Send text message |
sendMediaMessage |
Send media (image/video/document) |
sendTemplateMessage |
Send template (Cloud API only) |
sendLocationMessage |
Send location with coordinates (WWebJS + Cloud API) v3.4+ |
getMessage |
Get message status |
listMessages |
List messages |
revokeMessage |
Revoke/delete a sent message (WWebJS only) v2.84+ |
reactToMessage |
React to a message with emoji (WWebJS only) v2.85+ |
getMessageInfo |
Get detailed delivery info (ACK level, group read receipts) v2.85+ |
forwardMessage |
Forward message to another chat (WWebJS only) v2.85+ |
editMessage |
Edit a sent text message (~15 min window, WWebJS only) v3.4+ |
downloadMedia |
Download media (image/video/audio/doc) from message as base64 (WWebJS only) v3.4+ |
getQuotedMessage |
Get the original message that was quoted/replied to (WWebJS only) v3.4+ |
starMessage |
Star (favorite) a message (WWebJS only) v3.4+ |
unstarMessage |
Remove star (unfavorite) from a message (WWebJS only) v3.4+ |
getMessageReactions |
Get all emoji reactions on a message (WWebJS only) v3.4+ |
pinMessage |
Pin a message in chat for a specified duration (WWebJS only) v3.4+ |
unpinMessage |
Unpin a pinned message in chat (WWebJS only) v3.4+ |
getMessageMentions |
Get @mentions in a message - returns list of mentioned contacts (WWebJS only) v3.4+ |
getPollVotes |
Get votes from a poll message - returns list of voters with selected options (WWebJS only) v3.4+ |
Scheduled Messages (4 tools)
| Tool | Description |
|---|---|
scheduleMessage |
Schedule future message |
listScheduledMessages |
List scheduled messages |
cancelScheduledMessage |
Cancel pending message |
getScheduledMessageStats |
Get statistics |
Webhooks (6 tools)
| Tool | Description |
|---|---|
createWebhook |
Configure webhook URL |
listWebhooks |
List webhooks |
updateWebhook |
Update webhook |
deleteWebhook |
Delete webhook |
listWebhookDeliveries |
Delivery history |
testWebhook |
Test webhook |
Contacts (3 tools - WWebJS only)
| Tool | Description |
|---|---|
listContacts |
List contacts |
blockContact |
Block contact |
unblockContact |
Unblock contact |
Chats (6 tools - WWebJS only)
| Tool | Description |
|---|---|
listChats |
List conversations |
archiveChat |
Archive conversation |
unarchiveChat |
Unarchive conversation |
muteChat |
Mute conversation |
pinChat |
Pin conversation |
setChatState |
Set chat state - typing/recording indicators (WWebJS only) v2.85+ |
Groups (12 tools)
| Tool | Description | Driver |
|---|---|---|
createGroup |
Create new group | Both |
getGroupInfo |
Get group details | Both |
listGroups |
List all groups | WWebJS |
addGroupParticipants |
Add members to group | Both |
removeGroupParticipants |
Remove members from group | Both |
updateGroup |
Update group name/description | Both |
promoteGroupAdmin |
Promote member to admin | WWebJS |
demoteGroupAdmin |
Demote admin to member | WWebJS |
updateGroupSettings |
Update group settings | WWebJS |
getGroupInviteCode |
Get group invite link | WWebJS |
revokeGroupInvite |
Revoke invite link | WWebJS |
leaveGroup |
Leave a group | Both |
Monitoring (2 tools)
| Tool | Description |
|---|---|
getHealthStatus |
System health status |
getMetrics |
Prometheus metrics |
Usage Examples
Example 1: Tool Discovery
// Request: List all available tools
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 1
}
// Response
{
"jsonrpc": "2.0",
"result": {
"tools": [...49 tools...]
},
"id": 1
}
Example 2: Call a Tool
// Request: Get health status
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "getHealthStatus",
"arguments": {}
},
"id": 2
}
// Response
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "{\"status\":\"healthy\",\"database\":\"connected\",\"redis\":\"connected\"}"
}
]
},
"id": 2
}
Example 3: Send Message via Tool
// Request: Send WhatsApp message
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "sendTextMessage",
"arguments": {
"sessionId": "my-session",
"to": "+5511999999999",
"text": "Hello from MCP!"
}
},
"id": 3
}
// Response
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "{\"id\":\"msg_123\",\"status\":\"queued\",\"sessionId\":\"my-session\"}"
}
]
},
"id": 3
}
Example 4: Complete Workflow
// Step 1: Create session
tools/call โ createSession({ sessionId: "support" })
// Step 2: Connect session (generate QR)
tools/call โ connectSession({ sessionId: "support" })
// Step 3: Get QR code
tools/call โ getQRCode({ sessionId: "support" })
// Returns: { qr: "data:image/png;base64,..." }
// Step 4: Wait for session ready (webhook or polling)
// Step 5: Send message
tools/call โ sendTextMessage({
sessionId: "support",
to: "+5511999999999",
text: "Hello from MCP Server!"
})
Example 5: Revoke Message (v2.84+)
// Request: Delete message for everyone
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "revokeMessage",
"arguments": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"revokeType": "everyone"
}
},
"id": 5
}
// Response (Success)
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "{\"success\":true,\"message\":\"Message revoked successfully (everyone)\",\"revokedAt\":\"2025-11-23T06:00:00.000Z\",\"revokeType\":\"everyone\"}"
}
]
},
"id": 5
}
// Natural language usage with Claude Code
"Delete the message with ID 550e8400-e29b-41d4-a716-446655440000 for everyone"
"Revoke my last sent message"
"Delete message abc-123 only for me"
// Important notes
- Only supported on WWebJS driver (Cloud API will return error)
- Messages can be deleted for everyone within ~48 hours
- Requires message status: sent, delivered, or read
- revokeType: "me" (delete for yourself) or "everyone" (default)
Example 6: React to Message (v2.85+)
// Request: Add emoji reaction to a message
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "reactToMessage",
"arguments": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"emoji": "๐"
}
},
"id": 6
}
// Response (Success)
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "{\"success\":true,\"message\":\"Reaction \\\"๐\\\" added successfully\",\"data\":{\"messageId\":\"550e8400-e29b-41d4-a716-446655440000\",\"emoji\":\"๐\",\"reactedAt\":\"2025-11-23T08:00:00.000Z\"}}"
}
]
},
"id": 6
}
// Natural language usage with Claude Code
"React to message 550e8400 with thumbs up emoji"
"Add heart reaction โค๏ธ to the last message"
"React with ๐ to message abc-123"
// Important notes
- Only supported on WWebJS driver (Cloud API will return error)
- Common emojis: ๐, โค๏ธ, ๐, ๐ฎ, ๐ข, ๐
- Message must have status: sent, delivered, or read
Example 7: Get Message Delivery Info (v2.85+)
// Request: Get detailed delivery information
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "getMessageInfo",
"arguments": {
"messageId": "550e8400-e29b-41d4-a716-446655440000"
}
},
"id": 7
}
// Response (Success - Individual Chat)
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "{\"success\":true,\"data\":{\"id\":\"550e8400-e29b-41d4-a716-446655440000\",\"ack\":3,\"timestamp\":\"2025-11-23T08:00:00.000Z\"}}"
}
]
},
"id": 7
}
// Natural language usage with Claude Code
"Get delivery info for message 550e8400"
"Check if my last message was read"
"Show delivery details for message abc-123"
// ACK Levels
- -1: Error (message failed)
- 0: Pending (queued)
- 1: Server (sent to WhatsApp)
- 2: Device (delivered to recipient)
- 3: Read (read by recipient)
- 4: Played (voice/video played)
// Important notes
- Only supported on WWebJS driver
- Group chats include deliveredTo, readBy, playedBy arrays
Example 8: Forward Message (v2.85+)
// Request: Forward message to another chat
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "forwardMessage",
"arguments": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"targetChatId": "5511888888888@c.us"
}
},
"id": 8
}
// Response (Success)
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "{\"success\":true,\"message\":\"Message forwarded successfully\",\"data\":{\"messageId\":\"550e8400-e29b-41d4-a716-446655440000\",\"targetChatId\":\"5511888888888@c.us\",\"forwardedAt\":\"2025-11-23T08:00:00.000Z\"}}"
}
]
},
"id": 8
}
// Natural language usage with Claude Code
"Forward message 550e8400 to +5511888888888"
"Forward the last message to chat 5511999999999@c.us"
"Forward message abc-123 to group 120363..."
// Important notes
- Only supported on WWebJS driver (Cloud API will return error)
- Message must have status: sent, delivered, or read
- newDriverMessageId may not be available (WWebJS limitation)
Example 9: Edit Message (v3.4+)
// Request: Edit a sent text message
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "editMessage",
"arguments": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"newContent": "Updated message content"
}
},
"id": 9
}
// Response (Success)
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "{\"success\":true,\"message\":\"Message edited successfully\",\"data\":{\"messageId\":\"550e8400-e29b-41d4-a716-446655440000\",\"newContent\":\"Updated message content\",\"editedAt\":\"2025-11-28T10:00:00.000Z\"}}"
}
]
},
"id": 9
}
// Natural language usage with Claude Code
"Edit message 550e8400 to say 'Updated content'"
"Change my last message to 'Fixed typo here'"
"Update the message I sent to 'Correct information'"
// Important notes
- Only supported on WWebJS driver (Cloud API will return error)
- Only text messages can be edited (not media/location/contacts)
- Messages can only be edited within ~15 minutes of sending
- Message must have status: sent, delivered, or read
Example 10: Download Media (v3.4+)
// Request: Download media from a message
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "downloadMedia",
"arguments": {
"messageId": "550e8400-e29b-41d4-a716-446655440000"
}
},
"id": 10
}
// Response (Success)
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "{\"success\":true,\"message\":\"Media downloaded successfully\",\"data\":{\"messageId\":\"550e8400-e29b-41d4-a716-446655440000\",\"base64\":\"/9j/4AAQSkZJRgABAQAAAQABAAD/...\",\"mimetype\":\"image/jpeg\",\"filename\":\"photo.jpg\",\"filesize\":245678}}"
}
]
},
"id": 10
}
// Natural language usage with Claude Code
"Download the media from that message"
"Get the image from message 550e8400"
"Save the photo that was sent to me"
// Important notes
- Only supported on WWebJS driver (Cloud API provides URLs in webhooks)
- Message must contain media (image, video, audio, document)
- Returns base64-encoded data with mimetype and optional filename/filesize
Example 11: Send Location (v3.4+)
// Request: Send a location message
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "sendLocationMessage",
"arguments": {
"sessionId": "my-session-001",
"to": "5511999999999",
"latitude": -23.5505199,
"longitude": -46.6333094,
"description": "Sรฃo Paulo, Brazil",
"address": "Av. Paulista, 1578 - Bela Vista"
}
},
"id": 11
}
// Response (Success)
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "{\"success\":true,\"message\":\"Location message sent successfully\",\"data\":{\"id\":\"550e8400-e29b-41d4-a716-446655440000\",\"driverMessageId\":\"true_5511999999999@c.us_ABCDEF123456\",\"sessionId\":\"my-session-001\",\"to\":\"5511999999999\",\"latitude\":-23.5505199,\"longitude\":-46.6333094,\"description\":\"Sรฃo Paulo, Brazil\",\"address\":\"Av. Paulista, 1578 - Bela Vista\",\"status\":\"sent\",\"timestamp\":\"2025-11-28T14:00:00.000Z\"}}"
}
]
},
"id": 11
}
// Natural language usage with Claude Code
"Send my location to contact 5511999999999"
"Share the coordinates -23.55, -46.63 with that user"
"Send the restaurant location to the group"
// Important notes
- Supported on WWebJS and Cloud API drivers
- Latitude must be between -90 and 90
- Longitude must be between -180 and 180
- Description and address are optional (max 256 chars each)
Example 12: Get Quoted Message (v3.4+)
// Request: Get the quoted/replied-to message
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "getQuotedMessage",
"arguments": {
"messageId": "msg-uuid-of-reply-message"
}
},
"id": 12
}
// Response (Success - message is a reply)
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "{\"success\":true,\"message\":\"Quoted message retrieved successfully\",\"data\":{\"id\":\"true_5511999999999@c.us_AAABBCCCDDD\",\"from\":\"5511888888888@c.us\",\"to\":\"5511999999999@c.us\",\"body\":\"Original message text here\",\"type\":\"chat\",\"timestamp\":\"2025-11-28T10:30:00.000Z\",\"hasMedia\":false}}"
}
]
},
"id": 12
}
// Response (Success - message is NOT a reply)
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "{\"success\":true,\"message\":\"Message is not a reply\",\"data\":null}"
}
]
},
"id": 12
}
// Natural language usage with Claude Code
"Get the original message that was replied to in message xyz"
"What message was this replying to?"
"Show me the quoted message context"
// Important notes
- WWebJS driver only
- Returns null if message is not a reply
- Useful for building conversation threads
Example 13: Star/Unstar Message (v3.4+)
// Request: Star (favorite) a message
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "starMessage",
"arguments": {
"messageId": "msg-uuid-here"
}
},
"id": 13
}
// Response (Success)
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "{\"success\":true,\"message\":\"Message starred successfully\",\"data\":{\"messageId\":\"msg-uuid-here\",\"starredAt\":\"2025-11-29T10:30:00.000Z\"}}"
}
]
},
"id": 13
}
// Request: Unstar a message
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "unstarMessage",
"arguments": {
"messageId": "msg-uuid-here"
}
},
"id": 14
}
// Natural language usage with Claude Code
"Star this important message for later"
"Favorite message xyz"
"Remove the star from this message"
"Unstar message xyz"
// Important notes
- WWebJS driver only
- Starred messages appear in chat's "Starred Messages" section
- Useful for bookmarking important messages
Example 14: Set Chat State (v2.85+)
// Request: Show typing indicator
POST https://apiwts.top/mcp
Authorization: Bearer YOUR_API_KEY
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "setChatState",
"arguments": {
"sessionId": "my-session-001",
"chatId": "5511999999999@c.us",
"state": "typing"
}
},
"id": 9
}
// Response (Success)
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "{\"success\":true,\"message\":\"Chat state \\\"typing\\\" set successfully\",\"data\":{\"chatId\":\"5511999999999@c.us\",\"state\":\"typing\",\"appliedAt\":\"2025-11-23T08:00:00.000Z\"}}"
}
]
},
"id": 9
}
// Natural language usage with Claude Code
"Show typing indicator to +5511999999999"
"Simulate recording state in chat 5511999999999@c.us"
"Stop typing indicator in the current chat"
// States
- typing: Shows "typing..." indicator
- recording: Shows "recording..." indicator (voice messages)
- stop: Clears any active state
// Important notes
- Only supported on WWebJS driver
- State clears automatically after ~20-30 seconds
- Best practice: Send actual message within 5-10 seconds
- Use case: Simulate human-like bot behavior
Integration with Claude Code
To use this MCP server with Claude Code CLI:
# Register MCP server
claude mcp add --transport http whatsapp-api https://apiwts.top/mcp \
--header "Authorization: Bearer YOUR_API_KEY"
# List available tools
claude mcp list whatsapp-api
# Use via natural language
claude ask "Use whatsapp-api to send message 'Hello!' to +5511999999999 via session 'test'"
Error Handling
MCP follows JSON-RPC 2.0 error format:
// Error response
{
"jsonrpc": "2.0",
"error": {
"code": -32600,
"message": "Invalid Request",
"data": {
"details": "Missing required parameter: sessionId"
}
},
"id": 1
}
// Common error codes
-32700: Parse error
-32600: Invalid Request
-32601: Method not found
-32602: Invalid params
-32603: Internal error
Testing & Debug
MCP Inspector (Official Tool)
npx @modelcontextprotocol/inspector https://apiwts.top/mcp
Visual interface to test tools, see responses, and debug errors.
Manual Testing with cURL
# List tools
curl -X POST https://apiwts.top/mcp \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
# Call tool
curl -X POST https://apiwts.top/mcp \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"getHealthStatus","arguments":{}},"id":2}'
- Natural Language: Use WhatsApp API via plain English commands
- No Documentation Required: AI agent knows all endpoints and parameters
- Autocomplete: Tools and parameters are discoverable
- Standardized: Works with any MCP-compatible AI agent
Additional Resources
๐ง Troubleshooting & Common Errors
Error: Missing Required Parameters
JSON-RPC Error Code: -32602
-
Typo in parameter name (e.g.,
scheduledForinstead ofscheduledAt) - Parameter not sent in request
-
Parameter with value
null,undefined, or empty string -
Using wrong parameter name (e.g.,
idinstead ofwebhookId)
Example Error Response
{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "ValidationError"
},
"result": {
"content": [
{
"type": "text",
"text": "{
\"code\": -32602,
\"error\": \"ValidationError\",
\"message\": \"Missing required parameters for scheduleMessage: scheduledAt\",
\"data\": {
\"toolName\": \"scheduleMessage\",
\"received\": [\"sessionId\", \"to\", \"text\", \"scheduledFor\"],
\"expected\": [\"sessionId\", \"to\", \"text\", \"scheduledAt\"],
\"missing\": [\"scheduledAt\"],
\"hint\": \"Did you mean 'scheduledAt' instead of 'scheduledFor'?\"
}
}"
}
]
},
"id": 1
}
How to Fix
- Check the error message for the exact parameter name required
- Look at the
hintfield for typo suggestions - Compare
receivedvsexpectedparameters - Correct the parameter name in your request
Common Typos & Fixes
| โ Wrong (Common Typo) | โ Correct | Tool |
|---|---|---|
scheduledFor |
scheduledAt |
scheduleMessage |
id |
webhookId |
updateWebhook, deleteWebhook, testWebhook |
sesionId |
sessionId |
All session tools |
chatid |
chatId |
All chat tools |
contactid |
contactId |
blockContact, unblockContact |
Required Parameters Quick Reference
| Tool | Required Parameters |
|---|---|
sendTextMessage |
sessionId, to, text |
sendMediaMessage |
sessionId, to, mediaType, mediaUrl |
sendTemplateMessage |
sessionId, to, templateName, language |
scheduleMessage |
sessionId, to, text, scheduledAt |
getMessage |
sessionId, messageId |
listMessages |
sessionId |
createSession |
sessionId |
getSession |
sessionId |
deleteSession |
sessionId |
getQRCode |
sessionId |
getSessionStatus |
sessionId |
connectSession |
sessionId |
restartSession |
sessionId |
getSessionHealth |
sessionId |
createWebhook |
url, events |
updateWebhook |
webhookId |
deleteWebhook |
webhookId |
listWebhookDeliveries |
webhookId |
testWebhook |
webhookId |
listContacts |
sessionId |
blockContact |
sessionId, contactId |
unblockContact |
sessionId, contactId |
listChats |
sessionId |
archiveChat |
sessionId, chatId |
unarchiveChat |
sessionId, chatId |
muteChat |
sessionId, chatId |
pinChat |
sessionId, chatId |
Validation Features (v2.62+)
- Typo Detection: Levenshtein distance algorithm suggests correct parameter names
- Clear Error Messages: Exact parameter names that are missing
- JSON-RPC 2.0 Compliance: Standard error code -32602 for invalid params
- Helpful Hints: Automatic suggestions for common typos (edit distance โค 2)
hint field first - it often contains the exact fix you need!
โก Advanced Features
This section covers advanced configuration and deployment patterns for the WhatsApp API.
Session Reliability
The API includes automatic session recovery features:
- Auto-Reconnection: Automatic retry on temporary disconnections (3 attempts, 10s interval)
- Orphan Recovery: Sessions restored automatically on container restart
- Detached Frame Detection: Automatic recovery from Puppeteer corruption errors
- Health Monitoring: Continuous health checks with automatic status updates
Blue-Green Deployment
For zero-downtime deployments, the API supports blue-green architecture:
- Dual Environments: Blue and Green containers with isolated session volumes
- Instant Rollback: Traffic switch in under 5 seconds via symlink
- Session Sync: Automatic session synchronization between environments
- Shared Infrastructure: PostgreSQL and Redis shared between environments
Rate Limiting
Quality-based adaptive rate limiting protects your account:
- GREEN Rating: Normal sending limits
- YELLOW Rating: Reduced limits, monitoring required
- RED Rating: Severely restricted, review account health
- Recipient Cooldown: 6-10 second delays between messages to same recipient
๐ง Roadmap
Planned features for future releases. These endpoints are documented for reference but are not currently available.
โ Recently Delivered Message Types (v3.76.0)
The following endpoints, previously listed as "Planned", are now available in production:
Send location with GPS coordinates. Drivers: WWebJS + Cloud API
(v3.76.0+). Payload accepts description (canonical) or
name (alias). See Send Location.
Send contact card (vCard). Drivers: WWebJS + Cloud API. Simple
canonical shape ({name, phone, organization?, email?}). CRLF/null-byte
in fields rejected at schema (vCard injection protection).
Create interactive poll (WWebJS only โ Cloud API returns 422).
question max 255 chars; options 2-12 items, each max 100
chars.
Planned Group Features
Update group profile picture
Remove group profile picture
๐ ๏ธ Error Handling
HTTP Status Codes
| Code | Meaning | Solution |
|---|---|---|
| 200 | Success | Request completed successfully |
| 400 | Bad Request |
Invalid parameters or session not READY โ check the message field
|
| 401 | Unauthorized | Check API Key - verify in admin panel |
| 403 | Forbidden | Authenticated but not allowed for this resource/tenant |
| 404 | Not Found | Session doesn't exist - create session first |
| 409 | Conflict | Session already authenticated |
| 410 | Gone | Resource no longer available (e.g. expired QR code) |
| 422 | Unprocessable Entity |
Operation not supported by this driver (e.g. unsupported media type or location) โ
check code and supportedDrivers
|
| 429 | Rate Limited | Too many requests - wait 60 seconds |
| 500 | Server Error | Try again later - check status page |
| 503 | Service Unavailable | Tenant inactive or a dependency is temporarily unavailable |
Error Response Format
{
"statusCode": 401,
"error": "Unauthorized",
"message": "Invalid API Key",
"timestamp": "2025-01-15T10:35:00.000Z"
}
Universal Error Handler
const handleApiError = (response) => {
switch (response.status) {
case 401:
return 'Invalid API Key - Check credentials';
case 404:
return 'Session not found - Create session first';
case 409:
return 'Session already authenticated';
case 429:
return 'Rate limit exceeded - Wait 60 seconds';
case 500:
return 'Server error - Try again later';
default:
return `Unexpected error: ${response.status}`;
}
};
โ ๏ธ Common Pitfalls & Solutions
1. CORS Issues
โ Solution: Use proxy configuration, never direct external URLs in browser
// โ WRONG - Direct external URL
fetch('http://external-api.com/endpoint')
// โ
CORRECT - Relative URL with proxy
fetch('/api/v1/sessions')
2. Phone Number Format
โ Solution: Use international format with country code (E.164)
// โ
CORRECT - International format with country code
const validPhoneNumbers = [
'+5511999999999', // Brazil
'+41764791479', // Switzerland
'+14155552671', // United States
'+447911123456', // United Kingdom
'+8613912345678', // China
'+81312345678', // Japan
'+491512345678', // Germany
];
// โ WRONG - Missing country code
const invalidPhoneNumbers = [
'11999999999', // Ambiguous - which country?
'7641234567', // Could be Russia (+7) or Kazakhstan
];
// Phone number formatting helper
const formatPhoneNumber = (phone) => {
// Remove all non-digit characters
const cleaned = phone.replace(/\D/g, '');
// Add + prefix if not present
return phone.startsWith('+') ? phone : `+${cleaned}`;
};
// Usage examples
const phoneNumber1 = formatPhoneNumber('+5511999999999'); // +5511999999999
const phoneNumber2 = formatPhoneNumber('41764791479'); // +41764791479
3. API Key Management
โ Solution: Use environment variables or secure storage
// โ WRONG - Hardcoded API key
const API_KEY = 'sk_live_a1b2c3d4e5f6...'; // Never do this!
// โ
CORRECT - Environment variable
const API_KEY = import.meta.env.VITE_WHATSAPP_API_KEY;
// โ
CORRECT - User input with localStorage
const [apiKey, setApiKey] = useState(
localStorage.getItem('whatsapp_api_key') || ''
);
4. Orphan Sessions & QR Code Errors
โ Solution: POST the session again - API will auto-recover and recreate the driver
Cause: This error occurs when:
- The application was restarted
- Session exists in database but driver is not loaded in memory
- These are called "orphan sessions"
How to Fix:
// Step 1: Try creating the session again
const response = await fetch('https://apiwts.top/api/v1/sessions', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'scheduler'
})
});
// If session exists but driver is missing:
// API will automatically detect and recover the orphan session
// Returns 201 Created (not 409 Conflict)
// Step 2: Get QR code (should work now)
const qrResponse = await fetch('https://apiwts.top/api/v1/sessions/scheduler/qr', {
headers: { 'X-API-Key': 'your-api-key' }
});
Alternative (Manual Recovery):
// If auto-recovery doesn't work:
// 1. Delete the session
await fetch('https://apiwts.top/api/v1/sessions/scheduler', {
method: 'DELETE',
headers: { 'X-API-Key': 'your-api-key' }
});
// 2. Create it again
await fetch('https://apiwts.top/api/v1/sessions', {
method: 'POST',
headers: { 'X-API-Key': 'your-api-key', 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: 'scheduler' })
});
5. Message Sending Errors
โ Solution: Ensure session is READY before sending messages
Common Causes:
- Session not authenticated - Session is in QR_PENDING, INITIALIZING, or other non-READY state
- Session disconnected - WhatsApp session was disconnected and needs re-authentication
- Invalid phone number - Phone number format is incorrect (must be international format)
Solution - Check Session Status Before Sending:
// Step 1: Check session status
const sessionResponse = await fetch('https://apiwts.top/api/v1/sessions/my-session', {
headers: { 'X-API-Key': 'your-api-key' }
});
const session = await sessionResponse.json();
// Step 2: Only send if session is READY
if (session.state === 'READY') {
// Send message
const response = await fetch('https://apiwts.top/api/v1/messages/text', {
method: 'POST',
headers: {
'X-API-Key': 'your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'my-session',
to: '+5511999999999',
text: 'Hello World!'
})
});
} else {
// โ ๏ธ Do NOT immediately /reconnect on a non-READY state.
// DISCONNECTED can be a TRANSIENT internal-recovery blip that auto-recovers in ~18-27s
// (up to ~90s). Re-poll first; only reconnect if it persists past a grace window.
console.warn(`Session not READY yet: ${session.state} โ re-polling before any action`);
const READY = 'READY';
const GRACE_MS = 45000; // ~30-45s grace before treating DISCONNECTED as real
const POLL_MS = 3000;
const deadline = Date.now() + GRACE_MS;
let state = session.state;
while (state !== READY && Date.now() < deadline) {
await new Promise(r => setTimeout(r, POLL_MS));
const r = await fetch('https://apiwts.top/api/v1/sessions/my-session/status', {
headers: { 'X-API-Key': 'your-api-key' }
});
state = (await r.json()).state;
}
if (state === READY) {
// Recovered on its own โ safe to send now (no reconnect needed)
} else {
// Still not READY after the grace window โ now it is reasonable to act
// (QR_PENDING โ show QR; persistent DISCONNECTED/FAILED โ POST /reconnect)
console.error(`Session still not READY after ${GRACE_MS}ms: ${state}`);
}
}
if (state === 'READY') send(); else reconnect(); fires a
redundant /reconnect during the normal internal-recovery window,
which can prolong the very outage it was meant to fix. Always re-poll
/status for ~30-45s first (as above); only /reconnect if the
session is still not READY after the grace window. See
Session States.
Error Response Examples:
// Error 400 - Session not ready
{
"statusCode": 400,
"error": "Bad Request",
"message": "Session my-session is not ready (current state: QR_PENDING). Please ensure the session is authenticated before sending messages."
}
// Error 404 - Session not found
{
"statusCode": 404,
"error": "Not Found",
"message": "Session my-session not found"
}
6. Session Management
โ Solution: Monitor session status, implement reconnection logic
// Monitor session status
const checkSessionStatus = async (sessionId) => {
const response = await fetch(`/api/v1/sessions/${sessionId}`, {
headers: { 'X-API-Key': apiKey }
});
const session = await response.json();
if (session.state === 'DISCONNECTED') {
console.log('Session disconnected - need to re-authenticate');
// Get new QR code
}
};
6.1 Auto-Reconnection (v2.72+)
๐ฏ Benefit: Sessions recover automatically from network issues without manual intervention
How Auto-Reconnection Works
- Temporary Disconnections: Auto-reconnect enabled (3 attempts, 10s interval)
- Permanent Disconnections: Require manual reconnection via dashboard or API
- Session State: Only READY/AUTHENTICATED sessions attempt auto-reconnect
Disconnection Types
| Reason | Type | Behavior |
|---|---|---|
| CONNECTION_LOST | Temporary | Auto-reconnect (3 attempts) |
| TIMEOUT | Temporary | Auto-reconnect (3 attempts) |
| CONFLICT | Temporary | Auto-reconnect (3 attempts) |
| LOGOUT | Permanent | Manual reconnect required |
| NAVIGATION | Permanent | Manual reconnect required |
| UNPAIRED | Permanent | Manual reconnect required |
Frontend Auto-Reconnect (Dashboard)
When clicking "Gerar QR Code" after disconnection:
- Frontend detects QR code unavailable (404)
- Automatically calls reconnect endpoint
- Waits 5 seconds for session initialization
- Retrieves and displays new QR code
- No intermediate prompts - seamless UX
Manual Reconnection via API
/restart to disconnect and re-initialize a
session. This preserves the session record and triggers a new QR code generation.
// Restart a disconnected session (using /restart endpoint)
const restartSession = async (sessionId) => {
// Step 1: Restart the session (disconnects + re-initializes)
const response = await fetch(`/api/v1/sessions/${sessionId}/restart`, {
method: 'POST',
headers: { 'X-API-Key': apiKey }
});
if (response.ok) {
console.log('Session restarting...');
// Step 2: Wait for QR code generation (typically 3-5 seconds)
setTimeout(async () => {
const qrResponse = await fetch(`/api/v1/sessions/${sessionId}/qr`, {
headers: { 'X-API-Key': apiKey }
});
if (qrResponse.ok) {
const blob = await qrResponse.blob();
const qrUrl = URL.createObjectURL(blob);
// Display QR code for user to scan
console.log('QR code ready:', qrUrl);
}
}, 5000);
}
};
// Alternative: Use /connect to re-initialize without full restart
const connectSession = async (sessionId) => {
const response = await fetch(`/api/v1/sessions/${sessionId}/connect`, {
method: 'POST',
headers: { 'X-API-Key': apiKey }
});
// Returns 201 if connection initiated, 200 if already connected
};
6.2 Zombie Session Detection (v3.8.24+)
๐ฏ Solution: Multi-signal detection with automatic recovery
What is a Zombie Session?
A zombie session is a WhatsApp Web session that:
- Appears healthy: Status shows READY, health check passes
- API responds success: Send message returns "sent" with valid driverMessageId
- Messages never arrive: No ACK events, no delivery confirmation
- Silent failure: No errors, no warnings - just silent message loss
Detection Signals (3 independent checks)
| Signal | Trigger Condition | What it Means |
|---|---|---|
| ACK Rate | < 20% ACKs in last 3 messages (5 min window) | Messages sent but not confirmed by WhatsApp |
| Heartbeat | 2+ consecutive health check failures | WWebJS client not responding properly |
| Error Rate | > 50% errors in last 60 seconds | High failure rate in message polling |
Zombie Confirmation: 2 or more warning flags = ZOMBIE CONFIRMED
Automatic Recovery Flow
- ๐ Detection: Multi-signal check confirms zombie state
-
๐ค Webhook:
session.zombie_detectedsent to your endpoint - ๐ Recovery: Automatic shutdown + reconnection (if AUTO_RECONNECT_ENABLED=true)
- โ
Success:
session.recoveredwebhook OR - โ Failure:
session.recovery_failedwebhook
Webhook Payloads
// session.zombie_detected
{
"event": "session.zombie_detected",
"sessionId": "sensyxboutique",
"data": {
"metrics": {
"warningFlags": ["ACK_RATE", "HEARTBEAT"],
"messagesSent": 5,
"acksReceived": 0,
"ackRate": 0,
"heartbeatFailures": 2,
"detectedAt": "2026-01-03T05:00:00.000Z"
},
"action": "auto_reconnect_initiated",
"timestamp": 1735880400000
}
}
// session.recovered
{
"event": "session.recovered",
"sessionId": "sensyxboutique",
"data": {
"recoveryTime": 1735880430000,
"timestamp": 1735880430000
}
}
// session.recovery_failed
{
"event": "session.recovery_failed",
"sessionId": "sensyxboutique",
"data": {
"error": "Failed to reconnect: timeout",
"timestamp": 1735880460000
}
}
Environment Configuration
# Enable/Disable zombie detection
ZOMBIE_DETECTION_ENABLED=true
ZOMBIE_DRY_RUN=false
# ACK Monitoring
ZOMBIE_MIN_MESSAGES_FOR_CHECK=3
ZOMBIE_ACK_RATE_THRESHOLD=0.2
ZOMBIE_ACK_WINDOW_MS=300000
# Heartbeat
ZOMBIE_HEARTBEAT_INTERVAL_MS=30000
ZOMBIE_MAX_HEARTBEAT_FAILURES=2
# Error Rate
ZOMBIE_ERROR_WINDOW_MS=60000
ZOMBIE_ERROR_RATE_THRESHOLD=0.5
# Recovery
AUTO_RECONNECT_ENABLED=true
ZOMBIE_WARNINGS_TO_CONFIRM=2
SLA Guarantees
| Metric | Target | Description |
|---|---|---|
| Detection Time | < 2 minutes | From first failed message to zombie confirmed |
| Recovery Time | < 30 seconds | From zombie detected to session reconnected |
| Webhook Notification | < 5 seconds | Webhook sent immediately after detection |
session.zombie_detected webhook to
monitor zombie events in your logging/alerting system.
๐ก Complete Examples
React Component Example
import React, { useState } from 'react';
const WhatsAppIntegration = () => {
const [apiKey, setApiKey] = useState('');
const [sessionId, setSessionId] = useState('my-session');
const [qrCode, setQrCode] = useState(null);
const [loading, setLoading] = useState(false);
const testConnection = async () => {
setLoading(true);
try {
const response = await fetch('/health', {
headers: { 'X-API-Key': apiKey }
});
if (response.ok) {
alert('โ
API Connected!');
} else {
alert('โ Connection failed');
}
} catch (error) {
alert('โ Network error');
}
setLoading(false);
};
const getQrCode = async () => {
setLoading(true);
try {
const response = await fetch(`/api/v1/sessions/${sessionId}/qr`, {
headers: { 'X-API-Key': apiKey }
});
if (response.ok) {
const blob = await response.blob();
setQrCode(URL.createObjectURL(blob));
}
} catch (error) {
alert('โ Failed to get QR code');
}
setLoading(false);
};
const sendMessage = async () => {
try {
const response = await fetch('/api/v1/messages/text', {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId,
to: '+5511999999999',
text: 'Hello from React!'
})
});
if (response.ok) {
alert('โ
Message sent!');
}
} catch (error) {
alert('โ Failed to send message');
}
};
return (
<div>
<h1>WhatsApp Integration</h1>
<input
type="password"
placeholder="API Key"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
<input
type="text"
placeholder="Session ID"
value={sessionId}
onChange={(e) => setSessionId(e.target.value)}
/>
<button onClick={testConnection} disabled={loading}>
Test Connection
</button>
<button onClick={getQrCode} disabled={loading}>
Get QR Code
</button>
<button onClick={sendMessage} disabled={loading}>
Send Test Message
</button>
{qrCode && (
<div>
<h3>Scan with WhatsApp</h3>
<img src={qrCode} alt="QR Code" />
</div>
)}
</div>
);
};
export default WhatsAppIntegration;
๐ API Information
Access OpenAPI specifications and API metadata programmatically.
GET /api/v1/openapi.json
Get the complete OpenAPI 3.0 specification as JSON (machine-readable API documentation).
// Request (no authentication required)
const response = await fetch('https://apiwts.top/api/v1/openapi.json');
const spec = await response.json();
// Response 200 - OpenAPI Specification
{
"openapi": "3.0.0",
"info": {
"title": "WhatsApp Multi-Driver API",
"version": "2.52.0",
"description": "Enterprise-grade WhatsApp API with multi-driver support"
},
"servers": [
{
"url": "https://apiwts.top/api/v1",
"description": "Production server"
}
],
"paths": {
"/sessions": { ... },
"/messages/text": { ... }
},
"components": {
"schemas": { ... },
"securitySchemes": { ... }
}
}
GET /api/v1/openapi.yaml
Get the complete OpenAPI 3.0 specification as YAML (human-readable format).
// Request (no authentication required)
const response = await fetch('https://apiwts.top/api/v1/openapi.yaml');
const specYaml = await response.text();
// Response 200 - OpenAPI Specification (YAML)
openapi: 3.0.0
info:
title: WhatsApp Multi-Driver API
version: 2.52.0
description: Enterprise-grade WhatsApp API with multi-driver support
servers:
- url: https://apiwts.top/api/v1
description: Production server
paths:
/sessions:
post:
summary: Create a new WhatsApp session
...
GET /api/v1/info
Get API version, status, and capabilities (lightweight metadata endpoint).
// Request (no authentication required)
const response = await fetch('https://apiwts.top/api/v1/info');
// Response 200
{
"name": "WhatsApp Multi-Driver API",
"version": "2.52.0",
"description": "Enterprise-grade WhatsApp API with multi-driver support",
"environment": "production",
"documentation": {
"swagger": "https://apiwts.top/docs",
"openapi": "https://apiwts.top/api/v1/openapi.json"
},
"capabilities": {
"drivers": [
"wwebjs",
"cloud"
],
"authentication": [
"apiKey",
"jwt"
],
"features": [
"multi-tenant",
"rate-limiting",
"message-queue",
"webhooks",
"media-upload",
"qr-code",
"auto-reconnect"
]
},
"support": {
"contact": "support@apiwhatsapp.com",
"issues": "https://github.com/Moser007/apiwhatsapp/issues"
}
}