---
title: Claim flow (service_auth & anonymous)
description: How an agent turns a registration into a read-only, account-scoped access token by having a signed-in user authorize it.
---

# Claim flow

A **claim** binds an agent's registration to a signed-in user's organization (or a single
customer within it), granting a **read-only** (`api.read`) access token. Agents never gain
write access. There are two ways to reach a claim:

- **`service_auth`** registration starts a claim ceremony immediately and returns the
  `user_code` directly to the calling agent (a trusted, device-flow-style integration).
- An **`anonymous`** registration (see [Agent authentication](/api/agent-auth)) is usable
  immediately and can *optionally* be upgraded later via `POST /agent/identity/claim`: the
  `user_code` is never returned to the agent on this path; only the signed-in user sees it.

## 1. Register with `service_auth`

`POST https://uptimeify.io/agent/identity`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | string | yes | `"service_auth"` |
| `login_hint` | string | yes | Email of the user who will authorize the agent. Must contain `@`. |

```bash
curl -X POST https://uptimeify.io/agent/identity \
  -H 'Content-Type: application/json' \
  -d '{"type":"service_auth","login_hint":"user@deinkunde.com"}'
```

```json
{
  "registration_id": "reg_…",
  "claim": {
    "user_code": "WDJB-MJHT",
    "verification_uri": "https://uptimeify.io/claim",
    "expires_in": 600,
    "interval": 5
  },
  "claim_token": "clm_…",
  "post_claim_scopes": ["api.read"]
}
```

Display `verification_uri` and `user_code` to your user (an RFC 8628 device-flow-style
prompt). The registration starts `status: pending` and mints no access token until the
ceremony completes. Poll step 3 below with the `claim_token`.

`user_code` and `claim_token` both expire after `expires_in` seconds (600s / 10 minutes);
call `POST /agent/identity` again to start a fresh ceremony if it lapses.

## 2. Or: claim an existing `anonymous` registration

`POST https://uptimeify.io/agent/identity/claim`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `claim_token` | string | yes | The `clm_…` returned from an earlier `POST /agent/identity {"type":"anonymous"}` call. |
| `email` | string | yes | Email of the user who will authorize the claim. Must contain `@`. |

```bash
curl -X POST https://uptimeify.io/agent/identity/claim \
  -H 'Content-Type: application/json' \
  -d '{"claim_token":"clm_…","email":"user@deinkunde.com"}'
```

```json
{
  "verification_uri": "https://uptimeify.io/claim?claim_attempt=cat_…",
  "claim_attempt_token": "cat_…",
  "expires_in": 600,
  "interval": 5
}
```

Note what is **absent**: `user_code`. On this path the agent never learns the code, only
the signed-in, bound-email user sees it, on the `/claim` page. The registration itself stays
`active` and keeps working with its `tools.public` scope throughout: claiming only adds
`api.read`, it never revokes anonymous access while the ceremony is pending. Calling this
endpoint again before it expires overwrites the prior code/attempt token and resets the
10-minute window.

A registration can only be claimed once: a second `POST /agent/identity/claim` (or a
`service_auth` claim, which is always pre-bound) after it's already been confirmed returns
`409 claimed_or_in_flight`.

## 3. Human consent at `/claim`

The bound user must be signed in to Uptimeify as the exact `login_hint` / `email` used above,
then either type the `user_code` shown by the agent, or open the `verification_uri` link
(which carries `?claim_attempt=…` on the anonymous path). The Uptimeify web app resolves that
into the confirmation screen via two session-cookie-authenticated endpoints: these are not
called by the agent directly, they exist for completeness of the flow:

- `GET /api/agent-claim/context?claim_attempt=<token>`: returns `{ registration_id,
  agent_type, scope, bound_email, user_code, expires_at }` for the signed-in bound user, so
  the UI can show what's being authorized (and pre-fill the code on the anonymous path).
- `POST /api/agent-claim/confirm { user_code }`: the signed-in user submits the code to
  authorize. On success: `{ ok: true, registration_id }`.

Confirming binds the registration to the user's organization, or, if the user's role
restricts them to exactly one customer, to that single customer. An `admin` or an
unrestricted `editor` binds the whole organization; a `readonly` (or `editor`) user
restricted to exactly one customer binds that customer; any other case (no customer
representable as a single scope, e.g. a role of `responder`, or a `readonly` user assigned to
several customers) fails the confirmation closed: the agent will see `expired_token` /
`invalid_claim_token` rather than ever receiving an over-broad or ambiguous token.

## 4. Poll for completion

