---
title: "auth.md"
description: "How agents discover, register, claim, and use Wabery credentials."
canonical: "https://wabery.com/auth/"
last-updated: "2026-11-01"
---

# auth.md

This file tells AI agents how to get Wabery access for a person (`agent_auth`). The person does not need an account yet: the approval page signs them up, creates their first project, and returns them to approve. Humans can also create keys in the dashboard.

## Discover

- REST resource: `https://api.wabery.com/v1`
- Protected resource metadata: https://api.wabery.com/.well-known/oauth-protected-resource
- Authorization server: https://app.wabery.com
- Authorization server metadata: https://app.wabery.com/.well-known/oauth-authorization-server (read `token_endpoint`, `revocation_endpoint`, and the `agent_auth` block)
- MCP resource: `https://mcp.wabery.com`
- Skill: this document at https://wabery.com/auth.md

Unauthenticated probes to the API return `401` with `WWW-Authenticate: Bearer resource_metadata="https://api.wabery.com/.well-known/oauth-protected-resource"`.

## Pick a method

`identity_types_supported` is `["service_auth"]`. Ask the person for the email they use, or want to use, for Wabery. `anonymous` and `identity_assertion` are not enabled.

Use the person's own email, not a mailbox you created: the account belongs to that address. Connecting WhatsApp needs the number's owner (Facebook login and phone), so send them the link from `POST /v1/onboarding/handoffs` (see Finish onboarding).

## Register

```http
POST https://app.wabery.com/api/agent/identity
Content-Type: application/json

{"type": "service_auth", "login_hint": "you@example.com", "agent_name": "Claude Code"}
```

`agent_name` is optional and is shown on the approval page. `scope` is optional: a space-separated subset of `post_claim_scopes`. Omit it for the default agent scopes.

```json
{
  "registration_id": "reg_...",
  "type": "service_auth",
  "claim_token": "clm_...",
  "claim_token_expires": "2026-10-23T12:15:00.000Z",
  "post_claim_scopes": ["projects:read", "projects:write", "..."],
  "claim": {
    "user_code": "123456",
    "verification_uri": "https://app.wabery.com/authorize-agent",
    "verification_uri_complete": "https://app.wabery.com/authorize-agent?code=123456",
    "expires_in": 900,
    "interval": 5
  }
}
```

Keep `claim_token` secret. It is the only way to collect the credential.

## Claim

1. Show the person `verification_uri_complete` (or `verification_uri`) and `user_code` in one message.
2. The person opens the link and signs in or creates an account with the `login_hint` email. A new account gets a workspace and an API-first project.
3. The person checks the code, picks a project, and approves or declines. Only workspace owners and admins can approve.
4. Poll the token endpoint, no faster than `interval` seconds:

```http
POST https://app.wabery.com/api/mcp/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=clm_...
```

While the person has not answered, the response is `400` with `{"error": "authorization_pending"}`. After approval:

```json
{
  "access_token": "wab_live_...",
  "token_type": "Bearer",
  "scope": "projects:read projects:write ...",
  "project_id": "...",
  "project_name": "My business",
  "api_base_url": "https://api.wabery.com/v1"
}
```

The token is returned once and does not expire on a timer. Store it in a secret store or the `WABERY_API_KEY` environment variable and reuse it; never commit it. Stop polling after `expires_in` seconds. If the person has not answered, tell them the link expired and register again only when they ask. Start a new registration only if a request returns `401`.

If the person signs in with a different email than `login_hint`, or is not an owner or admin of the workspace, they cannot approve. The claim then ends with `expired_token`; ask them which email to use and register again.

## Use the credential

```http
GET https://api.wabery.com/v1/me
Authorization: Bearer wab_live_...
```

The key is scoped to `project_id` from the token response. Use it for REST, the `wabery` CLI (`WABERY_API_KEY`), or the local MCP server (`npx @wabery/cli mcp` with `WABERY_API_KEY` set). Hosted MCP at `https://mcp.wabery.com` uses its own OAuth sign-in instead of this key.

Omit `scope` at registration when you will set up and publish for the user. The default includes `projects:write` for the webhook, `config:write` for the hosted agent, `channels:write` for connection links, and `agents:publish`; narrowing it can block those steps. A `403 insufficient_scope` means the key lacks a scope; register again with the scopes you need.

The `wabery login --email you@example.com` command runs this whole flow for you.

