societyai.org
Preparing the collaboration space...
Society AI society-ai.org
Developers

API reference

Every endpoint with example requests and responses, scopes, claims, and error codes.

Every example on this page comes from real server responses, with tokens cut short. Replace YOUR_CLIENT_ID and kelas.society-ai.org with your own.

Endpoints

PurposeMethodPath
DiscoveryGET/.well-known/openid-configuration
Start sign-inGET (browser)/oauth2/auth
Swap code, renew tokensPOST/oauth2/token
Member dataGET/userinfo
Signing keysGET/.well-known/jwks.json
Revoke a tokenPOST/oauth2/revoke
Sign outGET (browser)/oauth2/sessions/logout
Back-channel logoutPOST (to your server)the URL you registered

Every path starts with https://accounts.society-ai.org. Read them from discovery if your library supports it, so you don't have to write them out.

Discovery
curl https://accounts.society-ai.org/.well-known/openid-configuration

scopes_supported in discovery only lists the built-in scopes. profile, email, and membership still work once they are approved for your app.

Start sign-in: GET /oauth2/auth

Send the member's browser here. Not in an iframe or a WebView.

Sign-in URL
https://accounts.society-ai.org/oauth2/auth
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https%3A%2F%2Fkelas.society-ai.org%2Fauth%2Fcallback
  &scope=openid%20profile%20email%20membership
  &state=RANDOM_PER_LOGIN
  &code_challenge=BASE64URL_SHA256_OF_VERIFIER
  &code_challenge_method=S256
ParameterRequiredValue
response_typeYescode
client_idYesYour Client ID
redirect_uriYesOne of your registered callback URLs, exactly
scopeYesSpace separated, always including openid
stateYesNew random text every time, at least 8 characters. Check it again on the callback.
code_challengeYesBase64url of the SHA-256 of your code_verifier
code_challenge_methodYesS256
promptNonone for automatic sign-in without a screen (see Automatic sign-in and sign-out). login to make the member enter a new code, for example before changing security settings.
max_ageNoMaximum sign-in age in seconds. If the member last entered a code longer ago than that, they are asked for a new one.
nonceNoIf set, the same value comes back in the ID token

On success the browser comes back to your callback:

Successful callback
https://kelas.society-ai.org/auth/callback
  ?code=ory_ac_...
  &scope=openid+profile+email+membership
  &state=RANDOM_PER_LOGIN

On failure your callback gets error and error_description, for example error=invalid_scope or error=login_required. If the client_id or redirect_uri is wrong, the browser does not come back to your app. The member sees "Sign-in is unavailable right now" on society-ai.org, and the reason is in error_description in that page's address.

Swap the code for tokens: POST /oauth2/token

Do this from your server, within 10 minutes of the callback. Each code works once.

App with a server
curl -X POST https://accounts.society-ai.org/oauth2/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  -d redirect_uri=https://kelas.society-ai.org/auth/callback \
  -d code_verifier="$VERIFIER"

Apps without a server send client_id in the body, without a secret:

App without a server
curl -X POST https://accounts.society-ai.org/oauth2/token \
  -d client_id="$CLIENT_ID" \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  -d redirect_uri=https://kelas.society-ai.org/callback \
  -d code_verifier="$VERIFIER"

The response:

200 OK
{
  "access_token": "ory_at_...",
  "expires_in": 899,
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "ory_rt_...",
  "scope": "openid profile email membership offline_access",
  "token_type": "bearer"
}
FieldNotes
access_tokenFor calling /userinfo. Not a JWT, so don't try to read it. Valid for 15 minutes.
id_tokenA JWT with the member's data. Check its signature before using it. Valid for 15 minutes.
refresh_tokenOnly present when offline_access was requested and approved. Valid for 30 days.
expires_inSeconds left on the access token

What's in the ID token

ID token payload
{
  "iss": "https://accounts.society-ai.org",
  "sub": "01a15d78-725f-73c5-98b5-63fd015a97d2",
  "aud": ["YOUR_CLIENT_ID"],
  "exp": 1792640870,
  "iat": 1792639970,
  "auth_time": 1792639970,
  "sid": "ca7ee7c7-fb98-4ccc-a4af-8d32f23d1d61",
  "name": "Aruna Noviyanti",
  "locale": "id",
  "email": "aruna@society-ai.org",
  "email_verified": true,
  "membership": {
    "status": "active",
    "number": "8316202601000015",
    "expires_at": "2027-10-21T14:58:57+07:00"
  },
  "role": "member"
}
ClaimScopeNotes
subopenidThe member ID. Never changes. Use it to recognise the member.
auth_timeopenidWhen the member last entered a code. After prompt=login or max_age, check that it is recent.
sidopenidThe browser session ID. Used for back-channel logout.
iss, aud, exp, iatopenidAlways check: iss equals the issuer, aud contains your Client ID, exp has not passed.
nameprofileThe member's name
localeprofilePreferred language: id or en
email, email_verifiedemailEmail and whether it is verified. Emails can change.
membership.statusmembershipnone, active, expired, or cancelled
membership.numbermembership16-digit member number, or null
membership.expires_atmembershipEnd date (ISO 8601), or null
rolemembershipmember, staff, moderator, owner, or admin. This is the role in Society AI. Permissions inside your service are still yours to decide.

