# Circl: authentication for agents

Circl gives an agent read-only access to a Brand's data through an MCP server (`/mcp` on the Circl API) and a REST API (`/v1`, [OpenAPI](https://joincircl.co/openapi.json)). The Brand user signs in and approves the agent on a consent screen; there is no API key and no token to paste. Only the flow below exists.

## Discover

Call `POST <mcp_url>` with no token. The answer is `401` with a header:

```
WWW-Authenticate: Bearer resource_metadata="<api origin>/.well-known/oauth-protected-resource/mcp"
```

Fetch that URL (RFC 9728). It returns `resource` (the MCP URL) and `authorization_servers`. Fetch the authorization server's metadata (RFC 8414): `<authorization server>/.well-known/oauth-authorization-server` (or the OpenID configuration) for the `authorize`, `token` and `registration` endpoints.

## Pick a method

OAuth 2.1 authorization code with PKCE (`S256`). Dynamic client registration is open. There is no other method: no API key, no personal access token, no client credentials.

## Register

`POST` the registration endpoint (RFC 7591) with `client_name` (shown to the user), `redirect_uris`, `grant_types: ["authorization_code", "refresh_token"]` and `token_endpoint_auth_method: "none"`. Keep the returned `client_id`. Redirect URIs are matched exactly.

## Claim

Send the user to the `authorize` endpoint with `client_id`, `response_type=code`, `redirect_uri`, `code_challenge`, `code_challenge_method=S256` and `state`. Request the scope `email` or none; custom scopes are refused. The user lands on Circl's consent screen (`/oauth/consent`), signs in if needed, reads who is asking and what is shared, then chooses « Autoriser » or « Refuser ». Your `redirect_uri` receives `code` and `state` (or `error=access_denied`). Check `state`.

## Exchange

`POST` the `token` endpoint (`application/x-www-form-urlencoded`) with `grant_type=authorization_code`, `code`, `redirect_uri`, `client_id` and `code_verifier`. The answer holds `access_token`, `refresh_token` and `expires_in` (one hour). Renew with `grant_type=refresh_token`; refresh tokens rotate, so keep the new one each time.

## Use the access_token

Send `Authorization: Bearer <access_token>` on every request: `POST <mcp_url>` (MCP Streamable HTTP, JSON) or `GET https://joincircl.co/v1/...` (REST, JSON). The same token reads the REST API (`GET /v1/brands`, then the `/v1/brands/{brandId}/...` endpoints, see [/docs](https://joincircl.co/docs)) and `/mcp`. Start with `list_brands` to get a `brand_id`. All tools are read-only; the user must belong to at least one Brand.

## Scopes

One implicit read-only grant, `read`: the Brands the user belongs to. Do not send a `scope` parameter other than `email`; custom scopes are refused by the authorization server.

## Errors

- `401` with `WWW-Authenticate`: no token, expired, revoked, or not an agent token. Refresh, or restart at Discover.
- `403 forbidden`: the user has no Brand space.
- `429 rate_limited`: 60 requests a minute per user; wait and retry.
- A tool error comes back as a tool result with `isError: true` and a message that says what to fix.

## Revocation

The user removes an agent in Circl, « Connexions » › « Agents connectés ». The token stops working at once and the refresh token with it; connect again from Discover. There is no revocation endpoint for the agent to call.
