Skip to main content
Version: v0.7.6

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.

REST auth is header-only

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):

RoleCan do
viewerRead-only routes (list sessions, read message history, view contacts)
operatorEverything a viewer can, plus write and action routes (send messages, manage groups, manage webhooks)
adminEverything, 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 kindRepresentationExample
Message timestampsUnix epoch seconds (a number)1719312000
Entity audit fields (createdAt, updatedAt, expiresAt, startedAt)ISO-8601 UTC string2026-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:

StatusMeaningWhen
200 OKSuccessA successful GET, or a write that the controller maps to 200
201 CreatedCreatedA successful POST (the NestJS default for POST)
202 AcceptedAcceptedA bulk send was queued for background processing
204 No ContentSuccess, empty bodyA successful DELETE
400 Bad RequestValidation or precondition failedA bad or unknown body field, an invalid value, or a business precondition such as "session not started"
401 UnauthorizedAuth failedMissing, invalid, expired, or revoked key; a blocked source IP; or a key used outside its session scope
403 ForbiddenRole too lowA valid, in-scope key whose role is below the route's requirement
404 Not FoundNo such resourceThe session, message, webhook, or batch does not exist
409 ConflictUniqueness violationA duplicate value, such as creating a session with a name that already exists
413 Payload Too LargeMedia too bigBase64 media exceeds the media byte cap (default 50 MiB)
500 Internal Server ErrorServer or engine errorThe send failed at the WhatsApp engine, or an unexpected server error
503 Service UnavailableNot readyThe readiness probe failed, or the app is draining during shutdown
Rate limiting

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.