API conventions
The rules that hold across every OpenWA endpoint: where the API lives, how you authenticate, what a response looks like, and which status codes you will hit. Read this once, then use the API reference for per-endpoint detail.
This documentation targets OpenWA v0.7.6.
Base URL and the /api prefix
Every REST route is mounted under a global /api prefix on port 2785.
http://localhost:2785/api
In local development that is http://localhost:2785/api. In production, OpenWA serves plain HTTP and expects you to terminate TLS at a reverse proxy; substitute your own origin and keep the /api prefix:
https://wa.example.com/api
All examples on this site use the local base URL.
Versioning
The API is not URL-versioned — there is no /v1 segment. The version is the OpenWA release you run. Check it any time with the public health endpoint:
curl http://localhost:2785/api/health
{ "status": "ok", "timestamp": "2026-06-25T12:34:56.789Z", "version": "0.7.6" }
Pin your deployment to a known release and read the field reference for that version. When a field or default is version-sensitive, this documentation says so.
Authentication
Every route is protected by an API-key guard unless it is explicitly public (health checks, and the metrics endpoint which uses a separate bearer token). Send your key in the X-API-Key request header:
curl http://localhost:2785/api/sessions \
-H "X-API-Key: YOUR_API_KEY"
YOUR_API_KEY is the key OpenWA seeds on first run (printed to the startup log and written to data/.api-key). Use that admin key to mint scoped, lower-privilege keys through the auth resource. The key is bearer-equivalent — anyone holding it can act as you — so never send it over plaintext http:// outside local development, and never put it in a URL.
A query-parameter API key (?apiKey=) is not accepted on REST routes. The header is the only way in.
For the full sign-in walkthrough and how to create scoped keys, see the authentication guide.
Roles
Each key carries one of three roles, enforced as a minimum-rank hierarchy (viewer < operator < admin):
| Role | Can do |
|---|---|
viewer | Read-only routes (list sessions, read message history, view contacts) |
operator | Everything a viewer can, plus write and action routes (send messages, manage groups, manage webhooks) |
admin | Everything, plus key management and global settings |
A key may also be scoped to specific sessions and source IPs. The scope and IP checks run before the role check, so a request outside a key's allowed sessions or IPs is rejected with 401 even when the role would otherwise pass.
Request format
Send a JSON body on POST, PUT, and PATCH requests, with a Content-Type header:
curl -X POST http://localhost:2785/api/sessions \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "my-bot" }'
Request bodies are strictly validated. A field that is not declared on the endpoint's schema is rejected — an unknown field returns 400, it is not silently ignored. Validation failures also return 400 with a message array listing each field-level problem.
Pagination
List endpoints that can return large result sets accept limit and offset query parameters. Defaults and ceilings are per-endpoint — for example, session chats and groups default to limit=1000, while message history defaults to limit=50 and clamps to a maximum of 100. There is no global pagination envelope; check each endpoint in the API reference for its exact bounds.
curl "http://localhost:2785/api/sessions/SESSION_ID/messages?limit=50&offset=0" \
-H "X-API-Key: YOUR_API_KEY"
Response format
OpenWA returns the raw handler payload — there is no { success, data, meta } wrapper. Read fields directly off the response.
A resource route returns the object as-is:
{
"id": "8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a",
"name": "my-bot",
"status": "ready"
}
Most list routes return a bare JSON array:
[
{ "id": "…", "name": "session-a" },
{ "id": "…", "name": "session-b" }
]
A few paginated list routes instead return a small wrapper such as { "messages": [...], "total": 42 }. The per-endpoint reference states the exact shape.
Timestamps
OpenWA uses two timestamp representations — know which a field is:
| Field kind | Representation | Example |
|---|---|---|
| Message timestamps | Unix epoch seconds (a number) | 1719312000 |
Entity audit fields (createdAt, updatedAt, expiresAt, startedAt) | ISO-8601 UTC string | 2026-06-25T09:20:00.000Z |
Error format
Errors use the NestJS default envelope. The HTTP status sits on the status line and is mirrored in statusCode; there is no application-specific error code field:
{
"statusCode": 404,
"message": "Session 'my-bot' not found",
"error": "Not Found"
}
For validation failures (400), message is an array of field-level strings instead of a single string:
{
"statusCode": 400,
"message": ["name must be longer than or equal to 3 characters"],
"error": "Bad Request"
}
Status codes
These are the codes you will actually encounter:
| Status | Meaning | When |
|---|---|---|
200 OK | Success | A successful GET, or a write that the controller maps to 200 |
201 Created | Created | A successful POST (the NestJS default for POST) |
202 Accepted | Accepted | A bulk send was queued for background processing |
204 No Content | Success, empty body | A successful DELETE |
400 Bad Request | Validation or precondition failed | A bad or unknown body field, an invalid value, or a business precondition such as "session not started" |
401 Unauthorized | Auth failed | Missing, invalid, expired, or revoked key; a blocked source IP; or a key used outside its session scope |
403 Forbidden | Role too low | A valid, in-scope key whose role is below the route's requirement |
404 Not Found | No such resource | The session, message, webhook, or batch does not exist |
409 Conflict | Uniqueness violation | A duplicate value, such as creating a session with a name that already exists |
413 Payload Too Large | Media too big | Base64 media exceeds the media byte cap (default 50 MiB) |
500 Internal Server Error | Server or engine error | The send failed at the WhatsApp engine, or an unexpected server error |
503 Service Unavailable | Not ready | The readiness probe failed, or the app is draining during shutdown |
A global rate limiter applies per client IP across three windows: 10 requests / 1 s, 100 / 60 s, and 1000 / 1 h. Exceeding any window returns 429 Too Many Requests with a Retry-After header. The limits are configurable through environment variables, and the health and metrics routes are exempt. See the configuration reference for the env overrides.
A worked request
Send a text message and read the response:
curl -X POST http://localhost:2785/api/sessions/SESSION_ID/messages/send-text \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "chatId": "628123456789@c.us", "text": "Hello from OpenWA!" }'
{ "messageId": "true_628123456789@c.us_3EB0ABCD", "timestamp": 1719312000 }
The response is the raw payload: messageId is the engine's WhatsApp message id, and timestamp is epoch seconds.
Request lifecycle
Every authenticated request passes the same guards before reaching a handler:
Next steps
- Browse every endpoint, field, and schema in the API reference.
- Get a key and connect a session in the quick start.
- Drive the API from JavaScript with the SDK.