## Finish onboarding

In this section, **the user** is the person you work for who approved your access. Their **customers** are the people who message their WhatsApp number. If the user is an agency, the agency confirms publishes and passes links to their client as needed.

Every remaining setup step works from the API. Send `Authorization: Bearer wab_live_...` on every request below, and read the checklist again after each step:

```http
GET https://api.wabery.com/v1/onboarding
Authorization: Bearer wab_live_...
```

The response has `status` (`in_progress` or `live`), `next_step`, `routing_mode`, and ordered `steps`. Each step has an `id`, a `status` (`done`, `todo`, `waiting_on_customer`, `blocked`), the `actor` who acts next (`agent` means you, `customer` means the user), plain-text `instructions`, `api` hints, a `customer_link` to send the user when there is one, and `details` such as Meta requirements. Onboarding is finished when `status` is `live`.

### Choose who answers messages

Ask the user before you build anything. There are two options:

- **Their own bot answers** (`EXTERNAL`, the default for projects created through agent sign-up): Wabery forwards each inbound message to a public HTTPS webhook and sends the replies the bot makes through the API. Wabery does not run the bot's server; the user hosts it, or you build the logic in Wabery with hosted functions or the hosted agent. Message payloads and signature verification: https://wabery.com/docs/webhooks/.
- **Wabery's hosted AI agent answers** (`AGENT`): switch with `PATCH /v1/projects/{project_id}` and `{"routing_mode": "AGENT"}`. The checklist then includes the `publish_agent` step.

Switching the project's `routing_mode` applies immediately to every channel in the project and to the user's sandbox messages, which reach the hosted agent's Draft only while the project is `AGENT`. Use this order:

- **No live number yet:** switch the project to `AGENT` first, then build the Draft, test it on the sandbox, connect the number, and publish.
- **Number already live on the webhook:** to avoid an outage, switch the project to `AGENT` and immediately keep the live number on the webhook with `PATCH /v1/channels/{channel_id}` and `{"routing_mode": "EXTERNAL"}`. Send the channel request right after the project request; the project switch resets every channel, so the channel cannot be set first. If the channel request fails, immediately set the project back to `EXTERNAL` and tell the user. The sandbox now reaches the Draft while customers still reach the user's bot. Build and test the Draft, then publish with `approve_routing_switch: true` once the user confirms; that moves the number to the hosted agent.

Tell the user what changes before each switch. To go back, set the project and channel to `EXTERNAL`; the webhook URL is kept.

### Steps

1. **runtime**
   - Webhook (`EXTERNAL`): `PATCH https://api.wabery.com/v1/projects/{project_id}` with `{"webhook_url": "https://..."}`. The response includes `webhook_secret`; give it to the user's app to verify each signed delivery (see https://wabery.com/docs/webhooks/). The app replies with `POST /v1/messages`.
   - Hosted agent (`AGENT`): export the current config with `GET /v1/config`, add or edit `hosted_agents` (include the project id and, once connected, the WhatsApp `channel_ids`), check it with `POST /v1/config/validate` and `POST /v1/config/diff`, then `PUT /v1/config`. The schema is at `GET /v1/config/schema`. For the hosted agent, apply writes only the Draft. The same config also creates, updates, and deletes Flow drafts, automations, and webhook settings, which are not part of that Draft; automation and webhook changes can affect live traffic as soon as you apply. Always start from the export, show the user the diff when it changes anything other than the hosted agent's Draft, and apply only after they agree.
2. **sandbox_test**: send the user `customer_link`. On their phone it opens WhatsApp with the project's join code; they send it to the shared Wabery sandbox number, then their next messages reach the project. The step is `done` after their first message arrives. For a hosted agent, sandbox messages always reach the Draft, never Live, so this is how the user tests changes before publishing. List only the user's own channels in `channel_ids`, never the sandbox. Sandbox delivery is billed to the workspace wallet at Meta's rate; see https://wabery.com/pricing/.
3. **connect_whatsapp**: ask whether the user keeps using the WhatsApp Business app on the number, then create a link:

