High-level guide for calling the Allure Connect REST API. For the machine-readable contract and try-it console, use GET /api/docs/openapi (OpenAPI JSON) and /api/docs (interactive Scalar UI) on your deployment.
Partner upload flow (presigned URL, storage PUT, process): SCORM API reference
Legacy note: Some older docs referred to
POST /api/v1/packagesas a multipart upload. The current app exposesGET /api/v1/packagesfor listing; large package ingestion usesPOST /api/v1/packages/upload-url→PUTto storage →POST /api/v1/packages/process. Small direct uploads may usePOST /api/v1/packages/upload(see OpenAPI and api-reference).
Table of Contents
- Quick Start
- Authentication
- Rate Limiting
- Endpoints
- Error Handling
- Best Practices
- SDKs & Tools
- Additional Resources
Quick Start
Base URL
Production: https://app.allureconnect.com
Development: http://localhost:3000API Version
Current: v1 — routes are prefixed with /api/v1/ except health checks.
Quick example (recommended large upload)
Mint a presigned URL from your backend, upload the ZIP (browser or server), then process:
const base = 'https://app.allureconnect.com';
const mint = await fetch(`${base}/api/v1/packages/upload-url`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
tenant_id: tenantId,
uploaded_by: 'user-123',
filename: 'course.zip',
file_size: fileSizeBytes,
}),
});
if (!mint.ok) {
throw new Error(`Mint failed: ${mint.status} ${await mint.text()}`);
}
const ticket = await mint.json();
const put = await fetch(ticket.presigned_url, {
method: 'PUT',
headers: ticket.required_put_headers,
body: zipBlob,
});
if (!put.ok) {
throw new Error(`Storage PUT failed: ${put.status} ${await put.text()}`);
}
const proc = await fetch(`${base}/api/v1/packages/process`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
tenant_id: tenantId,
uploaded_by: 'user-123',
storage_path: ticket.storage_path,
original_filename: 'course.zip',
}),
});
if (!proc.ok) {
throw new Error(`Process failed: ${proc.status} ${await proc.text()}`);
}
await proc.json();Authentication
Use an API key per tenant. Prefer Authorization: Bearer <api_key>; X-API-Key: <api_key> is also supported. If both are sent, Bearer wins.
Authorization: Bearer <api_key>X-API-Key: ac_live_...Example
curl -H "Authorization: Bearer $CONNECT_API_KEY" \
https://app.allureconnect.com/api/v1/packagesValidate an API Key
Use GET /api/v1/keys/me for deployment health checks and credential rollout verification. The response contains only non-secret metadata about the authenticated key and tenant. It never returns the API key, hashed secret, or suffix.
curl -H "Authorization: Bearer $CONNECT_API_KEY" \
https://app.allureconnect.com/api/v1/keys/me{
"ok": true,
"key": {
"id": "key_abc123",
"tenant_id": "tenant_acme",
"scope": "public_api",
"mode": "live",
"is_sandbox": false,
"label": "Production integration",
"source": "convex"
}
}API Key Scopes
API keys use public_api, internal_product, or admin scope. See the OpenAPI spec and dashboard for each route's accepted scopes.
Keys you create in the dashboard or through the child-workspace API use the public_api scope. Allure Connect issues internal_product and admin keys itself. A request for either scope is refused with API_KEY_SCOPE_NOT_ALLOWED (403 on the child-workspace API) unless the caller resolves to a trusted issuer: an Allure Connect platform operator, or a workspace that Allure Connect has grandfathered for privileged keys. Existing keys keep their scope.
Obtaining API Keys
Create and manage keys in the Connect dashboard (Build → API keys, or /dashboard/integrations?tab=keys).
See also: API key security
Rate Limiting
Rate limits prevent abuse and ensure fair usage across all tenants.
Limits
Rate limits are applied per operation (not per scope):
POST /api/v1/packages/upload-url: 120 requests per minute (per API key)POST /api/v1/packages/process: 60 requests per minute (per workspace)
Other partner API endpoints do not have separate hard application-layer rate limits. See Rate Limiting Guide for full details.
Rate Limit Headers
Responses include rate limit information:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 119
X-RateLimit-Reset: 1640995200Note: The
X-RateLimit-Limitvalue reflects the limit for the specific operation:120forPOST /api/v1/packages/upload-urland60forPOST /api/v1/packages/process. Other partner API endpoints do not carry a separate application-layer rate-limit header.
Exceeded Rate Limit
HTTP/1.1 429 Too Many Requests
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded"
}
}Solution: Wait until the reset time or implement exponential backoff.
Endpoints
Health Check
GET /api/health
Check service health status.
Authentication: None required
Response:
{
"status": "healthy",
"service": "scorm-api",
"version": "1.5.0",
"timestamp": "2025-01-01T12:00:00Z",
"checks": {
"database": true
}
}Packages
List Packages
GET /api/v1/packages
List all SCORM packages for the authenticated tenant.
Query Parameters:
limit(integer, default: 50, max: 500) - Results per pageoffset(integer, default: 0) - Number of packages to skiptenant_id(deprecated) - Ignored; the API key always determines the tenant
Example:
curl -H "Authorization: Bearer $CONNECT_API_KEY" \
"https://app.allureconnect.com/api/v1/packages"Response:
{
"packages": [
{
"id": "pkg_123",
"tenantId": "tenant_456",
"slug": "workplace-safety-essentials",
"title": "Workplace Safety Essentials",
"contentType": "scorm",
"status": "ready",
"currentVersion": "1.2",
"launchCount30d": 18,
"storageMb": 5,
"updatedAt": "2026-09-01T12:00:00.000Z"
}
],
"pagination": {
"total": 142,
"limit": 50,
"offset": 0,
"hasMore": true,
"totalIsLowerBound": false
}
}Advance offset by limit while hasMore is true. If totalIsLowerBound is true, total is a bounded lower estimate rather than an exact catalog count.
Upload package (large files)
Use POST /api/v1/packages/upload-url (alias POST /api/v1/packages/upload-sessions) to mint a short-lived presigned URL, PUT the ZIP to that URL, then POST /api/v1/packages/process. See the SCORM API reference.
For small files in trusted contexts, POST /api/v1/packages/upload accepts multipart/form-data (subject to platform body limits). Details: api-reference.
Get Package
GET /api/v1/packages/{id}
Get details of a specific package, including version history.
Response:
{
"package": {
"id": "pkg_123",
"tenantId": "tenant_456",
"slug": "course-title",
"title": "Course Title",
"contentType": "scorm",
"status": "ready",
"currentVersion": "1.2",
"launchCount30d": 18,
"storageMb": 5,
"updatedAt": "2026-09-01T12:00:00.000Z"
},
"versions": [
{
"id": "ver_1",
"packageId": "pkg_123",
"versionLabel": "1.2",
"contentType": "scorm",
"status": "ready",
"manifestVersion": "SCORM 1.2",
"uploadedAt": "2026-09-01T12:00:00.000Z",
"storageKey": "packages/tenant_456/pkg_123/1.2/course.zip"
}
]
}Archive Package
DELETE /api/v1/packages/{id}
Soft-archive a SCORM package. The package is hidden from default listing 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.
Response:
{
"success": true,
"package_id": "pkg-123"
}Sessions
List Sessions
GET /api/v1/sessions
List learner sessions with optional filtering.
Query Parameters:
package_id(string) - Filter by packageuser_id(string) - Filter by the external learner/user referencelimit(integer, default: 20, max: 100)offset(integer, default: 0) - Row offsetpage(deprecated integer alias, default: 1) - Converted to an offset whenoffsetis not supplied
These are the only supported parameters. Unrecognized ones (for example
completion_statusorsuccess_status) are ignored, not rejected — the response is an unfiltered page. Filter client-side oncompletion_statusin the response body.
Example:
curl -H "X-API-Key: ac_live_..." \
"https://app.allureconnect.com/api/v1/sessions?package_id=pkg-123"Response:
{
"sessions": [
{
"id": "770e8400-e29b-41d4-a716-446655440000",
"session_id": "770e8400-e29b-41d4-a716-446655440000",
"package_id": "pkg_123",
"tenant_id": "tenant_456",
"user_id": "user_abc",
"learner_id": "user_abc",
"learner_name": "Jordan Lee",
"status": "completed",
"completion_status": "completed",
"progress_percent": 100,
"last_activity_at": "2026-09-01T13:00:00.000Z",
"content_type": "scorm",
"packageId": "pkg_123",
"learnerName": "Jordan Lee",
"completionStatus": "completed",
"progressPercent": 100,
"lastActivityAt": "2026-09-01T13:00:00.000Z"
}
],
"pagination": {
"total": 150,
"limit": 20,
"offset": 0,
"has_more": true,
"hasMore": true,
"total_is_lower_bound": false,
"totalIsLowerBound": false,
"page": 1,
"total_pages": 8,
"has_next": true,
"has_prev": false
}
}Use offset and has_more for new integrations. The page, total_pages, has_next, and has_prev fields are deprecated compatibility aliases. When total_is_lower_bound is true, total and the derived total_pages value are lower bounds; continue while has_more is true.
Get Session
GET /api/v1/sessions/{id}
Get detailed session information including full CMI data.
Response:
{
"id": "770e8400-e29b-41d4-a716-446655440000",
"package_id": "pkg_123",
"user_id": "user_abc",
"completion_status": "completed",
"success_status": "passed",
"score": { "scaled": 0.95, "raw": 95, "min": 0, "max": 100 },
"cmi_data": {
"cmi.core.lesson_status": "completed",
"cmi.core.score.raw": "95"
},
"session": {
"id": "770e8400-e29b-41d4-a716-446655440000",
"packageId": "pkg_123",
"completionStatus": "completed",
"progressPercent": 100
}
}The root snake-case fields are the partner compatibility contract. The nested session object is the richer camel-case player model.
Claim a hosted runtime
POST /api/v1/sessions/{id}
Exclusive ownership is opt-in: an authenticated POST /api/v1/packages/{packageId}/launch may send runtime_protocol: "exclusive_v1". Omission preserves legacy hosted-player and partner writes. The choice is stored on the session, remains through resume/reset, and cannot be downgraded by omission. An authorized launch can upgrade its scoped existing session without resetting CMI or progress.
Read nested session.runtimeProtocol (legacy or exclusive_v1). The shared hosted player claims only an opted-in session; claiming a legacy session returns 409 RUNTIME_PROTOCOL_NOT_ENABLED. For an opted-in session, atomically claim its current attempt using the same API-key or session-launch-token authentication as session writes:
{
"runtime_id": "48bc29ec-d648-4ffd-b5db-8231e1c3d8d2",
"expected_runtime_id": null,
"attempt": 2
}Use the runtimeOwnerId and attempts from the nested GET session as the expected owner and attempt. The strict body accepts UUID runtime IDs (expected owner may be null) and a positive integer attempt. Success returns { "session": ... } with the current CMI snapshot, runtimeProtocol, runtimeOwnerId, and runtimeMode: "record" | "review". Start from that returned state. Retrying the same claim preserves its mode; a changed owner/attempt returns 409 RUNTIME_SUPERSEDED / ATTEMPT_SUPERSEDED.
Review does not replace an active recording owner saving final answers. An unowned review is claimed read-only. Reset/new-attempt starts unclaimed and retains the selected protocol. Legacy hosted-player and partner writes can continue to share their session, including omitted-version writes. An existing owner always retains write fencing; a protocol label cannot bypass it. See the session API reference for full ownership semantics.
Update Session
PUT /api/v1/sessions/{id}
Update session CMI data (learner progress). Authenticate with a tenant-authorized API key or this session's launch token.
Include version for optimistic locking. A claimed attempt additionally requires its owning runtime_id UUID and exact attempt, even when omitting version for an unload flush. Only VERSION_CONFLICT permits refreshing state and retrying; RUNTIME_SUPERSEDED and ATTEMPT_SUPERSEDED mean stop the stale writer. A runtime identifier never replaces authentication.
Request Body:
{
"cmi_data": {
"cmi.core.lesson_status": "completed",
"cmi.core.score.raw": "95"
},
"completion_status": "completed",
"success_status": "passed",
"score": {
"scaled": 0.95,
"raw": 95,
"min": 0,
"max": 100
},
"session_time": "PT1H",
"suspend_data": "..."
}Response:
{
"success": true,
"session_id": "770e8400-e29b-41d4-a716-446655440000",
"updated_at": "2025-01-01T01:00:00Z"
}Dispatches
Create Dispatch
POST /api/v1/dispatches
Create a dispatch for external SCORM package distribution.
Request Body:
{
"package_id": "pkg-123",
"dispatch_name": "Partner Training",
"registration_limit": 100,
"expires_in_hours": 720,
"allowed_domains": ["partner.com", "*.partner.com"]
}Response:
{
"dispatch": {
"id": "dispatch_456",
"tenantId": "tenant_789",
"tenant_id": "tenant_789",
"packageId": "pkg_123",
"package_id": "pkg_123",
"label": "Partner Training",
"destination": "partner-lms",
"registration_limit": 100,
"expires_at": "2025-02-01T00:00:00Z",
"status": "active",
"launchUrl": "https://app.allureconnect.com/player/dispatch/dispatch_456",
"launch_url": "https://app.allureconnect.com/player/dispatch/dispatch_456"
},
"package_id": "pkg_123",
"dispatch_token": "<legacy-token>",
"dispatch_url": "https://app.allureconnect.com/player/dispatch/dispatch_456",
"expires_at": "2025-02-01T00:00:00Z"
}List Dispatches
GET /api/v1/dispatches
List all dispatches for the authenticated tenant.
This endpoint currently returns the complete tenant-scoped list and does not accept pagination parameters.
Response:
{
"dispatches": [
{
"id": "dispatch_456",
"tenantId": "tenant_789",
"packageId": "pkg_123",
"label": "Partner Training",
"destination": "partner-lms",
"status": "active",
"launches30d": 25,
"seatsUsed": 25,
"seatsCap": 100,
"expiresAt": "2025-02-01T00:00:00Z",
"updatedAt": "2026-09-01T12:00:00.000Z"
}
]
}Update Dispatch Attribution
PATCH /api/v1/dispatches/{id}
OpenAPI operation: updateDispatchAttribution. Updates organization attribution only (organization_id or camelCase organizationId). This partner route does not change label, expiry, registration limits, allowed domains, or status.
Request body (one of):
{ "organization_id": "org_abc123" }{ "organizationId": "org_abc123" }Response: updated dispatch record including attribution fields.
Note: Partner dispatch routes support
GETand attributionPATCHonly — there is no partnerDELETE. Lifecycle actions (deactivate, zip export, activity) use the Connect dashboard //api/customer/dispatches/*surfaces.
Usage Pricing
Get Usage Pricing
GET /api/v1/usage/pricing
Return billing-aligned usage for the authenticated tenant. The endpoint supports tenant-wide summaries and per-user attribution for partners that need to reconcile learner usage back to their own workspace or customer records.
Query Parameters:
user_id(string, optional) - Scope the response to launches where the launchuser_idmatches this value.learner_id(string, optional) - Alias-style learner scope for integrations that store learner ids separately from user ids.workspace_external_id(string, optional) - Echoed in the response scope for partner reconciliation.
Tenant scope is resolved from the API key; callers do not need to pass tenant_id.
Example:
curl -H "X-API-Key: ac_live_..." \
"https://app.allureconnect.com/api/v1/usage/pricing?user_id=trainingos-user-123&workspace_external_id=workspace_123"Response:
{
"period_start": "2026-06-01T00:00:00.000Z",
"period_end": "2026-07-01T00:00:00.000Z",
"active_learners": 1,
"learner_count": 1,
"package_count": 2,
"launch_count": 4,
"dispatch_count": 3,
"currency": "usd",
"estimated_cost_cents": 0,
"included_learners": 250,
"overage_learners": 0,
"learner_overage_unit_cents": 0,
"scope": {
"tenant_id": "tenant-456",
"user_id": "trainingos-user-123",
"learner_id": null,
"workspace_external_id": "workspace_123"
},
"source": "allure_connect",
"updated_at": "2026-06-01T12:00:00.000Z"
}xAPI (Experience API)
Store xAPI Statements
POST /api/v1/xapi/statements
Store one or more xAPI statements.
Request Body (Single):
{
"actor": {
"name": "John Doe",
"mbox": "mailto:john@example.com"
},
"verb": {
"id": "http://adlnet.gov/expapi/verbs/completed",
"display": { "en-US": "completed" }
},
"object": {
"id": "https://app.allureconnect.com/packages/pkg-123",
"definition": {
"name": { "en-US": "Course Title" },
"type": "http://adlnet.gov/expapi/activities/course"
}
},
"result": {
"score": { "scaled": 0.95, "raw": 95, "min": 0, "max": 100 },
"success": true,
"completion": true,
"duration": "PT1H30M"
},
"timestamp": "2025-01-01T12:00:00Z"
}Request Body (Multiple):
[
{ /* statement 1 */ },
{ /* statement 2 */ }
]Response (single or multiple):
{
"success": true,
"statement_ids": ["550e8400-e29b-41d4-a716-446655440000"]
}An identical retry of an existing statement ID is idempotent. Reusing that ID with changed content returns 409 STATEMENT_CONFLICT; do not mutate the payload and retry under the same ID. Generic writes cannot create private native-form outcomes or overwrite one by reusing its ID.
Query xAPI Statements
GET /api/v1/xapi/statements
Query xAPI statements with filters.
Query Parameters:
statement_id(string) - Get a specific statementactor_mbox(email) - Filter by actor mailboxverb_id(URL) - Filter by verb IRIactivity_id(URL) - Filter by activity IRIsession_id(string) - Filter by Connect sessionpackage_id(string) - Filter by Connect packagepage(integer, default: 1)limit(integer, default: 25) - A page holds at most 100 statements; values from 101 to 500 are accepted and return 100cursor(string) - Continuation from a previous response'spagination.nextCursor. Takes precedence overpage.
Example:
curl -H "X-API-Key: ac_live_..." \
"https://app.allureconnect.com/api/v1/xapi/statements?verb_id=http://adlnet.gov/expapi/verbs/completed"Response:
{
"statements": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"actor": { /* ... */ },
"verb": { /* ... */ },
"object": { /* ... */ },
"result": { /* ... */ },
"timestamp": "2025-01-01T12:00:00Z",
"stored": "2025-01-01T12:00:00.123Z"
}
],
"pagination": {
"page": 1,
"limit": 25,
"total": 250,
"totalPages": 10,
"truncated": false,
"nextCursor": "eyJ2IjoxLCJwIjoyLCJsIjoyNX0"
}
}Reading every statement. Follow pagination.nextCursor: pass it back as cursor, with the same filters, until it is null. Each request is one bounded read, so the walk works for a workspace of any size and returns each statement once, newest first. A page can hold fewer statements than limit (even none) while nextCursor is still set; only null means the end.
The continuation is sealed. It cannot be read or altered, and it is valid only for the workspace and the filters of the request that returned it. 400 INVALID_CURSOR means it is malformed or altered, was issued to another workspace or for other filters, or was issued while the workspace still fit in one read and it no longer does. In every case, start the walk again without it. Requests that carry a cursor are rate limited per API key (300 per minute by default; 429 RATE_LIMIT_EXCEEDED with Retry-After). Requests without one are not limited.
page, total and totalPages keep their meaning whenever the workspace — or the slice a session_id, activity_id, package_id or statement_id filter selects — fits in one read (10,000 raw rows / 8 MiB). Beyond that, pagination.truncated is true, total and totalPages describe only the newest window, and nextCursor is the way to the rest. actor_mbox and verb_id have no index; on a large workspace they examine a bounded number of statements per request and return a continuation.
Order in a large workspace. A workspace that fits in one read is ordered by stored time, newest first, as always. A larger one is ordered by when each statement was first stored (its creation), newest first. The two differ only for a statement that is stored again later: for example a SCORM completed or passed statement re-stored during a long session. In a small workspace it moves to the top; in a large one it keeps its original place. The standard LRS read (GET /api/lrs/xapi/statements) always answers in stored order, but works on the 10,000 most recently created statements, so in a larger workspace a since query can miss an older statement that was stored again recently.
Generic statement list/detail and actor/activity analytics exclude private native-form outcomes. Every generic read is bounded to 10,000 raw tenant rows and 8 MiB per backend query, before private rows are filtered; larger workspaces are read page by page as described above and below.
Native LMS outcomes use a separate service integration, POST /api/v1/lms/form-outcomes, requiring a stored tenant internal_product key; ordinary public API keys and Clerk browser sessions cannot use it. The LMS checks /api/v1/lms/form-outcomes/authority before delivery. This adapter accepts completion/optional quiz score metadata, never answers or demographics, and does not establish learner mastery. Its customer-facing form reports suppress groups below the required privacy thresholds and do not appear in generic xAPI reads.
Get xAPI Statement
GET /api/v1/xapi/statements/{id}
Retrieve a specific xAPI statement.
Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"actor": { /* ... */ },
"verb": { /* ... */ },
"object": { /* ... */ },
"result": { /* ... */ },
"timestamp": "2025-01-01T12:00:00Z"
}Actor Performance Analytics
GET /api/v1/xapi/analytics/actors
Get aggregated performance metrics by learner.
Query Parameters:
actor_mbox(email) - Filter by actor emailmin_activities(integer) - Minimum unique activitiessort(string) - Sort field (total_statements, avg_score_scaled, etc.)order(asc/desc, default: desc)page(integer, default: 1)limit(integer, default: 20, max: 100)allow_partial(true) - Accept totals that do not cover the whole workspace (see Coverage)cursor(string) - Thecontinuationof a previous partial response; requiresallow_partial=true
Response:
{
"actors": [
{
"actor_mbox": "john@example.com",
"actor_name": "John Doe",
"total_statements": 45,
"unique_activities": 12,
"completed_count": 10,
"passed_count": 9,
"failed_count": 1,
"avg_score_scaled": 0.87,
"scored_count": 9,
"first_activity": "2025-01-01T00:00:00Z",
"last_activity": "2025-01-15T14:30:00Z"
}
],
"pagination": { /* ... */ },
"incomplete": false,
"continuation": null,
"scannedStatements": 45
}Coverage. Analytics are computed by reading the workspace's statements in bounded pages. A 200 without allow_partial is always complete, for a workspace of any size: incomplete is false and continuation is null.
The cost grows with the number of statements, so one request stops after 250,000 statements or about 20 seconds. What happens then depends on the caller:
- Without
allow_partial=truethe request fails:500with"code": "XAPI_ANALYTICS_INCOMPLETE". A partial answer is never returned to a client that did not ask for one. Treat it as "analytics unavailable", not as zero activity. - With
allow_partial=truethe request returns200withincomplete: true,scannedStatements, acoveragesentence saying what the totals cover, and acontinuation. Passing that back ascursor(again withallow_partial=true) returns the totals of the next, older segment.
Partial tables cannot be merged into exact totals. Each segment is its own table: avg_score_scaled, unique_activities / unique_learners, the min_activities / min_learners filters, sort and pagination are all computed per segment. Only the plain counts (total_statements, completed_count, passed_count, failed_count) add up across segments for the same actor or activity. scored_count is the number of statements behind avg_score_scaled, so a client can at least re-weight averages across segments.
Large workspaces. When a workspace needs more than one bounded read (roughly more than 4,900 statements), every request — each page, each sort order — is a full pass over its statements. Such passes are rate limited per API key (6 per minute by default; 429 RATE_LIMIT_EXCEEDED with Retry-After), and only the first 1,000 rows of a table (page × limit) can be requested (400 ANALYTICS_PAGE_WINDOW_EXCEEDED). Ask for the rows you need in as few requests as possible (limit=100). Workspaces that fit in one read are not affected by either rule. The same fields and rules apply to the activity analytics below.
Activity Performance Analytics
GET /api/v1/xapi/analytics/activities
Get aggregated performance metrics by activity.
Query Parameters:
activity_id(URL) - Filter by activity IRIactivity_type(URL) - Filter by activity typepackage_id(UUID) - Filter by SCORM packagemin_learners(integer) - Minimum unique learnerssort(string) - Sort fieldorder(asc/desc, default: desc)page(integer, default: 1)limit(integer, default: 20, max: 100)allow_partial(true) - Accept totals that do not cover the whole workspace (see Coverage)cursor(string) - Thecontinuationof a previous partial response; requiresallow_partial=true
Response:
{
"activities": [
{
"activity_id": "https://app.allureconnect.com/packages/pkg-123",
"activity_type": "http://adlnet.gov/expapi/activities/course",
"package_id": "pkg-123",
"total_statements": 250,
"unique_learners": 50,
"completed_count": 45,
"passed_count": 42,
"failed_count": 3,
"avg_score_scaled": 0.82,
"first_activity": "2025-01-01T00:00:00Z",
"last_activity": "2025-01-15T16:00:00Z"
}
],
"pagination": { /* ... */ }
}Error Handling
All errors follow a consistent format:
{
"error": "Human-readable error message",
"code": "ERROR_CODE",
"details": {}
}Common Error Codes
| Status | Code | Description |
|---|---|---|
| 400 | BAD_REQUEST | Invalid request format |
| 400 | VALIDATION_ERROR | Failed validation |
| 401 | UNAUTHORIZED | Missing/invalid API key |
| 403 | FORBIDDEN | Insufficient permissions |
| 403 | QUOTA_EXCEEDED | Quota limit exceeded |
| 404 | NOT_FOUND | Resource not found |
| 404 | PACKAGENOTFOUND | SCORM package not found |
| 404 | SESSIONNOTFOUND | Session not found |
| 409 | CONFLICT | State conflict |
| 429 | RATELIMITEXCEEDED | Too many requests |
| 503 | BACKEND_UNAVAILABLE | Temporary backend failure; retry after Retry-After |
| 500 | INTERNAL_ERROR | Server error |
| 500 | DATABASE_ERROR | Database operation failed |
Error Response Examples
Validation Error:
{
"error": "Validation failed",
"code": "VALIDATION_ERROR",
"details": {
"fieldErrors": {
"email": ["Invalid email format"],
"age": ["Must be greater than 0"]
}
}
}Quota Exceeded:
{
"error": "Package limit exceeded",
"code": "QUOTA_EXCEEDED",
"details": {
"limit": 100,
"current": 100
}
}Retryable backend failure:
HTTP/1.1 503 Service Unavailable
Retry-After: 5
Content-Type: application/json
{
"error": "The learning record backend is temporarily unavailable. Retry the request.",
"code": "BACKEND_UNAVAILABLE",
"retryable": true
}Best Practices
1. Always Check Response Status
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
console.error(`API Error [${error.code}]: ${error.error}`);
throw new Error(error.error);
}
const data = await response.json();2. Implement Retry Logic
async function fetchWithRetry(url, options, maxRetries = 3) {
const method = (options.method || 'GET').toUpperCase();
const headers = new Headers(options.headers);
const mayReplay =
['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS'].includes(method) ||
headers.has('Idempotency-Key');
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
// Don't retry on client errors (4xx)
if (response.status >= 400 && response.status < 500) {
return response;
}
if (response.ok) {
return response;
}
const failure = await response.clone().json().catch(() => null);
if (!mayReplay || failure?.retryable !== true || i === maxRetries - 1) {
return response;
}
const retryAfter = response.headers.get('Retry-After');
const retryAfterSeconds = retryAfter === null ? Number.NaN : Number(retryAfter);
const retryDelay =
Number.isFinite(retryAfterSeconds) && retryAfterSeconds >= 0
? retryAfterSeconds * 1000
: 1000 * Math.pow(2, i);
await new Promise(resolve => setTimeout(resolve, retryDelay));
} catch (error) {
// A network failure has no retryable response body. Replay only requests
// whose HTTP semantics or idempotency key make retrying safe.
if (!mayReplay || i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
}
throw new Error('Max retries exceeded');
}Do not automatically replay POST or PATCH requests unless the endpoint supports an Idempotency-Key. For HTTP failures, retry only when the response explicitly returns retryable: true.
3. Handle Rate Limiting
async function fetchWithRateLimit(url, options) {
const response = await fetch(url, options);
if (response.status === 429) {
const resetTime = response.headers.get('X-RateLimit-Reset');
const waitTime = (parseInt(resetTime!) - Date.now() / 1000) * 1000;
await new Promise(resolve => setTimeout(resolve, waitTime));
return fetchWithRateLimit(url, options); // Retry
}
return response;
}4. Use Pagination
async function fetchAllPackages(apiKey) {
const allPackages = [];
const limit = 100;
let offset = 0;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`https://app.allureconnect.com/api/v1/packages?limit=${limit}&offset=${offset}`,
{ headers: { 'X-API-Key': apiKey } }
);
const { packages, pagination } = await response.json();
allPackages.push(...packages);
hasMore = pagination?.hasMore ?? packages.length === limit;
if (hasMore && packages.length === 0) {
throw new Error('Package pagination did not advance');
}
offset += packages.length;
}
return allPackages;
}5. Monitor API Usage
Track your API usage to avoid hitting limits:
const response = await fetch(url, options);
const rateLimitRemaining = response.headers.get('X-RateLimit-Remaining');
const rateLimitReset = response.headers.get('X-RateLimit-Reset');
console.log(`Remaining requests: ${rateLimitRemaining}`);
console.log(`Reset time: ${new Date(parseInt(rateLimitReset!) * 1000)}`);SDKs & Tools
Official SDKs
Coming Soon:
- Node.js SDK
- Python SDK
- PHP SDK
- .NET SDK
OpenAPI specification
- JSON (served by the app):
GET /api/docs/openapi— e.g.https://app.allureconnect.com/api/docs/openapi - Interactive docs:
/api/docs(Scalar UI)
Use with Postman, Insomnia, OpenAPI Generator, etc.
There is no separate /api/docs/openapi.yaml route; import the JSON above if your tool requires a file download.
Additional Resources
Documentation
- Package upload options — Partner/backend mint → PUT → process flow
- API versioning strategy — Versioning notes (see also
lib/openapi.tsinfo.version) - API key security
- SCORM API reference
- Changelog — Version history and upgrade guides
Specifications
- SCORM 1.2 Specification (opens in a new tab)
- SCORM 2004 4th Edition (opens in a new tab)
- xAPI 1.0.3 Specification (opens in a new tab)
Support
- Documentation: <https://www.allureconnect.com/docs>
- Interactive API reference: <https://www.allureconnect.com/api/docs>
- Email: <info@allureconnect.com>
- Status: <https://www.allureconnect.com/status>
API path version: v1 (/api/v1/...) OpenAPI document: served at /api/docs/openapi
Optional hosted foreground activity
Learner launch producers may add learner_activity: "foreground_v1" alongside runtime_protocol: "exclusive_v1" for SCORM. Omission leaves legacy and author-preview behavior unchanged. Resume retains the opt-in. The hosted player observes trusted pointer, touch and keyboard input in visible accessible documents, including same-origin nested course frames. It does not infer input from CMI commits, load/focus, timers or polling. Inaccessible external frames and completed review runtimes are not observed. DOM isTrusted is input provenance, not hardware attestation.
The hosted producer sends POST /api/v1/sessions/{sessionId}/activity with a session launch Bearer token and { "runtime_id": "<current-owner-uuid>", "attempt": 1, "sequence": 1 }. Sequence increases per owner. The server checks tenant/session/recording owner/attempt and learner opt-in; it ignores old sequences and throttles accepted captures to one per minute. It accepts no learner identity or timestamp in the request. There is no offline replay/keepalive retry.
A successful capture snapshots server time and the exact session/package/learner/attempt into the existing signed webhook queue as session.foreground_activity (sessions/all subscriptions). Its data contains provenance: "foreground_v1", session_id, package_id, learner_id, attempt_number and millisecond captured_at. Delivery retry or a later attempt does not alter this snapshot. Receivers must verify the signature and their existing session binding, apply capture time monotonically, and never count delivery time or revive data whose retention deadline already elapsed. This event grants no completion, score or mastery.
Deploy the compatible Connect backend and LMS receiver before enabling the launch flag. This additive activity change requires its own signed transport and hosted-input acceptance; prior runtime progress/ownership evidence does not certify it.