membership and role are read fresh every time a new token is made, renewals included.

Renew tokens

Refresh
curl -X POST https://accounts.society-ai.org/oauth2/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=refresh_token \
  -d refresh_token="$REFRESH_TOKEN"

The response looks like the code swap, including a new refresh token. The old refresh token stops working right away, so always keep the latest. Using the old one returns invalid_grant with "The refresh token was already used".

If the member's account was suspended, or the member was removed from a testing app's tester list, renewal is refused with 403 access_denied. Treat the member as signed out.

Member data: GET /userinfo

Userinfo
curl https://accounts.society-ai.org/userinfo \
  -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK
{
  "sub": "01a15d78-725f-73c5-98b5-63fd015a97d2",
  "name": "Aruna Noviyanti",
  "locale": "id",
  "email": "aruna@society-ai.org",
  "email_verified": true,
  "membership": {
    "status": "active",
    "number": "8316202601000015",
    "expires_at": "2027-10-21T14:58:57+07:00"
  },
  "role": "member",
  "iss": "https://accounts.society-ai.org",
  "aud": ["YOUR_CLIENT_ID"],
  "auth_time": 1792639970,
  "iat": 1792639970
}

It holds the same claims as the ID token for the granted scopes. Use it if you'd rather not check JWTs yourself, or to check an access token sent to your API.

Signing keys: GET /.well-known/jwks.json

Tokens are signed with RS256. Cache the JWKS, for example for an hour. If a token has a kid you don't know, fetch the JWKS once more before rejecting it, because keys can be replaced.

Revoke a token: POST /oauth2/revoke

Revoke
curl -X POST https://accounts.society-ai.org/oauth2/revoke \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d token="$REFRESH_TOKEN"

Replies 200 with an empty body. Use it when a member disconnects your service from their account, or when you drop a refresh token you no longer need.

Sign out: GET /oauth2/sessions/logout

Clear your own session first, then send the browser to:

Sign-out URL
https://accounts.society-ai.org/oauth2/sessions/logout?id_token_hint=LATEST_ID_TOKEN

Use the latest ID token you received. The member is signed out of society-ai.org and every other service, then sees "You are signed out". For now members are not sent back to your app, so don't send post_logout_redirect_uri.

Back-channel logout (called on your server)

When a member signs out on society-ai.org or in another service, Society AI sends a POST to your back-channel URL with a logout_token form field. It holds a JWT like this:

logout_token payload
{
  "iss": "https://accounts.society-ai.org",
  "aud": ["YOUR_CLIENT_ID"],
  "iat": 1792640105,
  "jti": "9f71b9cd-1d82-4454-8601-b452f6d27d56",
  "sid": "96e2228d-3558-47a6-a1b6-d6afdabaf0f0",
  "events": {
    "http://schemas.openid.net/event/backchannel-logout": {}
  }
}

Your server must:

  1. Check its signature with the JWKS.
  2. Make sure iss equals the issuer and aud contains your Client ID.
  3. Make sure events contains http://schemas.openid.net/event/backchannel-logout and there is no nonce.
  4. Delete the session that matches sid. The token has no sub, so store the sid and session pair when the member signs in.
  5. Reply 200. The route has no CSRF, no login, and must not redirect.

Error codes

StatuserrorWhenWhat to do
400invalid_grantCode already used or expired, wrong code_verifier, or an old refresh tokenStart sign-in again
401invalid_clientWrong Client ID or secretCheck .env. The secret may have been replaced.
401request_unauthorizedWrong or expired access token on /userinfoRenew the token, or ask the member to sign in again
403access_deniedAccount suspended, or not a tester of a testing appSign the member out of your service
callbackinvalid_scopeA scope that isn't approved for your appMatch the scopes to what the admin approved
callbacklogin_requiredprompt=none and the member isn't signed inShow the page as a guest

Error responses from /oauth2/token look like this:

400 Bad Request
{
  "error": "invalid_grant",
  "error_description": "The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, ..."
}
Next section Automatic sign-in and sign-out

Start here support@society-ai.org