```http
POST https://api.wabery.com/v1/onboarding/handoffs
Authorization: Bearer wab_live_...
Content-Type: application/json

{"type": "connect_whatsapp", "connection_mode": "NEW_CLOUD_API"}
```

   Use `"BUSINESS_APP_COEXISTENCE"` if they keep the app. The response has `id`, `status`, `expires_at`, and `url`. Send `url` to the user; it is returned only once, works once, and expires after 7 days. They sign in to Facebook and connect the number with Meta, without a Wabery login. While they are working on it, poll `GET https://api.wabery.com/v1/onboarding/handoffs/{id}` about every 30 seconds for up to 15 minutes; after that, stop and check again when they say they finished, until `status` is `completed` (it then includes `channel_id`). If the link is lost or `expired`, create a new one. Moving a number from another WhatsApp API provider is not available through links; the user does that in the dashboard.
4. **publish_agent** (`AGENT` projects only): after testing the Draft, read the review and show the user the `changes`, the affected `deployments`, any `approvals_required`, and any `blockers`:

```http
GET https://api.wabery.com/v1/agent/publish-review
Authorization: Bearer wab_live_...
```

   Publish only after the user explicitly confirms those changes:

```http
POST https://api.wabery.com/v1/agent/publish
Authorization: Bearer wab_live_...
Content-Type: application/json

{"draft_revision_id": "...", "configuration_version": 12, "customer_confirmed": true}
```

   Copy `draft_revision_id` and `configuration_version` from the review the user saw (`draft_revision_id` is `null` when there are no unpublished Draft changes, for example when publishing the current Live version to a newly connected channel); if the Draft changed since, you get `409 review_outdated` and must ask again. Pass `deployment_id` when the review lists more than one deployment. Each deployment's `approvals_required` lists `approve_routing_switch` and `approve_all_live_channels` when they apply; `approve_automations_only` is requested by a `422` when AI replies are unavailable. These booleans default to `false`; set one to `true` only when the user agreed to it: `approve_routing_switch` (the channel still forwards to the webhook; publishing moves it to the hosted agent), `approve_all_live_channels` (apply the Draft to every live channel), `approve_automations_only` (go live with automations while AI replies are unavailable, for example before wallet credit). A `422 publish_blocked` response says what is missing. `customer_confirmed` is your statement that the user confirmed; never send it otherwise. The user is responsible for what goes live, and every later change needs a new review and confirmation. Requires the `agents:publish` scope.
5. **go_live**: when Meta still needs something, such as a payment method on the WhatsApp Business Account, `details` says what. Tell the user exactly that. The step is `done` when real customers can message the number.

When a step is `waiting_on_customer`, tell the user what to do, then stop polling. Check the checklist again when they say they finished, or at most every few minutes if you are still in the same session. Never re-create links or registrations on your own while waiting.

Use the `Idempotency-Key` header when you retry writes that support it.

MCP clients use `wabery_get_onboarding`, `wabery_create_onboarding_handoff`, `wabery_get_onboarding_handoff`, `wabery_get_agent_publish_review`, and `wabery_publish_agent`. The CLI has `wabery onboard`. Human guide: https://wabery.com/docs/ai-agent-setup/

## Errors

| Error | Where | What to do |
| --- | --- | --- |
| `invalid_request` | identity | Fix the JSON body. |
| `invalid_login_hint` | identity | Send the person's email as `login_hint`. |
| `invalid_scope` | identity | Request a subset of `post_claim_scopes`, or omit `scope`. |
| `anonymous_not_enabled`, `identity_assertion_not_enabled` | identity | Use `service_auth`. |
| `rate_limit_exceeded` | identity | Wait for `Retry-After`. |
| `authorization_pending` | token | Wait `interval` seconds and poll again. |
| `slow_down` | token | Add 5 seconds to your polling interval. |
| `access_denied` | token | The person declined. Stop unless they ask to retry. |
| `expired_token` | token | The code expired or is unknown. Tell the user, and register again when they ask. |
| `invalid_grant` | token | The claim was already redeemed or is malformed. Register again. |

API calls can also return `403 insufficient_scope`, `429 rate_limit` or `monthly_quota` (honor `RateLimit-*` and `Retry-After`), and `429 message_pair_rate_limit` or `503 message_rate_limit_unavailable` (the message was not queued; retry with the same idempotency key after `Retry-After`).

## Revocation

- The person can revoke the key at any time in [Access → API keys](https://app.wabery.com/access?tab=api-keys).
- Hosted MCP OAuth tokens: `revocation_endpoint` https://app.wabery.com/api/mcp/oauth/revoke, or **Access → Connections**.
- Support: help@wabery.com
