Loopwise Docs
Reference

Token Endpoints

Technical reference for the OAuth 2.0 token, refresh, and revocation endpoints.

Token lifecycle

Token endpoint

POST /api/oauth/token

Exchanges an authorization code for an access token, or refreshes an existing token.

Authorization Code grant

ParameterTypeRequiredDescription
grant_typestringYesMust be authorization_code
codestringYesThe authorization code from the callback
redirect_uristringYesMust match the URI used in the authorization request
client_idstringYesYour application's client ID
code_verifierstringYesThe PKCE code verifier (plain-text, 43-128 chars)
client_secretstringConditionalRequired for confidential clients

Response

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 7200,
  "refresh_token": "def456...",
  "scope": "openid courses:read students:read",
  "created_at": 1710000000,
  "school_id": "550e8400-e29b-41d4-a716-446655440000",
  "school_subdomain": "demo"
}
FieldDescription
access_tokenBearer token for API requests
token_typeAlways Bearer
expires_inToken lifetime in seconds (default: 7200 / 2 hours)
refresh_tokenToken to obtain a new access token
scopeGranted scopes (may be a subset of what was requested)
school_idThe UUID of the authorized school
school_subdomainThe subdomain of the authorized school

Refresh Token grant

ParameterTypeRequiredDescription
grant_typestringYesMust be refresh_token
refresh_tokenstringYesA valid refresh token
client_idstringYesYour application's client ID
client_secretstringConditionalRequired for confidential clients

Response

Same format as the authorization code grant response.

Refresh token rotation

Every successful refresh returns a new refresh_token in the response. The previous refresh token is immediately revoked.

Your client must persist the new refresh token and use it for the next refresh. Replaying an old token returns invalid_grant.

Example: handling token rotation
async function refreshAccessToken(storedRefreshToken) {
  const res = await fetch(TOKEN_URL, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "refresh_token",
      refresh_token: storedRefreshToken,
      client_id: CLIENT_ID,
      // Include client_secret for confidential clients:
      // client_secret: CLIENT_SECRET,
    }),
  });

  if (!res.ok) throw new Error(`Refresh failed: ${res.status}`);

  const { access_token, refresh_token, expires_in } = await res.json();

  // Always persist the latest refresh_token
  await store.set("oauth_tokens", {
    access_token,
    refresh_token: refresh_token ?? storedRefreshToken,
    expires_at: Date.now() + expires_in * 1000,
  });

  return access_token;
}

Revocation endpoint

POST /api/oauth/revoke

Revokes an access token or refresh token per RFC 7009.

ParameterTypeRequiredDescription
tokenstringYesThe token to revoke
token_type_hintstringNoaccess_token or refresh_token
client_idstringYesYour application's client ID

The endpoint always returns 200 OK, even if the token was already revoked or invalid. This prevents token-guessing attacks.


Introspection endpoint

POST /api/oauth/introspect

Returns metadata about a token per RFC 7662.

ParameterTypeRequiredDescription
tokenstringYesThe token to introspect
client_idstringYesYour application's client ID

Response (active token)

{
  "active": true,
  "scope": "openid courses:read",
  "client_id": "abc123",
  "token_type": "Bearer",
  "exp": 1710007200
}

Response (inactive/invalid token)

{
  "active": false
}

UserInfo endpoint

GET /api/oauth/userinfo
POST /api/oauth/userinfo

Returns identity claims about the authenticated user per OpenID Connect Core 1.0 §5.3. The openid scope is required.

Request

Include the access token as a Bearer token in the Authorization header:

Authorization: Bearer <access_token>

Response

The claims returned depend on the scopes granted to the token:

{
  "sub": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Jane Doe",
  "email": "jane@example.com",
  "email_verified": true
}
ClaimScope requiredDescription
subopenidUnique user identifier (UUID)
nameprofileUser's display name
emailemailUser's email address
email_verifiedemailWhether the email has been verified

Error responses

HTTP StatusErrorWhen
401invalid_tokenMissing, expired, or revoked token
403invalid_scopeToken does not include the openid scope

Error responses

All token endpoints return errors in the standard OAuth 2.0 format:

{
  "error": "invalid_grant",
  "error_description": "The authorization code has expired or has already been used."
}
ErrorHTTP StatusDescription
invalid_request400Missing or malformed parameter
invalid_client401Client authentication failed
invalid_grant400Code expired, already used, or verifier mismatch
unauthorized_client400Client not authorized for this grant type
unsupported_grant_type400Only authorization_code and refresh_token are supported

On this page