Authentication: How to Connect
Step-by-step guide for authenticating with TimeBack Platform APIs using OAuth 2.0 client credentials.
Authentication Model
TimeBack Platform uses OAuth 2.0 client credentials for server-to-server authentication. This is the only authentication method currently supported; user-based authentication may be added in the future.
Environments
TimeBack provides two environments for development and production:
Staging
- Base URL:
https://platform.dev.timeback.com - LTI Issuer:
https://staging.timeback.com - LTI JWKS URL:
https://platform.dev.timeback.com/.well-known/jwks.json
Use staging for all development and testing. This environment is separate from production data.
Production
- Base URL:
https://platform.timeback.com - LTI Issuer:
https://timeback.com - LTI JWKS URL:
https://platform.timeback.com/.well-known/jwks.json
Request production credentials only when your integration is tested and ready to launch.
Step 1: Request Credentials
Contact timeback@trilogy.com to request OAuth client credentials.
Provide in your request:
- App name and description
- Environment (staging or production)
- Which platform APIs you need access to
- Intended use case
You will receive:
- Client ID
- Client Secret
- List of authorized scopes
Step 2: Exchange Credentials for Access Token
Make a POST request to the token endpoint:
curl -X POST https://platform.timeback.com/auth/1.0/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "<CLIENT_ID>:<CLIENT_SECRET>" \
-d "grant_type=client_credentials&scope=<SCOPES>"
Replace <CLIENT_ID>, <CLIENT_SECRET>, and <SCOPES> with your credentials.
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "https://purl.imsglobal.org/spec/caliper/v1p2/scope/events.write"
}
Step 3: Use the Access Token
Include the access token in all API requests:
curl -X POST https://platform.timeback.com/events/1.0/ \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-d '{
"@context": "http://purl.imsglobal.org/ctx/caliper/v1p2",
"type": "SessionEvent",
"actor": "urn:uuid:student-id",
"action": "Started",
"eventTime": "2025-12-15T10:00:00Z",
"edApp": "urn:uuid:your-app-id"
}'
Token Management
Caching Tokens
Tokens expire after 1 hour. Cache the token and reuse it for multiple requests:
let accessToken: string | null = null;
let tokenExpiresAt: number | null = null;
async function getValidToken(): Promise<string> {
const now = Date.now();
// Return cached token if still valid
if (accessToken && tokenExpiresAt && now < tokenExpiresAt) {
return accessToken;
}
// Fetch new token
const response = await fetch('https://platform.timeback.com/auth/1.0/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
},
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: scopes.join(' '),
}),
});
const tokenData = await response.json();
accessToken = tokenData.access_token;
// Cache with 10% safety buffer (54 minutes for 1-hour tokens)
const expirationBuffer = (tokenData.expires_in || 3600) * 0.9;
tokenExpiresAt = now + expirationBuffer * 1000;
return accessToken;
}
Handling Token Expiry
If an API call returns 401 Unauthorized, refresh your token and retry once:
const token = await getValidToken();
const response = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
// ... rest of request
});
// Token expired mid-request - refresh and retry
if (response.status === 401) {
const newToken = await getValidToken(); // Forces refresh
return await fetch(url, {
headers: { Authorization: `Bearer ${newToken}` },
// ... rest of request
});
}
Scopes
Scopes control what your credentials can access. TimeBack uses 1EdTech standard scope URIs:
| Scope | Capability |
|---|---|
https://purl.imsglobal.org/spec/caliper/v1p2/scope/events.write |
Send Caliper learning events |
https://purl.imsglobal.org/spec/or/v1p2/scope/roster.readonly |
Read student roster data |
Request only the scopes your app needs. Scopes are assigned during credential setup and cannot be changed without contacting the TimeBack team.
Common Pitfalls
Not caching tokens
// ❌ Bad: Fetch new token for every request
async function sendEvent(event) {
const token = await fetchNewToken(); // Wastes time, hits rate limits
await sendEventWithToken(token, event);
}
// ✅ Good: Cache and reuse tokens
async function sendEvent(event) {
const token = await getValidToken(); // Returns cached if still valid
await sendEventWithToken(token, event);
}
Not handling 401 responses
// ❌ Bad: No retry on token expiry
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
// Fails permanently if token expired
// ✅ Good: Refresh and retry
if (response.status === 401) {
const newToken = await refreshToken();
return await fetch(url, { headers: { Authorization: `Bearer ${newToken}` } });
}
Related Docs
Level 0: Register Your App
Submit your app for registration to get client credentials and appear in TimeBack's app catalog.
Level 2: Send Caliper Events
Use your authenticated credentials to send learning activity events to TimeBack Platform.