OAuth Quickstart
Get up and running with Loopwise OAuth 2.0 in minutes.
This guide walks you through the complete OAuth 2.0 integration flow with Loopwise — from registering a client to making your first authenticated API request.
Prerequisites
- A Loopwise school with admin access
- A server-side application that can make HTTPS requests
- A publicly accessible redirect URI
Overview
Loopwise uses the Authorization Code flow with PKCE (RFC 7636):
For a detailed explanation of each step, see Authorization Code Flow.
Step 1: Register your application
You can register an OAuth application in two ways:
Option A: Admin UI
Navigate to your school's admin panel at Settings > OAuth Applications and create a new application. You'll receive a client_id and client_secret.
Option B: Dynamic Client Registration
Send a POST request to the registration endpoint:
curl -X POST https://your-school-domain.com/api/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}'The response includes your client_id:
{
"client_id": "abc123...",
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"scope": "openid profile email courses:read courses:write students:read students:write analytics:read curriculum:read curriculum:write orders:read school:read"
}Step 2: Redirect the user to authorize
Build the authorization URL and redirect the user's browser:
https://your-school-domain.com/oauth/authorize?
response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://myapp.com/callback
&scope=openid+profile+email+courses:read+students:read
&state=RANDOM_STATE_VALUE
&code_challenge=PKCE_CHALLENGE
&code_challenge_method=S256The user will see a consent screen showing the requested scopes. The token will be scoped to the school where your OAuth application is registered.
Step 3: Exchange the code for tokens
After the user authorizes, Loopwise redirects to your redirect_uri with a code parameter. Exchange it for tokens:
curl -X POST https://your-school-domain.com/api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=AUTHORIZATION_CODE" \
-d "redirect_uri=https://myapp.com/callback" \
-d "client_id=YOUR_CLIENT_ID" \
-d "code_verifier=PKCE_VERIFIER"Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "def456...",
"scope": "openid profile email courses:read students:read",
"school_id": "550e8400-e29b-41d4-a716-446655440000",
"school_subdomain": "demo"
}Step 4: Make API requests
Use the access token in the Authorization header:
curl https://your-school-domain.com/api/v1/courses \
-H "Authorization: Bearer ACCESS_TOKEN"Refreshing tokens
Access tokens expire after 2 hours (30 days for Loopwise CLI). Use the refresh token to get a new access token:
curl -X POST https://your-school-domain.com/api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=REFRESH_TOKEN" \
-d "client_id=YOUR_CLIENT_ID"Response:
{
"access_token": "new-access-token...",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "new-refresh-token...",
"scope": "openid courses:read students:read"
}Token rotation policy
You must persist the new refresh_token from every refresh response. Loopwise uses refresh token rotation — the old refresh token is revoked immediately after use. If you discard the new one, your integration silently loses access with no error until the next refresh attempt fails.
Every successful call to /api/oauth/token with grant_type=refresh_token returns a new refresh token alongside the new access token. The previous refresh token is revoked immediately and cannot be reused.
This is a security measure (RFC 6749 §10.4) — if a refresh token is leaked, it can only be used once before it's invalidated.
Correct implementation:
async function refreshAccessToken(storedRefreshToken) {
const res = await fetch(`${SCHOOL_URL}/api/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: storedRefreshToken,
client_id: CLIENT_ID,
}),
});
if (!res.ok) {
// Token may have been rotated already — re-authenticate
throw new Error(`Refresh failed: ${res.status}`);
}
const data = await res.json();
// IMPORTANT: persist the NEW refresh token for next time
await db.set('oauth_tokens', {
access_token: data.access_token,
refresh_token: data.refresh_token, // ← always save the new one
expires_at: Date.now() + data.expires_in * 1000,
});
return data.access_token;
}Common mistake — only saving the access token:
// ❌ WRONG: discards the new refresh_token
const data = await res.json();
await db.set('access_token', data.access_token);
// Next refresh attempt will fail with invalid_grant because
// the old refresh_token was already revokedWhat happens when refresh fails
If you submit a revoked or expired refresh token, the server returns:
{
"error": "invalid_grant",
"error_description": "The provided authorization grant is invalid, expired, or revoked. For refresh tokens: token rotation is enabled, and each /oauth/token response returns a new refresh_token that must be stored and used for the next request. For authorization codes: codes are single-use and expire after 10 minutes."
}If you submit a refresh token that was already used (rotation reuse), the server returns a more specific message:
{
"error": "invalid_grant",
"error_description": "The refresh token has already been used. Refresh token rotation is enabled — each call to /oauth/token returns a new refresh_token that must be persisted. The previous token is revoked immediately."
}Recovery: Re-authenticate the user by starting the authorization code flow again.
Token lifecycle summary
| Token | Lifetime | Rotation |
|---|---|---|
| Access token | 2 hours (30 days for CLI) | No rotation — just expires |
| Refresh token | Valid until used or revoked | Rotated on every use — new token in response |
| Authorization code | 10 minutes | Single-use |
Next steps
- Available scopes — see what data you can access
- Authorization Code Flow — understand the flow in detail
- Token Endpoints — full token endpoint reference