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, andtoken_endpoint_auth_methodisnone. - PKCE is required,
S256only.plainis 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 |