Allure Connect API (overview)
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:3000
API 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/packages
Validate 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.
Obtaining API Keys
Create and manage keys in the Connect dashboard (Dashboard → Integrations → 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: 1640995200
Note: 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": "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.
Update Session
PUT /api/v1/sessions/{id}
Update session CMI data (learner progress).
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"]
}
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, max: 500)
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
}
}
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)
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,
"first_activity": "2025-01-01T00:00:00Z",
"last_activity": "2025-01-15T14:30:00Z"
}
],
"pagination": { /* ... */ }
}
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)
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 | PACKAGE_NOT_FOUND | SCORM package not found |
| 404 | SESSION_NOT_FOUND | Session not found |
| 409 | CONFLICT | State conflict |
| 429 | RATE_LIMIT_EXCEEDED | 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
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