# auth.md for SEO Bandwagon

How AI agents and developers get access to SEO Bandwagon's MCP server and API.
There are two doors to the MCP server: https://mcp.seobandwagon.com/mcp, where the
client signs a person in with OAuth, and https://seobandwagon.com/api/mcp, where it sends an
API key. The REST API at https://seobandwagon.com takes API keys.

OAuth is available for the MCP server at https://mcp.seobandwagon.com/mcp, whose authorization server is https://mcp.seobandwagon.com. The protected resource metadata below is for this site, where access is by API key, so it lists no authorization server: a token from the OAuth server works only at https://mcp.seobandwagon.com/mcp.

## OAuth (MCP clients)

- Authorization server: https://mcp.seobandwagon.com, metadata (RFC 8414) at
  https://mcp.seobandwagon.com/.well-known/oauth-authorization-server.
- Protected resource: https://mcp.seobandwagon.com/mcp, metadata (RFC 9728) at
  https://mcp.seobandwagon.com/.well-known/oauth-protected-resource/mcp. A request without a token gets 401 with a
  `WWW-Authenticate` challenge naming it, which is how a client finds the
  server.
- Clients identify themselves with a Client ID Metadata Document (an https
  URL as the `client_id`), or register at https://mcp.seobandwagon.com/register.
  Redirect addresses must be https, or http on the person's own computer.
- Authorization code with PKCE (S256) only. Access tokens last
  1 hour; refresh tokens last 90 days and rotate
  on use. Revoke a token at https://mcp.seobandwagon.com/token (RFC 7009).
- The person signs in on seobandwagon.com (Google, or a link sent by email)
  and chooses what to allow. A client that names no scope is offered the read
  scopes: `gsc:read`, `ga4:read`, `rank:read`, `clients:read`, `account:read`.

Scopes, and what each lets an app do:

- `gsc:read`: See your Google Search Console data: your sites, search performance, URL inspection and sitemaps.
- `gsc:write`: Submit and remove sitemaps in your Google Search Console. Changes things.
- `ga4:read`: See your Google Analytics 4 reports and property settings.
- `rank:read`: See the keywords you track and their Google positions.
- `clients:read`: See your client records and the audit reports attached to them.
- `clients:write`: Create client records, add and delete their audit reports, and make share links (administrators only). Changes things.
- `account:read`: See your plan, what it includes and how much of it you are using.
- `billing:write`: Start a Pro checkout or open your billing portal. Nothing is charged or cancelled until you do it yourself on Stripe's page. Changes things.

`clients:write` can be allowed only by an administrator, and
`billing:write` starts unticked, so the person must choose it. A tool that needs a scope
the connection lacks answers 403 `insufficient_scope` with a challenge
naming the scopes to ask for; the client can then ask the person again.

## Discover

Protected resource metadata (RFC 9728) for this site's API keys, as served at
https://seobandwagon.com/.well-known/oauth-protected-resource:

```json
{
  "resource": "https://seobandwagon.com",
  "resource_name": "SEO Bandwagon API",
  "authorization_servers": [],
  "bearer_methods_supported": [
    "header"
  ],
  "scopes_supported": [
    "mcp"
  ],
  "resource_documentation": "https://seobandwagon.com/docs/api"
}
```

The authorization server metadata at https://mcp.seobandwagon.com/.well-known/oauth-authorization-server
carries an auth.md `agent_auth` block for agents that register without a
browser: `identity_endpoint` (also `register_uri`) is
https://seobandwagon.com/api/v1/signup and `claim_endpoint` (also `claim_uri`) is
https://seobandwagon.com/api/v1/signup/verify, the email-code flow under "Without a
browser" below. Its `service_auth` type and `verified_email` assertion
name that flow; no ID-JAG is accepted. The credential it issues is an
`api_key` for https://seobandwagon.com/api/mcp and the REST API, not an OAuth token.

## What's protected

- MCP server, OAuth door: https://mcp.seobandwagon.com/mcp. Every tool needs its own
  scope (listed above) in the connection's grant.
- MCP server, API-key door: https://seobandwagon.com/api/mcp. Every tool requires an API key
  with the `mcp` scope.
- REST API, described at https://seobandwagon.com/openapi.json: each operation names
  what it accepts, usually an API key with a given scope (`captures` for
  /api/v1/captures) or a signed-in session.
- ACP checkout, https://seobandwagon.com/api/acp: an API key with the `billing` scope as
  the Bearer token (discovery: https://seobandwagon.com/.well-known/acp.json).
- Public (no auth): /api/health, all /.well-known/ discovery endpoints,
  and UCP checkout at https://seobandwagon.com/api/ucp, which identifies the platform by its
  UCP-Agent profile (discovery: https://seobandwagon.com/.well-known/ucp). Both
  checkouts end the same way: a person signs in at the session's
  continue_url and pays on Stripe.

## Register

OAuth clients need no setup here: they identify themselves with a Client ID
Metadata Document, or register at https://mcp.seobandwagon.com/register (see OAuth
above). For an API key:

1. Create an SEO Bandwagon account at https://seobandwagon.com/auth/signin (Google, or
   a link sent by email), or without a browser (below).
2. Mint an API key yourself at https://seobandwagon.com/dashboard/api-keys — tick the
   `mcp` scope for MCP access. Up to 5 live keys per account, shown once at
   creation; only a hash is stored. Admins can also mint one via
   scripts/create-api-key.js.

