# Authentication

One header, on every request except the self-describing index.

```
Authorization: Bearer <your api key>
```

Tokens are issued on your [profile page](https://publicwww.com/profile/api) - up to ten per account, each revocable on its own, so a leaked one can be deleted without touching the others. A paid plan is required: without one, every endpoint except `/` and `/v1/account` answers `403 plan_required`.

An application can also get a token for you through OAuth 2.1: you sign in, see what it asks for and press Allow. Its token goes in the same header and works the same way.

## Why not `?key=`

A key in the query string ends up in places you did not put it: web server access logs, browser history, proxy logs, and the `Referer` header of anything the response links to. The API therefore does not accept it, and answers `401 missing_key` saying so.

The old `?export=` urls on the main site do still accept `?key=`, because scripts written years ago depend on it and taking it away would break them. That is the one place it lives - see [the old export urls](https://publicwww.com/docs/api/legacy/).

## Checking a key works

`/v1/account` is the cheapest call: it spends no quota and works even on an account with no plan, so it answers both "is this key valid" and "what am I entitled to".

```
curl -H "Authorization: Bearer $KEY" https://api.publicwww.com/v1/account
```

```
{
"plan": "enterprise",
"plan_until": 1819461840,
"full_access": true,
"quota": {
"searches": { "limit": 300, "used": 12, "resets_at": 1787961600 },
"snippets": { "limit": 100, "used": 3,  "resets_at": 1787961600 }
},
"limits": {
"disclosed_positions": 4294967295,
"max_per_page": 1000000,
"max_per_page_snippets": 10000
}
}
```

## What can go wrong

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `missing_key` | No `Authorization: Bearer` header. A key in the query string does not count. |
| 401 | `invalid_key` | The key does not name an account. Check for a stray newline or quote. |
| 403 | `plan_required` | The key is fine; the account has no paid plan. |

A `401` also carries a `WWW-Authenticate: Bearer` header, so http clients that handle authentication generically behave sensibly.

## OAuth 2.1 for applications

An application that works on behalf of other people - an assistant, an integration, a hosted service - should not ask each of them to copy a token. It sends them to PublicWWW instead: they sign in, approve the application, and it receives a token of its own. That token is sent as `Authorization: Bearer` like any other and opens the whole API and the [MCP server](https://publicwww.com/docs/mcp/) at `https://api.publicwww.com/mcp`, under the account's plan, quota and rate limit.

| What | Where |
| --- | --- |
| Authorization server metadata (RFC 8414) | `https://publicwww.com/.well-known/oauth-authorization-server` |
| Protected resource metadata (RFC 9728) | `https://api.publicwww.com/.well-known/oauth-protected-resource` |
| Authorization endpoint | `https://publicwww.com/oauth/authorize` |
| Token endpoint | `https://publicwww.com/oauth/token` |
| Revocation endpoint (RFC 7009) | `https://publicwww.com/oauth/revoke` |

### Identifying the application

There is no client registration. The `client_id` is the https URL of a small JSON document the application publishes - a client metadata document. PublicWWW reads it every time someone connects, so the name and return addresses are always current, and the person approving sees which host published them.

```
{
"client_id": "https://app.example.com/oauth/client.json",
"client_name": "Example App",
"redirect_uris": ["https://app.example.com/oauth/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
```

- `client_id` inside the document must be exactly the URL it is served from. The document is fetched over https, from a URL with a path, without following redirects; it must answer within 5 seconds and stay under 64 KB.
- `redirect_uris` are https addresses, or http on `127.0.0.1`, `localhost` or `[::1]` for an application running on the person's own computer - there any port matches. Custom schemes such as `myapp://` are not accepted.
- Every application is a public client: the token request carries no secret, whatever `token_endpoint_auth_method` the document names. The authorization code is protected by PKCE instead.

### The flow

Authorization code with PKCE; `S256` is the only method. Send the person to the authorization endpoint:

```
https://publicwww.com/oauth/authorize
?response_type=code
&client_id=https%3A%2F%2Fapp.example.com%2Foauth%2Fclient.json
&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback
&code_challenge=<BASE64URL(SHA-256(code_verifier))>
&code_challenge_method=S256
&state=<random>
```

They sign in with a one-time code sent by email if they have not already, see the application's name, the host of its document and where they will return, and press Allow or Cancel. Back at `redirect_uri` come `code`, your `state` and `iss=https://publicwww.com` (RFC 9207). A code is valid for ten minutes and works once. Exchange it:

```
curl https://publicwww.com/oauth/token \
-d grant_type=authorization_code \
-d code="$CODE" \
-d code_verifier="$VERIFIER" \
-d client_id=https://app.example.com/oauth/client.json \
-d redirect_uri=https://app.example.com/oauth/callback
```

```
{ "access_token": "<token>", "token_type": "Bearer", "scope": "mcp" }
```

`scope` may be left out: there is one scope, `mcp`, and it covers the whole API. `resource` (RFC 8707) may be left out too; if sent, it is `https://api.publicwww.com/mcp` or `https://api.publicwww.com`.

### How long a token lives

Until it is revoked - there is no expiry and no refresh token. An integration that works today keeps working tomorrow without anyone touching it. A token is revoked only on purpose: the person disconnects the application on their [profile page](https://publicwww.com/profile/api), the application revokes it itself, or the account is deleted.

```
curl https://publicwww.com/oauth/revoke \
-d token="$TOKEN" \
-d client_id=https://app.example.com/oauth/client.json
```

The revocation endpoint always answers `200`, whether or not the token existed.

### OAuth errors

| Where | Code | Meaning |
| --- | --- | --- |
| Authorization | error page | The `client_id` document could not be read, or `redirect_uri` is not listed in it. The person is not sent back: an address that was not verified is never followed. |
| Authorization | `invalid_request` | No `code_challenge`, or a method other than `S256`. |
| Authorization | `unsupported_response_type` | Anything but `response_type=code`. |
| Authorization, token | `invalid_target` | A `resource` other than the API. |
| Authorization | `access_denied` | The person pressed Cancel. |
| Token | `invalid_grant` | The code is unknown, used, expired or issued to another `client_id`; or `code_verifier` or `redirect_uri` does not match. |
| Token | `unsupported_grant_type` | Anything but `authorization_code`. |

Authorization errors other than the error page come back to `redirect_uri` as `error`, `error_description`, `state` and `iss`; token errors are a `400` with the same two fields in JSON.

Source: https://publicwww.com/docs/api/authentication/
