# spookat auth.md

For AI agents that set up or run the Spookat chat widget for a site: coding agents (Claude Code, Cursor, Codex, …), site builders, and autopilot agents that answer visitors. The whole manual is at https://spookat.com/llms.txt.

Three ways in. Pick one, then do its steps in order, don't skip ahead:

- **You're an MCP client and your human is at a browser:** add the server with no key and sign in (see "Sign in with OAuth").
- **Your human has a Spookat account already:** ask them for a key (https://app.spookat.com → /install → secret keys) and go to "Use the key".
- **No account yet:** register the site yourself (below). Don't send them to sign up: the claim link does that.

## Sign in with OAuth (MCP clients)

`https://api.spookat.com/mcp` answers 401 with `WWW-Authenticate: Bearer resource_metadata="https://api.spookat.com/.well-known/oauth-protected-resource/mcp"`. From there it's the MCP authorization spec: authorization code with PKCE (S256), a client metadata document (`client_id` is its https URL) or dynamic client registration at `https://api.spookat.com/oauth2/register`. The human signs in at https://app.spookat.com, ticks their sites and picks a scope: `setup`, `chats` (setup plus answering) or `admin` (everything, plus `add_site`). No scope gets `delete_message`: it takes back what a key sent, and a sign-in has no key. Access tokens last an hour, refresh tokens 30 days and rotate. With several sites, every tool takes `site` (the domain). Tokens work on /mcp only.

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

## Discover

- Protected resource: `GET https://api.spookat.com/.well-known/oauth-protected-resource`
- Authorization server: `GET https://api.spookat.com/.well-known/oauth-authorization-server`, its `agent_auth` block has the endpoints below. Identity types: `anonymous` only.

## 1. Register (anonymous)

```
POST https://api.spookat.com/agent/identity
Content-Type: application/json

{ "type": "anonymous", "domain": "example.com" }
```

`domain` is the site the chat goes on (subdomains included). You get back:

- `identity_assertion`: exchange it right away (step 2). Valid 1 hour.
- `claim_token` (`clm_…`): keep it in memory, never show it. You poll with it (step 4).
- `verification_uri`: the claim link for your human.
- `site.snippet`: the one tag to put before `</body>`. `site.site_key` is public.
- `pre_claim_scopes`: `["preclaim"]`, `post_claim_scopes`: `["agent", "admin"]`.

Unclaimed after 7 days, the site and its keys are deleted. Errors: `400 invalid_request` (no or bad domain), `429 slow_down` (a few registrations a minute).

The domain registered before, by you or anyone? You still get a new, separate site, and the response adds `warning` (its first line in `message` too): how many other unclaimed registrations the domain has, and whether a site in someone's account has it, live or not. If one of those is yours, use its key instead: a human with an account can give you one, or sign in with OAuth. When a site on the domain already went live with the snippet on its homepage, the warning ends with "The domain's trial is used": yours goes live without a trial, until a human picks a plan. Nothing about the others (keys, ids, owners) is ever shown.

## 2. Exchange the assertion

```
POST https://api.spookat.com/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<identity_assertion>
```

`access_token` is a pre-claim key (`sk_live_…`, scope `preclaim`). It does the setup on the MCP server and nothing else: `get_config`, `set_style`, `set_font`, `set_accent`, `set_position`, `set_greeting`, `set_question`, `set_css`, `preview`, `get_snippet`, `publish`. `publish` saves your look straight to the widget: nothing is live before the claim, and your human claims the finished look. Connecting Slack or Discord, plans and chats wait for the claim. Each exchange revokes the previous pre-claim key.

## 3. The claim

Install the snippet, set the look, then give your human `verification_uri`. Or have us email it:

```
POST https://api.spookat.com/agent/identity/claim
Content-Type: application/json

{ "claim_token": "clm_…", "email": "their@email.com" }
```

One email a day per address, with nothing you wrote in it. They open the link, sign in or sign up (the link works as an invite, no card), and claim the site. They pick what you keep: an `admin` key (keep setting it up) or an `agent` key (read the setup, answer chats).

The site goes live when it's claimed and the widget has loaded on the domain, in either order. The 14-day trial starts then. One trial per domain: once a site on it has gone live and we found the snippet on its homepage, every later site on that domain starts without one. Before the claim, the widget shows but says the chat opens soon, and takes no messages.

## 4. Poll for the post-claim key

```
POST https://api.spookat.com/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=clm_…
```

- `400 authorization_pending`: not claimed yet. Poll every 5 seconds, or later.
- `400 claim_expired`: nobody claimed it in 7 days. Register again.
- `200`: `access_token` is your new key, scope `admin` or `agent`. The pre-claim key is dead from the claim on. You get it once: after that, `400 invalid_grant`, and a human makes new keys in the dashboard.

## Use the key

Every key (`sk_live_…`) belongs to one site and goes in the `Authorization` header. What each scope does:

- `preclaim`: `get_config`, `set_style`, `set_font`, `set_accent`, `set_position`, `set_greeting`, `set_question`, `set_css`, `preview`, `get_snippet`, `publish`.
- `agent`: `get_config`, `preview`, `get_snippet`, `list_convos`, `read_convo`, `reply`, `add_note`, `handoff`, `end_convo`, `delete_message`, `get_autopilot`. Reads the setup, the chats and the autopilot rules (`get_autopilot`), replies, adds notes, hands off to a human, deletes what it sent (`delete_message`).
- `admin`: every tool: also changes the setup and publishes it, sets the autopilot rules (`set_autopilot`), gives a chat back to autopilot (`give_back`), assigns, closes, deletes and exports. Plus `add_site` when a human made the key in the dashboard or signed in with OAuth; the post-claim key doesn't get it.

No scope pays, cancels, or makes or lists keys, or revokes any key but the one it holds (see "Revoke"). `change_plan` (admin) returns a checkout link a human pays through, or the billing page if the site already pays. Setup changes show up in the site's activity log.

Remote MCP (Streamable HTTP):

```
claude mcp add --transport http spookat https://api.spookat.com/mcp --header "Authorization: Bearer sk_live_…"
```

REST, base url `https://api.spookat.com/v1` (not for pre-claim keys):

```
GET https://api.spookat.com/v1/convos
Authorization: Bearer sk_live_…
```

Errors come as `{ "error": "…", "code": "…" }`: 401 `unauthorized` (no key, or a revoked one), 403 `forbidden_scope` (the scope can't do that), 429 with `Retry-After` (slow down).

## Revoke

`POST https://api.spookat.com/oauth2/revocation` with `token=sk_live_…` (RFC 7009) kills that key. Humans revoke in the dashboard too.

## Without a key

WebMCP: while the dashboard tab is open at https://app.spookat.com, a browser agent can call the same setup tools there with the human's session and no key.

No MCP client at all? A browser agent on any page of https://spookat.com gets this whole flow as WebMCP tools: `register_site` (steps 1 and 2), the pre-claim setup tools, then `claim_site` (step 3: opens the claim page, or emails the link).

The widget itself needs no credentials: the snippet only carries the public site key (`site_live_…`).