`POST https://uptimeify.io/oauth2/token` (`application/x-www-form-urlencoded`)

| Field | Required | Description |
| --- | --- | --- |
| `grant_type` | yes | `urn:uptimeify:agent-auth:grant-type:claim` |
| `claim_token` | yes | The `clm_…` from step 1 (or the original anonymous registration in step 2). |

```bash
curl -X POST https://uptimeify.io/oauth2/token \
  --data-urlencode 'grant_type=urn:uptimeify:agent-auth:grant-type:claim' \
  --data-urlencode 'claim_token=clm_…'
```

Poll no faster than the `interval` from step 1/2 (5 seconds). Each poll returns one of:

| Status | HTTP | `error` | Meaning |
| --- | --- | --- | --- |
| Not yet confirmed | 400 | `authorization_pending` | Keep polling at `interval`. |
| Polled too fast | 400 | `slow_down` | Back off: you polled faster than `interval`. |
| Token/registration gone | 400 | `expired_token` | The claim token is unknown, its registration was revoked/expired, the outer 24h window elapsed, or it was already redeemed by a previous poll. |
| Confirmed | 200 | - | Success: see below. |

On success:

```json
{
  "access_token": "wsma_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "api.read",
  "identity_assertion": "eyJ…"
}
```

The `claim_token` is single-use: it is atomically consumed on the first successful poll, so
concurrent/retried pollers can never both mint a token. Redeeming it also revokes any prior
`tools.public` access tokens for the same registration in the same all-or-nothing operation:
an anonymous registration's pre-claim tokens stop working once it's claimed.

Store the returned `identity_assertion` (a ~24h EdDSA JWT, same as the base flow) and
re-exchange it via the `urn:ietf:params:oauth:grant-type:jwt-bearer` grant (see
[Agent authentication](/api/agent-auth#exchange-for-an-access-token)) to mint fresh `api.read`
access tokens after this one expires. You do not need to repeat the claim ceremony.

## 5. Use the token (read-only)

The `wsma_` access token from a completed claim is read-only and scoped to the authorizing
user's organization or single customer: `GET /api/websites*`, `GET /api/incidents*`, and
`GET /api/health`. Any write, or any request outside that allowlist, returns `403` with
`data.code = agentTokenReadOnly`.

## Not yet available

`identity_assertion` as a registration `type` (ID-JAG, letting an already-trusted external
OIDC identity mint a token directly, without any claim ceremony) is **not** enabled yet. Do
not rely on it; `POST /agent/identity {"type":"identity_assertion"}` currently returns
`400 issuer_not_enabled`.

## Common errors

| HTTP | `error` / `data.code` | Where | Meaning |
| --- | --- | --- | --- |
| 400 | `service_auth_not_enabled` | `POST /agent/identity` | `service_auth` is not enabled on this deployment. |
| 400 | `invalid_request` | `POST /agent/identity`, `POST /agent/identity/claim`, `/api/agent-claim/*` | Missing/invalid `login_hint`, `claim_token`, `email`, or `user_code`. |
| 400 | `invalid_claim_token` | `POST /agent/identity/claim` | Unknown claim token, or the token's registration is not `anonymous`, or it was revoked. |
| 404 | `invalid_claim_token` | `GET /api/agent-claim/context` | The `claim_attempt` token is unknown/expired, or the signed-in user is not the bound email, indistinguishably (fails closed). |
| 400 | `invalid_claim_token` | `POST /api/agent-claim/confirm` | The `user_code` is wrong/expired, or the confirming user's access can't be resolved to a single org/customer scope. |
| 409 | `claimed_or_in_flight` | `POST /agent/identity/claim` | The registration is already claimed (`organizationId` is already set). |
| 400 | `claim_expired` | `POST /agent/identity/claim` | The outer 24h claim window (from registration creation) has elapsed. |
| 400 | `authorization_pending` | `POST /oauth2/token` (claim grant) | The user has not confirmed yet: keep polling. |
| 400 | `slow_down` | `POST /oauth2/token` (claim grant) | Polling faster than `interval` (5s), back off. |
| 400 | `expired_token` | `POST /oauth2/token` (claim grant) | The claim token/registration expired, was revoked, or was already redeemed. |
| 403 | `agentTokenReadOnly` | Any `/api/*` call with a `wsma_` token | Write attempt, or a path outside the token's read-only allowlist. |
| 429 | - | Any endpoint above | Rate limit exceeded (see [Agent authentication](/api/agent-auth#common-errors)). |

See the [error codes reference](/api/error-codes-and-known-pitfalls#agent-auth-error-codes)
for the full, canonical list.
