Uptimeify Docs
Agentic registration

Identity assertion (ID-JAG)

How an agent signed in at a trusted identity provider registers with an ID-JAG, gets a user's approval for read access and selected changes, and how the provider revokes it.

Identity assertion (ID-JAG)

An agent that already holds an identity from a trusted identity provider can register with an Identity Assertion JWT Authorization Grant (ID-JAG) instead of anonymous or service_auth. If the verified email in that assertion belongs to an Uptimeify account, the account holder approves the agent once on the /claim page and chooses which changes it may make; from then on the agent re-registers with a fresh ID-JAG and receives its token without another approval.

This registration type is off unless Uptimeify trusts at least one identity provider. Check the authorization-server metadata: only when agent_auth.identity_types_supported contains "identity_assertion" are the endpoints on this page live. Otherwise POST /agent/identity {"type":"identity_assertion"} returns 400 issuer_not_enabled and POST /agent/event/notify returns 404.

1. Register with an ID-JAG

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

FieldTypeRequiredDescription
typestringyes"identity_assertion"
assertion_typestringyes"urn:ietf:params:oauth:token-type:id-jag"
assertionstringyesThe ID-JAG issued by your identity provider.
scopestringnoSpace-separated scopes you are asking for, for example "api.read api.write.monitors". Unknown values are ignored; without any known scope the request counts as "api.read".

The ID-JAG must:

  • be signed by a key from the JWKS of an issuer on Uptimeify's trust list (iss),
  • carry aud = https://uptimeify.io/api/, an unexpired exp and a jti that has never been used before (every jti is accepted exactly once),
  • carry email with email_verified: true,
  • carry an auth_time no older than 3600 seconds; otherwise the answer is 401 login_required with max_age: 3600, and you need a fresh sign-in at your provider.
curl -X POST https://uptimeify.io/agent/identity \
  -H 'Content-Type: application/json' \
  -d '{"type":"identity_assertion","assertion_type":"urn:ietf:params:oauth:token-type:id-jag","assertion":"<ID-JAG>","scope":"api.read api.write.monitors"}'

The response is one of three outcomes. All of them are sent with Cache-Control: no-store.

Approval needed: 401 interaction_required

The email belongs to an Uptimeify account, and this agent has not been approved yet.

{
  "error": "interaction_required",
  "registration_id": "reg_…",
  "claim": {
    "user_code": "WDJB-MJHT",
    "verification_uri": "https://uptimeify.io/claim",
    "verification_uri_complete": "https://uptimeify.io/claim?claim_attempt=cat_…",
    "expires_in": 600,
    "interval": 5
  },
  "claim_token": "clm_…",
  "requested_scope": "api.read api.write.monitors"
}

Send your user to verification_uri_complete. Signed in as the account with that email, they see what you asked for: read access, and every requested write area as a separate checkbox. No write area is selected by default; the user ticks only what the agent really needs. Then poll POST /oauth2/token with the claim grant and the claim_token, exactly as in Claim flow, step 4. The success response carries the approved scope and an identity_assertion for later exchanges.

An email match never links an account on its own: nothing is granted until the signed-in account holder confirms on /claim.

Already approved: 200 with an account assertion

The registration for your provider identity (iss, sub) has been approved before.

{
  "registration_id": "reg_…",
  "identity_assertion": "<jwt>",
  "scope": "api.read api.write.monitors",
  "token_endpoint": "https://uptimeify.io/oauth2/token"
}

scope is what you asked for if the approval covers it, otherwise everything that was approved. Without a scope field you receive everything that was approved. Exchange the identity_assertion via the jwt-bearer grant as described in Agent authentication.

No matching account: 200 with tools.public

No Uptimeify account has this email. The agent is registered and can use the public check tools straight away, nothing more.

{
  "registration_id": "reg_…",
  "identity_assertion": "<jwt>",
  "scope": "tools.public",
  "token_endpoint": "https://uptimeify.io/oauth2/token"
}

2. What the agent may do

  • Never more than the approving user. Every time a token is minted, Uptimeify reads that user's current role and customer assignment. If the role has changed since the approval, the token gets the current one; the role is never stored with the registration.
  • Only what was approved. An exchange asking for a scope that was not approved is refused with 400 invalid_grant (Identity-assertion grant refused: scope_exceeds_grant.).
  • Global supporters never write. For such an account every write area is dropped at approval and again at every exchange; asking for write access alone then returns 400 invalid_grant (nothing_left).
  • A changed customer assignment needs a new approval. If the user's customer binding differs from the one approved, the exchange returns 400 invalid_grant (binding_changed).
  • No user, no token. If the approving user has been deleted, the exchange returns 400 invalid_grant (no_human).
  • Writes follow the same rules as every agent token: they need the write kill-switch on this deployment, and each write area reaches only its own paths; see error codes. Where Uptimeify records who made a change, a change made by the agent is attributed to the approving user.

3. Revocation

The account holder sees the agent under Settings → Connected apps as <provider host> (Agent) and can revoke it there (see Connected apps). Revocation ends the registration and every access token derived from it immediately.

The identity provider can revoke it too, with a Security Event Token (RFC 8417) pushed to Uptimeify (RFC 8935):

POST https://uptimeify.io/agent/event/notify

curl -X POST https://uptimeify.io/agent/event/notify \
  -H 'Content-Type: application/secevent+jwt' \
  --data-binary '<SET>'

The SET must be signed by a trusted issuer, carry aud = https://uptimeify.io/api/ and the sub of the ID-JAG, and name one of the event types listed in the metadata under agent_auth.events_supported:

  • https://schemas.openid.net/secevent/risc/event-type/session-revoked
  • https://schemas.openid.net/secevent/caep/event-type/session-revoked

A SET that revoked a registration is answered with 202 and an empty body. Anything else (signature, issuer, audience, event type, unknown sub) is answered with 400 and {"err":"invalid_request"}, without saying which check failed, and revokes nothing. The endpoint URL is also published as agent_auth.events_endpoint.

Common errors

HTTPerror / errWhereMeaning
400issuer_not_enabledPOST /agent/identityUptimeify trusts no identity provider on this deployment.
400invalid_requestPOST /agent/identityWrong assertion_type, missing assertion, or the ID-JAG failed a check (signature, untrusted issuer, audience, expiry, reused jti, unverified email).
401login_requiredPOST /agent/identityauth_time is older than max_age (3600 s): sign in again at your provider.
401interaction_requiredPOST /agent/identityNot an error to retry: the account holder has to approve on /claim first.
400invalid_grantPOST /oauth2/token (jwt-bearer)The exchange exceeds the approval, the customer binding changed, the user was deleted, or nothing is left after dropping write access.
400expired_tokenPOST /oauth2/token (claim grant)As in the claim flow; also when the approving user can no longer be resolved at the moment of minting.
400invalid_request (err)POST /agent/event/notifyWrong Content-Type, empty body, or the SET was not accepted.
404-POST /agent/event/notifyUptimeify trusts no identity provider, or requested on a custom domain.

See the error codes reference for the full list.

On this page