<!--
Sitemap:
- [Paybox](/index): The non-custodial wallet for AI agents
- [Getting started](/getting-started)
- [Credentials & agents](/concepts/model): The Paybox model
- [Approvals & passkeys](/concepts/approvals)
- [Request lifecycle](/concepts/requests): From intent to result
- [MCP connector](/connect/mcp)
- [OAuth 2.1](/connect/oauth)
- [MCP tools](/reference/mcp-tools): What an agent can call
- [API & endpoints](/api-reference)
- [SDK & CLI](/sdk-cli)
-->

# OAuth 2.1

Paybox is an **OAuth 2.1 Authorization Server** implementing the MCP
authorization spec. Third-party apps and MCP clients connect with standard
**authorization-code + PKCE**; after the user consents, your app holds a scoped
access token it presents on the MCP endpoint.

All endpoints live on the API origin:

```txt
https://api.paybox.sh
```

The consent screen the user sees is served from the app origin
(`https://app.paybox.sh`). Everything else below is on the API origin.

:::info
Most integrations don't implement this by hand — an MCP client (Claude.ai, Grok,
…) drives the whole flow when you add the **[MCP connector](/connect/mcp)** URL.
This page is for building that client, or integrating directly.
:::

## Flow

::::steps

### Discover

Fetch the metadata documents (no auth):

```txt
GET /.well-known/oauth-authorization-server
GET /.well-known/oauth-protected-resource
```

The authorization-server document tells you every endpoint and capability:

```json
{
  "issuer": "https://api.paybox.sh",
  "authorization_endpoint": "https://api.paybox.sh/oauth/authorize",
  "token_endpoint": "https://api.paybox.sh/oauth/token",
  "registration_endpoint": "https://api.paybox.sh/oauth/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"],
  "scopes_supported": ["mcp", "offline_access"]
}
```

The protected-resource document points at the MCP resource and its authorization
server:

```json
{
  "resource": "https://api.paybox.sh/mcp",
  "authorization_servers": ["https://api.paybox.sh"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["mcp"]
}
```

### Register (dynamic)

Register a public client (RFC 7591). Paybox supports public clients only —
`token_endpoint_auth_method` must be `none`; there is no client secret.

```http
POST /oauth/register
Content-Type: application/json

{
  "client_name": "Acme Agent",
  "redirect_uris": ["https://acme.example.com/oauth/callback"],
  "token_endpoint_auth_method": "none"
}
```

```json
// 201 Created
{
  "client_id": "pbx-oauth-a1b2c3d4e5f6",
  "client_name": "Acme Agent",
  "redirect_uris": ["https://acme.example.com/oauth/callback"],
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "client_id_issued_at": 1749200000
}
```

`redirect_uris` must be HTTPS (or `http://localhost` for local dev).

### Authorize (PKCE)

Send the user to `/oauth/authorize` with a PKCE `code_challenge` (S256). Paybox
validates the request, then redirects the user to the consent screen, where they
sign in, **select which credentials to grant** and the approval mode per
credential, and approve with a passkey.

```txt
GET /oauth/authorize
  ?response_type=code
  &client_id=pbx-oauth-a1b2c3d4e5f6
  &redirect_uri=https://acme.example.com/oauth/callback
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
  &scope=mcp%20offline_access
  &resource=https://api.paybox.sh/mcp
  &state=xyz
```

On approval the user's browser is redirected back to your `redirect_uri` with a
`code` (and your `state`):

```txt
https://acme.example.com/oauth/callback?code=<authorization_code>&state=xyz
```

### Exchange

Exchange the code (with your PKCE `code_verifier`) for tokens:

```http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=pbx-oauth-a1b2c3d4e5f6
&code=<authorization_code>
&redirect_uri=https://acme.example.com/oauth/callback
&code_verifier=<pkce_verifier>
```

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "…",
  "scope": "mcp offline_access"
}
```

Now call the [MCP endpoint](/connect/mcp) with `Authorization: Bearer <access_token>`.

::::

## Scopes

| Scope | Grants |
| --- | --- |
| `mcp` | Access to the MCP tools. |
| `offline_access` | A `refresh_token` so the client can stay connected. Opt-in — omit it and no refresh token is issued. |

## Tokens

| Token | Format | TTL | Notes |
| --- | --- | --- | --- |
| Authorization code | opaque | 5 min | Single-use. Replaying a used code revokes the client (treated as compromise). |
| Access token | JWT (HS256) | 60 min | Audience-bound to the MCP `resource`. Presented as a bearer on `/mcp`. |
| Refresh token | opaque | 30 days, sliding | **Rotated on every use** — the response returns a new one; replace your stored token. Replaying a rotated token revokes the client. |

The token's authority is exactly the **grant set** the user approved — which
credentials, and the approval mode per credential. Revoking the client (from the
app's **Clients** screen) immediately kills its access tokens and refresh chain.

### Refresh

```http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&client_id=pbx-oauth-a1b2c3d4e5f6
&refresh_token=<current_refresh_token>
```

The response is a new access token **and a new refresh token**. Always store the
new refresh token; the old one is now spent.

## Notes

* **PKCE S256 is required.** `code_challenge_method` must be `S256`.
* `redirect_uri` is matched **exactly** against what was registered. A
  mismatched or malformed `redirect_uri` is rejected with an error page, not a
  redirect (sending errors to an unverified URI is unsafe).
* Sensitive operations still pause for the **user's passkey** regardless of the
  app's token — the token alone never bypasses step-up. See
  **[Request lifecycle](/concepts/requests)**.

## Errors

Token-endpoint failures use standard OAuth error codes:

| Error | Cause |
| --- | --- |
| `invalid_grant` | Code expired, already used, or PKCE verification failed. |
| `invalid_client` | Unknown `client_id`. |
| `invalid_request` | Missing or malformed parameters. |
| `invalid_scope` | Requested scope exceeds what was granted. |

A `/mcp` call with a missing or invalid bearer returns `401` with a
`WWW-Authenticate` header pointing back at `/.well-known/oauth-protected-resource`
so the client can re-run discovery and re-authorize.
