# Authentication

API keys, abilities, live and test, and how to keep keys safe.

> **In a nutshell:** Send your API key as a Bearer token on every request. sk_live_ keys work on real businesses, sk_test_ keys on sandbox businesses only. Each key has abilities; a request without the needed ability gets 403 missing_ability.

## API keys

Every request carries one of your product's API keys:

```http
Authorization: Bearer sk_live_4gVx...
Accept: application/json
```

- **`sk_live_...`** keys work on your real businesses. **`sk_test_...`** keys work on
  sandbox businesses only - a separate set, with simulated WhatsApp that never reaches
  Meta (see [Testing](/docs/testing)). The two never see each other's businesses.
- Keys are created by a PaalChat operator and shown **once**. PaalChat stores only a
  hash and the first characters (shown as `prefix`), so a lost key cannot be recovered -
  it is replaced instead. Revoked or expired keys stop working at once (`401`).
- A key can be limited to your servers' IP addresses; from anywhere else it is refused.
- The prefixes let secret scanners spot leaked keys. Older `<id>|ptw_...` tokens still
  work, as live keys.
- Operator console logins do not work on the API; only API keys do.
- If your product is suspended, every key is refused with `403 product_suspended`.

> [!WARNING]
> Don't use your token in a frontend. Never call PaalChat from a browser or mobile
> app: all requests must come from your server, and your frontend talks to your server.

## Abilities

Each token has abilities. Ask only for the ones your integration needs.

| Ability | Grants |
|---|---|
| `businesses.read` | [List businesses](/docs/api/list-businesses), [Get business](/docs/api/get-business) |
| `businesses.write` | [Create or update business](/docs/api/upsert-business) |
| `connections.read` | [WhatsApp state](/docs/api/get-whatsapp), [phone numbers](/docs/api/list-phone-numbers) |
| `connections.manage` | [Create connect link](/docs/api/create-connect-link), [Disconnect WABA](/docs/api/disconnect-waba) |
| `templates.read` | [List templates](/docs/api/list-templates) |
| `templates.manage` | [Create template](/docs/api/create-template), [Delete template](/docs/api/delete-template) |
| `messages.read` | [Get message](/docs/api/get-message) |
| `messages.send` | [Send message](/docs/api/send-message) |
| `contacts.read` | [List contacts](/docs/api/list-contacts), [Get contact](/docs/api/get-contact), [contact fields](/docs/api/list-contact-fields) |
| `contacts.write` | [Create or update contact](/docs/api/upsert-contact), [Update contact](/docs/api/update-contact), [Define a contact field](/docs/api/define-contact-field) |
| `conversations.read` | [List conversations](/docs/api/list-conversations), [Get conversation](/docs/api/get-conversation), [List notes](/docs/api/list-conversation-notes) |
| `businesses.read` (also) | [Get usage](/docs/api/get-usage) |
| `messages.send` (also, test keys) | [Simulate an incoming message](/docs/api/sandbox-incoming), [Simulate a delivery status](/docs/api/sandbox-status) |
| `messages.urgent` | `urgent: true` on [Send message](/docs/api/send-message) - skip quiet hours |
| `data.manage` | [Export business data](/docs/api/create-export), [Get export](/docs/api/get-export), [Erase contact](/docs/api/erase-contact) |
| `campaigns.read` / `campaigns.manage` | [Campaigns](/docs/campaigns): list, get, preview / create, launch, pause, resume, cancel |
| `automations.read` / `automations.manage` | [Automations](/docs/automations): list, get, runs / create, update, delete |
| `billing.manage` | [Get billing](/docs/api/get-billing), [Pay invoice](/docs/api/pay-invoice) |
| `conversations.manage` | [Update conversation](/docs/api/update-conversation), [Add note](/docs/api/add-conversation-note) |
| `inbox.manage` | [Sign staff in to the inbox](/docs/api/inbox-sign-in), [List inbox members](/docs/api/list-inbox-members), [Remove inbox access](/docs/api/disable-inbox-member) |

`webhooks.read` / `webhooks.manage` are reserved for
endpoints that are coming; a token can hold them already.

> [!NOTE]
> Until 1 October 2026 the scopes were `businesses:read`, `businesses:write`,
> `whatsapp:read`, `whatsapp:connect` and `whatsapp:send`. Tokens issued with them
> were converted automatically (`whatsapp:read` became `connections.read`,
> `templates.read` and `messages.read`); `GET /me` shows a token's current scopes.

## Check a token

[`GET /me`](/docs/api/get-me) returns your product and the token's abilities. Call it
when your app starts and fail loudly if an ability you need is missing.

## Issuing, rotating and revoking

Operators manage tokens on the PaalChat server:

```bash
php artisan products:token multi-skuul --name=production --ability=messages.send --ability=connections.read --ability=templates.read
php artisan products:token multi-skuul --name=production --all --rotate   # replace it
php artisan products:token multi-skuul --list
php artisan products:token multi-skuul --revoke=12
```

Rotate immediately if a token may have leaked.

## Errors

| HTTP | `error.code` | Meaning |
|---|---|---|
| 401 | `unauthenticated` | Missing, wrong, revoked or expired token |
| 403 | `missing_ability` | The token lacks the ability for this endpoint |
| 403 | `product_suspended` | Your product is suspended on PaalChat |
| 403 | `product_token_required` | The credential is not a product token |
