# StatOSS agent registration (auth.md)

You are an agent. StatOSS (status pages, uptime monitoring, errors and logs)
implements [auth.md v0.6](https://github.com/workos/auth.md). Without an account you
can register anonymously, solve a short proof of work, and build a draft: status
pages with their monitors and components, and apps for errors and logs. Nothing
in a draft is public, checked or mailed. When your person claims you, they sign
in, pick one of their teams and type a code you show them; the draft is then
made on that team and you act for them through the API and MCP. Follow the
steps in order.

Guide for people: https://statoss.com/docs/agents#agent-registration

## At a glance

| Item | Value |
| --- | --- |
| Unclaimed registration, its assertion and its draft | 24 hours, then deleted |
| Access token | 60 minutes, at most 5 live at once |
| Proof-of-work challenge | 5 minutes, once |
| Claim code (`user_code`) | 15 minutes, 5 wrong tries; 10 attempts per registration |
| Claimed assertion | 30 days; then your person claims again |
| Draft | 3 pages, 25 monitors and components, 3 apps, 64 KB |
| Anonymous registrations | 5 an hour per IP address |
| Challenges | 30 an hour per IP address |

## Step 1: Discover

- Protected Resource Metadata: https://statoss.com/.well-known/oauth-protected-resource
- Authorization Server Metadata: https://statoss.com/.well-known/oauth-authorization-server

A 401 from the API carries `WWW-Authenticate: Bearer resource_metadata="..."`.
The `agent_auth` block of the second document lists the endpoints below.

## Step 2: Pick a method

- **anonymous**: you have no identity for your person. You get a draft at once;
  the claim comes when your person is ready.
- **service_auth**: you have your person's email address. No draft: nothing works
  until they claim you, and then you work on their team directly.
- **identity_assertion**: no identity provider is trusted here; it answers
  `issuer_not_enabled`.

## Step 3: Register

### anonymous

Get a challenge:

```http
POST https://statoss.com/api/agent/identity/challenge
```

```json
{
  "challenge": "pow1.eyJ2Ijox...",
  "algorithm": "sha256-zero-bits",
  "difficulty": 18,
  "count": 16,
  "expires_at": "2026-10-23T10:05:00.000Z"
}
```

For each `i` from 0 to `count - 1`, find a decimal nonce (at most 16 digits)
such that SHA-256 of the UTF-8 string `<challenge>.<i>.<nonce>` starts with
`difficulty` zero bits. Expected work is `count x 2^difficulty` hashes, a few
seconds on one core.

```python
import hashlib

def solve(challenge: str, difficulty: int, count: int) -> list[str]:
    nonces = []
    for i in range(count):
        n = 0
        while int.from_bytes(hashlib.sha256(f"{challenge}.{i}.{n}".encode()).digest(), "big") >> (256 - difficulty):
            n += 1
        nonces.append(str(n))
    return nonces
```

```js
import { createHash } from "node:crypto";

function solve(challenge, difficulty, count) {
  const nonces = [];
  for (let i = 0; i < count; i++) {
    let n = 0;
    while (BigInt("0x" + createHash("sha256").update(`${challenge}.${i}.${n}`).digest("hex")) >> BigInt(256 - difficulty)) n++;
    nonces.push(String(n));
  }
  return nonces;
}
```

Then register:

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

{
  "type": "anonymous",
  "client_name": "my-agent",
  "proof_of_work": { "challenge": "pow1.eyJ2Ijox...", "nonces": ["81723", "40211", "..."] }
}
```

`client_name` is optional, at most 64 characters, and is shown to people as the
name you gave yourself. Without `proof_of_work` the answer is 400
`proof_of_work_required` with a fresh challenge in the body. Response (200):

```json
{
  "registration_id": "reg_...",
  "registration_type": "anonymous",
  "identity_assertion": "<JWT>",
  "assertion_expires": "<24 hours from now>",
  "pre_claim_scopes": ["draft"],
  "claim_url": "https://statoss.com/api/agent/identity/claim",
  "claim_token": "clm_...",
  "claim_token_expires": "<24 hours from now>",
  "post_claim_scopes": ["read","write"],
  "draft": { "endpoint": "https://statoss.com/api/v1/agent/draft", "lifetime_seconds": 86400 }
}
```

`claim_token` is returned once. Keep it in memory for Step 4.

### service_auth

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

{ "type": "service_auth", "login_hint": "person@example.com", "client_name": "my-agent" }
```

The response carries `registration_id`, `claim_url`, `claim_token`,
`claim_token_expires`, `post_claim_scopes` and the first attempt as
`claim: { user_code, expires_in, verification_uri, interval }`. Continue at 4b.

## Step 3b: Build the draft (anonymous)

Trade the assertion for a token (Step 5); it starts with `sag_` and reaches the
draft only. Every other API call answers 403 `claim_required`.

```http
PUT https://statoss.com/api/v1/agent/draft
Authorization: Bearer sag_...
Content-Type: application/json

{
  "pages": [
    {
      "name": "Acme status",
      "slug": "acme",
      "components": [{ "name": "Support", "groupName": "People" }],
      "monitors": [
        { "name": "API", "type": "http", "url": "https://api.acme.example/health" },
        { "name": "Nightly backup", "type": "heartbeat", "periodMinutes": 1440 }
      ]
    }
  ],
  "apps": [{ "name": "Checkout", "platform": "node", "page": 0 }]
}
```

A monitor takes the fields of `POST /api/v1/pages/{pageId}/monitors` (MonitorInput
in https://statoss.com/api/v1/openapi.json) and is checked by the same rules, so a draft that is accepted
can be made. `slug` is the address you wish for; the plan or a taken address
may give another. `apps[].page` is the place of a draft page, from 0.
`apps[].platform` picks the setup snippet: one of `javascript`, `node`, `nextjs`, `react`, `python`, `go`, `php`, `ruby`, `rust`,
or left out. A PUT
replaces the whole draft; GET reads it back. A 400 names what is wrong, such as
`pages[0].monitors[1]: Enter a full URL, starting with http:// or https://.`

## Step 4: Claim ceremony

### 4a. Start an attempt

service_auth: the `claim` block from Step 3 is your first attempt. anonymous:

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

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

```json
{
  "registration_id": "reg_...",
  "claim_attempt_id": "cla_...",
  "status": "initiated",
  "expires_at": "<15 minutes from now>",
  "claim_attempt": {
    "user_code": "123456",
    "expires_in": 900,
    "verification_uri": "https://statoss.com/agent/claim?attempt=cat_...",
    "interval": 5
  }
}
```

Only someone signed in with that address, confirmed, can finish the attempt.
The same call starts a new attempt when the code ran out; the old link stops
working.

### 4b. Hand off to your person

Show both in one message:

> Open this link, sign in (or sign up), and enter this 6-digit code: **123456**
> https://statoss.com/agent/claim?attempt=cat_...

The code goes into the page, not back to you. The page shows them the name you
gave yourself, when you registered, what your draft holds, the team to put it
in, what the plan leaves out, and what you can do after.

### 4c. Poll for completion

```http
POST https://statoss.com/oauth/token
Content-Type: application/x-www-form-urlencoded

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

| Answer | Meaning |
| --- | --- |
| `authorization_pending` | Not done yet. Poll again after `interval` seconds. |
| `slow_down` | You polled faster than every 5 seconds. Add 5 to your interval. |
| `expired_token` | The code ran out, was entered wrong 5 times, or no attempt is open. Start one (4a); if that answers `claim_expired`, register again. |
| `access_denied` | Your person declined. Register again if they ask you to. |
| `invalid_grant` | Unknown claim token, one collected more than 10 minutes ago or after you exchanged the claimed assertion, or the person disconnected you. |

Success:

```json
{
  "access_token": "sat_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "read write",
  "identity_assertion": "<JWT>",
  "assertion_expires": "<30 days from now>",
  "draft_result": { "pages": [...], "apps": [...], "leftOut": [...] }
}
```

If that answer is lost, poll again: for 10 minutes after the first success,
and until you first exchange the claimed assertion (Step 5), a poll hands out a
new token and assertion, and the ones before stop working. After that the claim
token answers `invalid_grant`.
`draft_result` lists the pages, monitors and apps made, with their ids and a
`dashboardUrl` where your person sees each page and app. `leftOut` names
each page, monitor, component or app that was not made, with why: do not set
anything up for those, such as an SDK for an app that is not there. Your
pre-claim token and assertion stop working.

## Step 5: Exchange the assertion

```http
POST https://statoss.com/oauth/token
Content-Type: application/x-www-form-urlencoded

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

```json
{ "access_token": "...", "token_type": "Bearer", "expires_in": 3600, "scope": "draft" }
```

A claimed assertion gets a `sat_` token with `"scope": "read write"`. The same
assertion mints tokens until it expires. `resource` is optional; it can only be
https://statoss.com/api/v1. `invalid_grant` means the registration ran out, was declined,
claimed (use the new assertion), or disconnected: register again. There is no
refresh token.

## Step 6: Use the access token

`Authorization: Bearer sat_...` on the REST API (https://statoss.com/api/v1, OpenAPI at
https://statoss.com/api/v1/openapi.json) and the MCP server (https://statoss.com/api/v1/mcp). You act for your person on the
team they picked: status pages, monitors, incidents, apps and their DSN keys,
and the rest of the API, writing while they can manage the team. You never see
or make API keys, never change the team's people, and never touch billing:
those answer 403 `agent_refused`.

On a 401 with a token that worked, exchange the assertion again once; if that
answers `invalid_grant`, register again.

## Revocation

- `POST https://statoss.com/oauth/revoke` with `token=<access_token>` (form-encoded) ends one token.
  Always 200. The assertion still works.
- Your person disconnects you under Settings, Connected apps: your tokens stop
  at once and the assertion answers `invalid_grant`.
- An unclaimed registration is deleted 24 hours after it was made.
- https://statoss.com/api/agent/event/notify takes Security Event Tokens (RFC 8935) from trusted identity
  providers. None is trusted, so it answers 400 `invalid_issuer`.

## Errors

The registration endpoints answer `{ "error", "error_description" }`; the token
endpoint uses OAuth's (RFC 6749); the REST API answers `{ "error": { "code", "message" } }`.

| Code | Where | What to do |
| --- | --- | --- |
| `proof_of_work_required` (400) | identity | Solve the challenge in the body and send it. |
| `invalid_challenge` (400) | identity | Wrong, expired or used. Solve the fresh challenge in the body. |
| `issuer_not_enabled` (400) | identity | Register as anonymous or service_auth. |
| `invalid_login_hint` (400) | identity, claim | Give one plain email address. |
| `invalid_request` (400) | any | Fix the request. |
| `invalid_claim_token` (401) | claim | Register again. |
| `claimed_or_in_flight` (409) | claim | Claimed already. Poll the claim grant. |
| `claim_expired` (410) | claim | Ran out or declined. Register again. |
| `too_many_attempts` (429) | claim | 10 attempts used. Register again. |
| `rate_limited` (429) | any | Wait for `Retry-After` seconds. |
| `authorization_pending`, `slow_down`, `expired_token`, `access_denied` (400) | token, claim grant | See 4c. |
| `invalid_grant` (400) | token | Register again. |
| `invalid_target` (400) | token | Leave `resource` out or name https://statoss.com/api/v1. |
| `unsupported_grant_type` (400) | token | Use the jwt-bearer or the claim grant. |
| `claim_required` (403) | API | Your token reaches the draft only until you are claimed. |
| `agent_token_required` (403) | draft | The draft takes an unclaimed agent's token. |
| `agent_refused` (403) | API | API keys and the team's people are not an agent's. |
