# 06 - API Specification ## 6.1 API Overview ### Base URL ``` Production: https://your-domain.com/api Development: http://localhost:2785/api ``` ### Authentication ```http # API Key in Header X-API-Key: your-api-key-here # Or in Query Parameter (not recommended) GET /api/sessions?apiKey=your-api-key-here ``` ### Common Headers ```http Content-Type: application/json Accept: application/json X-API-Key: your-api-key X-Request-ID: optional-tracking-id (recommended) ``` ## 6.2 Response Format ### Success Response ```json { "success": true, "data": {}, "meta": { "timestamp": "2025-02-02T10:00:00.000Z", "requestId": "550e8400-e29b-41d4-a716-446655440000", "version": "0.1.0" } } ``` ### Error Response ```json { "success": false, "error": { "code": "ERROR_CODE", "message": "Human readable message", "details": {} }, "meta": { "timestamp": "2025-02-02T10:00:00.000Z", "requestId": "550e8400-e29b-41d4-a716-446655440000" } } ``` ### Request ID Convention - Recommended format: `req_` (example: `req_1706868000000`). - `X-Request-ID` is returned in `meta.requestId` when provided by the client. ### Timestamp Conventions - All API timestamps use ISO 8601 UTC (example: `2026-02-02T10:00:00.000Z`). - If you need the original WhatsApp timestamp (epoch seconds), the field is named `waTimestamp`. ### Error Codes #### General Error Codes | Code | HTTP Status | Description | |------|-------------|-------------| | `VALIDATION_ERROR` | 400 | Invalid request parameters | | `UNAUTHORIZED` | 401 | Missing or invalid API key | | `FORBIDDEN` | 403 | Insufficient permissions | | `NOT_FOUND` | 404 | Resource not found | | `RATE_LIMITED` | 429 | Too many requests | | `INTERNAL_ERROR` | 500 | Server error | #### Session Error Codes | Code | HTTP Status | Description | |------|-------------|-------------| | `SESSION_NOT_FOUND` | 404 | Session with the given ID was not found | | `SESSION_NOT_READY` | 400 | Session not authenticated/connected | | `SESSION_ALREADY_EXISTS` | 409 | Session with the given name already exists | | `SESSION_INITIALIZING` | 400 | Session is currently initializing | | `SESSION_QR_EXPIRED` | 400 | QR code expired, regeneration required | | `SESSION_AUTH_FAILED` | 401 | WhatsApp authentication failed | | `SESSION_DISCONNECTED` | 400 | Session disconnected from WhatsApp | | `SESSION_LOGGED_OUT` | 400 | Session already logged out from WhatsApp | | `SESSION_LIMIT_REACHED` | 403 | Maximum session limit reached | | `SESSION_BANNED` | 403 | WhatsApp number has been banned | #### Message Error Codes | Code | HTTP Status | Description | |------|-------------|-------------| | `MESSAGE_SEND_FAILED` | 500 | Failed to send message to WhatsApp | | `MESSAGE_NOT_FOUND` | 404 | Message not found | | `MESSAGE_INVALID_CHAT_ID` | 400 | Invalid chatId format | | `MESSAGE_NUMBER_NOT_ON_WHATSAPP` | 400 | Number is not registered on WhatsApp | | `MESSAGE_MEDIA_TOO_LARGE` | 413 | Media size exceeds limit | | `MESSAGE_MEDIA_DOWNLOAD_FAILED` | 400 | Failed to download media from URL | | `MESSAGE_MEDIA_INVALID_FORMAT` | 400 | Unsupported media format | | `MESSAGE_TEXT_TOO_LONG` | 400 | Message text exceeds character limit | | `MESSAGE_BLOCKED_CONTACT` | 403 | Cannot send to a blocked contact | | `MESSAGE_RATE_LIMITED` | 429 | Too many messages in a short time | | `MESSAGE_QUOTED_NOT_FOUND` | 400 | Quoted message not found | #### Webhook Error Codes | Code | HTTP Status | Description | |------|-------------|-------------| | `WEBHOOK_NOT_FOUND` | 404 | Webhook not found | | `WEBHOOK_URL_INVALID` | 400 | Invalid webhook URL | | `WEBHOOK_URL_UNREACHABLE` | 400 | Webhook URL is unreachable | | `WEBHOOK_DUPLICATE` | 409 | Webhook with this URL already exists | | `WEBHOOK_LIMIT_REACHED` | 403 | Maximum webhook limit per session reached | #### Group Error Codes | Code | HTTP Status | Description | |------|-------------|-------------| | `GROUP_NOT_FOUND` | 404 | Group not found | | `GROUP_NOT_ADMIN` | 403 | Not a group admin | | `GROUP_PARTICIPANT_EXISTS` | 409 | Participant already exists in group | | `GROUP_PARTICIPANT_NOT_FOUND` | 404 | Participant not found in the group | | `GROUP_INVITE_INVALID` | 400 | Invalid invite link | | `GROUP_NAME_TOO_LONG` | 400 | Group name exceeds character limit | #### Contact Error Codes | Code | HTTP Status | Description | |------|-------------|-------------| | `CONTACT_NOT_FOUND` | 404 | Contact not found | | `CONTACT_BLOCKED` | 400 | Contact is already blocked | | `CONTACT_NOT_BLOCKED` | 400 | Contact is not blocked | ## 6.3 Media Specifications ### Supported Media Types & Limits ```mermaid flowchart LR subgraph Media["Media Types"] IMG[Image] VID[Video] AUD[Audio] DOC[Document] STK[Sticker] end subgraph Limits["Size Limits"] L1[16 MB] L2[64 MB] L3[16 MB] L4[100 MB] L5[500 KB] end IMG --> L1 VID --> L2 AUD --> L3 DOC --> L4 STK --> L5 ``` ### Image | Attribute | Value | |-----------|-------| | **Max File Size** | 16 MB | | **Supported Formats** | JPEG, PNG, WebP, GIF | | **Max Resolution** | 4096 x 4096 pixels | | **Recommended** | JPEG for photos, PNG for graphics | ```json { "image": { "url": "https://example.com/image.jpg", "mimetype": "image/jpeg" }, "caption": "Optional caption (max 1024 chars)" } ``` ### Video | Attribute | Value | |-----------|-------| | **Max File Size** | 64 MB (WhatsApp limit: 16 MB in many regions) | | **Supported Formats** | MP4, 3GP, AVI, MKV | | **Recommended Codec** | H.264 video, AAC audio | | **Max Duration** | ~3 menit (tergantung resolusi) | | **Recommended Resolution** | 720p or lower | ```json { "video": { "url": "https://example.com/video.mp4", "mimetype": "video/mp4" }, "caption": "Optional caption (max 1024 chars)", "gifPlayback": false } ``` ### Audio | Attribute | Value | |-----------|-------| | **Max File Size** | 16 MB | | **Supported Formats** | MP3, OGG, M4A, AMR, WAV | | **Voice Note (PTT)** | OGG Opus codec (auto-converted) | | **Max Duration** | ~15 menit | ```json { "audio": { "url": "https://example.com/audio.mp3", "mimetype": "audio/mpeg" }, "ptt": true } ``` ### Document | Attribute | Value | |-----------|-------| | **Max File Size** | 100 MB | | **Supported Formats** | PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, TXT, ZIP, dll | | **Filename** | Max 100 karakter | ```json { "document": { "url": "https://example.com/file.pdf", "mimetype": "application/pdf" }, "filename": "report-2025.pdf", "caption": "Optional caption" } ``` ### Sticker | Attribute | Value | |-----------|-------| | **Max File Size** | 500 KB | | **Format** | WebP | | **Dimensions** | 512 x 512 pixels | | **Animated** | Supported (WebP animated) | ```json { "sticker": { "url": "https://example.com/sticker.webp", "mimetype": "image/webp" } } ``` ### Media Input Options Media can be sent using three approaches: ```typescript // Option 1: URL (recommended for large files) { "image": { "url": "https://example.com/image.jpg" } } // Option 2: Base64 (for small files < 5MB) { "image": { "base64": "data:image/jpeg;base64,/9j/4AAQ..." } } // Option 3: File path (for local files - server side only) { "image": { "path": "/uploads/image.jpg" } } ``` ### Text Message Limits | Type | Max Length | |------|------------| | Regular text message | 65,536 characters | | Caption (image/video/document) | 1,024 characters | | Group name | 100 characters | | Group description | 2,048 characters | | Contact name | 100 characters | ## 6.4 API Endpoints ### 6.4.1 Sessions #### Create Session ```mermaid sequenceDiagram Client->>API: POST /sessions API->>Engine: Initialize Engine-->>API: QR Code API-->>Client: Session created + QR ``` ```http POST /api/sessions ``` **Request Body:** ```json { "name": "my-bot", "webhook": { "url": "https://your-server.com/webhook", "events": ["message.received", "message.sent"] } } ``` **Response (201 Created):** ```json { "success": true, "data": { "id": "sess_abc123", "name": "my-bot", "status": "INITIALIZING", "qr": null, "createdAt": "2025-02-02T10:00:00.000Z" } } ``` --- #### List Sessions ```http GET /api/sessions ``` **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `status` | string | Filter by status | | `page` | number | Page number (default: 1) | | `limit` | number | Items per page (default: 20) | **Response (200 OK):** ```json { "success": true, "data": [ { "id": "sess_abc123", "name": "my-bot", "status": "CONNECTED", "phoneNumber": "628123456789", "createdAt": "2025-02-02T10:00:00.000Z" } ], "pagination": { "page": 1, "limit": 20, "total": 1, "totalPages": 1 } } ``` --- #### Get Session ```http GET /api/sessions/:sessionId ``` **Response (200 OK):** ```json { "success": true, "data": { "id": "sess_abc123", "name": "my-bot", "status": "CONNECTED", "phoneNumber": "628123456789", "pushName": "My Bot", "platform": "android", "connectedAt": "2025-02-02T10:05:00.000Z", "createdAt": "2025-02-02T10:00:00.000Z" } } ``` --- **Session Status Values (API):** | Value | Description | |-------|-------------| | `INITIALIZING` | Session created, engine booting | | `SCAN_QR` | QR code ready / waiting for scan | | `CONNECTING` | QR scanned, connecting to WhatsApp | | `CONNECTED` | Connected and ready | | `DISCONNECTED` | Connection lost or logged out | | `FAILED` | Fatal error, manual intervention required | **Internal โ†” API Status Mapping:** | Internal Status (engine/db) | API Status | |-----------------------------|------------| | `initializing` | `INITIALIZING` | | `qr_ready` | `SCAN_QR` | | `connecting` | `CONNECTING` | | `ready` | `CONNECTED` | | `disconnected` | `DISCONNECTED` | | `error` | `FAILED` | > Internal statuses appear in engine logs and database records. API responses and webhooks always use the API status values above. --- #### Get Session QR Code ```http GET /api/sessions/:sessionId/qr ``` **Response (200 OK):** ```json { "success": true, "data": { "code": "2@ABC123...", "image": "data:image/png;base64,..." } } ``` --- #### Delete Session ```http DELETE /api/sessions/:sessionId ``` **Response (200 OK):** ```json { "success": true, "data": { "message": "Session deleted successfully" } } ``` --- #### Logout Session ```http POST /api/sessions/:sessionId/logout ``` **Response (200 OK):** ```json { "success": true, "data": { "message": "Session logged out successfully" } } ``` --- ### 6.4.2 Messages #### Send Text Message ```mermaid flowchart LR A[Client] -->|POST| B[/messages/send-text] B --> C{Validate} C -->|OK| D[Queue] D --> E[Send] E --> F[Response] ``` ```http POST /api/sessions/:sessionId/messages/send-text ``` **Request Body:** ```json { "chatId": "628123456789@c.us", "text": "Hello, World!", "options": { "quotedMessageId": "optional_message_id", "mentionedIds": ["628111111111@c.us"] } } ``` **Response (200 OK):** ```json { "success": true, "data": { "messageId": "true_628123456789@c.us_3EB0ABC123", "status": "sent", "timestamp": "2025-02-02T10:00:00.000Z" } } ``` --- #### Send Image ```http POST /api/sessions/:sessionId/messages/send-image ``` **Request Body:** ```json { "chatId": "628123456789@c.us", "image": { "url": "https://example.com/image.jpg" }, "caption": "Check this out!" } ``` **Alternative - Base64:** ```json { "chatId": "628123456789@c.us", "image": { "base64": "data:image/jpeg;base64,/9j/4AAQ..." }, "caption": "Check this out!" } ``` **Response (200 OK):** ```json { "success": true, "data": { "messageId": "true_628123456789@c.us_3EB0ABC124", "status": "sent", "timestamp": "2025-02-02T10:00:00.000Z" } } ``` --- #### Send Video ```http POST /api/sessions/:sessionId/messages/send-video ``` **Request Body:** ```json { "chatId": "628123456789@c.us", "video": { "url": "https://example.com/video.mp4" }, "caption": "Watch this!" } ``` --- #### Send Audio ```http POST /api/sessions/:sessionId/messages/send-audio ``` **Request Body:** ```json { "chatId": "628123456789@c.us", "audio": { "url": "https://example.com/audio.mp3" }, "ptt": true } ``` | Field | Type | Description | |-------|------|-------------| | `ptt` | boolean | Send as voice note (push-to-talk) | --- #### Send Document ```http POST /api/sessions/:sessionId/messages/send-document ``` **Request Body:** ```json { "chatId": "628123456789@c.us", "document": { "url": "https://example.com/file.pdf" }, "filename": "report.pdf", "caption": "Here is the document" } ``` --- #### Send Location ```http POST /api/sessions/:sessionId/messages/send-location ``` **Request Body:** ```json { "chatId": "628123456789@c.us", "latitude": -6.2088, "longitude": 106.8456, "description": "Jakarta, Indonesia" } ``` --- #### Send Contact ```http POST /api/sessions/:sessionId/messages/send-contact ``` **Request Body:** ```json { "chatId": "628123456789@c.us", "contact": { "name": "John Doe", "phone": "628987654321" } } ``` --- #### Send Bulk Messages Send messages to multiple recipients in a single request. The system processes them asynchronously with delays between messages to reduce ban risk. ```mermaid flowchart LR A[Client] -->|POST| B[/messages/send-bulk] B --> C[Validate] C --> D[Create Batch Job] D --> E[Queue Messages] E --> F[Process with Delay] F --> G[Batch ID Response] ``` ```http POST /api/sessions/:sessionId/messages/send-bulk ``` **Request Body:** ```json { "batchId": "batch_custom_123", "messages": [ { "chatId": "628123456789@c.us", "type": "text", "content": { "text": "Hello {name}!" }, "variables": { "name": "John" } }, { "chatId": "628987654321@c.us", "type": "image", "content": { "image": { "url": "https://example.com/promo.jpg" }, "caption": "Special offer for {name}!" }, "variables": { "name": "Jane" } } ], "options": { "delayBetweenMessages": 5000, "randomizeDelay": true, "stopOnError": false } } ``` **Request Fields:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `batchId` | string | No | Custom batch ID (auto-generated if not provided) | | `messages` | array | Yes | Array of messages (max 100 per request) | | `messages[].chatId` | string | Yes | Recipient chat ID | | `messages[].type` | string | Yes | Message type: `text`, `image`, `video`, `audio`, `document` | | `messages[].content` | object | Yes | Message content based on type | | `messages[].variables` | object | No | Variables for template substitution | | `options.delayBetweenMessages` | number | No | Delay in ms (default: 3000, min: 1000) | | `options.randomizeDelay` | boolean | No | Add random 0-2s to delay (default: true) | | `options.stopOnError` | boolean | No | Stop batch on first error (default: false) | **Response (202 Accepted):** ```json { "success": true, "data": { "batchId": "batch_abc123xyz", "status": "processing", "totalMessages": 2, "estimatedCompletionTime": "2025-02-02T10:05:00.000Z", "statusUrl": "/api/sessions/sess_abc123/messages/batch/batch_abc123xyz" } } ``` --- #### Get Batch Status ```http GET /api/sessions/:sessionId/messages/batch/:batchId ``` **Response (200 OK):** ```json { "success": true, "data": { "batchId": "batch_abc123xyz", "status": "completed", "progress": { "total": 100, "sent": 95, "failed": 5, "pending": 0 }, "results": [ { "chatId": "628123456789@c.us", "status": "sent", "messageId": "true_628123456789@c.us_3EB0ABC123" }, { "chatId": "628111111111@c.us", "status": "failed", "error": { "code": "MESSAGE_NUMBER_NOT_ON_WHATSAPP", "message": "Number not registered on WhatsApp" } } ], "startedAt": "2025-02-02T10:00:00.000Z", "completedAt": "2025-02-02T10:08:30.000Z" } } ``` --- #### Cancel Batch ```http POST /api/sessions/:sessionId/messages/batch/:batchId/cancel ``` **Response (200 OK):** ```json { "success": true, "data": { "batchId": "batch_abc123xyz", "status": "cancelled", "progress": { "total": 100, "sent": 45, "failed": 2, "cancelled": 53 } } } ``` --- #### Get Messages ```http GET /api/sessions/:sessionId/chats/:chatId/messages ``` **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `limit` | number | Number of messages (default: 50) | | `before` | string | Get messages before this ID | **Response (200 OK):** ```json { "success": true, "data": [ { "id": "true_628123456789@c.us_3EB0ABC123", "from": "628123456789@c.us", "to": "628987654321@c.us", "body": "Hello!", "type": "chat", "waTimestamp": 1706868000, "timestamp": "2025-02-02T10:00:00.000Z", "fromMe": false, "hasMedia": false } ] } ``` --- ### 6.4.3 Contacts #### Get All Contacts ```http GET /api/sessions/:sessionId/contacts ``` **Response (200 OK):** ```json { "success": true, "data": [ { "id": "628123456789@c.us", "name": "John Doe", "pushName": "John", "isMyContact": true, "isBlocked": false } ] } ``` --- #### Check Number Exists ```http GET /api/sessions/:sessionId/contacts/check/:phone ``` **Response (200 OK):** ```json { "success": true, "data": { "exists": true, "chatId": "628123456789@c.us" } } ``` --- #### Get Profile Picture ```http GET /api/sessions/:sessionId/contacts/:contactId/profile-picture ``` **Response (200 OK):** ```json { "success": true, "data": { "url": "https://pps.whatsapp.net/..." } } ``` --- ### 6.4.4 Groups #### Get All Groups ```http GET /api/sessions/:sessionId/groups ``` **Response (200 OK):** ```json { "success": true, "data": [ { "id": "628123456789-1234567890@g.us", "name": "Family Group", "description": "Family chat", "participantsCount": 10, "createdAt": "2024-01-01T00:00:00.000Z" } ] } ``` --- #### Get Group Info ```http GET /api/sessions/:sessionId/groups/:groupId ``` **Response (200 OK):** ```json { "success": true, "data": { "id": "628123456789-1234567890@g.us", "name": "Family Group", "description": "Family chat", "owner": "628123456789@c.us", "participants": [ { "id": "628123456789@c.us", "isAdmin": true, "isSuperAdmin": true } ], "createdAt": "2024-01-01T00:00:00.000Z" } } ``` --- #### Create Group ```http POST /api/sessions/:sessionId/groups ``` **Request Body:** ```json { "name": "New Group", "participants": [ "628123456789@c.us", "628987654321@c.us" ] } ``` --- ### 6.4.5 Webhooks #### Register Webhook ```http POST /api/sessions/:sessionId/webhooks ``` **Request Body:** ```json { "url": "https://your-server.com/webhook", "events": [ "message.received", "message.sent", "message.ack", "session.status" ], "secret": "your-webhook-secret", "headers": { "X-Custom-Header": "value" } } ``` **Response (201 Created):** ```json { "success": true, "data": { "id": "wh_xyz789", "url": "https://your-server.com/webhook", "events": ["message.received", "message.sent"], "active": true, "createdAt": "2025-02-02T10:00:00.000Z" } } ``` --- #### List Webhooks ```http GET /api/sessions/:sessionId/webhooks ``` --- #### Delete Webhook ```http DELETE /api/sessions/:sessionId/webhooks/:webhookId ``` --- ### 6.4.6 Health #### Basic Health Check ```http GET /health ``` **Response (200 OK):** ```json { "status": "ok", "timestamp": "2026-02-02T10:30:00Z" } ``` The basic endpoint is public. The detailed endpoint requires an API key. #### Detailed Health Check ```http GET /health/detailed ``` **Response (200 OK):** ```json { "status": "ok", "version": "0.1.0", "uptime": 3600, "timestamp": "2026-02-02T10:30:00Z", "checks": { "database": "ok", "redis": "ok", "sessions": { "total": 5, "connected": 4, "disconnected": 1 } } } ``` ## 6.4 Webhook Events ### Event Structure ```json { "event": "message.received", "timestamp": "2025-02-02T10:00:00.000Z", "sessionId": "sess_abc123", "deliveryId": "dlv_550e8400e29b41d4a716446655440000", "idempotencyKey": "msg_628123456789_1706868000000", "data": {}, "signature": "sha256=..." } ``` ### Webhook Idempotency OpenWA provides an idempotency mechanism to prevent duplicate processing on the client side. #### Idempotency Headers & Fields | Field/Header | Description | |--------------|-------------| | `deliveryId` | Unique ID for each delivery attempt | | `idempotencyKey` | Unique key per event (same across retries) | | `X-OpenWA-Delivery-Id` | Header carrying the delivery ID | | `X-OpenWA-Idempotency-Key` | Header carrying the idempotency key | | `X-OpenWA-Retry-Count` | Retry attempt number (0 = first attempt) | #### Idempotency Key Format ``` Format: {event_type}_{unique_identifier}_{timestamp} Examples: - message.received: msg_{messageId}_{timestamp} - message.sent: msg_{messageId}_{timestamp} - message.ack: ack_{messageId}_{ackLevel}_{timestamp} - session.status: sess_{sessionId}_{status}_{timestamp} - group.join: grp_{groupId}_{participantId}_{timestamp} ``` #### Client-Side Idempotency Implementation ```typescript // Recommended: Store processed idempotency keys const processedEvents = new Map(); // key -> timestamp const IDEMPOTENCY_TTL = 24 * 60 * 60 * 1000; // 24 hours async function handleWebhook(req: Request, res: Response) { const idempotencyKey = req.headers['x-openwa-idempotency-key'] as string; const retryCount = parseInt(req.headers['x-openwa-retry-count'] as string) || 0; // Check if already processed if (processedEvents.has(idempotencyKey)) { console.log(`Duplicate event ignored: ${idempotencyKey} (retry: ${retryCount})`); return res.status(200).json({ status: 'duplicate_ignored' }); } try { // Process the webhook await processEvent(req.body); // Mark as processed processedEvents.set(idempotencyKey, Date.now()); // Cleanup old entries periodically cleanupOldEntries(); return res.status(200).json({ status: 'processed' }); } catch (error) { // Return 500 to trigger retry return res.status(500).json({ error: error.message }); } } function cleanupOldEntries() { const now = Date.now(); for (const [key, timestamp] of processedEvents.entries()) { if (now - timestamp > IDEMPOTENCY_TTL) { processedEvents.delete(key); } } } ``` #### Database-Based Idempotency (Production) ```sql -- Create idempotency table CREATE TABLE webhook_idempotency ( idempotency_key VARCHAR(255) PRIMARY KEY, processed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), response_data JSONB ); -- Index for cleanup CREATE INDEX idx_webhook_idempotency_processed_at ON webhook_idempotency(processed_at); -- Cleanup job (run daily) DELETE FROM webhook_idempotency WHERE processed_at < NOW() - INTERVAL '24 hours'; ``` ```typescript // Production implementation with PostgreSQL async function handleWebhookWithDB(req: Request, res: Response) { const idempotencyKey = req.headers['x-openwa-idempotency-key'] as string; try { // Try to insert (will fail if duplicate) await db.query(` INSERT INTO webhook_idempotency (idempotency_key) VALUES ($1) ON CONFLICT (idempotency_key) DO NOTHING RETURNING idempotency_key `, [idempotencyKey]); const result = await db.query( 'SELECT 1 FROM webhook_idempotency WHERE idempotency_key = $1', [idempotencyKey] ); if (result.rowCount === 0) { // Already processed return res.status(200).json({ status: 'duplicate_ignored' }); } // Process the webhook await processEvent(req.body); return res.status(200).json({ status: 'processed' }); } catch (error) { return res.status(500).json({ error: error.message }); } } ``` ### Event Types ```mermaid flowchart TB subgraph Events["Webhook Events"] direction TB subgraph Message["Message Events"] MR[message.received] MS[message.sent] MA[message.ack] MRV[message.revoked] end subgraph Session["Session Events"] SS[session.status] SQ[session.qr] SA[session.authenticated] SD[session.disconnected] end subgraph Group["Group Events"] GJ[group.join] GL[group.leave] GU[group.update] end end ``` ### message.received ```json { "event": "message.received", "timestamp": "2025-02-02T10:00:00.000Z", "sessionId": "sess_abc123", "data": { "id": "true_628123456789@c.us_3EB0ABC123", "from": "628123456789@c.us", "to": "628987654321@c.us", "body": "Hello!", "type": "chat", "waTimestamp": 1706868000, "timestamp": "2025-02-02T10:00:00.000Z", "isGroup": false, "hasMedia": false, "contact": { "name": "John Doe", "pushName": "John" } } } ``` ### message.ack ```json { "event": "message.ack", "timestamp": "2025-02-02T10:00:00.000Z", "sessionId": "sess_abc123", "data": { "messageId": "true_628123456789@c.us_3EB0ABC123", "ack": 3, "ackName": "read" } } ``` | Ack | Name | Description | |-----|------|-------------| | 0 | error | Error | | 1 | pending | Pending | | 2 | sent | Sent to server | | 3 | delivered | Delivered | | 4 | read | Read | | 5 | played | Played (audio) | ### session.status ```json { "event": "session.status", "timestamp": "2025-02-02T10:00:00.000Z", "sessionId": "sess_abc123", "data": { "status": "CONNECTED", "phoneNumber": "628123456789" } } ``` ## 6.5 Webhook Signature Verification ```javascript const crypto = require('crypto'); function verifyWebhookSignature(payload, signature, secret) { const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(JSON.stringify(payload)) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } // Usage in Express app.post('/webhook', (req, res) => { const signature = req.headers['x-openwa-signature']; if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } // Process webhook... res.status(200).send('OK'); }); ``` ## 6.6 Rate Limiting | Endpoint Category | Rate Limit | |-------------------|------------| | Session management | 10 req/min | | Send message | 60 req/min | | Read operations | 120 req/min | | Webhook management | 10 req/min | **Rate Limit Headers:** ```http X-RateLimit-Limit: 60 X-RateLimit-Remaining: 45 X-RateLimit-Reset: 1706868060 ``` ## 6.7 WebSocket Real-time API In addition to the REST API, OpenWA provides WebSocket for real-time updates. > [!IMPORTANT] > The WebSocket format below is **canonical**. Client/server implementations must follow this `WSRequest/WSResponse` structure. ### Connection **WebSocket URL:** ``` wss://your-domain.com/ws wss://your-domain.com/ws?apiKey=your-api-key # browser fallback ``` **Header (recommended for non-browser clients):** ```http X-API-Key: your-api-key ``` ```javascript // Browser (query param) const ws = new WebSocket('wss://your-domain.com/ws?apiKey=your-api-key'); ``` ### Message Protocol All messages use JSON format: ```typescript // Client -> Server (Request) interface WSRequest { type: 'subscribe' | 'unsubscribe' | 'ping'; payload?: { sessionId?: string; events?: string[]; }; requestId?: string; // For tracking responses } // Server -> Client (Response/Event) interface WSResponse { type: 'subscribed' | 'unsubscribed' | 'event' | 'error' | 'pong'; payload: any; requestId?: string; timestamp: string; // ISO 8601 UTC } ``` ### Subscribe / Unsubscribe ```javascript // Subscribe to specific session ws.send(JSON.stringify({ type: 'subscribe', payload: { sessionId: 'sess_abc123', events: ['message.received', 'message.ack', 'session.status'] }, requestId: 'req_001' })); // Subscribe to all sessions ws.send(JSON.stringify({ type: 'subscribe', payload: { sessionId: '*', events: ['*'] } })); // Unsubscribe ws.send(JSON.stringify({ type: 'unsubscribe', payload: { sessionId: 'sess_abc123' } })); ``` ### Event Types | Event | Description | |-------|-------------| | `session.status` | Session status changed (INITIALIZING, SCAN_QR, CONNECTING, CONNECTED, DISCONNECTED, FAILED) | | `session.qr` | New QR code generated | | `session.authenticated` | Session authenticated | | `session.disconnected` | Session disconnected | | `message.received` | New incoming message | | `message.sent` | Message sent successfully | | `message.ack` | Message acknowledgment (delivered, read) | | `message.revoked` | Message was deleted | | `group.join` | Group join event | | `group.leave` | Group leave event | | `group.update` | Group info updated | ### Event Payload Examples **session.qr:** ```json { "type": "event", "payload": { "event": "session.qr", "sessionId": "sess_abc123", "data": { "code": "2@ABC123XYZ...", "image": "data:image/png;base64,..." } }, "timestamp": "2026-02-02T10:00:00.000Z" } ``` **message.received:** ```json { "type": "event", "payload": { "event": "message.received", "sessionId": "sess_abc123", "data": { "messageId": "true_628123456789@c.us_3EB0ABC", "chatId": "628123456789@c.us", "from": "628123456789@c.us", "type": "text", "body": "Hello!", "waTimestamp": 1706868000, "timestamp": "2026-02-02T10:00:00.000Z", "isGroup": false } }, "timestamp": "2026-02-02T10:00:00.000Z" } ``` ### Keep-Alive ```javascript // Client should send ping every 30 seconds setInterval(() => { ws.send(JSON.stringify({ type: 'ping', requestId: 'ping-001' })); }, 30000); ``` ### Quick Client Example (JavaScript) ```javascript const ws = new WebSocket('ws://localhost:2785/ws?apiKey=your-api-key'); let pingInterval = null; ws.onopen = () => { ws.send(JSON.stringify({ type: 'subscribe', payload: { sessionId: 'sess_abc123', events: ['message.received', 'session.status'] }, requestId: 'req_001' })); pingInterval = setInterval(() => { ws.send(JSON.stringify({ type: 'ping', requestId: `ping_${Date.now()}` })); }, 30000); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'event') { console.log('Event:', msg.payload.event, msg.payload.data); } }; ws.onclose = () => { if (pingInterval) clearInterval(pingInterval); }; ``` ### Error Handling ```json { "type": "error", "payload": { "code": "SESSION_NOT_FOUND", "message": "Session sess_xxx not found" }, "requestId": "req_001", "timestamp": "2026-02-02T10:00:00.000Z" } ``` ## 6.8 OpenAPI/Swagger Swagger UI tersedia di: ``` http://localhost:2785/api/docs ``` OpenAPI JSON: ``` http://localhost:2785/api/docs-json ``` ## 6.9 SDK & Code Examples ### Official SDKs (Coming Soon) | Language | Package | Status | |----------|---------|--------| | JavaScript/TypeScript | `@openwa/sdk` | ๐Ÿ”œ Planned | | Python | `openwa-python` | ๐Ÿ”œ Planned | | PHP | `openwa/php-sdk` | ๐Ÿ”œ Planned | | Go | `openwa-go` | ๐Ÿ”œ Planned | ### cURL Examples ```bash # Create session curl -X POST http://localhost:2785/api/sessions \ -H "Content-Type: application/json" \ -H "X-API-Key: your-api-key" \ -H "X-Request-ID: req_1706868000000" \ -d '{"name": "my-bot"}' # Send text message curl -X POST http://localhost:2785/api/sessions/sess_abc123/messages/send-text \ -H "Content-Type: application/json" \ -H "X-API-Key: your-api-key" \ -H "X-Request-ID: req_1706868000001" \ -d '{ "chatId": "628123456789@c.us", "text": "Hello from OpenWA!" }' # Send image curl -X POST http://localhost:2785/api/sessions/sess_abc123/messages/send-image \ -H "Content-Type: application/json" \ -H "X-API-Key: your-api-key" \ -H "X-Request-ID: req_1706868000002" \ -d '{ "chatId": "628123456789@c.us", "image": {"url": "https://example.com/image.jpg"}, "caption": "Check this out!" }' ``` ### JavaScript/Node.js Example ```javascript const axios = require('axios'); const api = axios.create({ baseURL: 'http://localhost:2785/api', headers: { 'X-API-Key': 'your-api-key', 'Content-Type': 'application/json', 'X-Request-ID': `req_${Date.now()}` } }); // Send message async function sendMessage(sessionId, chatId, text) { const response = await api.post( `/sessions/${sessionId}/messages/send-text`, { chatId, text } ); return response.data; } // Usage sendMessage('sess_abc123', '628123456789@c.us', 'Hello!') .then(result => console.log('Sent:', result.data.messageId)) .catch(err => console.error('Error:', err.response?.data)); ``` ### Python Example ```python import time import requests class OpenWA: def __init__(self, base_url, api_key): self.base_url = base_url self.headers = { 'X-API-Key': api_key, 'Content-Type': 'application/json', 'X-Request-ID': f'req_{int(time.time() * 1000)}' } def send_text(self, session_id, chat_id, text): response = requests.post( f'{self.base_url}/sessions/{session_id}/messages/send-text', headers=self.headers, json={'chatId': chat_id, 'text': text} ) return response.json() def send_image(self, session_id, chat_id, image_url, caption=None): response = requests.post( f'{self.base_url}/sessions/{session_id}/messages/send-image', headers=self.headers, json={ 'chatId': chat_id, 'image': {'url': image_url}, 'caption': caption } ) return response.json() # Usage client = OpenWA('http://localhost:2785/api', 'your-api-key') result = client.send_text('sess_abc123', '628123456789@c.us', 'Hello from Python!') print(f"Message ID: {result['data']['messageId']}") ``` ### WebSocket Client Example (JavaScript) ```javascript const ws = new WebSocket('ws://localhost:2785/ws?apiKey=your-api-key'); let pingInterval = null; ws.onopen = () => { ws.send(JSON.stringify({ type: 'subscribe', payload: { sessionId: 'sess_abc123', events: ['message.received', 'session.status'] }, requestId: 'req_001' })); pingInterval = setInterval(() => { ws.send(JSON.stringify({ type: 'ping', requestId: `ping_${Date.now()}` })); }, 30000); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'event') { console.log('Event:', msg.payload.event, msg.payload.data); } }; ws.onclose = () => { if (pingInterval) clearInterval(pingInterval); }; ``` ---
[โ† 05 - Database Design](./05-database-design.md) ยท [Documentation Index](./README.md) ยท [Next: 07 - API Collection โ†’](./07-api-collection.md)