Authenticate requests with an API key
Every OpenWA REST route is protected by an API key sent in the X-API-Key header. This guide shows you how to send that key, how a key's role and scope decide what it can do, and how to mint, inspect, and revoke keys through the API.
- A running OpenWA instance — see Installation.
- The seeded admin key from first boot (covered in The first admin key).
curl, or the JS SDK (@rmyndharis/openwa).
Send the key on every request
Authenticate by adding the X-API-Key header to each request. The base URL in these examples is http://localhost:2785/api (the /api global prefix on the default port 2785); in production, OpenWA sits behind your own domain and TLS.
curl http://localhost:2785/api/sessions \
-H "X-API-Key: YOUR_API_KEY"
A successful call returns the resource:
[
{ "id": "main", "status": "connected", "createdAt": "2026-06-25T09:30:00.000Z" }
]
Replace YOUR_API_KEY with a real key — a full key looks like owa_k1_ followed by 64 hex characters. Add Content-Type: application/json only when you send a request body. The same key authenticates the JS SDK:
import { OpenWAClient } from "@rmyndharis/openwa";
const client = new OpenWAClient({
baseUrl: "http://localhost:2785",
apiKey: "YOUR_API_KEY",
});
const sessions = await client.sessions.list();
OpenWA accepts the key as Authorization: Bearer YOUR_API_KEY in addition to X-API-Key. Use X-API-Key as the canonical form so every example on this site stays consistent.
The first admin key
On its first boot, when no keys exist yet, OpenWA seeds one admin key. It is printed to the startup log and written to data/.api-key (mode 0600; /app/data/.api-key in Docker). On later restarts the log shows only a masked fingerprint — the full key stays in that file and in the dashboard.
Use this admin key to mint scoped, lower-privilege keys for your integrations, then keep the admin key for management only. If you set API_MASTER_KEY in your environment, OpenWA seeds that value instead of a random one — see Configuration.
Roles
Every key carries exactly one role. Roles are hierarchical — a higher role satisfies any route that requires a lower one, so an admin key passes an operator-guarded route.
| Role | Rank | Can do |
|---|---|---|
viewer | 1 | Read-only routes — list sessions, read messages, view contacts and groups. |
operator | 2 | Everything viewer can, plus write and action routes — send messages, manage groups, manage webhooks. |
admin | 3 | Everything, plus admin-only routes — API-key management. |
When role is omitted at creation, the key defaults to operator.
A key whose role is below what a route requires gets 403 Forbidden. This is distinct from 401 Unauthorized, which means the key itself was missing, invalid, revoked, expired, or out of scope. See Errors.
Scope a key to sessions and IPs
Beyond its role, a key can be narrowed so it works only for certain sessions or from certain source IPs. Both fields are optional; an empty or absent list means "no restriction" for that dimension.
| Field | Type | Effect |
|---|---|---|
allowedSessions | string[] | Session ids the key may touch. A scoped key acts only on these sessions; list endpoints return only their data. |
allowedIps | string[] | Allowlist of exact IPs and CIDR ranges (for example 203.0.113.50, 10.0.0.0/8). |
Scope is enforced during key validation, before the role check. A request from a session or IP outside the key's allowlist is rejected with 401 Unauthorized — even if the role would otherwise permit it. A non-empty allowedIps fails closed: if the client IP can't be determined, the request is also rejected.
The client IP is taken from the socket unless the request arrives from a host listed in TRUSTED_PROXIES, in which case X-Forwarded-For is honored. If you run OpenWA behind a reverse proxy and use allowedIps, set TRUSTED_PROXIES so the real client IP is resolved. See Configuration.
Manage keys
API keys are managed under /api/auth/api-keys. Every management route requires an admin key.
A new key's full plaintext value is returned only in the creation response, under apiKey. Store it securely right away — it is hashed at rest and cannot be retrieved later. List and get responses return only keyPrefix (the first 12 characters, for example owa_k1_01234), never the full key.
Create a key
POST /api/auth/api-keys
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | Yes | Friendly label. |
role | "admin" | "operator" | "viewer" | No | Defaults to operator. |
allowedIps | string[] | No | IP / CIDR allowlist. |
allowedSessions | string[] | No | Session-id allowlist. |
expiresAt | string (ISO 8601) | No | Key stops validating after this time. |
curl -X POST http://localhost:2785/api/auth/api-keys \
-H "X-API-Key: YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Production Bot",
"role": "operator",
"allowedIps": ["203.0.113.50", "10.0.0.0/8"],
"allowedSessions": ["main"],
"expiresAt": "2027-12-31T23:59:59Z"
}'
The 201 Created response carries the one-time plaintext key in apiKey:
{
"id": "3f2a1c9e-1b2d-4a5f-9c8e-aa11bb22cc33",
"name": "Production Bot",
"keyPrefix": "owa_k1_01234",
"role": "operator",
"allowedIps": ["203.0.113.50", "10.0.0.0/8"],
"allowedSessions": ["main"],
"isActive": true,
"expiresAt": "2027-12-31T23:59:59.000Z",
"usageCount": 0,
"createdAt": "2026-06-25T09:30:00.000Z",
"apiKey": "owa_k1_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
Copy apiKey now. Every later response for this key omits it and returns only keyPrefix.
List, get, update, revoke, delete
| Operation | Request | Result |
|---|---|---|
| List all keys | GET /api/auth/api-keys | 200 array of keys (no plaintext). |
| Get one key | GET /api/auth/api-keys/{id} | 200 single key. |
| Update mutable fields | PUT /api/auth/api-keys/{id} | 200 updated key. |
| Revoke (deactivate) | POST /api/auth/api-keys/{id}/revoke | 200, sets isActive: false. |
| Delete permanently | DELETE /api/auth/api-keys/{id} | 204 No Content. |
PUT accepts the same fields as create (all optional): name, role, allowedIps, allowedSessions, expiresAt. Send only the fields you want to change. To replace a key's IP allowlist, PUT it with the new allowedIps array:
curl -X PUT http://localhost:2785/api/auth/api-keys/3f2a1c9e-1b2d-4a5f-9c8e-aa11bb22cc33 \
-H "X-API-Key: YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{ "allowedIps": ["203.0.113.50"] }'
To take a key out of service, revoke rather than delete it — revoke flips isActive to false (the key then fails with 401) while keeping the record:
curl -X POST http://localhost:2785/api/auth/api-keys/3f2a1c9e-1b2d-4a5f-9c8e-aa11bb22cc33/revoke \
-H "X-API-Key: YOUR_ADMIN_KEY"
{
"id": "3f2a1c9e-1b2d-4a5f-9c8e-aa11bb22cc33",
"name": "Production Bot",
"keyPrefix": "owa_k1_01234",
"role": "operator",
"isActive": false,
"usageCount": 412,
"createdAt": "2026-06-25T09:30:00.000Z"
}
Use DELETE only when you want the record gone entirely.
Check a key with /auth/validate
Any valid key can confirm its own validity and resolve its role. This is the quickest way to test that a key works and to read its role from a client.
POST /api/auth/validate
curl -X POST http://localhost:2785/api/auth/validate \
-H "X-API-Key: YOUR_API_KEY"
{ "valid": true, "role": "operator" }
A missing or invalid key returns 401 Unauthorized rather than { "valid": false }, because the global guard rejects it before this handler runs. The SDK wraps the same call:
const { valid, role } = await client.auth();
// { valid: true, role: "operator" }
Authorize Swagger UI
The bundled Swagger UI (served by your instance) uses the same X-API-Key scheme. Click Authorize, paste your key, and every "Try it out" request carries it. There is no separate token — it is the same key you send with curl or the SDK.
Errors
| Status | When | Fix |
|---|---|---|
401 Unauthorized | Key missing, invalid, revoked (isActive: false), expired, or outside allowedIps / allowedSessions. | Send a valid, active key from an allowed IP and session. Check expiresAt. |
403 Forbidden | Key is valid but its role rank is below the route's requirement (for example a viewer key on a send route, or any non-admin key on /api/auth/api-keys). | Use a key with a sufficient role, or raise the key's role with PUT. |
404 Not Found | The {id} in a management route does not match an existing key. | List keys to find the correct id. |
A scope violation deliberately returns 401, not 403 — the request is treated as unauthenticated for that resource, so a session- or IP-restricted key reveals nothing about routes it cannot reach.
Next steps
- Create your first session with an operator key.
- Set up webhooks — operator-level routes that consume these keys.
- Configuration —
API_MASTER_KEY,TRUSTED_PROXIES, and the seeded key. - API reference — the complete auth endpoint list and field schemas.