Integrate secure authentication and AI rights management into your applications with our OAuth 2.0 API
OAuth 2.0 with PKCE flow, AES-256-GCM encryption, and zero-knowledge architecture ensure maximum security for your users.
Simple REST API with comprehensive documentation and SDKs for popular programming languages.
Access user's AI rights tokens (AIVID, AIPLT, SLIVR) and verified identity information with consent.
const AIVERID = 'https://aiverid-backend-production.up.railway.app';
// 1. Send the user to the authorization endpoint
const authUrl = new URL(`${AIVERID}/oauth/authorize`);
authUrl.searchParams.append('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.append('redirect_uri', 'YOUR_CALLBACK_URL');
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('scope', 'profile email');
authUrl.searchParams.append('state', generateRandomState());
// Optional: require a verified identity for this flow (1-4).
// The user is prompted to step up before consent is granted.
// authUrl.searchParams.append('min_identity_level', '2');
window.location.href = authUrl.toString();
// 2. Exchange the code for tokens (server-side β never in the browser)
const code = new URLSearchParams(window.location.search).get('code');
const response = await fetch(`${AIVERID}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code,
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET', // omit for public clients; send code_verifier instead
redirect_uri: 'YOUR_CALLBACK_URL'
})
});
const { access_token, refresh_token } = await response.json();
// 3. Read the profile
const me = await fetch(`${AIVERID}/oauth/userinfo`, {
headers: { Authorization: `Bearer ${access_token}` }
}).then(r => r.json());
// me.sub / me.aiverid β the member number (opaque string, e.g. "TH...")
// me.picture β profile photo URL (not me.avatar_url)
// me.verification_level β 1 Basic Β· 2 Verified Β· 3 Approved Β· 4 Certified
Your redirect_uri is compared by exact string match β no host aliasing,
no path normalisation, no trailing-slash forgiveness. Register and send the
canonical host your app actually serves,
character for character.
This is the integration bug that costs the most time, because it fails
after a successful authorization and looks like a broken state check:
register https://www.example.xyz/callback while your app canonicalises
www to the apex, and the browser lands on the redirect, gets bounced to
the apex, and arrives at your callback without the session cookie you set on the
apex β so your own state comparison fails and loops. Nothing in the
AIVerID response says "host mismatch", and nothing can: the hub redirected to
precisely the URI you registered. Check the canonical host first whenever a callback
reports an invalid or missing state.
If your app answers on both hosts, register both β but still send the canonical one.
Give each distinct flow its own callback path
(/oauth/callback for sign-in, a separate one for any agent or
elevated-scope grant) so the two can never share state or be confused in a log.
Clearing your own session is not enough β the tokens AIVerID issued you stay valid
until they expire. Your sign-out MUST also revoke them
(RFC 7009).
Revoke each token separately: revocation matches an exact token, so the access token
and the refresh token need one call each. The examples here post JSON; the endpoint
also accepts standard application/x-www-form-urlencoded bodies, so a
generic RFC 7009 client library works unmodified.
Two rules that are easy to get wrong:
session cookies must be cleared server-side
β document.cookie cannot touch an httpOnly cookie and fails
silently, leaving a "signed-out" user fully signed in; and
never send your client secret from browser code
β revocation accepts client_id + token without a secret, precisely so
public and browser-side clients can call it.
// server-side route β e.g. POST /api/auth/logout
// async so a synchronous throw becomes a rejection allSettled can absorb
const revoke = async (token) =>
fetch('https://aiverid-backend-production.up.railway.app/oauth/revoke', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_id: CLIENT_ID, token }), // no secret needed
})
// best-effort: an AIVerID outage must never trap a user in a session
await Promise.allSettled([revoke(accessToken), revoke(refreshToken)])
// then clear YOUR session cookies here, server-side, whatever happened above
Three failure modes found in real Ver* integrations β check all of them before calling sign-out done:
undefined,
returns early, and revokes nothing β while looking finished in the diff. Add a
route-level test asserting the token passed to /oauth/revoke is the
one your session actually stores, not undefined.
SameSite=None cookies (any PWA), a foreign
<img src=".../signout"> signs your users out β and once you
revoke correctly, force-revokes their hub tokens too.
if (!CLIENT_ID) return silently disables revocation while login keeps
working. The client_id is public by design β fall back to the literal string
instead of skipping the call.
Domain=.example.xyz is not deleted by a
host-only expiry β the browser keeps the original and the "signed-out" user stays
signed in on every subdomain. Expire with the exact Domain/Path used at login (or
clear both variants), and assert the Set-Cookie headers in a production-mode test.
This ends the session for your application only. Users who want to sign out of everything at once can do it from their AIVerID dashboard β every connected app can be revoked individually, or all at once with Sign out everywhere.
Skip this section unless you are exposing an API that an AI agent calls on a user's behalf. Ordinary sign-in needs nothing here.
AIVerID issues a distinct kind of access token for a delegation: the user names an agent, approves a narrow set of scopes, and gets one revoke button per agent in their dashboard β killing that agent without touching their own session or any other agent. Your job at the other end is to accept only those tokens on your agent API.
Accept a request on your agent surface only when every one of these holds at once. Any single one missing is a refusal:
active: truetoken_use is "agent"grant_id is presentaud equals your own resource identifier, compared as an exact string
And three things you must never do:
never accept a token with no aud
β no "missing means fine" fallback, that gap is the entire reason this exists;
never treat scope alone as proof of agency;
and never verify the JWT yourself β the
signing secret never leaves AIVerID, so introspection is the only verification path,
which is also what makes a revoke take effect in seconds rather than at expiry.
Why so strict: the agent_name on an authorization request is supplied by
the caller, so it is a label, not evidence. Anyone who can drive your client's OAuth
flow can leave it out and get an ordinary web token carrying the same scopes. The only
fields that are evidence are the ones AIVerID stamps and you cannot forge β
token_use, grant_id, and aud.
A client that has not registered a resource cannot open an agent flow at all β the authorization request is refused rather than producing a token no one is allowed to accept. Send us the identifier you want and we add it to your client.
RFC 8707 requires the value to be an absolute URI and forbids a fragment component. The rest of what we enforce is stricter than the RFC, and we would rather you knew which is which than mistake our policy for the standard:
https only β the RFC does not constrain the scheme.?, # or @ markers; the RFC is silent on these, but URL parsers disagree about them, so we reject the raw string before one gets to normalize it.
Keep version numbers in your endpoint paths, not in the identifier, so v1
costs you nothing later.
Once registered, resource becomes mandatory on your agent authorization
requests and must match the registered value exactly; sending a different one, or
none, is rejected with invalid_target. Your ordinary web sign-ins are
unaffected and continue to receive tokens with no aud.
POST https://aiverid-backend-production.up.railway.app/oauth/introspect
Content-Type: application/x-www-form-urlencoded
token=<the bearer token>&client_id=<yours>&client_secret=<yours>
// active response β agent fields appear only for agent tokens
{
"active": true,
"sub": "TH...",
"client_id": "aiv_yourapp_...",
"scope": "yourapp:read yourapp:propose",
"exp": 1787136103,
"iat": 1787132503,
"token_type": "access_token",
"username": "TH...",
"verification_level": 1,
"verification_badge": "basic",
"aud": "https://yourapp.xyz/api/agent",
"token_use": "agent",
"grant_id": "agt_0123456789abcdef0123456789abcdef",
"act": { "sub": "agt_0123...", "name": "Claude Code on Job's laptop" }
}
Introspect with the same client_id the token was issued to β AIVerID
answers active: false for another client's token, so one service can
never probe another's sessions. Cache the result for at most 60 seconds: longer and a
revoked agent keeps working, and your users' "last used" column goes stale, because
introspection is what marks an agent as active.
Host this yourself so an agent can discover where to authenticate without being told
(RFC 9728).
The path of your resource identifier is inserted after the well-known suffix β
an identifier of https://yourapp.xyz/api/agent is published at
/.well-known/oauth-protected-resource/api/agent, not at the bare
well-known path. An identifier with no path uses the bare path.
GET https://yourapp.xyz/.well-known/oauth-protected-resource/api/agent
Content-Type: application/json
Cache-Control: public, max-age=3600
Access-Control-Allow-Origin: *
{
"resource": "https://yourapp.xyz/api/agent",
"authorization_servers": [
"https://aiverid-backend-production.up.railway.app"
],
"scopes_supported": ["yourapp:read", "yourapp:validate", "yourapp:propose"],
"bearer_methods_supported": ["header"],
"resource_name": "YourApp Agent API",
"resource_documentation": "https://yourapp.xyz/docs/agent"
}
resource is the only required member, and its value must be byte-identical
to what you registered with us. Do not publish jwks_uri: AIVerID signs with
HS256 and there are no public keys to fetch β a JWKS pointer would invite exactly the
offline verification that makes revocation stop working.
// expired, revoked, wrong audience, or not an agent token β 401 WWW-Authenticate: Bearer realm="yourapp.xyz", error="invalid_token", error_description="The access token expired", resource_metadata="https://yourapp.xyz/.well-known/oauth-protected-resource/api/agent" // authenticated, but the grant lacks the scope β 403 WWW-Authenticate: Bearer realm="yourapp.xyz", error="insufficient_scope", scope="yourapp:propose", resource_metadata="https://yourapp.xyz/.well-known/oauth-protected-resource/api/agent"
The resource_metadata pointer is what lets a well-built agent recover on
its own instead of waiting for a human to reconfigure it. Two details from
RFC 6750:
a request carrying no credentials at all should get the bare challenge with
no error code, and a wrong audience is
invalid_token β say that it failed, never which of the four checks it
failed, or you have written an oracle.
tokens and business are refused to
agents at the hub, before consent renders β an agent authorization that asks for
either is rejected outright, so do not design around it. Name your agent scopes after
your service (yourapp:read, yourapp:propose) and register
them with us.
If you expose your agent API as a remote MCP server, a user can add it to claude.ai, Claude Desktop or ChatGPT as a custom connector by pasting its URL. In that flow the host is the OAuth client, not your service. AIVerID does not offer dynamic client registration (RFC 7591) β an anonymous registrant would choose its own name on the consent screen, which is a phishing kit on the one screen every user trusts. Instead each host has a pre-registered public client, which is the alternative the MCP authorization specification itself describes:
client_id aiv_claude redirect https://claude.ai/api/mcp/auth_callback
client_id aiv_chatgpt redirect https://chatgpt.com/connector_platform_oauth_redirect
client_secret none β public client, PKCE S256 required
resource the canonical URI of your MCP server, exactly as your
protected-resource metadata publishes it
The user enters the client id in the host's advanced OAuth settings and leaves the secret blank. There is no secret on purpose: an id that every user of a connector pastes cannot keep a companion secret, and a secret that is public knowledge is worse than none. Possession is proven by PKCE, which both hosts implement.
Every authorization from these clients is an agent flow. The host never names itself,
so AIVerID supplies the name β the grant appears as Claude
or ChatGPT under the user's connected
agents with its own revoke button, the token carries token_use: "agent",
a grant_id and an aud equal to your MCP server URI, and the
four checks above apply unchanged. The account scopes tokens and
business are refused before consent, exactly as for any agent.
To open your MCP server to these hosts, send us its canonical URI and the scopes it
accepts; we add them to the host clients. One caution on the identifier: MCP hosts send
the URL the user pasted as the resource, so publish that exact value in
your protected-resource metadata and compare aud against it at the MCP
surface. If your MCP handler calls an inner API of your own, mint or map credentials
there β never forward the host's token, whose audience is the MCP server and nothing
else.
Endpoints, scopes and supported flows are published as OAuth 2.0 Authorization
Server Metadata (RFC 8414), so your client library can discover them instead of
hard-coding URLs. AIVerID is OAuth 2.0 with PKCE β not OpenID Connect: there is no
id_token and no JWKS, and identity claims come from the userinfo endpoint.
GET https://aiverid-backend-production.up.railway.app/.well-known/oauth-authorization-server
{
"issuer": "https://aiverid-backend-production.up.railway.app",
"authorization_endpoint": ".../oauth/authorize",
"token_endpoint": ".../oauth/token",
"userinfo_endpoint": ".../oauth/userinfo",
"introspection_endpoint": ".../oauth/introspect",
"revocation_endpoint": ".../oauth/revoke",
"scopes_supported": ["business", "email", "profile", "tokens", ...],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "none"],
"revocation_endpoint_auth_methods_supported": ["client_secret_post", "none"],
"openid_connect_supported": false
}
Read this document rather than copying values from here β scopes_supported
grows as applications register resource scopes, and requesting a scope your client is
not registered for is rejected with invalid_scope.
Register your application to get OAuth credentials
Email us at developers@aiverid.com