← docs

OAuth 2.0

Use OAuth when the person running your software is not the person who owns the data — an agent, a hosted integration, anything installed by someone else. For your own scripts, an API token from settings is simpler and does the same job.

The authorization server is https://xeve.io. Its metadata is published at https://xeve.io/.well-known/oauth-authorization-server (RFC 8414) and is the only thing you need to hard-code:

{
  "issuer": "https://xeve.io",
  "authorization_endpoint": "https://xeve.io/oauth/authorize",
  "token_endpoint": "https://xeve.io/oauth/token",
  "registration_endpoint": "https://xeve.io/oauth/register",
  "revocation_endpoint": "https://xeve.io/oauth/revoke",
  "code_challenge_methods_supported": ["S256"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_methods_supported": ["none"]
}

The API at https://api.xeve.io is the protected resource, and publishes its own metadata (RFC 9728) pointing back at that issuer.

What this server does and does not do

  • Public clients only. Every client here is a desktop agent, a CLI or a browser extension, and none can keep a secret. There is no client_secret, and token_endpoint_auth_method is none.
  • PKCE is required, S256 only. plain is not offered.
  • Redirect URIs are matched exactly. No prefix matching, no wildcards.
  • Authorization codes are single use. A second use is treated as a leak: every token from that grant is revoked, not just the replay.
  • Refresh tokens rotate. Presenting a rotated one is treated the same way.
  • A refresh may narrow a grant, never widen it.

1. Register

Dynamic client registration (RFC 7591) is open — an agent nobody has pre-registered can introduce itself. Registration grants nothing on its own; no user has approved anything yet.

curl -sX POST https://xeve.io/oauth/register \
  -H 'Content-Type: application/json' \
  -d '{
    "client_name": "My Agent",
    "redirect_uris": ["http://127.0.0.1:7777/callback"],
    "scope": "read:activity read:code"
  }'
{
  "client_id": "xeve_client_…",
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"]
}

A redirect URI must be https, http on loopback, or a private-use scheme such as myapp://callback.

2. Send the user to the authorization endpoint

https://xeve.io/oauth/authorize
  ?response_type=code
  &client_id=xeve_client_…
  &redirect_uri=http://127.0.0.1:7777/callback
  &scope=read:activity%20read:code
  &state=<random>
  &code_challenge=<base64url(sha256(verifier))>
  &code_challenge_method=S256
  &resource=https://api.xeve.io/mcp

The user signs in if they are not already, sees exactly which scopes are being asked for, and approves or declines. The response comes back to your redirect URI with code, your state, and iss=https://xeve.io — compare that last one against the issuer you recorded before you spend the code (RFC 9207).

resource is optional here and required by MCP clients (RFC 8707). Whatever you send at this step must be sent again at the next one, unchanged.

3. Exchange the code

curl -sX POST https://xeve.io/oauth/token \
  -d grant_type=authorization_code \
  -d client_id=xeve_client_… \
  -d code=<code> \
  -d code_verifier=<verifier> \
  -d redirect_uri=http://127.0.0.1:7777/callback
{
  "access_token": "xeve_dt_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "xeve_rt_…",
  "scope": "read:activity read:code"
}

The access token is an ordinary xeve bearer token: send it as Authorization: Bearer … to any endpoint in the API reference, within the scopes it carries.

4. Refresh

curl -sX POST https://xeve.io/oauth/token \
  -d grant_type=refresh_token \
  -d client_id=xeve_client_… \
  -d refresh_token=xeve_rt_…

Each refresh returns a new pair. Store the new refresh token and discard the old one: presenting a rotated token revokes the whole grant, because the only way that happens legitimately is never.

5. Revoke

curl -sX POST https://xeve.io/oauth/revoke -d token=xeve_rt_…

RFC 7009. Always answers 200, whether or not the token existed — anything else would say which tokens are live. Revoking a refresh token revokes the access token it minted, so "disconnect" means disconnected now rather than in an hour.

The user can revoke the same grant from settings, where OAuth tokens appear under the name of the app that asked for them.

Errors

error Means
invalid_client Unknown client_id
invalid_request A missing or malformed parameter — error_description says which
invalid_grant Code or refresh token unknown, expired, replayed, or bound to a different client or redirect
invalid_scope An unknown scope, or a refresh trying to widen a grant
invalid_target A resource that does not match the one authorized
unsupported_grant_type Only authorization_code and refresh_token exist
access_denied The user declined

This page is also served as Markdown: curl -H 'Accept: text/markdown' https://xeve.io/docs/oauth