SCORM API Reference
Complete reference documentation for all SCORM API endpoints, request/response schemas, authentication requirements, and error handling.
Table of Contents
- Overview
- Authentication
- Base URLs
- API Versioning
- Rate Limiting
- Error Handling
- API Endpoints
- Request/Response Examples
Overview
The SCORM API is a RESTful API that provides endpoints for managing SCORM packages, learning sessions, analytics, and integrations. All endpoints return JSON responses and use standard HTTP status codes.
API Features
- SCORM 1.2 & 2004 Support: Full support for both SCORM versions
- Multi-tenant Architecture: Complete data isolation per tenant
- Optimistic Locking: Version-based concurrency control
- Rate Limiting: Per-tenant rate limits to ensure fair usage
- Webhooks: Real-time event notifications
- xAPI Integration: Automatic SCORM to xAPI conversion
Authentication
The SCORM API supports two authentication methods:
API Key Authentication
For programmatic access (server-to-server), use API key authentication:
X-API-Key: your-api-key-here
OR
Authorization: Bearer your-api-key-here
API Key Scopes:
| Scope | Permissions |
|---|---|
public_api |
Standard partner access (upload, launch, sessions, dispatches, webhooks) |
internal_product |
Internal product/service access (elevated operations) |
admin |
System administration |
Note: Fine-grained scopes may be added in future releases.
Clerk Authentication
For web application users (browser-based), authentication is handled via Clerk session cookies. Customer routes (/api/customer/*) automatically filter data by the authenticated user's tenant.
See: API Key Security Guide for detailed authentication documentation.
Base URLs
Production: https://app.allureconnect.com
Development: http://localhost:3000
API Versioning
The API uses URL-based versioning:
- v1:
/api/v1/*- Current stable version - Customer Routes:
/api/customer/*- Web application routes (Clerk auth) - Admin Routes:
/api/admin/*- System administration routes
Rate Limiting
Rate limits are applied per endpoint and keyed by API key or workspace:
| Endpoint | Default limit | Scope |
|---|---|---|
POST /api/v1/packages/upload-url (and /upload-sessions) |
120 req/min | per API key |
POST /api/v1/packages/process |
60 req/min | per workspace |
There is no single global per-scope (read/write/admin) RPM table. Limits may vary by deployment and plan; consult the rate-limit response headers or Rate Limiting Guide for details.
Rate Limit Headers (when present):
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1697544600
See: Rate Limiting Guide for detailed information.
Error Handling
All errors follow this standard format:
{
"error": "Human-readable error message",
"code": "ERROR_CODE",
"details": {
"field": "Additional context"
}
}
HTTP Status Codes:
| Code | Meaning | Common Error Codes |
|---|---|---|
200 |
Success | - |
201 |
Created | - |
400 |
Bad Request | INVALID_REQUEST, MISSING_FILE, INVALID_FILE_TYPE |
401 |
Unauthorized | UNAUTHORIZED, INVALID_API_KEY |
403 |
Forbidden | FORBIDDEN, INSUFFICIENT_SCOPES |
404 |
Not Found | PACKAGE_NOT_FOUND, SESSION_NOT_FOUND |
409 |
Conflict | VERSION_CONFLICT |
413 |
Payload Too Large | FILE_TOO_LARGE |
429 |
Too Many Requests | RATE_LIMIT_EXCEEDED |
500 |
Internal Server Error | INTERNAL_ERROR |
See: Error Codes Reference for complete error code documentation.
API Endpoints
Health Check
GET /api/health
Check if the API is running and healthy.
Authentication: None required
Response (200):
{
"status": "ok",
"timestamp": "2025-01-15T10:30:00.000Z",
"version": "1.0.0",
"database": "connected",
"storage": "available"
}
Packages
POST /api/v1/packages
Not implemented in the current Connect route handler (GET lists packages only). Do not use this path for uploads.
Use instead:
POST /api/v1/packages/upload-url(or aliasPOST /api/v1/packages/upload-sessions) to mint a presigned URLPUTthe ZIP topresigned_urlwithrequired_put_headersPOST /api/v1/packages/processto validate and publish
Partner runbook: docs/API/package-upload-integration.md.
For small direct multipart uploads (subject to platform body limits), the app exposes POST /api/v1/packages/upload — see the OpenAPI spec (GET /api/docs/openapi).
GET /api/v1/packages
List all packages for the authenticated tenant.
Authentication: API Key (public_api, internal_product, or admin scope)
Query Parameters:
tenant_id(optional): Ignored for tenancy — the server resolves the tenant from the API key. May be included for logging/auditing purposes only.limit(optional): Number of results (default: 50, max: 100)offset(optional): Pagination offset (default: 0)
Response (200):
{
"packages": [
{
"id": "pkg_abc123",
"title": "Introduction to Safety Training",
"version": "1.2",
"created_at": "2025-01-15T10:30:00.000Z"
}
],
"pagination": {
"total": 1,
"limit": 50,
"offset": 0,
"hasMore": false
}
}
GET /api/v1/packages/{packageId}
Get detailed information about a specific package, including its version history.
Authentication: API Key (public_api, internal_product, or admin scope)
Response (200):
{
"package": {
"id": "pkg_abc123",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Introduction to Safety Training",
"version": "1.2",
"scorm_version": "1.2",
"launch_url": "index.html",
"file_size_bytes": 5242880,
"created_at": "2025-01-15T10:30:00.000Z",
"updated_at": "2025-01-15T10:30:00.000Z"
},
"versions": [
{
"revision": 1,
"created_at": "2025-01-15T10:30:00.000Z",
"file_size_bytes": 5242880
}
]
}
DELETE /api/v1/packages/{packageId}
Soft-archive a package. The package is marked as archived and hidden from default list results, but is not permanently deleted from storage. Storage quota is not immediately freed. To permanently delete a package and release storage, use the Connect dashboard.
Authentication: API Key (public_api, internal_product, or admin scope)
Response (200): Archive result
Note:
PATCH /api/v1/packages/{packageId}is not implemented. Package metadata updates are available via the Connect dashboard.
POST /api/v1/packages/{packageId}/launch
Create a new session and get the player URL.
Authentication: API Key (public_api, internal_product, or admin scope)
Request Body:
{
"user_id": "660e8400-e29b-41d4-a716-446655440000",
"session_id": "770e8400-e29b-41d4-a716-446655440000"
}
Response (200):
{
"launch_url": "https://app.allureconnect.com/player/770e8400-e29b-41d4-a716-446655440000?token=eyJhbGciOi...",
"session_id": "770e8400-e29b-41d4-a716-446655440000",
"package_id": "pkg_abc123",
"learner_id": "660e8400-e29b-41d4-a716-446655440000",
"content_type": "scorm",
"expires_in_seconds": 14400
}
launch_urlalready embeds the signed session token (?token=<jwt>). Treat it as opaque — do not strip query parameters or hand-reconstruct the player URL.
GET /api/v1/packages/{packageId}/versions
Get version history for a package.
Authentication: API Key (public_api, internal_product, or admin scope)
Response (200):
{
"versions": [
{
"revision": 3,
"created_at": "2025-01-15T10:30:00.000Z",
"uploaded_by": "user-123",
"file_size_bytes": 5242880
}
]
}
POST /api/v1/packages/multipart/init
Initialize multipart upload for large packages.
Authentication: API Key (public_api, internal_product, or admin scope)
Request Body:
{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"uploaded_by": "user-123",
"filename": "large-course.zip",
"file_size": 262144000
}
file_size is required so the server can enforce the effective upload ceiling
(the lower of the configured deployment ceiling—500 MB by default—and the plan tier)
at init (413 FILE_TOO_LARGE), plan the exact part count, and sign each part
URL for its expected byte length.
Response (200):
{
"upload_id": "upload_abc123",
"multipart_upload_id": "2~x9Yf...r2-issued-id",
"storage_path": "tenant/multipart/tmp_123.zip",
"part_size_bytes": 52428800,
"max_upload_mb": 500,
"part_count": 5
}
POST /api/v1/packages/multipart/part-url
Get presigned URL for uploading a part.
Authentication: API Key (public_api, internal_product, or admin scope)
Request Body:
{
"upload_id": "upload_abc123",
"part_number": 1
}
Response (200):
{
"url": "https://storage.example.com/upload?presigned=...",
"expires_in": 3600,
"content_length_bytes": 52428800
}
PUT exactly content_length_bytes raw bytes to url and save the ETag response header for the
complete call (the R2 bucket CORS policy must expose ETag for browser
clients).
POST /api/v1/packages/multipart/complete
Complete multipart upload.
Authentication: API Key (public_api, internal_product, or admin scope)
Request Body:
{
"upload_id": "upload_abc123",
"parts": [
{ "part_number": 1, "etag": "\"etag1\"" },
{ "part_number": 2, "etag": "\"etag2\"" }
]
}
Response (200):
{
"upload_id": "upload_abc123",
"storage_path": "tenant/multipart/tmp_123.zip",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"uploaded_by": "user-123",
"filename": "large-course.zip",
"next_step": "Call POST /api/v1/packages/process with tenant_id, uploaded_by, storage_path, original_filename"
}
POST /api/v1/packages/multipart/abort
Abort an in-flight multipart upload and free its staged parts in storage. Call
this when an upload is interrupted or abandoned so orphaned parts do not
accumulate; then retry with a fresh init.
Authentication: API Key (public_api, internal_product, or admin scope)
Request Body:
{
"upload_id": "upload_abc123"
}
Response (200):
{
"upload_id": "upload_abc123",
"storage_path": "tenant/multipart/tmp_123.zip",
"status": "aborted",
"aborted": true
}
Idempotent: repeat aborts return "status": "already_aborted", and aborting a
finished upload returns "status": "already_completed" (the stored object is
untouched), both with "aborted": false. Unknown upload ids return 404
UPLOAD_NOT_FOUND.
POST /api/v1/packages/process
Process an uploaded package (from multipart or direct upload).
Authentication: API Key (public_api, internal_product, or admin scope)
Request Body:
{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"uploaded_by": "user-123",
"storage_path": "tenant/uploads/tmp_123.zip",
"original_filename": "course.zip",
"validate_only": false
}
Response (200): Processing result (upload_id, manifest, optional package with package_id, etc.) — see OpenAPI ProcessPackageResponse at GET /api/docs/openapi.
POST /api/v1/packages/upload-url
Get a presigned URL for HTTP PUT direct upload to object storage (Cloudflare R2 when configured). Same behavior as POST /api/v1/packages/upload-sessions (alias).
Authentication: API key (public_api, internal_product, or admin scope) — Authorization: Bearer <api_key> or X-API-Key: <api_key>.
Request Body (see lib/contracts/connect-upload.ts uploadUrlRequestSchema):
| Field | Type | Required | Notes |
|---|---|---|---|
tenant_id |
string | Yes | Must match the tenant for the API key; server resolves tenant from the key. |
uploaded_by |
string | Yes | Defaults to the API key id if omitted or empty. |
filename |
string | Yes | Must end in .zip. |
file_size |
number | Yes | Size in bytes; effective limit is the lower of the deployment ceiling and tenant plan. |
{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"uploaded_by": "user-123",
"filename": "course.zip",
"file_size": 10485760
}
Response (200): (uploadUrlResponseSchema)
{
"upload_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"storage_path": "tenant/uploads/abc/course.zip",
"presigned_url": "https://<r2-host>/<bucket>/...",
"storage_type": "r2",
"bucket": "allurelms",
"max_upload_mb": 500,
"presigned_expires_in_seconds": 3600,
"presigned_expires_at": "2026-04-14T12:00:00.000Z",
"upload_method": "PUT",
"required_put_headers": {
"Content-Type": "application/zip"
},
"expires_at": "2026-04-14T12:00:00.000Z"
}
Send PUT to presigned_url with the ZIP body and at least Content-Type: application/zip as required by required_put_headers. Then call POST /api/v1/packages/process with storage_path and related fields.
Response (413): The shared UploadPlanLimitResponse includes
max_upload_mb, limit_source, and suggested_plan. When the current platform
ceiling binds, suggested_plan is null; do not direct the customer to upgrade.
Partner runbook: docs/API/package-upload-integration.md.
GET /api/v1/packages/upload-limit
Get the configured maximum upload size and storage backend flags.
Authentication: API Key (public_api, internal_product, or admin scope)
Response (200): (uploadLimitResponseSchema)
{
"max_upload_mb": 500,
"storage_type": "r2",
"r2_enabled": true
}
Sessions
GET /api/v1/sessions
List and filter sessions.
Authentication: API Key (public_api, internal_product, or admin scope)
Query Parameters:
tenant_id(required): Tenant UUIDuser_id(optional): Filter by user IDpackage_id(optional): Filter by package IDcompletion_status(optional):not_attempted,incomplete,completedsuccess_status(optional):unknown,passed,faileddate_from(optional): ISO 8601 datetimedate_to(optional): ISO 8601 datetimepage(optional): Page number (default: 1)limit(optional): Results per page (default: 20, max: 100)sort_by(optional):created_at,updated_at,completion_status(default:updated_at)sort_order(optional):asc,desc(default:desc)
Response (200):
{
"sessions": [
{
"id": "session-789",
"package_id": "pkg_abc123",
"tenant_id": "tenant-456",
"user_id": "user-abc",
"completion_status": "completed",
"success_status": "passed",
"score": {
"scaled": 0.95,
"raw": 95,
"min": 0,
"max": 100
},
"attempts": 1,
"time_spent_seconds": 3600,
"session_time": "PT1H",
"created_at": "2025-01-15T00:00:00Z",
"updated_at": "2025-01-15T01:00:00Z",
"package": {
"title": "Course Title",
"version": "1.2"
}
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"total_pages": 8
}
}
GET /api/v1/sessions/{sessionId}
Get session data and CMI information.
Authentication: API Key (public_api, internal_product, or admin scope) OR Launch token
Query Parameters:
token(optional): Launch token for player access
Response (200):
{
"id": "770e8400-e29b-41d4-a716-446655440000",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "660e8400-e29b-41d4-a716-446655440000",
"package_id": "pkg_abc123",
"cmi_data": {
"cmi.core.lesson_status": "incomplete",
"cmi.core.score.raw": "75",
"cmi.core.score.max": "100",
"cmi.core.session_time": "PT15M30S"
},
"completion_status": "incomplete",
"success_status": "unknown",
"score": {
"scaled": 0.75,
"raw": 75,
"max": 100,
"min": 0
},
"time_spent_seconds": 930,
"version": 3,
"created_at": "2025-01-15T10:00:00.000Z",
"updated_at": "2025-01-15T10:15:30.000Z"
}
PUT /api/v1/sessions/{sessionId}
Update session data and CMI information.
Authentication: API Key (public_api, internal_product, or admin scope)
Request Body:
{
"version": 3,
"cmi_data": {
"cmi.core.lesson_status": "completed",
"cmi.core.score.raw": "85",
"cmi.core.score.max": "100",
"cmi.core.session_time": "PT20M45S"
},
"completion_status": "completed",
"success_status": "passed",
"score": {
"scaled": 0.85,
"raw": 85,
"max": 100,
"min": 0
},
"session_time": "PT20M45S"
}
Important: The version field is required for optimistic locking. If you receive a 409 Conflict error, fetch the latest session data and retry with the updated version.
Response (200): Updated session object
Error Responses:
409- Version conflict (fetch latest and retry)
POST /api/v1/sessions/{sessionId}/refresh-token
Refresh a bearer token for the same active SCORM session.
Authentication: Launch token (Authorization: Bearer <launch-token>) or tenant API key
Response (200):
{
"success": true,
"session_id": "770e8400-e29b-41d4-a716-446655440000",
"token": "eyJhbGciOiJIUzI1NiIs...",
"expires_in_seconds": 14400
}
Dispatches
GET /api/v1/dispatches
List dispatch packages.
Authentication: API Key (public_api, internal_product, or admin scope)
Query Parameters:
tenant_id(optional): Ignored for tenancy — the server resolves the tenant from the API key.limit(optional): Number of results (default: 50)offset(optional): Pagination offset (default: 0)
Response (200):
{
"dispatches": [
{
"id": "dispatch-123",
"package_id": "pkg_abc123",
"label": "Client Distribution",
"destination": "client-lms",
"created_at": "2025-01-15T10:30:00.000Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}
POST /api/v1/dispatches
Create a dispatch package.
Authentication: API Key (public_api, internal_product, or admin scope)
Request Body:
{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"package_id": "pkg_abc123",
"label": "Client Distribution",
"destination": "acme-lms",
"registration_limit": 100,
"expires_in_hours": 720,
"allowed_domains": ["acme.com", "training.acme.com"]
}
Response (201):
{
"dispatch": {
"id": "dispatch-123",
"packageId": "pkg_abc123",
"launchUrl": "https://app.allureconnect.com/player/dispatch/dispatch-123",
"status": "active"
},
"dispatch_url": "https://app.allureconnect.com/player/dispatch/dispatch-123"
}
GET /api/v1/dispatches/{dispatchId}
Get dispatch package details.
Authentication: API Key (public_api, internal_product, or admin scope)
Response (200): Dispatch detail object (see OpenAPI getDispatch)
PATCH /api/v1/dispatches/{dispatchId}
Update the organization/customer-group attribution for a dispatch (updateDispatchAttribution). This is the only partner-API PATCH operation on dispatches — it does not update label, expiry, registration limit, or allowed domains (those are managed via the Connect dashboard / customer routes).
Authentication: API Key (public_api, internal_product, or admin scope)
Request Body (one field required):
| Field | Type | Description |
|---|---|---|
organization_id |
string | null |
Organization ID to assign; null clears attribution |
organizationId |
string | null |
Camel-case alias for organization_id |
Response (200): Updated dispatch with id, organizationId, organization_id, organizationName, and launch_url
Note:
DELETE /api/v1/dispatches/{id},GET .../zip, andGET .../launchesare not implemented at the partner API (/api/v1/) level. To download a SCORM ZIP or view launch statistics for a dispatch, use the Connect dashboard or the Clerk-authenticated customer routes (/api/customer/dispatches/{id}/zip).
POST /api/v1/dispatches/launch
Launch a dispatch package (for third-party LMSs).
Authentication: Launch token (included in dispatch package)
Query Parameters:
token(required): Dispatch launch token
Response (200):
{
"launch_url": "https://app.allureconnect.com/player/session-456?token=eyJhbGciOi...",
"session_id": "session-456"
}
Launch Link Health Check (self-service)
Integrators who have a launch URL but aren't sure whether it will work can validate it structurally before embedding it. The endpoint is unauthenticated and read-only — it never returns session data, only whether the URL is well-formed and the token (if present) is valid.
GET /api/v1/launch-links/validate?url=…
POST /api/v1/launch-links/validate { "url": "…" }
Response (200): always 200. The status field distinguishes outcomes.
{
"status": "ok",
"message": "Session token is valid and not expired.",
"remediation": "No action needed — this URL should load for learners.",
"detail": {
"host": "app.allureconnect.com",
"pathname": "/player/ses_1",
"tokenPresent": true,
"linkType": "session"
}
}
status values:
| Status | Meaning |
|---|---|
ok |
URL is valid, token is valid, link should load. |
missing_token |
/player/<id> without a ?token=, or a truncated dispatch URL. |
expired_token |
Token parsed but past its TTL — re-mint. |
invalid_token |
Token failed signature verification — regenerate from the correct environment. |
not_a_player_url |
URL doesn't match a player surface path. |
invalid_url |
URL wasn't provided or couldn't be parsed. |
Use this endpoint from your integrator tooling (CLI, CI, integration tests) to catch broken launch URLs before they ship to an LMS.
Webhooks
GET /api/v1/webhooks
List webhooks for a tenant.
Authentication: API Key (public_api, internal_product, or admin scope)
Response (200):
{
"webhooks": [
{
"id": "webhook-123",
"url": "https://hooks.example.com/scorm",
"eventScope": "sessions",
"status": "active",
"created_at": "2025-01-15T10:30:00.000Z"
}
]
}
POST /api/v1/webhooks
Create a webhook endpoint. Subscribe to an event scope (packages, sessions, usage, or all) — not a single event type.
Authentication: API Key (public_api, internal_product, or admin scope)
Request Body:
{
"url": "https://hooks.example.com/scorm",
"secret": "replace-with-at-least-16-random-characters",
"label": "Production LMS",
"eventScope": "sessions"
}
The tenant is resolved from the API key — tenant_id is not required in the body.
Response (201):
{
"webhook": {
"id": "whk_abc123",
"url": "https://hooks.example.com/scorm",
"eventScope": "sessions",
"status": "active",
"signingSecretConfigured": true
}
}
See: Webhook Setup Guide for detailed webhook documentation.
xAPI
POST /api/v1/xapi/statements
Create xAPI statements (Learning Record Store).
Authentication: API Key (public_api, internal_product, or admin scope)
Request Body:
{
"actor": {
"mbox": "mailto:learner@example.com",
"name": "John Doe"
},
"verb": {
"id": "http://adlnet.gov/expapi/verbs/completed",
"display": { "en-US": "completed" }
},
"object": {
"id": "https://example.com/activities/course-123",
"definition": {
"name": { "en-US": "Safety Training" }
}
},
"result": {
"score": {
"scaled": 0.85,
"raw": 85,
"max": 100
},
"success": true,
"completion": true
}
}
Response (200):
{
"statement_id": "550e8400-e29b-41d4-a716-446655440000",
"stored": "2025-01-15T10:30:00.000Z"
}
GET /api/v1/xapi/statements
Query xAPI statements.
Authentication: API Key (public_api, internal_product, or admin scope)
Query Parameters:
tenant_id(required): Tenant UUIDactor(optional): Filter by actor (JSON)verb(optional): Filter by verb IDactivity(optional): Filter by activity IDsince(optional): ISO 8601 datetimeuntil(optional): ISO 8601 datetimelimit(optional): Results per page (default: 20, max: 100)format(optional):ids,exact,canonical(default:exact)
Response (200):
{
"statements": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"actor": { ... },
"verb": { ... },
"object": { ... },
"timestamp": "2025-01-15T10:30:00.000Z"
}
],
"more": "https://api.example.com/statements?cursor=..."
}
GET /api/v1/xapi/statements/{statementId}
Get a specific xAPI statement.
Authentication: API Key (public_api, internal_product, or admin scope)
Response (200): xAPI statement object
GET /api/v1/xapi/analytics/activities
Get activity analytics.
Authentication: API Key (public_api, internal_product, or admin scope)
Query Parameters:
tenant_id(required): Tenant UUIDactivity_id(optional): Filter by activitydate_from(optional): ISO 8601 datetimedate_to(optional): ISO 8601 datetime
Response (200):
{
"activities": [
{
"activity_id": "https://example.com/activities/course-123",
"total_statements": 150,
"unique_actors": 45,
"completions": 30
}
]
}
GET /api/v1/xapi/analytics/actors
Get actor analytics.
Authentication: API Key (public_api, internal_product, or admin scope)
Query Parameters:
tenant_id(required): Tenant UUIDactor(optional): Filter by actor (JSON)date_from(optional): ISO 8601 datetimedate_to(optional): ISO 8601 datetime
Response (200):
{
"actors": [
{
"actor": {
"mbox": "mailto:learner@example.com",
"name": "John Doe"
},
"total_statements": 25,
"activities_completed": 5
}
]
}
Quotas
GET /api/v1/quotas
Get quota information for the authenticated tenant.
Authentication: API Key (public_api, internal_product, or admin scope)
Response (200):
{
"package_quota": {
"limit": 100,
"used": 45,
"remaining": 55
},
"storage_quota": {
"limit_bytes": 53687091200,
"limit_gb": 50,
"used_bytes": 21474836480,
"used_gb": 20,
"remaining_bytes": 32212254720,
"remaining_gb": 30
}
}
See: Quota Management Guide for detailed information.
Content
GET /api/v1/content/{packageId}/...
Serve SCORM package content files.
Authentication: Launch token or API Key (public_api, internal_product, or admin scope)
Path Parameters:
packageId: Package UUID...: Relative path to content file within package
Response (200): File content with appropriate Content-Type header
Example:
GET /api/v1/content/pkg_abc123/index.html
GET /api/v1/content/pkg_abc123/assets/styles.css
Customer Routes
Customer routes use Clerk session authentication (browser cookie) and automatically filter data by the authenticated user's tenant. They are not part of the partner API key surface.
For the live, authoritative catalog of customer routes, use the interactive OpenAPI docs at /api/docs (Scalar UI) or download the spec from GET /api/docs/openapi.
Sample real paths (as of this writing — see app/api/customer/ for the canonical inventory):
| Method | Path | Description |
|---|---|---|
GET |
/api/customer/packages |
List packages (dashboard view) |
GET/DELETE |
/api/customer/packages/{id} |
Get or archive a package |
GET/POST |
/api/customer/dispatches |
List or create dispatches |
GET/PATCH/DELETE |
/api/customer/dispatches/{id} |
Manage a dispatch |
GET |
/api/customer/dispatches/{id}/zip |
Download dispatch SCORM ZIP |
GET/POST |
/api/customer/webhooks |
Manage webhook endpoints |
GET |
/api/customer/api-keys/{apiKeyId}/activity |
API key activity log |
GET |
/api/customer/billing |
Billing info |
POST |
/api/customer/billing/portal |
Billing portal URL |
GET/POST |
/api/customer/reports/* |
Learner progress reports |
GET |
/api/customer/activity |
Tenant activity feed |
GET/POST |
/api/customer/organizations |
Organization management |
GET/POST |
/api/customer/connections/* |
Connector integrations |
GET/POST |
/api/customer/competency/* |
Competency/skill graph (CALE) — see /api/docs |
Credentials: Your Workspace ID and API keys are at Dashboard → Integrations (or
/dashboard/integrations?tab=keys) — not under Settings → Tenant Details.
Admin Routes
Admin routes require system administrator (Clerk) authentication and can access all tenants. They are internal/ops surfaces, not partner-facing.
For the live catalog, use GET /api/docs/openapi or browse app/api/admin/.
Sample real paths:
| Method | Path | Description |
|---|---|---|
GET |
/api/admin/tenants |
List all tenants |
GET |
/api/admin/packages |
Packages across all tenants |
GET |
/api/admin/audit-logs |
Audit logs |
GET |
/api/admin/metrics/usage |
Usage metrics |
GET |
/api/admin/webhooks |
All webhook endpoints |
GET |
/api/admin/contracts |
Billing contracts |
GET |
/api/admin/integration-incidents |
Integration incident log |
Request/Response Examples
Complete Integration Example
See the Integration Guides for complete examples in various languages and frameworks:
Handling Version Conflicts
When updating sessions, always include the version field for optimistic locking:
async function updateSessionWithRetry(sessionId: string, updates: any, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
// Step 1: Get current session
const session = await fetch(`/api/v1/sessions/${sessionId}`, {
headers: { 'X-API-Key': apiKey }
}).then(r => r.json());
// Step 2: Merge your changes
const mergedData = {
...session.cmi_data,
...updates.cmi_data
};
// Step 3: Attempt update with current version
const response = await fetch(`/api/v1/sessions/${sessionId}`, {
method: 'PUT',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
version: session.version, // Use current version
cmi_data: mergedData,
...updates
})
});
if (response.ok) {
return await response.json(); // Success!
}
if (response.status === 409) {
console.log(`Version conflict, retrying... (${attempt + 1}/${maxRetries})`);
continue; // Retry with fresh data
}
throw new Error(`Update failed: ${response.status}`);
}
throw new Error('Max retries exceeded for version conflict');
}
Error Handling with Retry
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, options);
// Don't retry client errors (4xx)
if (response.status >= 400 && response.status < 500) {
return response;
}
// Success
if (response.ok) {
return response;
}
// Retry server errors (5xx) with exponential backoff
if (attempt < maxRetries - 1) {
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
console.log(`Server error, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
} catch (error) {
// Network error - retry with backoff
if (attempt < maxRetries - 1) {
const delay = Math.pow(2, attempt) * 1000;
console.log(`Network error, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
throw new Error('Max retries exceeded');
}
Additional Resources
- Error Codes Reference - Complete error code documentation
- Rate Limiting Guide - Rate limiting details
- Quota Management Guide - Quota information
- Webhook Setup Guide - Webhook configuration
- CMI Data Guide - Understanding SCORM CMI data
- Package Validation Guide - SCORM package requirements
Last Updated: 2025-01-15
API Version: 1.0.0