# Blockquote authentication


You are an agent. Blockquote does **not** support agentic registration. No endpoint issues a
credential to an unattended agent. This file follows the WorkOS auth.md walkthrough order
(https://workos.com/auth-md) and states, step by step, what Blockquote really serves. Read it
before you spend a request on a flow that does not exist here.

Two hosts appear below. `https://blockquote.io` is the resource server: it serves the REST API
under `/api/v1` and every document named here. `https://mcp.blockquote.io/mcp` is the Model
Context Protocol endpoint, which answers the same product without a browser. Blockquote runs no
authorization server, so neither host is one.

Start with no credential. Most read paths answer anonymously, and the MCP endpoint runs a free
scan for a caller that presents nothing. Read on only when you need an account-scoped call.

## Start without an account

Use the public MCP endpoint for unattended scans. No credential or signup is required.
Anonymous REST scan creation requires a browser Turnstile token; use MCP from a terminal.

Read the check catalog first. This request does not create a scan or use scan quota:

```sh
curl https://mcp.blockquote.io/chatgpt/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  --data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_checks","arguments":{"category":"schema"}}}'
```

The response contains the schema check catalog in `result.content`.
Use `start_scan` with `{"url":"https://your-public-site.example"}` to create a public report.
Poll `get_scan` with the returned `scanId` about every eight seconds until `status` is `done`.
Stop on quota errors and wait for the reported reset. Do not send private URLs or credentials.

For local parser tests, [read a synthetic report](https://blockquote.io/api/v1/sandbox/scan/sandbox-example).
This fixture does not fetch a website, create a report, or consume quota.

For account access, [probe account usage](https://blockquote.io/api/v1/account/usage).
An unauthenticated request returns the `401` challenge described below.

## Step 1 — Discover

Discovery is one hop here, not two.

Every `401` from the REST API carries the pointer in a `WWW-Authenticate` header:

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://blockquote.io/.well-known/oauth-protected-resource"
```

### 1a. Fetch the Protected Resource Metadata

```http
GET https://blockquote.io/.well-known/oauth-protected-resource
```

The document is RFC 9728 metadata. Read four fields:

- `resource` — `https://blockquote.io`, the API you are calling.
- `resource_name` — the display name, for a consent prompt you show your user.
- `scopes_supported` — `read`, `scan`, `monitors`. A key carries the scopes it was created with.
- `bearer_methods_supported` — `header`, so the credential travels in `Authorization`.

The document carries no `authorization_servers` array. That absence is the answer, not an
omission: there is no authorization server to name.

### 1b. Authorization Server metadata

Blockquote publishes none. There is no `/.well-known/oauth-authorization-server` on any
Blockquote host, and therefore no `issuer`, no `token_endpoint`, no `grant_types_supported`
and no `agent_auth` block. Do not probe for one.

The consequence is the whole point of this file: nothing machine-readable can hand you a
credential. The `agent_auth` fields a WorkOS-shaped agent looks for — an identity endpoint, a
claim endpoint, an events endpoint — do not exist here, because the flow behind them does not
exist here.

## Step 2 — Pick a method

You hold one of three things. Each maps to one method.

| What you hold | Method | Works unattended |
|---|---|---|
| Nothing | Anonymous access | Yes |
| A `bq_…` API key | Bearer key | Yes |
| Only your user's email address | Human sign-in | No |

`identity_assertion` is not a method here. Blockquote verifies no service-signed assertion and
no `id-jag` (`urn:ietf:params:oauth:token-type:id-jag`). Do not mint one. It is refused.

**Anonymous** is the method to try first. `GET /api/v1/scan/{id}`, `GET /api/v1/compare` and the
MCP tools `start_scan`, `get_scan`, `compare_scans`, `list_checks` and `explain_check` all answer
without a credential, metered per IP: 1 scan per day and 3 per month from one network.
Anonymous `POST /api/v1/scan` over REST is the one exception. It needs a Cloudflare Turnstile
token, which only a browser produces, and answers `403 turnstile_failed` without one. Call the
MCP endpoint instead; it needs no Turnstile token.

**Bearer key** raises those limits and unlocks the full fix list and the monitor tools. Get one
in step 3.

## Step 3 — Register

`register_uri`: https://blockquote.io/account

That is a web page, not an API endpoint. **A person** signs in there and creates the key. The
page answers `GET` and returns HTML; it accepts no `POST` that mints anything. The browser code
behind the button calls `POST /api/v1/keys`, and that route accepts the `sid` session cookie
only — an API key presented there answers `401 session_required`, by design, so that a leaked
key cannot mint its successor.

**You cannot complete this step.** No unattended path reaches it. Ask your user to do it:

1. The user opens https://blockquote.io/account and signs in.
2. API keys need the Pro or Agency plan. On a free account the create call answers
   `402 plan_required`.
3. The user names a key and picks its scopes from `read`, `scan` and `monitors`. `read` is
   always included. A key created with no scopes named carries `read` and `scan`.
4. The token appears once, in that one response. The user copies it to you.

A key is a long opaque string with the prefix `bq_`. It does not expire on a clock.

## Step 4 — Claim ceremony

There is none. Blockquote runs no claim ceremony: no `user_code`, no verification page for a
code, and no polling grant. An agent cannot start a registration now and have a user adopt it
later.

The nearest real flow is the passwordless sign-in, and it ends in a mailbox rather than in your
hands. It creates the account on first use and signs the same address in on every use after.
`/signup`, `/register`, `/sign-up` and `/join` do not exist and redirect to `/login`. In order:

1. POST `{"email":"you@example.com"}` to `/api/v1/auth/request-link`. The form at `/login` posts
   the same body, on the unversioned base its own browser code uses. Blockquote replies `200`
   and sends the address a one-time link, valid for 15 minutes.
   4 requests per address per UTC clock hour; the fifth answers `429` until the hour turns. A
   malformed address answers `400`.
2. The recipient opens the link in their mail client. It opens `/login/confirm?token=…`, a page
   that reads the token and changes nothing yet.
3. The recipient clicks "Confirm sign-in" on that page, which POSTs the token to
   `/api/v1/auth/callback`. That call consumes the token, creates the account automatically if
   the address has none yet, and sets the session cookie.

Only step 2 needs a human, and it is the step you cannot take: the token arrives by email, and
you cannot read that mailbox. With the token already in hand you can skip the confirm page and
POST `{"token":"…"}` to `/api/v1/auth/callback` yourself. The cross-site guard refuses only a
request whose `Origin` or `Sec-Fetch-Site` header names another site, so a plain HTTP client
passes. Reporting that the flow needs a manual email step is correct; guessing a shortcut around
the mailbox is not.

The session cookie the callback sets is not a substitute for a key. Key management, billing and
webhook configuration are session-only and refuse every API key; the rest of the API takes the
key.

## Step 5 — Exchange the assertion

There is nothing to exchange. Blockquote has no token endpoint, mints no `identity_assertion`,
and issues no refresh token. The `bq_` key from step 3 is the credential itself. Skip to step 6.

## Step 6 — Use the credential

Send the key in the `Authorization` header on every call:

```http
GET /api/v1/monitors HTTP/1.1
Host: blockquote.io
Authorization: Bearer bq_…
```

The MCP endpoint takes the same header. A call outside the key's scopes answers `403
insufficient_scope` and names the scope it wanted. Nothing here needs refreshing: the key stays
valid until a person revokes it, so a `401` on a key that worked yesterday means it was revoked,
not that it expired.

The whole REST surface is described by https://blockquote.io/api/v1/openapi.json.

## Errors

| Status | `error` | Where | What to do |
|---|---|---|---|
| 400 | `invalid_email` | `/api/v1/auth/request-link` | Send a well-formed address. |
| 401 | `unauthorized` | any authenticated route | You sent no credential. Get a key (step 3). |
| 401 | `invalid_api_key` | any authenticated route | The key is invalid or revoked. Ask for a new one. |
| 401 | `session_required` | `/api/v1/keys` | Key management needs the browser session. Stop. |
| 401 | `refresh_requires_account` | `POST /api/v1/scan` with `refresh` | Drop `refresh`, or use a key. |
| 402 | `plan_required` | `/api/v1/keys`, monitors | The account needs the Pro or Agency plan. |
| 403 | `turnstile_failed` | anonymous `POST /api/v1/scan` | Call the MCP endpoint, or send a key. |
| 403 | `insufficient_scope` | any scoped route | The key lacks the scope. A new key is needed. |
| 429 | `rate_limited` | `POST /api/v1/scan` | Read `RateLimit-Reset` and `Retry-After`, then wait. |
| 429 | `rate_limited` | `/api/v1/auth/request-link` | Wait for the `Retry-After` interval. |
| 429 | `rate_limited` | `/api/v1/webhooks/test` | Wait an hour before another test delivery. This response has no retry headers. |
| 429 | `quota_exceeded` | `POST /api/v1/scan` | The plan's scan budget is spent. Wait for the reset. |

Every `401` answers with the `WWW-Authenticate` header from step 1. Every refusal a browser could
satisfy but you cannot carries an `agent` block naming the MCP endpoint and the tool that does
the same job.

## Revocation

One layer, and a person owns it.

`revocation_uri`: https://blockquote.io/api/v1/keys/{id}

`DELETE` that path with the `sid` session cookie to revoke one key. The account owner can also
revoke it on https://blockquote.io/account. Revocation is immediate: the next call with that key
answers `401 invalid_api_key`.

**You cannot revoke your own key.** The route is session-only for the reason step 3 gives. If the
key you hold is compromised, tell your user, and let them revoke it.

There is no RFC 7009 revocation endpoint, because there is no OAuth credential to revoke. There
is no RFC 8935 security-event delivery, so nothing pushes a revocation notice anywhere. You learn
that a key died by getting `401 invalid_api_key` on your next call.
