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 runbook (presigned R2, CORS, process): Package upload API — partner integration
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 ac_live_...
X-API-Key: ac_live_...
Example
curl -H "Authorization: Bearer ac_live_..." \
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 ac_live_..." \
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 are scoped (e.g. read vs write vs admin). See the OpenAPI spec and dashboard.
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:
page(integer, default: 1) - Page numberlimit(integer, default: 20, max: 100) - Results per page
Example:
curl -H "Authorization: Bearer ac_live_..." \
"https://app.allureconnect.com/api/v1/packages"
Response:
{
"packages": [
{
"id": "pkg-123",
"tenant_id": "tenant-456",
"title": "Workplace Safety Essentials",
"version": "1.2",
"launch_url": "https://app.allureconnect.com/content/...",
"manifest_url": "https://app.allureconnect.com/content/.../imsmanifest.xml",
"file_size_bytes": 5242880,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
],
"pagination": {
"total": 42,
"limit": 20,
"offset": 0,
"hasMore": true
}
}
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 Package upload API — partner integration and R2 browser upload CORS.
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",
"tenant_id": "tenant-456",
"title": "Course Title",
"version": "1.2",
"launch_url": "https://app.allureconnect.com/content/...",
"manifest_url": "https://app.allureconnect.com/content/.../imsmanifest.xml",
"file_size_bytes": 5242880,
"metadata": {
"description": "Course description",
"sco_count": 5
},
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
},
"versions": [
{
"id": "ver_1",
"version_label": "1.2",
"status": "active"
}
]
}
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(UUID) - Filter by packageuser_id(UUID) - Filter by usercompletion_status(string) - Filter by completion (not_attempted, incomplete, completed)success_status(string) - Filter by success (unknown, passed, failed)page(integer, default: 1)limit(integer, default: 20, max: 100)
Example:
curl -H "X-API-Key: ac_live_..." \
"https://app.allureconnect.com/api/v1/sessions?package_id=pkg-123&completion_status=completed"
Response:
{
"sessions": [
{
"id": "session-789",
"package_id": "pkg-123",
"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-01T00:00:00Z",
"updated_at": "2025-01-01T01:00:00Z",
"package": {
"title": "Course Title",
"version": "1.2"
}
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"total_pages": 8
}
}
Get Session
GET /api/v1/sessions/{id}
Get detailed session information including full CMI data.
Response:
{
"session": {
"id": "session-789",
"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",
"cmi.suspend_data": "..."
},
"package": {
"title": "Course Title"
}
}
}
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": "session-789",
"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",
"tenant_id": "tenant-789",
"package_id": "pkg-123",
"dispatch_name": "Partner Training",
"registration_limit": 100,
"registration_count": 0,
"expires_at": "2025-02-01T00:00:00Z",
"status": "active",
"token": "eyJhbGciOiJIUzI1NiIs...",
"launch_url": "https://app.allureconnect.com/player/dispatch/eyJhbGciOiJIUzI1NiIs...",
"created_at": "2025-01-01T00:00:00Z"
}
}
List Dispatches
GET /api/v1/dispatches
List all dispatches for the authenticated tenant.
Query Parameters:
page(integer, default: 1)limit(integer, default: 20, max: 100)
Response:
{
"dispatches": [
{
"id": "dispatch-456",
"package_id": "pkg-123",
"dispatch_name": "Partner Training",
"registration_limit": 100,
"registration_count": 25,
"expires_at": "2025-02-01T00:00:00Z",
"status": "active",
"created_at": "2025-01-01T00:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 5,
"total_pages": 1
}
}
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.
Generate Dispatch ZIP
POST /api/v1/dispatches/{id}/generate
Trigger background generation of the offline dispatch ZIP file.
Response:
{
"success": true,
"message": "Dispatch generation queued.",
"dispatch_id": "dispatch-456"
}
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):
"550e8400-e29b-41d4-a716-446655440000"
Response (Multiple):
[
"550e8400-e29b-41d4-a716-446655440000",
"660f9511-f3ac-52e5-b827-557766551111"
]
Query xAPI Statements
GET /api/v1/xapi/statements
Query xAPI statements with filters.
Query Parameters:
statementId(UUID) - Get specific statementverb(URL) - Filter by verb IRIactivity(URL) - Filter by activity IRIregistration(UUID) - Filter by registrationsince(ISO 8601) - Statements stored sinceuntil(ISO 8601) - Statements stored untilpage(integer, default: 1)limit(integer, default: 20, max: 100)
Example:
curl -H "X-API-Key: ac_live_..." \
"https://app.allureconnect.com/api/v1/xapi/statements?verb=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": 20,
"total": 250,
"total_pages": 13
}
}
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": {
"code": "ERROR_CODE",
"message": "Human-readable error message",
"details": {}
},
"request_id": "correlation-id"
}
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 |
| 500 | INTERNAL_ERROR | Server error |
| 500 | DATABASE_ERROR | Database operation failed |
Error Response Examples
Validation Error:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"fieldErrors": {
"email": ["Invalid email format"],
"age": ["Must be greater than 0"]
}
}
}
}
Quota Exceeded:
{
"error": {
"code": "QUOTA_EXCEEDED",
"message": "Package limit exceeded",
"details": {
"limit": 100,
"current": 100
}
}
}
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.error.code}]: ${error.error.message}`);
throw new Error(error.error.message);
}
const data = await response.json();
2. Implement Retry Logic
async function fetchWithRetry(url, options, maxRetries = 3) {
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;
}
// Retry on server errors (5xx)
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
} catch (error) {
if (i === maxRetries - 1) throw error;
}
}
}
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 = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`https://app.allureconnect.com/api/v1/packages?page=${page}&limit=100`,
{ headers: { 'X-API-Key': apiKey } }
);
const { packages, pagination } = await response.json();
allPackages.push(...packages);
hasMore = pagination?.hasMore ?? packages.length === 100;
page++;
}
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 integration (presigned R2) — Partner/backend flow, env vars, Convex internal token, rate limits
- 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://docs.allurelms.com/api
- Email: engineering@allurelms.com
- Issues: https://github.com/AllureLMS/scorm-api/issues
- Status Page: https://status.allurelms.com
Community
- Discord: https://discord.gg/allurelms
- Twitter: @AllureLMS
- Blog: https://blog.allurelms.com
Last Updated: 2026-02-11
API path version: v1 (/api/v1/...)
OpenAPI document: info.version in lib/openapi.ts (served at /api/docs/openapi)