Skip to content

Error Handling

Work top-down: run the debug checklist first, then jump to your status code.

The Debug Checklist

Ninety percent of integration failures are one of these. Check in order:

  1. Token is present and freshAuthorization: Bearer eyJ… (capital B, one space). Decode it: echo $ACCESS_TOKEN | cut -d. -f2 | base64 -d | jq '{scope, exp, client_id}' — is exp (epoch seconds) in the future? Does scope contain what the endpoint needs?
  2. x-api-key header present — on every request, including /ping.
  3. Right environment, all three places — sandbox token endpoint + sandbox base URL + sandbox credentials. Any cross-wiring fails.
  4. Correct base URLhttps://sandbox.lms.api.fsri.org/api/partner/v1 (no trailing slash before the endpoint path).
  5. Content-Type matches the methodapplication/json for POST, application/merge-patch+json for PATCH (plain JSON PATCH is rejected).
  6. X-User-ID on user-scoped endpoints — content, refresh-url, complete, progress, transcripts, PDFs. Must be a UUID you registered.
  7. Body is valid JSON — trailing commas and smart quotes from copy-paste are classics.

Quick isolation test — proves connectivity + API key with no other moving parts:

curl -s -w "\nHTTP %{http_code}\n" -H "x-api-key: $API_KEY" "$BASE_URL/ping"

200 healthy here + failures elsewhere = your problem is the JWT or the request itself, not connectivity.

Reading Error Responses

Where an error comes from tells you what kind of problem you have:

Response body looks like Origin Problem class
{"@type": "hydra:Error", "hydra:description": …} The API application Request semantics (your input, resource state)
{"error": "validation_failed", "violations": […]} The API application Field-level input validation
{"message": "Unauthorized"} or similar terse JSON API gateway JWT missing/invalid
Empty body with 403 API gateway API key missing/invalid
{"error": "invalid_client"} etc. Token endpoint OAuth2 credentials/request

Error Reference by Status Code

400 Bad Request

The request itself is wrong — fix it; never retry unchanged.

Likely cause Check Fix
Validation failure Body has violations[]? Fix each propertyPath per its message
Malformed JSON Does the body parse? Repair JSON (watch trailing commas)
Missing X-User-ID on a user-scoped endpoint Error text names the header Send the learner's UUID
Malformed UUID in X-User-ID 36-char UUID format? Send the UUID exactly as registration returned it

Example validation body:

{
  "error": "validation_failed",
  "violations": [
    { "propertyPath": "email", "message": "This value is not a valid email address." }
  ]
}

401 Unauthorized

JWT missing, expired, malformed, or from the wrong environment.

Likely cause Check Fix
Token expired Decode: is exp past? Mint a new token; implement proactive refresh
Wrong environment's token Decoded iss — which pool? Sandbox tokens for sandbox, production for production
Header malformed Exactly Authorization: Bearer {token}? Fix casing/spacing; don't quote the token
Not a token at all Whole three-part JWT pasted? Re-extract access_token from the token response

Handling rule: refresh the token and retry once. If 401 persists, stop — it's configuration, not expiry.

403 Forbidden

Three distinct causes, distinguished by the body:

Body Cause Fix
Empty x-api-key missing, wrong, or disabled Send the key on every request; contact FSRI if it persists
Error mentioning scope/permission Token lacks the needed scope (e.g., write op with a read-only token) Mint with partner-api/read partner-api/write
Error re: user status The X-User-ID user is deactivated Reactivate via PATCH {"status": "active"} if intended

404 Not Found

The resource doesn't exist for you. Partner isolation returns 404 (never 403) for other partners' resources — "exists but not yours" and "doesn't exist" are deliberately indistinguishable.

Likely cause Fix
Stale/hardcoded ID Fresh IDs from list endpoints — sandbox refreshes can change them
UUID not yours Use only UUIDs from your own /users/register calls
Module not in a published course Only published content is reachable
Path typo /course-progresses (plural, hyphenated); /transcript/pdf (singular) for the full-transcript PDF

409 Conflict

POST /modules/{id}/complete was called on a SCORM module. SCORM reports its own completion — remove the "Mark as Complete" control for content_type: "scorm".

429 Too Many Requests

You exceeded your usage plan's rate or daily quota.

Handling: exponential backoff with jitter — 1s, 2s, 4s, 8s… cap ~30s, give up after ~5 tries — applied globally: one 429 means slow everything down, not just the failed call.

Prevention: spread scheduled syncs off the top of the hour; use updatedAt[after] delta syncs instead of full refetches; cache the catalog. Your plan's specific limits are in the Partner Area.

5xx Server Errors

Something failed on FSRI's side. Retry with the same backoff as 429. If sustained for more than a few minutes, capture timestamps and report it — and check whether /ping still returns healthy, which distinguishes "API down" from "one endpoint struggling."

Retry Rules Summary

Safe to retry Never retry unchanged
429, 5xx, network timeouts 400, 403, 404, 409
401 — once, after a token refresh

The mutating endpoints are retry-friendly by design: registration is idempotent per email, /complete is an idempotent no-op on repeat, and merge-PATCH with the same body converges. A timed-out write can be retried without double effect.

Common Integration Mistakes

  • Mixing up the two credential systems — Client ID/Secret belongs to the token endpoint; the API key belongs in x-api-key. Swapping them = invalid_client or empty-body 403.
  • Treating /ping as proof of auth — it validates only your API key. A green /ping with failing /courses means your token is the problem.
  • PATCH with application/json — profile updates require merge-patch. The symptom is a 4xx on a request that "looks right."
  • Persisting content URLs — they expire in ~60 minutes. Symptom: content plays in testing, breaks for real users later.
  • Minting a token per request — works in testing, rate-limits under load. Cache for the hour.
  • Hardcoding sandbox IDs — sandbox content re-syncs monthly. Discover IDs from list endpoints at runtime.
  • Retrying non-retryable errors400/403/404/409 will fail identically forever.
  • Calling /complete for SCORM — that's what the 409 is telling you.
  • Assuming one field-name convention — resources are camelCase; the two content endpoints are snake_case. Match each endpoint's example.

When You Contact Support

Include: environment, timestamp (with timezone), endpoint + method, full request minus credentials, response status + body, your Client ID (never the secret or API key), and expected vs. actual. Contracted partners: see Support & Escalation for the escalation path.