Skip to main content
CabalSpy ID is a standard OpenID Connect provider. Any certified OpenID Connect or OAuth 2.0 library works. The only flow is the authorization code flow, and PKCE with S256 is required for every client, confidential clients included.

Endpoints

All endpoints are listed in the discovery document.

Clients

Create a client in the API Dashboard on the CabalSpy ID tab. The Instance ID is the client_id (csid_...). Confidential clients (server-side apps) authenticate with a client secret (csid_sk_...). Only a hash of the secret is stored, so copy it when you create it. Several secrets can be active at once: when you roll a secret the old one stays valid for a time you choose, so you can deploy the new one without downtime. Public clients (browser apps, mobile apps) have no secret and use none as authentication method. PKCE protects their flow. Redirect URIs must be registered and match exactly. Live instances need every redirect URI and web origin on a verified domain.

Scopes

An instance can only request the scopes enabled for it in the dashboard. The user approves the scopes once per app. Asking for more scopes later shows the consent screen again.

Authorization request

Create a random code_verifier (43 to 128 characters), its S256 code_challenge, a random state and a random nonce, then redirect the user.
string
required
Always code.
string
required
Your Instance ID.
string
required
A registered redirect URI, matched exactly.
string
required
Space-separated scopes. Must contain openid.
string
required
Base64url SHA-256 of the code verifier (43 characters).
string
required
Always S256.
string
Random value you check on the callback. Up to 500 characters.
string
Random value that comes back in the ID token. Up to 300 characters.
string
none signs in silently or returns login_required or consent_required. consent always shows the consent screen. none cannot be combined with other values.
The user signs in to CabalSpy ID if needed, with X, Telegram or Google, and approves the scopes. CabalSpy ID then redirects back with code, state and iss. Check that state matches and that iss is https://id.cabalspy.xyz. The code is single use and valid for 60 seconds.
A request with an unknown client or an unregistered redirect URI is never redirected. CabalSpy ID shows an error page instead.

Token request

Exchange the code at the token endpoint with the code verifier.
Response
refresh_token is only returned when offline_access was granted.

Tokens

Verify ID tokens with the keys at https://id.cabalspy.xyz/oauth/jwks. Check the signature (ES256, select the key by kid), iss = https://id.cabalspy.xyz, aud = your client_id, exp, and nonce if you sent one. New signing keys are published in the JWKS before they are used, so cache the JWKS but refresh it when you see an unknown kid.
JWKS

Refresh tokens

With offline_access you get a refresh token. Exchange it for new tokens at the token endpoint.
Refresh tokens rotate. Every refresh returns a new refresh token and the old one stops working. If a used refresh token is presented again, the whole token family is revoked and the user has to sign in again, so always store the newest one. A refresh token expires after 30 days without use, and every family ends 90 days after the sign-in. A refresh fails with invalid_grant when the user removed access to your app.

Revocation

Revoke a refresh token when the user signs out of your app. This ends its whole family. Unknown tokens are not an error.
Access tokens are short-lived JWTs and expire on their own.

User info

Returns the claims for the granted scopes. The ID token itself carries sub and, with profile, name, preferred_username, picture and kol_badge. The full x, wallets and onchain claims come from the user info endpoint.

Claims

string
Stable user id. Use it as the user’s key in your database. It is the csid_id in the Data API.
string
Display name: from X, otherwise from Telegram or Google. Scope profile.
string
The X handle without the @. Users without X get tg.<telegram username> or id.<10 hex characters>, see Users without X. Scope profile.
string
Avatar URL from X or Google, can be null. Scope profile.
string
Public CabalSpy ID profile URL. Scope profile.
boolean
Whether the user holds the reviewed KOL badge. Always false without X. Scope profile.
object
X account details, or null when the user has no X account connected. Scope profile.
array
Verified wallets. Scope wallets.
object
Latest on-chain volume check, or null. Scope onchain.
boolean
Present and true for made-up sandbox test users. Never trust such an identity in production.

Users without X

People sign in to CabalSpy ID with X, with Telegram through the CabalSpy ID bot, or with Google as an alternative. Nothing changes in the flow for your app, but users who signed in with Telegram or Google and have not connected X look different: Handles of users without X always contain a dot, X handles never do. When a user connects X later, preferred_username, name, picture and x switch to their X account while sub stays the same, so always key your users on sub, never on the handle.
User info without X

Limited users

Your plan sets how many users you receive data for. Everyone can always sign in, but above the limit the person gets an anonymous sub that starts with anon_ and no profile, wallet or on-chain data. The ID token and the user info then look like this:
The anonymous sub is stable for that person and your domain, so you can still keep an account for them. After you upgrade, people who sign in again receive their real sub and data. See Plans and limits.

OAuth errors

The token, user info and revocation endpoints return errors in the standard OAuth format.