Skip to content

Authentication

The Two Credentials

Every authenticated request carries both of these headers:

Authorization: Bearer {access token from the token endpoint}
x-api-key: {{apiKey}}

They answer different questions:

  • The JWT bearer token (short-lived, 1 hour) proves who you are. It's validated cryptographically on every request — signature, expiry, issuer, and scopes.
  • The API key (static) identifies your traffic for rate limiting and quotas. Requests without it are rejected before authentication is even attempted.

Missing or invalid values fail differently, which helps diagnosis:

What's wrong Response
Missing/invalid/expired JWT 401 Unauthorized
Missing/invalid API key 403 Forbidden (empty body)
Both present and valid Request proceeds to the API

The one exception is GET /ping, which requires only the API key — it exists so you can verify connectivity independently of the OAuth2 flow.

Don't mix the credential systems up

The Client ID/Secret pair (26-char ID, long secret) belongs to the token endpoint; the API key (~40 chars) belongs in the x-api-key header. Swapping them produces invalid_client errors (at the token endpoint) or 403 Forbidden (at the API).

OAuth2 Client Credentials Flow

The API uses the OAuth2 client_credentials grant — the standard flow for machine-to-machine integration. There is no user login, no redirect, no consent screen: your server exchanges its credentials directly for a token.

Your LMS                       FSRI Token Endpoint                FSRI Partner API
   │                                   │                                │
   │ POST /oauth2/token                │                                │
   │ Authorization: Basic {base64(clientId:clientSecret)}               │
   │ grant_type=client_credentials     │                                │
   │ scope=partner-api/read partner-api/write                           │
   ├──────────────────────────────────►│                                │
   │                                   │ validates credentials,         │
   │                                   │ issues signed JWT              │
   │ ◄──────────────────────────────── ┤                                │
   │  { access_token, expires_in: 3600 }                                │
   │                                   │                                │
   │ GET /api/partner/v1/courses       │                                │
   │ Authorization: Bearer {token}  +  x-api-key: {key}                 │
   ├───────────────────────────────────┼───────────────────────────────►│
   │                                   │            validates JWT       │
   │ ◄─────────────────────────────────┼────────────────────────────────┤
   │  200 OK { ...courses }            │                                │

Requesting a Token

POST to the token endpoint with your client credentials as HTTP Basic auth and a form-encoded body:

curl -s -X POST "$TOKEN_URL" \
  -u "{{clientId}}:{{clientSecret}}" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&scope=partner-api/read partner-api/write"

(curl -u builds the Authorization: Basic ... header for you. In your own code, base64-encode clientId:clientSecret and send it as Authorization: Basic {encoded}.)

Scopes

Scope Grants
partner-api/read All read operations: catalog, users, progress, transcripts, content URLs
partner-api/write Mutations: user registration, profile updates, deactivation/deletion, completion notification

Request both unless FSRI has issued you a read-only client. Requesting a scope your client isn't allowed produces an invalid_scope error.

Success response

{
  "access_token": "eyJraWQiOiJ...",
  "expires_in": 3600,
  "token_type": "Bearer"
}

Token endpoint errors

error Meaning Fix
invalid_client Client ID or secret is wrong Check for copy/paste truncation; confirm you're using the Cognito client pair, not the API key
invalid_grant / unsupported_grant_type Body isn't grant_type=client_credentials Send exactly grant_type=client_credentials, form-encoded
invalid_scope Requested scope not allowed for your client Request only the scopes FSRI granted you

Inside the Access Token

The access token is a standard JWT (RS256-signed). You don't need to decode it to use it, but decoding helps when troubleshooting:

echo "$ACCESS_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .
{
  "sub": "{{clientId}}",
  "token_use": "access",
  "scope": "partner-api/read partner-api/write",
  "iss": "https://cognito-idp.us-west-2.amazonaws.com/{environment-specific pool id}",
  "exp": 1784736000,
  "iat": 1784732400,
  "client_id": "{{clientId}}"
}

Things worth checking when a token misbehaves: scope contains what you expect, client_id is yours, and exp (Unix epoch) hasn't passed. If scope is missing entirely, contact FSRI — your client's allowed scopes are misconfigured.

Token Lifecycle & Caching

Tokens are valid for 1 hour (expires_in: 3600). The token endpoint is not designed to be called per-request — cache the token and reuse it:

  1. Cache the token in memory (or your cache layer) keyed by environment.
  2. Refresh proactively ~5 minutes before expiry (at expires_in - 300 seconds).
  3. Retry on 401 once: if an API call returns 401, discard the cached token, fetch a fresh one, and retry the request one time. If it still fails, stop and investigate — repeated 401s mean a configuration problem, not an expiry race.

There are no refresh tokens in the client_credentials flow — "refreshing" simply means requesting a new token the same way.

The example clients implement this pattern exactly.

Credential Security

  • Server-side only. The client secret and API key must never reach a browser, mobile app, or public repository. All API traffic goes through your backend.
  • Use a secret manager (Vault, AWS Secrets Manager, or equivalent). Don't hardcode credentials or commit them — including in example/test files.
  • Don't log secrets or full tokens. Log the token's decoded claims if you need traceability, never the raw JWT.
  • Rotation. FSRI rotates credentials on request (e.g., suspected exposure) and as periodic hygiene. Rotation of the client secret and API key doesn't change your Client ID — build your configuration so credentials can be swapped without a deploy.
  • Environment separation. Never point production code at sandbox credentials or vice versa — tokens are rejected across environments by design, but the failure mode wastes debugging time.