# auth.md — Pickrate

You are an agent. Pickrate supports **agentic registration**: you can create a Pickrate workspace on
behalf of your user, instrument their site, and start measuring which AI agents read it — without a
human filling out a signup form.

Flow: discover → register → (claim if needed) → exchange for an access_token → call the API. Follow
the steps in order; do not skip ahead.

**What you get.** Pickrate is analytics for the agent channel: which AI agents crawl and read a
site, what they read, and which of those visits convert. Registering gets you a workspace and a
collector key. Installing the collector takes one line — see https://pickrate.io/setup.md once you're
registered.

## Step 1 — Discover

Two hops. If you got here from a 401, its `WWW-Authenticate` header carried the PRM URL.

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

That returns `resource` (the API root — use it as the `aud` for an ID-JAG) and
`authorization_servers`. Then fetch the authorization server metadata:

```http
GET https://pickrate.io/.well-known/oauth-authorization-server
```

Read the `agent_auth` block in full: `identity_endpoint` (Step 3), `claim_endpoint` (Step 4),
`identity_types_supported`, and `identity_assertion.trusted_issuers` — the providers whose ID-JAGs
this service accepts.

## Step 2 — Pick a method

1. **You can mint an ID-JAG for your user, audience-bound to `https://pickrate.io/api/`, and your provider is
   in `trusted_issuers`** → [identity_assertion](#identity_assertion).
2. **You have only the user's email** → [service_auth](#service_auth). Claim ceremony required.
3. **You have neither** → [anonymous](#anonymous). Recommended default for a coding agent setting up
   a site it's working on. Claim ceremony deferred until a human wants the data.

## Step 3 — Register

For `identity_assertion` and `service_auth`, confirm with your user first — surface Pickrate's name
and the scopes you'll act under. That's their only consent gate. `anonymous` needs no consent:
there is no user identity being asserted, and the workspace is write-only until a human claims it.

### anonymous

```http
POST https://pickrate.io/api/agent/identity
Content-Type: application/json

{ "type": "anonymous" }
```

Response (200):

```json
{
  "registration_id": "…",
  "registration_type": "anonymous",
  "identity_assertion": "<service-signed JWT>",
  "assertion_expires": "…",
  "pre_claim_scopes": ["analytics.write"],
  "claim_url": "https://pickrate.io/api/agent/identity/claim",
  "claim_token": "clm_…",
  "claim_token_expires": "…",
  "post_claim_scopes": ["analytics.read", "analytics.write"],
  "workspace": { "slug": "…" }
}
```

**Pre-claim scope is write-only, by design.** You can create a workspace, mint a collector key, and
send events. You cannot read anyone's analytics until a human claims the workspace. Exchange the
`identity_assertion` at [Step 5](#step-5--exchange-the-assertion) and start sending data; run
[Step 4](#step-4--claim-ceremony) whenever your user wants to see it.

`claim_token` is returned exactly once — hold it in memory for the ceremony; don't persist it.

### service_auth

```http
POST https://pickrate.io/api/agent/identity
Content-Type: application/json

{ "type": "service_auth", "login_hint": "user@example.com" }
```

Response (200) carries no `identity_assertion` — an email you were told is not an identity, so
nothing is issued and no workspace is provisioned until the ceremony completes. The response
includes a `claim` block (`user_code`, `verification_uri`, `expires_in`, `interval`); go to
[Step 4b](#4b-hand-off-to-the-user).

### identity_assertion

Check `agent_auth.identity_assertion.trusted_issuers` first. If your provider isn't listed, stop and
fall back to `service_auth` or `anonymous` — an untrusted issuer returns `issuer_not_enabled`.

Mint the ID-JAG with `aud` = `https://pickrate.io/api/`, `iss` = your provider's issuer URL,
`email_verified: true` (or `phone_number_verified: true`), a fresh `jti`, a near-term `exp`, and
`auth_time` — **required**, and no older than 3600 seconds.

```http
POST https://pickrate.io/api/agent/identity
Content-Type: application/json

{
  "type": "identity_assertion",
  "assertion_type": "urn:ietf:params:oauth:token-type:id-jag",
  "assertion": "<your ID-JAG JWT>"
}
```

Three outcomes:

- **200** — the delegation is known, or the identity is new to Pickrate. You get an
  `identity_assertion`; go to [Step 5](#step-5--exchange-the-assertion).
- **401 `interaction_required`** — the asserted email matches an existing Pickrate account but this
  `(iss, sub)` has no delegation yet. Pickrate will not bind a provider identity to someone's
  existing account on your say-so. The body carries a `claim` block; run [Step 4](#step-4--claim-ceremony).
- **401 `login_required`** — `auth_time` is missing or stale. Re-authenticate the user at your
  provider (`prompt=login`) and mint a fresh ID-JAG. Nothing at Pickrate helps here.

Each ID-JAG is single-use: a repeated `jti` is rejected.

## Step 4 — Claim ceremony

A signed-in human confirms a 6-digit code **you give them**. The code travels you → user; the user
authenticates to Pickrate and types it into a page Pickrate owns.

### 4a. Get the ceremony materials

`service_auth` and `interaction_required` responses already include a `claim` block — skip to 4b.
For **anonymous**, ask for one:

```http
POST https://pickrate.io/api/agent/identity/claim
Content-Type: application/json

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

The `email` is **required** and binds the ceremony to the human you're acting for — only that
signed-in user can complete it. Without it, anyone who saw the `user_code` could take the workspace.

### 4b. Hand off to the user

Surface `verification_uri` and `user_code` in one message:

> Open this link, sign in (or sign up), and enter this 6-digit code: **123456**
> https://pickrate.io/login?return_to=…

Be explicit that the code goes into the page they land on, not back to you.

### 4c. Poll for completion

```http
POST https://pickrate.io/api/oauth2/token
Content-Type: application/x-www-form-urlencoded

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

While waiting: `{ "error": "authorization_pending" }`. Honor `interval`
(5s); on `slow_down`, add 5s. On success you get a standard token
response plus an `identity_assertion` for the refresh path.

For **anonymous**, completing the ceremony **revokes every pre-claim access_token you hold** — drop
them and use the post-claim one. If the code window lapses (`expired_token`) but the registration is
still live, call `https://pickrate.io/api/agent/identity/claim` again with the same `claim_token` and `email` for a
fresh code. A `410 claim_expired` means start over at Step 3.

## Step 5 — Exchange the assertion

```http
POST https://pickrate.io/api/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<identity_assertion>&resource=https://pickrate.io/api/
```

Returns `access_token` (Bearer, 1h). The same assertion mints more access tokens until it expires.
`invalid_grant` means the assertion is expired, superseded, or revoked — restart at Step 3.

## Step 6 — Use the access_token

```http
POST https://pickrate.io/api/agent/keys
Authorization: Bearer <access_token>
Content-Type: application/json

{ "kind": "secret", "label": "collector" }
```

Returns a collector key (shown **once**). Requires `analytics.write`. With it:

- **Install the collector** — fetch https://pickrate.io/setup.md and follow it. One line in most stacks.
- **Send events** — `POST https://pickrate.io/api/collect` with the secret key, for touch / identify /
  convert. Docs: https://pickrate.io/docs/attribution.md
- **Read the workspace** — `GET https://pickrate.io/api/agent/workspace` with your access_token. Requires
  `analytics.read`, so this works only after a human has claimed the workspace. Before that you'll get
  `403 insufficient_scope` — that's the design, not a bug.

Refresh: re-run Step 5 when the access_token expires. There is no refresh_token; the two-step
pattern replaces it.

## Errors

| Code | Where | What to do |
| --- | --- | --- |
| `invalid_request` | `/agent/identity` | Fix the body / the ID-JAG claims. |
| `issuer_not_enabled` | `/agent/identity` | Your provider isn't trusted here. Use anonymous or service_auth. |
| `interaction_required` (401) | `/agent/identity` | Existing account matched. Run the claim ceremony from the body's `claim` block. |
| `login_required` (401) | `/agent/identity` | Re-authenticate the user at your provider; mint a fresh ID-JAG. |
| `invalid_claim_token` | `/agent/identity/claim` | Wrong or expired token. Restart at Step 3. |
| `claimed_or_in_flight` | `/agent/identity/claim` | Already claimed. Re-read the Step 3 response. |
| `claim_expired` (410) | `/agent/identity/claim` | Registration expired. Restart at Step 3. |
| `authorization_pending` | `/oauth2/token` | Keep polling at `interval`. |
| `slow_down` | `/oauth2/token` | Add 5s to your interval. |
| `expired_token` | `/oauth2/token` | Re-initiate the claim for a fresh code. |
| `invalid_grant` | `/oauth2/token` | Assertion expired/superseded/revoked. Restart at Step 3. |
| `unsupported_grant_type` | `/oauth2/token` | Use one of the two grants in AS metadata. |
| `insufficient_scope` (403) | resource API | Unclaimed workspaces are write-only. Run Step 4. |
| `rate_limited` (429) | any | Back off and retry. |

5xx → exponential backoff. 4xx → don't retry the same payload; act on the table.

## Revocation

- **Credential layer** — `POST https://pickrate.io/api/oauth2/revoke` with
  `token=<access_token>&token_type_hint=access_token` (RFC 7009). Kills one access_token; your
  assertion survives, so re-run Step 5.
- **Registration layer** — a provider can POST a Security Event Token to
  `https://pickrate.io/api/agent/event/notify` to kill a registration and everything derived from it. You'll
  discover it as `invalid_grant` at Step 5; restart at Step 3.

Full API reference: https://pickrate.io/docs
