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}` } });
}

Submit your app for registration to get client credentials and appear in TimeBack's app catalog.

Use your authenticated credentials to send learning activity events to TimeBack Platform.