# Authentication

> Configure the API key without exposing it.

Source page: https://docs.replynodes.com/docs/auth

# Authentication

ReplyNodes has two first-class authentication options:

- **MCP OAuth** — interactive MCP clients use Better Auth at
  [`https://auth.replynodes.com`](https://auth.replynodes.com) with the
  `mcp:read` scope.
- **API key** — headless clients, CI, SDKs, and manual MCP configurations use
  an `rn_test_*` or `rn_live_*` key.

OAuth access is resolved to the authenticated ReplyNodes organization. Clients
must not send an organization id, and API keys remain supported alongside
OAuth. Sign in or manage access at
[`https://platform.replynodes.com/sign-in`](https://platform.replynodes.com/sign-in) and manage access at [`https://platform.replynodes.com/api-keys`](https://platform.replynodes.com/api-keys).

The canonical OpenAPI document defines HTTP Bearer authentication for the public API. Send the raw key after the `Bearer` scheme in the `Authorization` header. Key formats documented by the contract are `rn_test_<64hex>` and `rn_live_<64hex>`; never place a real key in a URL, browser bundle, prompt, log, or repository.

The gateway CLI reads `FETCHER_API_KEY` and constructs this header for requests. Other clients should use their secret-store equivalent and follow the same HTTP header contract.

See [Quickstart](https://docs.replynodes.com/docs/quickstart) for a request that fails closed when the environment variable is absent.

## Agents

Start keyless: the capabilities listed in the [Agent Quickstart](https://docs.replynodes.com/docs/agent-quickstart)
work without a key up to their daily limits. Get a key only when a call
returns `429 anonymous_limit_reached` or `401`; that response's
`error.next_action` names the exact call (`method`, `url`,
`required_fields`), a `human_message` for the human, and
`retry_after_seconds`.

### Email sign-up (recommended)

No browser is needed. The human only reads one 6-digit code from their inbox.

1. Ask the human which email to use, then sign up. The response is immediate:

   ```bash
   curl -sS -X POST https://platform.replynodes.com/api/agent/signup \
     -H 'Content-Type: application/json' \
     -d '{"email":"you@example.com","agent_name":"my-agent"}'
   ```

   `201` (or `200` when the same email signs up again) returns `api_key` (a
   restricted key, shown once), `status: "restricted"`, `expires_at` (72 hours),
   `verification` (verify and resend endpoints, `code_sent`), `human_message`,
   and `status_url`. Show `human_message` to the human. Store `api_key` once as
   `REPLYNODES_API_KEY`; never print or log it. A restricted key already calls
   the keyless capabilities on its own daily allowance and never spends credits.
2. When the human pastes the code, verify with the same key:

   ```bash
   curl -sS -X POST https://platform.replynodes.com/api/agent/verify \
     -H "Authorization: Bearer ${REPLYNODES_API_KEY}" \
     -H 'Content-Type: application/json' -d '{"code":"123456"}'
   ```

   `200` returns `status: "verified"`, the new `expires_at` (30 days), and
   `workspace` (`created` or `existing`: an email that already has an account
   attaches to that workspace). The key string does not change. Repeating
   verify is safe, including after a timeout.
3. `GET https://api.replynodes.com/v1/agent/status` with the key returns its
   status, per-key quota (restricted) or credit balance, expiry, and
   `next_actions` with a `human_message` (verify, renew, add credits).

Codes are 6 digits, valid 10 minutes, 5 attempts; the newest code replaces
older ones. Resend with `POST https://platform.replynodes.com/api/agent/verify/resend`
and the same Bearer key (at most one code per minute and five per day per
email). Renew a key before it expires by signing up again with the same email
and verifying the new code.

When the workspace runs out of credits, credit-metered calls return HTTP `402` with
`code: "topup_required"` and `next_action.action: "top_up"`. Show its
`human_message`: only the human can add credits, on the billing page in
`next_action.url`. Retry once they confirm; do not retry in a loop.

| HTTP | `error` | Agent action |
| ---: | --- | --- |
| 400 | `email_not_accepted` | Ask for the human's real, non-disposable email. |
| 400 | `invalid_code` | Ask the human to re-read the newest email (`attempts_remaining`). |
| 400 | `code_expired`, `code_attempts_exhausted` | Resend (`next_action`), then ask for the newest code. |
| 401 | `invalid_api_key` | The key is superseded or expired; sign up again with the same email. |
| 403 | `agent_key_not_permitted` | Show `human_message`: a workspace admin must allow agent keys. |
| 409 | `account_sign_in_required` | Show `human_message`: the human signs in once on the web with an email code. |
| 410 | `onboarding_expired` | Sign up again. |
| 429 | `rate_limited`, `resend_limited` | Wait `Retry-After`. |
| 503 | `email_signup_unavailable` | Use the device flow in `next_action`. |
| 503 | `temporarily_unavailable` | Repeat the same request after `Retry-After`. |

On the API, a restricted key outside the keyless capabilities gets
`403 verification_required`, and a used-up per-key allowance gets
`429 restricted_limit_reached`; both carry `next_action: verify_email`.

### Legacy device flow

Agents can still obtain a read-only API key through a short claim approved once
by a human in a browser. Start a claim with:

```bash
curl -sS -X POST https://platform.replynodes.com/api/agent/claim \
  -H 'Content-Type: application/json' \
  -d '{"agent_name":"my-agent","agent_platform":"my-platform"}'
```

`agent_name` and `agent_platform` are optional labels. A successful response
contains `claim_id`, `device_code`, `user_code`, `verification_uri`,
`verification_uri_complete`, `expires_in` (`900` seconds, or 15 minutes),
`interval` (`3` seconds), and `docs`. Give the human the exact
`verification_uri_complete` link: it already includes the `user_code`. Wait
for that one approval, then poll at the returned interval:

```bash
curl -sS -X POST https://platform.replynodes.com/api/agent/claim/token \
  -H 'Content-Type: application/json' \
  -d "{\"claim_id\":\"${CLAIM_ID}\",\"device_code\":\"${DEVICE_CODE}\"}"
```

On `200`, store `api_key` once as `REPLYNODES_API_KEY`; never log it or ask
the token endpoint for it again. The returned `expires_at` is authoritative.
The default issued `agent_read` key lifetime is 30 days. If a later API call
returns `401`, start a new claim. The historical scope name is
`read:x_public`; it means public provider reads and is not a request to rename
the scope.

An agent key reaches only read-only provider `GET` and `HEAD` routes,
`/v1/capabilities`, and the provider `/v1/x` surface where that surface is
covered by provider-read routing. It cannot use non-`GET` methods or key,
dashboard, internal, billing, wallet, checkout, webhooks, usage, overview, or
me routes. Check `/v1/capabilities` for the current provider contract; this
page does not promise PDF conversion or another route outside that allowlist.

### Token polling outcomes

In this table, `—`/omitted status means the response body does not include the `status` field. The status is omitted on paths where the code does not return it, including some `503 temporarily_unavailable` responses.

| HTTP | `error` | `status` | `Retry-After` | Agent action |
| ---: | --- | --- | --- | --- |
| 200 | — | `approved` / `state: revealed` | — | Store `api_key` once with its `expires_at`; stop polling. |
| 428 | `authorization_pending` | `pending` | — | Wait `interval` seconds, then poll again. |
| 429 | `poll_rate_limited` | — | Returned header | Wait for `Retry-After`; do not loop faster. |
| 409 | `already_delivered` | `delivered` | — | Stop; the key was already revealed. Use the dashboard recovery details. |
| 409 | `already_delivered` | `delivery_uncertain` | — | Do not start a new claim blindly; check with the human / [Agent keys page](https://platform.replynodes.com/api-keys#agent-keys); then start a new claim if the key was not stored. |
| 410 | `expired_token` | `expired` when present | — | Stop and start a new claim. |
| 403 | `authorization_failed` | `authorization_failed` when present | — | Stop; after human approval, restart the claim. |
| 404 | `invalid_claim` | — | — | The claim id or device code is invalid; stop and start a new claim. |
| 503 | `temporarily_unavailable` | `delivery_uncertain` when returned; omitted on some paths | Returned header when present | Wait and retry while claim recovery remains possible. |
| 503 | `delivery_uncertain` | — | — | Retry while claim recovery remains possible. |
| 409 | `delivery_uncertain` | `delivery_uncertain` | — | Retries are exhausted; stop and use dashboard recovery. |

For the complete protocol, use [`https://replynodes.com/auth.md`](https://replynodes.com/auth.md).