API keys carry one or more scopes you choose at creation — `captures`,
`citations`, `link-building:read`, `link-building:write`, `score`,
`keywords`, `mcp`, `billing` (a key minted before scopes existed carries
`legacy:all`, granting every scope). Scopes are fixed at creation: a key
cannot gain `mcp` later — revoke it and mint a new one.

## Use

With OAuth, give the MCP client the address and let it sign in. Claude Code:

```
claude mcp add --transport http seobandwagon https://mcp.seobandwagon.com/mcp
```

then `/mcp` to sign in; in Claude.ai, add a custom connector with that address.

With an API key, send the key on every request, as an HTTP Bearer token:

```
Authorization: Bearer sbw_<48 hex characters>
```

Connect an MCP client, for example Claude Code:

```
claude mcp add --transport http seobandwagon https://seobandwagon.com/api/mcp \
  --header "Authorization: Bearer sbw_<your key>"
```

Every tool answers for the key's own account, and the tools that change
client records also need an admin account. The server has
42 tools: 33 read data and 9 change it (gsc_submit_sitemap, gsc_delete_sitemap, gsc_verify_domain, clients_create, client_audits_add, client_audits_delete, client_share_link, billing_checkout, billing_portal).

## Errors

- OAuth door (https://mcp.seobandwagon.com/mcp): 401 with a `WWW-Authenticate`
  challenge when the token is missing or no longer valid (sign in again);
  403 `insufficient_scope` with a challenge naming the scopes to ask for;
  502 when the server behind it cannot be reached (retry later, not a
  sign-in problem).
- API-key door (https://seobandwagon.com/api/mcp): 401 `invalid_token` when no key is sent (the challenge then
  carries no error code), or when the key is malformed, unknown or revoked;
  its `error_description` says which. A key without the `mcp` scope gets
  403 `insufficient_scope`. 503 with Retry-After means the key could not be
  checked; retry later. An account gets 120 requests a minute across its
  keys; past that the answer is 429 with Retry-After.
- REST API: 401 without a valid key or session; 403
  `{"error":"insufficient_scope","required":"<scope>"}` when the key lacks
  the route's scope; 503 when the key check itself fails (retry later).
- Rate-limited routes answer 429 with a Retry-After header, in seconds.
- Every error body carries `message`, `fix`, `docs_url` and
  `request_id`, and every code is listed at https://seobandwagon.com/docs/errors.

## Revocation

Remove an OAuth connection at https://seobandwagon.com/dashboard/connections; its tokens
stop working at once. A client can also revoke its own tokens at
https://mcp.seobandwagon.com/token (RFC 7009). Revoke a key at
https://seobandwagon.com/dashboard/api-keys; a revoked key is refused immediately.

### Without a browser

An agent or script can create an account, or add a key to an existing one,
with a code emailed to the address. The person who reads that inbox passes
the code back; nobody has to open a browser.

1. `POST https://seobandwagon.com/api/v1/signup` with `{"email": "you@example.com"}`.
   It always answers `202` with the same body, whether or not the address
   has an account. The email carries an 8-character code like
   `K7QM-4TZP` that works once and expires in 15 minutes; only the newest
   code counts.
2. `POST https://seobandwagon.com/api/v1/signup/verify` with
   `{"email": "you@example.com", "code": "K7QM-4TZP", "key_name": "optional"}`.
   Case and the hyphen are ignored, and 5 wrong tries lock the code. It
   answers `201` with `Cache-Control: no-store` and:
   - `key`: the API key, shown once. Send it as `Authorization: Bearer <key>`.
   - `key_id`: its id, for `DELETE /api/v1/keys?id=`.
   - `scopes`: `["mcp", "score", "keys"]`.
   - `account`: `{"email", "status": "created" | "existing", "plan"}`. A new
     account is on the free plan.
   - `next_steps`: what to call next: check the key (`GET /api/v1/me`),
     run an audit (`POST /api/search`), connect MCP (https://seobandwagon.com/api/mcp).
   - `docs`: this file and https://seobandwagon.com/docs/api.

Errors are JSON `{"error", "message", "fix", "docs_url"}`: `email_invalid`,
`email_not_allowed` (throwaway-mail domains), `email_undeliverable` (no MX
or A record), `signup_rate_limited` (429 with `Retry-After`),
`email_unavailable` (503 with `Retry-After`), `code_invalid` (with
`attempts_remaining`), `code_expired`, `code_locked`, and `key_limit`
(409, `limit: 5`; the code is not spent, so revoke a key and send it again).

The `keys` scope lets a key manage keys: `GET /api/v1/keys` lists the
account's live keys, `POST /api/v1/keys` mints a key with some of its own
scopes (never `keys`), and `DELETE /api/v1/keys?id=` revokes the key itself
or a key it created. Revoking a key also revokes the keys it created.
`legacy:all` never grants `keys`. Search Console tools need a person to
connect Google once, signed in, at https://seobandwagon.com/dashboard/connect.

## More

- API documentation: https://seobandwagon.com/docs/api
- OpenAPI description: https://seobandwagon.com/openapi.json
- API catalog: https://seobandwagon.com/.well-known/api-catalog
- MCP server card: https://seobandwagon.com/.well-known/mcp/server-card.json
- Buying Pro: a key with the `billing` scope starts a checkout with
  POST https://seobandwagon.com/api/v1/billing/checkout (or the billing_checkout MCP
  tool) and gets a Stripe link; a person pays, and the plan changes once
  Stripe confirms. See https://seobandwagon.com/docs/api#billing. Agents that
  speak UCP or ACP can start the same purchase there: the checkout session
  comes back requires_escalation with a continue_url for the person.
