# Blockquote API reference

API base: https://blockquote.io/api/v1

Full schemas: [OpenAPI document](https://blockquote.io/api/v1/openapi.json).
Authentication: [API keys and anonymous access](https://blockquote.io/auth.md).

## GET /api/v1/healthz

Health check

Reports that the Worker answers, and with `deep=1` that its D1 database and the monitoring heartbeat answer too. Anonymous, and it spends no scan quota — poll it as a liveness probe. The shallow form touches no storage; `deep=1` costs one D1 query and one KV read, so poll that form less often.

## GET /api/v1/chain/status

Pause state of the autonomous chain

Reports the pause switch of the repository's autonomous chain as its mirror last wrote it. `frozen` is true only when the switch is pressed. `stale` is true when no mirror copy exists, or the copy is older than 45 minutes: the writer is down and the switch is unknown. The two fields are independent; a caller that may act reads both false. Anonymous, one KV read, spends no scan quota, and is never cached.

## GET /api/v1/chain/board

The chain board: every object's stage and its last runs

The snapshot the autonomous chain's sweep writes every 10 minutes: one entry per open issue and pull request with its stage, how long it has held it, and the last run of each stage workflow against it; the sweep's last ten decisions with their reasons; and each routine's last heartbeat. `stale` is true when no snapshot exists or it is older than 45 minutes. Needs an API key with the `scan` scope, or a signed-in account on the Agency plan. One KV read, spends no scan quota, never cached.

## POST /api/v1/scan

Start a scan

Dispatches an asynchronous scan of an HTTPS URL. A fresh cached report is returned immediately (status done, cached true, scannedAt when it ran); otherwise poll GET /api/v1/scan/{id} or subscribe to /api/v1/scan/{id}/events. Send refresh true to skip the cache and scan the live page — a session or a live API key only. Rate-limited per IP for anonymous callers — a day bucket and a calendar-month bucket — and per plan for signed-in users; only a dispatched scan spends a slot, so a cached report is free. A replayed answer is served before the quota is read, so it carries no RateLimit fields and spends no slot.

## GET /api/v1/scan/{id}

Fetch a scan report

Returns the finished report, filtered for the viewer. A caller with no account keeps 1 recommendation in full and the rest locked by title; a free account keeps 3 recommendations, and a paid plan keeps every one. Send the `sid` cookie or a bearer API key to be recognised. With `Accept: text/markdown` the finished report is returned as one Markdown document instead of JSON — every check, score, and recommendation in a single agent-readable file.

## GET /api/v1/scan/{id}/events

Stream scan lifecycle events (SSE)

Server-Sent Events stream of `scan` events until the scan reaches a terminal state (done or error). Served by a Durable Object hub with a D1-polling fallback.

## GET /api/v1/compare

Compare two scans of one URL

What changed between two finished scans of the same URL: score and per-category deltas, every check whose verdict flipped, and recommendations split into resolved, new and still open. Name the pair with from (the baseline scan) and to (the newer one), or pass url to compare that page's two newest stored scans — one form or the other. With `Accept: text/markdown` the same comparison comes back as one Markdown section. Filtered for the viewer's plan like GET /api/v1/scan/{id}: fix titles beyond the free allowance are withheld from free-plan viewers.

## GET /api/v1/scan/{id}/ai-instructions

Fetch the report as a ready-to-paste AI agent prompt

The finished report wrapped in instructions for a coding or content agent: apply the fixes, in the order the report lists them, and how to verify. Plain Markdown, gated for the viewer's plan exactly like GET /api/v1/scan/{id}.

## GET /api/v1/history

List the account's scanned URLs

One entry per URL (deduplicated by URL hash): the most recent scan plus how many versions exist. Newest first, paged with cursor and limit; next_cursor is null on the last page.

## GET /api/v1/history/{urlHash}

List every scan of one URL

Newest first, paged with cursor and limit; the history list's `versions` field states the true total. next_cursor is null on the last page.

## DELETE /api/v1/history/{urlHash}

Delete every scan of one URL

Deletes all scan records and stored HTML snapshots for the URL. Not plan-gated: deleting your own stored scans survives a downgrade.

## DELETE /api/v1/history/scans/{id}

Delete one scan from the history

Deletes a single scan record and its stored HTML snapshot. Not plan-gated: deleting your own stored scans survives a downgrade.

## POST /api/v1/auth/request-link

Request a magic sign-in link

Emails a one-time link to the address, and creates the account on first use. This is the only way in: an agent cannot finish the flow, because the link arrives by email and a person must open it. That person then creates the `bq_…` API key on /account. Metered per address per hour, not against the scan quota.

## POST /api/v1/auth/callback

Complete magic-link sign-in

Consumes the token the confirm page read from its query string, sets the `sid` session cookie, and destroys any session already on the request. GET is not supported on this path on purpose — see /login/confirm, which never touches the token.

## POST /api/v1/auth/logout

Sign out

Destroys the session the `sid` cookie names and clears the cookie. It needs no credential and answers 200 whether or not a session was there. It does nothing to an API key: revoke one with DELETE /keys/{id}.

## GET /api/v1/auth/me

Current session

Anonymous callers get `{ authenticated: false }` rather than 401.

## POST /api/v1/billing/checkout

Start a Stripe checkout

Creates a Stripe-hosted checkout session for the Pro or Agency plan and returns its URL. A person opens that URL and pays; the plan starts when Stripe confirms. Session cookie only — a `bq_…` API key is refused here, so no agent can spend money on the account.

## POST /api/v1/billing/portal

Open the Stripe customer portal

Creates a Stripe-hosted portal session and returns its URL, where a person changes the plan, updates the payment method, or cancels. It needs a billing profile, so an account that never bought anything answers 400. Session cookie only, for the same reason checkout is.

## POST /api/v1/stripe/webhook

Stripe event webhook

Called by Stripe, not by API consumers. Authenticated via the `stripe-signature` header; duplicate event ids are acknowledged without reprocessing.

## GET /api/v1/monitors

List active monitors

An API key needs the `read` scope.

## POST /api/v1/monitors

Add a monitored URL

An API key needs the `monitors` scope.

## DELETE /api/v1/monitors/{id}

Deactivate a monitor

An API key needs the `monitors` scope.

## POST /api/v1/monitors/{id}/tracking-consent

Allow AI answer tracking for a monitored URL

Records the engines, the time and the consenting IP address. An account session only: an API key cannot consent for a person and is refused with 403.

## DELETE /api/v1/monitors/{id}/tracking-consent

Withdraw AI answer tracking consent

Clears the consent record. The next weekly run skips this URL. An account session only, like granting it.

## GET /api/v1/webhooks

Get the configured webhook endpoint

Works with the session cookie or a bearer API key. The `webhooks` scope is not offered at key creation, so a key created since scopes shipped answers 403 insufficient_scope here; a key issued before scopes existed carries `read`, `scan`, `monitors`, `webhooks` and `billing`, so it still reaches these routes.

## POST /api/v1/webhooks

Create or replace the webhook endpoint

Works with the session cookie or a bearer API key. The `webhooks` scope is not offered at key creation, so a key created since scopes shipped answers 403 insufficient_scope here; a key issued before scopes existed carries `read`, `scan`, `monitors`, `webhooks` and `billing`, so it still reaches these routes.

## DELETE /api/v1/webhooks

Delete the webhook endpoint

Works with the session cookie or a bearer API key. The `webhooks` scope is not offered at key creation, so a key created since scopes shipped answers 403 insufficient_scope here; a key issued before scopes existed carries `read`, `scan`, `monitors`, `webhooks` and `billing`, so it still reaches these routes.

## POST /api/v1/webhooks/rotate-secret

Rotate the signing secret

Works with the session cookie or a bearer API key. The `webhooks` scope is not offered at key creation, so a key created since scopes shipped answers 403 insufficient_scope here; a key issued before scopes existed carries `read`, `scan`, `monitors`, `webhooks` and `billing`, so it still reaches these routes.

## POST /api/v1/webhooks/enable

Re-enable deliveries

Works with the session cookie or a bearer API key. The `webhooks` scope is not offered at key creation, so a key created since scopes shipped answers 403 insufficient_scope here; a key issued before scopes existed carries `read`, `scan`, `monitors`, `webhooks` and `billing`, so it still reaches these routes.

## POST /api/v1/webhooks/disable

Pause deliveries

Works with the session cookie or a bearer API key. The `webhooks` scope is not offered at key creation, so a key created since scopes shipped answers 403 insufficient_scope here; a key issued before scopes existed carries `read`, `scan`, `monitors`, `webhooks` and `billing`, so it still reaches these routes.

## POST /api/v1/webhooks/test

Send a signed sample delivery

Delivers a recognizably synthetic payload shaped exactly like a weekly result and reports the endpoint's response. Limited to 5 per hour.

Works with the session cookie or a bearer API key. The `webhooks` scope is not offered at key creation, so a key created since scopes shipped answers 403 insufficient_scope here; a key issued before scopes existed carries `read`, `scan`, `monitors`, `webhooks` and `billing`, so it still reaches these routes.

## GET /api/v1/keys

List active API keys

Metadata only — the token itself is returned exactly once, by POST. Key management requires the session cookie; a bearer key cannot manage keys.

## POST /api/v1/keys

Create an API key

Returns the `bq_…` bearer token exactly once. Store it — only its hash is kept. At most 5 keys you create yourself; connected apps do not count against it.

## DELETE /api/v1/keys/{id}

Revoke an API key

Revokes one `bq_…` key immediately and for good. Every later call carrying it answers 401 invalid_api_key, and nothing is issued in its place — create the replacement on /account first. Session cookie only: a key cannot revoke itself or any other.

## GET /api/v1/account/usage

Plan, limits, and current usage

Works with the session cookie or a bearer API key. A presented but invalid/revoked key answers 401 invalid_api_key rather than degrading to anonymous.

## GET /api/v1/notifications

Read email notification preferences

`all` is the master switch: when it is false no notification email is sent, whatever the per-type flags say. The sign-in link is not a notification and is never suppressed.

## POST /api/v1/notifications

Update email notification preferences

A partial patch: only the flags named are written. Requires the session cookie; a bearer key is refused with `session_required`, because no grantable scope covers account settings.

## GET /api/v1/unsubscribe

Ask whether to turn all notification email off

The Unsubscribe link in every notification email footer. No session and no API key: the recipient may not be signed in on the device that opens the mail. This route reads no table and writes nothing. It answers a page with a Confirm button that posts back to the same path, so a mail gateway that prefetches every link cannot unsubscribe anyone. The page is the same whether the token is live, spent or malformed.

## POST /api/v1/unsubscribe

Turn all notification email off

Spends the token and turns the master switch off for its user. The per-type flags keep their stored values; the master switch suppresses every notification email whatever they say. The token is single-use and grants exactly this one write; it reads nothing, so a forwarded email cannot become account access. An unknown, spent or malformed token answers the same page with status 200, so the route cannot be used to test guessed tokens.

## GET /api/v1/openapi.json

This OpenAPI document

Serves this document, which is the file to generate a client from. Anonymous, cached five minutes, and free of every quota. Its `servers` entry names the versioned base, so a generated client calls the current version and never the superseded alias.

## GET /api/v1/docs

Interactive API reference

Serves the reference UI a person reads, rendered from this document. An agent should fetch openapi.json instead: the same content, without the HTML. Anonymous and free of every quota.

## GET /api/v1/batch

Read up to ten stored scan reports in one request

Supply comma-separated scan IDs. Each read uses the caller's session or bearer key and the normal report plan filter. Results preserve input order and duplicates. Each result contains its own status, headers, and JSON body, including errors. The outer response is 200 for a valid batch, even when individual reads fail. No scans are started. JSON only.

## GET /api/v1/openapi.json.md

Read the OpenAPI contract as Markdown

Contains the complete current OpenAPI JSON contract with document metadata.

## GET /api/v1/sandbox.md

Read the isolated sandbox guide

Lists executable synthetic fixture URLs and the boundaries of the read-only sandbox.

## GET /api/v1/sandbox/scan/sandbox-example.md

Read the synthetic sandbox report as Markdown

Contains the same synthetic JSON fixture as the report endpoint. No URL was scanned.

## GET /api/v1/sandbox/batch.md

Read sandbox batch usage and error documentation

Documents the required ids parameter and an executable fixture example. This page does not execute a batch.

## GET /api/v1/sandbox

Discover isolated synthetic report fixtures

Lists fixture IDs for client tests. No account, quota, or production data is used.

## GET /api/v1/sandbox/scan/{id}

Read a synthetic report fixture

Returns a fixed synthetic ScanReport example. It was not fetched or analyzed.

## GET /api/v1/sandbox/batch

Read up to ten synthetic report fixtures in one call

Reads fixture reports in input order. Unknown IDs fail the whole batch; no production calls occur.

## GET /api/v1/docs.md

Blockquote API reference in Markdown

Every operation, derived from the OpenAPI document. Anonymous and free of quotas.

## GET /api/v1/llms.txt

Scoped llms.txt for the API section

Serves a short plain-text map of this API — the interfaces, the calls an agent makes most, and how authentication works — for a reader that wants the shape before parsing the schema. Anonymous and free of every quota.
