API Overview
WAHooks REST API reference
Base URL: https://api.wahooks.com/api
Authentication
All endpoints (except health check) require authentication via the Authorization header:
Authorization: Bearer wh_your_api_tokenBoth API tokens (wh_...) and Supabase JWTs are accepted. See API Tokens for details.
Response format
All responses are JSON. Successful responses return the resource directly. Errors return:
{
"message": "Connection not found",
"error": "Not Found",
"statusCode": 404
}Status codes
| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created (POST requests) |
| 401 | Invalid or missing authentication |
| 403 | Forbidden (resource belongs to another user) |
| 404 | Resource not found |
| 503 | Service unavailable (worker still booting) |
Rate limiting
The API uses rate limiting to prevent abuse. If you exceed the rate limit, you'll receive a 429 response.
Connections
| Method | Endpoint | Description |
|---|---|---|
GET | /connections | List all connections |
POST | /connections | Create a new connection |
POST | /connections/get-or-create | Get a scannable connection (reuses idle or creates new) |
GET | /connections/:id | Get connection details |
PATCH | /connections/:id | Update connection (e.g. rename) |
DELETE | /connections/:id | Delete a connection |
POST | /connections/:id/restart | Restart a connection |
GET | /connections/:id/qr | Get QR code for linking |
GET | /connections/:id/chats | List chats (enriched: name, isGroup, lastMessage, unread) |
GET | /connections/:id/me | Get WhatsApp profile info |
GET | /connections/:id/media/:filename | Proxy a media file from the WAHA worker (streams the file) |
Message history & contacts (read)
| Method | Endpoint | Description |
|---|---|---|
GET | /connections/:id/chats | List chats — ?limit=&offset=&unread_only=true. Each item: id, name, isGroup, lastMessage {body,timestamp,fromMe}, unread |
GET | /connections/:id/chats/:chatId/messages | Page of messages, newest-first — ?limit=&before=<cursor> |
GET | /connections/:id/messages/:messageId?chatId= | A single message by id (chatId required) |
GET | /connections/:id/messages/:messageId/media?chatId= | Stream that message's media (chatId required) |
GET | /connections/:id/contacts | List contacts — ?limit=&offset=. Each item: jid, name, phoneNumber, isGroup |
Message shape (returned by the message endpoints):
{
"id": "true_111@g.us_ABC",
"chatId": "111@g.us",
"timestamp": 1782419739,
"fromMe": false,
"senderJid": "918383880642@s.whatsapp.net",
"senderPushName": "Alice",
"type": "text",
"text": "hello",
"quotedMessageId": null,
"media": null
}type is one of text, image, video, audio, document, location, contact. When
media is present, media.url is a WAHooks media-proxy URL you fetch with the same
Authorization: Bearer header.
Pagination. GET /chats/:chatId/messages returns:
{ "messages": [ /* newest-first */ ], "nextBefore": "MTA0", "historyStartsAt": null }Pass the returned nextBefore as ?before= to fetch the next (older) page. When
nextBefore is null you've reached the end, and historyStartsAt is set to the oldest
message's timestamp — use it to distinguish "no messages" from "history doesn't reach further".
These endpoints proxy WAHA's live NOWEB store. Media can return 404 for older messages — WhatsApp media keys expire and blobs live on the worker's local disk (no durable store in this phase). History reaches back only as far as WhatsApp's sync at pairing time. Server-side message search is not available.
Messaging
| Method | Endpoint | Description |
|---|---|---|
POST | /connections/:id/send | Send a text message |
POST | /connections/:id/send-image | Send an image (URL or base64) |
POST | /connections/:id/send-document | Send a document/file |
POST | /connections/:id/send-video | Send a video |
POST | /connections/:id/send-audio | Send audio/voice |
POST | /connections/:id/send-location | Send a location pin |
POST | /connections/:id/send-contact | Send a contact card |
POST | /connections/:id/react | React to a message with an emoji |
PUT | /connections/:id/messages/:messageId | Edit a sent message's text ({ chatId, text }) |
The send endpoint accepts an optional replyTo field (message ID) to quote a specific message.
Editing is subject to WhatsApp's rules: you can only edit your own messages, within ~15 minutes of sending. Later edits return 400. The send endpoints return the new message's id, which you pass to the edit endpoint.
All send endpoints include human-like presence by default (read receipt, typing indicator, random delay). Pass skipPresence: true in the request body to disable this when handling presence yourself.
Presence
| Method | Endpoint | Description |
|---|---|---|
POST | /connections/:id/mark-read | Send read receipt (blue ticks) |
POST | /connections/:id/typing | Start typing indicator |
POST | /connections/:id/typing/stop | Stop typing indicator |
All three accept { "chatId": "..." } in the request body.
Webhooks
| Method | Endpoint | Description |
|---|---|---|
GET | /connections/:cid/webhooks | List webhooks for a connection |
POST | /connections/:cid/webhooks | Create a webhook |
PUT | /webhooks/:id | Update a webhook |
DELETE | /webhooks/:id | Delete a webhook |
GET | /webhooks/:id/logs | Get delivery logs |
POST | /webhooks/:id/test | Send a test event |
API Tokens
| Method | Endpoint | Description |
|---|---|---|
GET | /tokens | List active tokens |
POST | /tokens | Create a new token |
DELETE | /tokens/:id | Revoke a token |
Real-time Events
| Method | Endpoint | Description |
|---|---|---|
WebSocket | /ws?token=wh_... | Real-time event stream. Receives all WAHA events for your connections. Auto-scoped to the authenticated user. |
Connect with any WebSocket client. Events arrive as JSON:
{
"event": "message",
"connectionId": "c1a2b3c4-...",
"payload": {
"from": "1234567890@c.us",
"body": "Hello"
},
"timestamp": "2026-03-27T12:00:00.000Z"
}When a message includes media (image, video, document, audio), the payload contains hasMedia: true and a media object with a proxied URL you can fetch directly:
{
"event": "message",
"connectionId": "c1a2b3c4-...",
"payload": {
"from": "1234567890@c.us",
"body": "Check this photo",
"hasMedia": true,
"media": {
"url": "https://api.wahooks.com/api/connections/c1a2b3c4-.../media/photo.jpeg",
"mimetype": "image/jpeg"
}
},
"timestamp": "2026-03-27T12:00:00.000Z"
}The media.url points to the media proxy endpoint (/connections/:id/media/:filename), which streams the file from the internal WAHA worker. The URL requires authentication (same Authorization header as other endpoints).
See the TypeScript SDK or Python SDK for client libraries.
Billing
| Method | Endpoint | Description |
|---|---|---|
GET | /billing/status | Subscription and slot status |
GET | /billing/can-create | Check if a new connection can be created |
PUT | /billing/slots | Set number of connection slots (charges immediately) |
POST | /billing/checkout | Create Stripe checkout session (first-time setup) |
POST | /billing/portal | Open Stripe customer portal |
Programmatic slot management
After completing checkout once (to add a payment method), you can scale slots entirely from code:
# Set to 10 slots — charges prorated difference immediately
curl -X PUT https://api.wahooks.com/api/billing/slots \
-H "Authorization: Bearer wh_..." \
-H "Content-Type: application/json" \
-d '{"quantity": 10}'Response:
{
"slots": 10,
"status": "upgraded",
"proratedAmount": 2.25,
"currency": "usd"
}status:"upgraded","downgraded", or"unchanged"proratedAmount: the amount charged (positive) or credited (negative) immediately- Maximum 100 slots per account by default. Contact support for higher limits.
- If payment fails, the request returns 400 and the subscription remains unchanged.
Other
| Method | Endpoint | Description |
|---|---|---|
GET | / | Health check (no auth required) |