Skip to content
Contents
Getting started

Authentication

Every request carries a bearer credential in the Authorization header. There is no second scheme and no alternative header — this page says exactly which credential to use where, and what happens when the wrong one arrives.

Bearer, and only bearer

Authentication is a single header. The server reads Authorization, matches the scheme case-insensitively, and takes the rest as the credential.

Header
Authorization: Bearer wa_live_<secret>

There is no x-api-key header

The string x-api-key appears in this platform's backend in exactly one place — the list of header names redacted from logs. No code path reads it as a credential. A request that sends only that header is a 401, and the 401 will not tell you why.

Two kinds of credential, one header

The same header carries either a workspace API key or a user access token. Which one you want depends on whether a machine or a person is acting.

CredentialShapeUsed for
API keywa_test_… / wa_live_…Server-to-server integrations. Names its own workspace and carries a fixed set of scopes.
User access tokeneyJhbGciOi…The console, and anything acting on behalf of a signed-in person. Short-lived, obtained from the login endpoint.

The server tells them apart by the credential's prefix, so you never declare which kind you are sending. A few endpoints accept only one of them: creating an API key and the QR pairing routes require a user token, because a machine credential has no person and no browser behind it.

Test and live keys

A key is created in one mode and stays in it. The mode is part of the credential, visible in its prefix, and it changes what the key is allowed to do.

ModePrefixDescription
testwa_test_Sandbox work. Required for every sandbox operation, including sending through a sandbox channel.
livewa_live_Production traffic. Refused by every sandbox operation.

A live key cannot touch the sandbox

Allocating, verifying, revoking a sandbox session and sending through a sandbox channel all refuse a live key with SANDBOX_TEST_KEY_REQUIRED. This is deliberate: it makes it impossible to send test traffic on a production credential by accident.

The X-Workspace-Id header

Every workspace-scoped endpoint needs to know which workspace it is acting in. Where that comes from depends on the credential.

  • With an API key: the workspace comes from the key, and the header is optional.
  • With a user access token: the header is required, because a person can belong to several workspaces. Omitting it is a 400 with WORKSPACE_CONTEXT_REQUIRED.
  • Sending a header that disagrees with an API key's own workspace is a 403, not a silent override.
curl
# An API key already names its workspace, so the header is optional...
curl https://whats.azzamkh.sa/api/v1/channels \
  -H "Authorization: Bearer $WA_API_KEY"

# ...but a USER access token belongs to a person, who may be in several.
curl https://whats.azzamkh.sa/api/v1/channels \
  -H "Authorization: Bearer <access token>" \
  -H "X-Workspace-Id: ws_..."

# Omitting it with a user token:
# 400 { "error": { "code": "WORKSPACE_CONTEXT_REQUIRED", ... } }

Scopes

An API key carries a fixed set of scopes chosen at creation. A request outside them is a 403 with the required permissions in details.

ScopeDescription
workspace.readdocs.pages.authentication.scopes.list.workspace.read
channels.readdocs.pages.authentication.scopes.list.channels.read
channels.managedocs.pages.authentication.scopes.list.channels.manage
messages.readdocs.pages.authentication.scopes.list.messages.read
messages.senddocs.pages.authentication.scopes.list.messages.send
contacts.readdocs.pages.authentication.scopes.list.contacts.read
contacts.managedocs.pages.authentication.scopes.list.contacts.manage
templates.managedocs.pages.authentication.scopes.list.templates.manage
campaigns.readdocs.pages.authentication.scopes.list.campaigns.read
campaigns.managedocs.pages.authentication.scopes.list.campaigns.manage
automations.managedocs.pages.authentication.scopes.list.automations.manage
webhooks.managedocs.pages.authentication.scopes.list.webhooks.manage

The default set has one hole in it

Omitting scopes, or sending an empty array, grants every scope except webhooks.manage. A key that needs to manage webhook endpoints must ask for that scope explicitly.

Creating a key

POST/v1/api-keys

Keys are created by a signed-in user, never by another key: a compromised key cannot mint itself a wider one.

curl
# A key can only be minted by a signed-in user, never by another key.
curl -X POST https://whats.azzamkh.sa/api/v1/api-keys \
  -H "Authorization: Bearer <access token>" \
  -H "X-Workspace-Id: ws_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Order service (test)",
    "mode": "test",
    "scopes": ["messages.send", "messages.read"]
  }'

# 201 Created — "secret" appears in THIS response and nowhere else.
# {
#   "id": "key_...",
#   "name": "Order service (test)",
#   "mode": "test",
#   "prefix": "wa_test",
#   "last4": "9f3a",
#   "scopes": ["messages.send", "messages.read"],
#   "expires_at": null,
#   "secret": "wa_test_..."
# }

The secret is shown once

The full credential appears in the creation response and nowhere else. Only a hash is stored, so it cannot be recovered — if it is lost, revoke the key and create another.

Rotating and revoking

There is no rotation endpoint. Rotation is create-then-revoke, which is the safer shape anyway: the old key keeps working while the new one is deployed.

DELETE/v1/api-keys/{keyId}
curl
curl -X DELETE https://whats.azzamkh.sa/api/v1/api-keys/key_... \
  -H "Authorization: Bearer <access token>" \
  -H "X-Workspace-Id: ws_..."

# 200 OK — repeating this returns the ORIGINAL revoked_at, not an error.
# { "id": "key_...", "revoked_at": "2026-08-13T10:00:00.000Z" }

In order:

  • Create a new key with the same scopes and mode.
  • Deploy it, and confirm traffic is flowing on it.
  • Revoke the old key. Revocation takes effect immediately, and repeating it is safe: it returns the original revocation time rather than an error.

Authentication failures

Every failure follows the standard envelope described in errors and limits.

HTTPCodeWhen it happens
401UNAUTHORIZEDNo credential, an unknown one, a revoked one, an expired one, or a key whose workspace is not active. These are deliberately indistinguishable.
403FORBIDDENA valid credential that is not allowed here — a missing scope, or an API key on a route that requires a user session.
400WORKSPACE_CONTEXT_REQUIREDA user access token with no workspace named in the path or the header.
403ACCOUNT_SUSPENDEDThe user account is suspended.
403WORKSPACE_SUSPENDEDThe workspace is suspended.

Handling keys

  • Read keys from the environment. Never commit one, and never put one in client-side code.
  • The API is a server-to-server surface. A key in a browser is a key you have published.
  • Ask for the scopes you use and no more. A send-only integration does not need contacts.manage.
  • Build against a test key. Switching to live should be a configuration change, not a code change.