WhatsApp API reference
WApenda turns your own WhatsApp number into a REST API. Send messages and media, manage groups and contacts, and receive every event on your webhook. The API is compatible with WaSender API — most integrations only change the base URL and key.
Base URL https://tekservix.com/wapenda/api
Format JSON (UTF-8) in and out
Auth Authorization: Bearer <key>
Authentication
There are two kinds of keys, both sent as a Bearer token:
| Key | Looks like | Use it for |
|---|---|---|
| Device API key | wpk_… | Everything a single WhatsApp number does: sending, contacts, groups, status. Find it under Devices → API key. |
| Personal access token | wpt_… | Managing devices across your account (/whatsapp-sessions). Create it under API & tokens. |
curl https://tekservix.com/wapenda/api/status -H "Authorization: Bearer wpk_your_device_key"
Responses & errors
Every response has success. Successful calls return data; failures return message (and errors for invalid fields).
{ "success": true, "data": { "msgId": 58213, "jid": "250788123456@s.whatsapp.net", "status": "in_progress" } }
{ "success": false, "message": "\"to\" is required (phone number in international format, or a group/channel JID).", "errors": { "to": "required" } }
| HTTP | Meaning |
|---|---|
| 400 / 422 | Invalid request — check message and errors. |
| 401 | Missing or wrong key. |
| 402 | Your plan is inactive or doesn’t cover this device. |
| 403 | Wrong key type, or the device is suspended. |
| 404 | Unknown endpoint or object. |
| 409 | The device isn’t linked (scan the QR code first), or the action doesn’t fit the current state. |
| 429 | Rate limit reached — see Retry-After. |
| 503 | The device is reconnecting or the service is restarting. Retry shortly. |
Limits & account protection
- Requests: 300 per minute per key (
X-RateLimit-Remainingheader). - Messages: unlimited on every plan. Sends are queued and go out one at a time per device, so
send-messageanswers immediately with"status": "in_progress"; track delivery with webhooks or message info. - Account protection (on by default, per device): random 5–12 s gaps between messages (1–3 s when replying inside a live conversation), a typing indicator, a warm-up for newly linked numbers (60 messages on day 1 rising to 1,500/day over 5 days), 250/hour, no identical message to the same person within 10 minutes, and a check that each new number is on WhatsApp. Turning it off allows ~1–2.5 s gaps and 10,000/day — only do that for opted-in traffic on an established number.
- Media: up to 64 MB per file (WhatsApp itself limits images/video to ~16 MB).
Send a message
/send-messageOne endpoint sends every message type. The aliases /send-image-message, /send-video-message, /send-document-message, /send-audio-message, /send-sticker-message, /send-contact-card, /send-location, /send-poll-message, /send-quoted-message, /send-message-with-mentions and /send-view-once-message accept the same body.
| Field | Type | Description |
|---|---|---|
to * | string | Phone number in international format (+250788123456 or 250788123456), a group JID (…@g.us) or a channel JID (…@newsletter). |
text | string | Message text, or the caption for image/video/document. |
imageUrl · videoUrl · audioUrl · documentUrl · stickerUrl | URL | Public https link to the file (or a link from /upload). Stickers must be .webp. |
fileName | string | Name shown for documents. |
mimeType | string | Optional override (detected automatically). |
ptt | boolean | Send audio as a voice note. |
contact | object | {"name": "Aline", "phone": "+250788123456"} |
location | object | {"latitude": -1.9441, "longitude": 30.0619, "name": "…", "address": "…"} |
poll | object | {"question": "…", "options": ["A","B"], "selectableCount": 1} (2–12 options) |
replyTo | string/int | Quote an earlier message: its msgId or WhatsApp message id. |
mentions | array | Phone numbers to @mention (also write @250788123456 in the text). |
viewOnce | boolean | Image/video/audio can be opened once. |
curl -X POST https://tekservix.com/wapenda/api/send-message \
-H "Authorization: Bearer $WAPENDA_KEY" -H "Content-Type: application/json" \
-d '{"to":"+250788123456","text":"Hello Aline, your order is ready 🎉"}'
{ "success": true, "data": { "msgId": 58213, "jid": "250788123456@s.whatsapp.net", "status": "in_progress" } }
Examples by type
// Image with caption
{ "to": "+250788123456", "text": "New arrivals 👗", "imageUrl": "https://example.com/catalogue.jpg" }
// Document
{ "to": "+250788123456", "text": "Your invoice", "documentUrl": "https://example.com/inv-1042.pdf", "fileName": "Invoice-1042.pdf" }
// Voice note
{ "to": "+250788123456", "audioUrl": "https://example.com/hello.ogg", "ptt": true }
// Location
{ "to": "+250788123456", "location": { "latitude": -1.9441, "longitude": 30.0619, "name": "Our shop" } }
// Contact card
{ "to": "+250788123456", "contact": { "name": "Support", "phone": "+250788000000" } }
// Poll
{ "to": "120363000000000000@g.us", "poll": { "question": "Best delivery day?", "options": ["Monday", "Friday"] } }
// Reply to a message, with a mention
{ "to": "120363000000000000@g.us", "text": "Thanks @250788123456!", "replyTo": 58190, "mentions": ["250788123456"] }
// Older WaSender style (base64) is accepted too
{ "to": "+250788123456", "document": { "data": "JVBERi0xLjQK…", "filename": "report.pdf" }, "caption": "Report" }
Info, edit, delete, resend
{id} is the msgId returned when sending (or a WhatsApp message id).
/messages/{id}/infoStatus (queued, sent, delivered, read, played, failed), error, WhatsApp key and timestamps.
/messages/{id}Edit your own text message: {"text": "corrected text"} (WhatsApp allows about 15 minutes).
/messages/{id}Delete for everyone.
/messages/{id}/resendQueue a failed message again.
Mark as read
/messages/read{ "msgId": "3EB0X123456789", "jid": "250788123456@s.whatsapp.net" }
// or the key from a webhook
{ "key": { "id": "3EB0X123456789", "remoteJid": "250788123456@s.whatsapp.net", "fromMe": false } }
Typing indicator
/send-presence-update{ "jid": "+250788123456", "type": "typing" } // typing | recording | paused | available | unavailable
Upload & decrypt media
/uploadMultipart file, or JSON {"base64": "…", "fileName": "photo.jpg"}. Returns a publicUrl to use in imageUrl/documentUrl… Files are kept for 48 hours.
/decrypt-mediaSend the webhook payload of a received image, video, audio or document ({"data": {"messages": {…}}}) and get a temporary publicUrl to the decrypted file.
Contacts
/on-whatsapp/{phone}{"exists": true, "jid": "…@s.whatsapp.net"} — check before you message a new number.
/contactsContacts synced from the phone, plus people who messaged you.
/contacts/{phone}/contacts{"phone": "+250788123456", "name": "Aline"} — save a name in WApenda.
/contacts/{phone}/picture/contacts/{phone}/block · /unblock/lid-from-pn/{phone} · /pn-from-lid/{lid}WhatsApp sometimes identifies people by a private “LID” instead of a phone number. Webhooks always include cleanedSenderPn when the phone number is known, and you can reply to either form.
Groups
/groups/groups{"subject": "VIP customers", "participants": ["250788123456", "250722000000"]}
/groups/{groupJid}/metadata · /participants · /picture · /invite-link/groups/{groupJid}/participants/add · /participants/remove{"participants": ["250788123456"]} (you must be an admin)
/groups/{groupJid}/participants/update{"participants": ["250788123456"], "action": "promote"} — or demote.
/groups/{groupJid}/settings{"subject": "…", "description": "…", "announce": true, "restrict": false} — announce: only admins send; restrict: only admins edit info.
/groups/invite/{code}/groups/invite/accept{"inviteCode": "AbCdEf123"}
/groups/{groupJid}/leaveStatus & user
/status{"status": "connected"} — one of connected, connecting, need_scan, disconnected, logged_out, expired.
/userThe linked WhatsApp account: {"id", "number", "name"}.
Manage devices (personal access token)
/whatsapp-sessionsYour devices, including each api_key and webhook_secret.
/whatsapp-sessions{ "name": "Sales line", "webhook_url": "https://your-app.com/wa", "webhook_events": ["messages.received","messages.update"],
"account_protection": true, "log_messages": true, "read_incoming_messages": false, "auto_reject_calls": false }
/whatsapp-sessions/{id}/whatsapp-sessions/{id}/whatsapp-sessions/{id}/whatsapp-sessions/{id}/connect{"method": "qr"}, or {"method": "code", "phone": "250788123456"} for an 8-character pairing code. Then poll:
/whatsapp-sessions/{id}/qrcode{"qrCode": "2@…", "qrImage": "data:image/png;base64,…", "pairingCode": null, "status": "need_scan"} — the QR changes every ~20 s.
/whatsapp-sessions/{id}/disconnect · /restart · /regenerate-key/whatsapp-sessions/{id}/message-logs?page=1&limit=20 · /session-logsA device key may also call these for its own device id.
Webhooks
Set a webhook URL per device (dashboard or PUT /whatsapp-sessions/{id}). We send a POST with JSON for each event you selected, and retry 3 times (after ~4 s and 30 s) unless you answer 2xx. Answer quickly and process in the background.
POST https://your-app.com/wa
Content-Type: application/json
X-Webhook-Signature: <your device's webhook secret>
X-WApenda-Event: messages.received
{ "event": "messages.received", "sessionId": 12, "timestamp": 1758822000,
"data": { "messages": {
"key": { "id": "3EB0X123456789", "fromMe": false, "remoteJid": "250788123456@s.whatsapp.net",
"addressingMode": "pn", "senderPn": "250788123456@s.whatsapp.net", "cleanedSenderPn": "250788123456" },
"messageBody": "Hello, is this available?", "pushName": "Aline", "type": "text",
"message": { "conversation": "Hello, is this available?" } } } }
Verify the request came from WApenda:
// Node.js (Express)
app.post('/wa', express.json(), (req, res) => {
if (req.get('X-Webhook-Signature') !== process.env.WAPENDA_WEBHOOK_SECRET) return res.sendStatus(401);
res.sendStatus(200); // acknowledge first
handle(req.body).catch(console.error); // then do the work
});
// PHP
if (!hash_equals(getenv('WAPENDA_WEBHOOK_SECRET'), $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '')) { http_response_code(401); exit; }
$event = json_decode(file_get_contents('php://input'), true);
Events
| Event | When |
|---|---|
messages.received | Any incoming message (chats, groups, channels). |
messages-personal.received · messages-group.received · messages-newsletter.received | Incoming, split by chat type. |
messages.upsert | Any new message, incoming or sent (including ones sent from the phone). |
message.sent | A message you queued was sent. Includes msgId and the WhatsApp key. |
messages.update | Status change: update.status 0 error, 2 sent, 3 delivered, 4 read, 5 played. Failed sends include update.error and your msgId. |
message-receipt.update | Per-recipient receipts in groups. |
messages.reaction · messages.deleted | Someone reacted to or deleted a message. |
session.status | {"status": "connected" | "connecting" | "need_scan" | "disconnected" | "logged_out" | "expired"} |
qrcode.updated | A new QR (qr) or pairing code (pairingCode) is ready. |
contacts.upsert · contacts.update | Contacts synced or changed. |
chats.upsert · chats.update · chats.delete | Chat list changes. |
groups.upsert · groups.update · group-participants.update | Joined a group, settings changed, members added/removed/promoted. |
call.received | Incoming voice/video call (from, isVideo). Turn on “Reject calls” to decline automatically. |
WaSender compatibility
Coming from WaSender API? Change the base URL and use your WApenda key:
- https://www.wasenderapi.com/api
+ https://tekservix.com/wapenda/api
| WaSender | WApenda |
|---|---|
POST /send-message and the send-* aliases with to, text, imageUrl, videoUrl, documentUrl, audioUrl, stickerUrl, contact, location, poll, replyTo, mentions, viewOnce | Same |
Response {"msgId", "jid", "status": "in_progress"} | Same |
/messages/{id}/info, edit, delete, resend, /messages/read, /send-presence-update, /upload, /decrypt-media | Same |
/contacts…, /on-whatsapp, /lid-from-pn, /pn-from-lid, /groups… | Same |
/whatsapp-sessions… with a personal access token | Same (connect supports qr and phone pairing codes) |
Webhook events, X-Webhook-Signature, data.messages.key.cleanedSenderPn | Same event names and fields; sessionId is your device id |
Not available: Passkey (WebAuthn) linking — use the QR code or a phone pairing code.