Claim flow (service_auth & anonymous)
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_authregistration starts a claim ceremony immediately and returns theuser_codedirectly to the calling agent (a trusted, device-flow-style integration).- An
anonymousregistration (see Agent authentication) is usable immediately and can optionally be upgraded later viaPOST /agent/identity/claim: theuser_codeis 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 @. |
curl -X POST https://uptimeify.io/agent/identity \
-H 'Content-Type: application/json' \
-d '{"type":"service_auth","login_hint":"user@deinkunde.com"}'{
"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 @. |
curl -X POST https://uptimeify.io/agent/identity/claim \
-H 'Content-Type: application/json' \
-d '{"claim_token":"clm_…","email":"user@deinkunde.com"}'{
"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). |
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:
{
"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) 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). |
See the error codes reference for the full, canonical list.