---
title: Plainrouter API and MCP authentication
description: Bearer-token authentication for the Plainrouter Conversion API plus OAuth 2.1 discovery, registration, human approval, credential use, errors, and revocation for Plainrouter MCP agents.
canonical: https://plainrouter.com/auth.md
last_updated: 2026-09-02
---

# Plainrouter API and MCP authentication

Plainrouter has two integration authentication surfaces. Server-to-server [Plainrouter Conversion API](/api/docs) requests use a workspace-issued bearer token in the `Authorization` header. Plainrouter MCP clients use standards-based OAuth 2.1 with human approval.

Codex, Claude, and other MCP clients should connect to `https://plainrouter.com/mcp`, open the browser authorization flow, and let the human user choose exactly one advertising account.

The human signs in with the existing Plainrouter email, Google, or GitHub login. An OAuth client receives no account access until the user approves it. Plainrouter never exposes the underlying Google, Meta, or other advertising-platform credential.

## Step 1 — Discover

The recommended discovery path is the Plainrouter MCP server. Its protected-resource metadata points clients to the authorization server, dynamic client registration endpoint, and this guide.

### Connect as an MCP client

Register the remote Streamable HTTP server:

```text
https://plainrouter.com/mcp
```

The MCP client discovers OAuth metadata automatically. In Codex, the equivalent setup is:

```shell
codex mcp add plainrouter --url https://plainrouter.com/mcp
codex mcp login plainrouter
```

The client opens a browser. The human signs in, chooses one ad account, reviews the permissions, and approves. The browser returns an authorization code to the client's exact registered callback URI. The client exchanges that one-use code with its PKCE verifier and stores the resulting bearer token.

### Discover OAuth metadata

Fetch Protected Resource Metadata:

```http
GET /.well-known/oauth-protected-resource/mcp HTTP/1.1
Host: plainrouter.com
Accept: application/json
```

Then fetch the advertised Authorization Server Metadata:

```http
GET /.well-known/oauth-authorization-server HTTP/1.1
Host: plainrouter.com
Accept: application/json
```

The metadata publishes `issuer`, `authorization_endpoint`, `token_endpoint`, `registration_endpoint`, supported scopes, and the required `S256` PKCE method.

## Step 2 — Pick a method

Use OAuth 2.1 Authorization Code with PKCE when the client can open a browser. Provisioned device clients may use the Device Authorization Grant fallback documented below. Plainrouter's `agent_auth` metadata publishes `register_uri` and a public-client registration method that does not require a pre-existing Plainrouter credential, but Plainrouter does not accept an agent-provider `identity_assertion` or ID-JAG in place of human approval.

## Step 3 — Register a public OAuth client

A public OAuth client can be registered without a pre-existing Plainrouter credential. Registration creates only an untrusted public client identifier; it does not create a user, token, agent principal, or ad-account grant.

```http
POST /oauth/register HTTP/1.1
Host: plainrouter.com
Accept: application/json
Content-Type: application/json

{
  "client_name": "Codex",
  "redirect_uris": ["http://127.0.0.1:1455/callback"]
}
```

Successful response:

```json
{
    "client_id": "019...",
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "redirect_uris": ["http://127.0.0.1:1455/callback"],
    "scope": "mcp:use",
    "token_endpoint_auth_method": "none"
}
```

Only registered HTTPS callbacks, approved native-client schemes, and loopback callbacks are accepted. Public clients do not receive or use a client secret.

## Step 4 — Claim the connection with human approval

Generate a high-entropy PKCE `code_verifier`, derive its SHA-256 `code_challenge`, and retain a random `state` value locally. Open:

```http
GET /oauth2/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=CALLBACK&scope=mcp%3Ause&state=STATE&code_challenge=CHALLENGE&code_challenge_method=S256 HTTP/1.1
Host: plainrouter.com
```

The human—not the agent—completes the Plainrouter login and consent screen. They must choose one active ad account they own. This approval is Plainrouter's claim step: it binds the public OAuth client to the human user and one ad account. Approval creates an agent authorization bound to the OAuth client, human user, requested permissions, and selected account.

Plainrouter redirects to the exact registered callback:

```text
CALLBACK?code=ONE_USE_CODE&state=STATE
```

The client must reject the callback if `state` does not exactly match its stored value.

### Exchange the approved code

```http
POST /oauth2/token HTTP/1.1
Host: plainrouter.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=CLIENT_ID
&redirect_uri=CALLBACK
&code=ONE_USE_CODE
&code_verifier=ORIGINAL_VERIFIER
```

The response contains an OAuth bearer access token, refresh token, token lifetime, and granted scope. Store these only in the client's secure credential storage. Never include them in prompts, logs, URLs, or repositories.

## Step 5 — Use the credential

For MCP, continue communicating with:

```http
POST /mcp HTTP/1.1
Host: plainrouter.com
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
```

For the REST account-context check:

```http
GET /api/v1/agent/context HTTP/1.1
Host: plainrouter.com
Accept: application/json
Authorization: Bearer ACCESS_TOKEN
```

Every request is resolved through the OAuth client authorization and therefore remains restricted to the one approved ad account. A request cannot substitute a different account identifier.

## OAuth scope and account permission

- OAuth scope `mcp:use` — connect to the Plainrouter MCP server.
- Bound account permission `ad-account.read` — read the one approved ad account and its Plainrouter context.
- Bound account permission `actions.propose` — submit typed budget and delivery-status proposals for that account.

`actions.propose` does not grant direct campaign mutation. `propose-actions` creates an idempotent proposal that passes the workspace policy gate; suggest-only work waits in the human approval queue, and approved work executes through the verifier and immutable audit log. The current executor is a simulator and does not call an ad platform.

## Device authorization fallback

Passport also exposes the OAuth Device Authorization Grant for provisioned device-flow clients:

- Device code endpoint: `POST /oauth2/device/code`
- Human verification page: `GET /oauth2/device`
- Token endpoint: `POST /oauth2/token`
- Grant type: `urn:ietf:params:oauth:grant-type:device_code`

Browser-capable Codex and Claude clients should prefer Authorization Code + PKCE.

## Errors

- `400 invalid_request` — fix missing or malformed registration or OAuth parameters; do not retry unchanged input.
- `400 invalid_grant` — discard the authorization code or refresh token and restart the authorization flow.
- `401 invalid_token` — stop using the rejected token and follow the `WWW-Authenticate: Bearer resource_metadata="..."` discovery hint.
- `403` — the authenticated user or agent lacks the requested account permission; request human review rather than broadening scope.
- `429` — respect `Retry-After` before retrying.

Never retry approval, token, or write operations indefinitely.

## Revocation

An agency owner can revoke a Plainrouter agent connection. Revocation disables its agent principal, access tokens, and refresh tokens. The client must discard rejected credentials and begin a new OAuth authorization if the user wants to reconnect.

Changing the ad account also revokes previously issued tokens before a new authorization can be used, preventing an old token from silently moving to another account.

## Security requirements

- Use PKCE `S256`; `plain` is not supported.
- Verify `state` and the exact callback URI.
- Never ask the human for their Plainrouter password, social-login credential, authorization code, or token.
- Request only the published scopes required for the task.
- Treat HTTP `401` as a stopped or revoked connection and do not retry indefinitely.
- Do not probe approval or token endpoints during passive discovery scans.

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
