Complete reference for all webhook events sent by the SCORM API.
Table of Contents
Event Overview
All webhook events follow a consistent structure:
{
"event": "event.type.name",
"tenant_id": "tnt_your_tenant",
"data": {
"audit_event_id": "evt_abc123",
"event_plane": null,
"entity_type": "session",
"entity_id": "session_abc123",
"severity": "info"
},
"timestamp": "2025-01-15T10:30:00.000Z",
"delivery_id": "whd_evt_abc123_endpoint1"
}Common Fields
- event (string): Event type identifier
- tenant_id (string): Tenant that triggered the event
- data (object): Event-specific payload
- timestamp (ISO 8601): When the event occurred
- delivery_id (string): Unique delivery identifier for tracking
Every data object carries these baseline fields, drawn from the underlying audit event:
- audit_event_id (string): The source audit event
- event_plane (string | null): Event plane, when set
- entity_type (string | null): Related entity type, when set
- entity_id (string | null): Related entity id, when set
- severity (string):
infoorwarn
Session events additionally include the session detail fields shown below. For session events, data.updated_at is the session detail timestamp. The top-level timestamp is the source webhook/audit event timestamp.
There is no
session.progressevent and no per-commit event. In-progress commits are not pushed by design — pollGET /api/v1/sessions/{sessionId}for live progress on an active session.
Event Types
An endpoint subscribes to a scope (packages, sessions, usage, dispatches, competency, or all) and receives delivered events in that scope.
Package Events (scope: packages)
package.launched— a package is launched for a learnerpackage.archived— a package is archivedpackage.version_activated— a new package version becomes active
Payload:
{
"event": "package.launched",
"tenant_id": "tnt_your_tenant",
"data": {
"audit_event_id": "evt_abc123",
"event_plane": null,
"entity_type": "package",
"entity_id": "pkg_abc123",
"severity": "info"
},
"timestamp": "2026-07-06T10:30:00.000Z",
"delivery_id": "whd_evt_abc123_endpoint1"
}Session Events (scope: sessions)
session.completed— a session reachescompleted/passedsession.failed— a session ends in a failed statesession.suspended— a session is suspended (learner exits mid-course)session.reset— a retake or reset started a new attempt (see below)
Session events carry the baseline fields plus session detail:
Payload:
{
"event": "session.completed",
"tenant_id": "tnt_your_tenant",
"data": {
"audit_event_id": "evt_abc123",
"event_plane": null,
"entity_type": "session",
"entity_id": "session_abc123",
"severity": "info",
"session_id": "session_abc123",
"package_id": "pkg_abc123",
"learner_id": "user_xyz789",
"completion_status": "completed",
"success_status": "passed",
"progress_percent": 100,
"score": {
"scaled": 0.85,
"raw": 85,
"min": 0,
"max": 100
},
"session_time": "PT1H0M0S",
"time_spent_seconds": 3600,
"updated_at": "2026-07-06T10:29:45.000Z"
},
"timestamp": "2026-07-06T10:30:00.000Z",
"delivery_id": "whd_evt_abc123_endpoint1"
}Use Cases:
- Issue completion certificates and update grade books
- Trigger follow-up actions on failed or suspended sessions
- Send completion notifications and award badges
session.reset
A retake or reset started a new attempt on the session. The session fields describe the new, current attempt. The reset block records why, and the previous attempt's results, which stay in Connect's history.
These actions trigger it:
POST /sessions/{id}/reset(api_reset);- a dashboard Reset progress (
dashboard_reset); - the player's Start over (
learner_start_over); - a launch with
new_attempt(launch_new_attempt); - a course set to start a new attempt on relaunch (
relaunch_policy).
{
"event": "session.reset",
"data": {
"entity_type": "session",
"entity_id": "session_abc123",
"session_id": "session_abc123",
"completion_status": "not_attempted",
"attempt_number": 2,
"reset": {
"trigger": "api_reset",
"note": "LMS admin reset course progress",
"previous_attempt_number": 1,
"attempt_number": 2,
"previous_attempt": {
"completion_status": "passed",
"success_status": "passed",
"score_scaled": 0.92,
"score_raw": 92,
"time_spent_seconds": 1480,
"progress_percent": 100
}
}
}
}Usage Events (scope: usage)
usage.threshold_reached.70— usage crosses 70% of the active-learner allotmentusage.threshold_reached.90— usage crosses 90%usage.threshold_reached.100— usage reaches 100%
Each threshold fires once per billing period, on the launch that moves the distinct active-learner count from below the threshold to at-or-above it.
Payload:
{
"event": "usage.threshold_reached.90",
"tenant_id": "tnt_your_tenant",
"data": {
"audit_event_id": "evt_abc123",
"event_plane": null,
"entity_type": null,
"entity_id": null,
"severity": "info"
},
"timestamp": "2026-07-06T10:30:00.000Z",
"delivery_id": "whd_evt_abc123_endpoint1"
}Dispatch Events (scope: dispatches)
dispatch.created— a dispatch is createddispatch.paused— launches are pauseddispatch.resumed— launches resumedispatch.archived— a dispatch is archiveddispatch.restored— an archived dispatch is restoreddispatch.extended— its expiry is extendeddispatch.expired— its configured expiry passesdispatch.deleted— a dispatch is deleted
dispatch.updated and high-volume dispatch.launched events are intentionally not delivered. The normal audit-event fan-out runs approximately every five minutes, so these notifications should not be described as instantaneous.
Every webhook request includes X-Webhook-ID, X-Delivery-ID, and X-Connect-Event-Type, plus the documented signature headers when a signing secret is configured.
Delivery Guarantees
At-Least-Once Delivery
The SCORM API guarantees at-least-once delivery:
- Events are delivered at least once
- Events may be delivered multiple times (duplicates)
- Failed deliveries are retried automatically
Retry Schedule
Failed deliveries are retried with increasing backoff between attempts:
| Attempt | Backoff before this attempt |
|---|---|
| 1 | Immediate |
| 2 | 60s |
| 3 | 5m |
| 4 | 15m |
| 5 | 60m |
A delivery gets up to 5 attempts. After the 5th failed attempt it is moved to the dead-letter state. Terminal responses (404/410) dead-letter immediately without further retries, since re-sending won't reach a missing endpoint.
Delivery Status
- queued: Waiting to be delivered (or waiting for its next retry)
- delivered: Successfully delivered (2xx response)
- failed: Last attempt failed; will be retried if attempts remain
- dead_letter: Max attempts exhausted, or a terminal/undeliverable response
Retry Logic
Automatic Retries
Connect automatically retries failed deliveries:
- Increasing backoff between attempts (60s → 5m → 15m → 60m)
- Up to 5 attempts, then dead-lettered
- 30-second timeout per attempt
Manual Retry
You can manually re-queue a failed or dead-lettered delivery from Build → Webhooks. The retry action is a Clerk-authenticated dashboard operation; /api/customer/* routes do not accept partner API keys.
Idempotency
Handling Duplicate Events
Since webhooks use at-least-once delivery, you may receive duplicate events. Implement idempotency:
const processedDeliveries = new Set<string>();
export async function POST(request: Request) {
const event = await request.json();
const deliveryId = event.delivery_id;
// Check if already processed
if (processedDeliveries.has(deliveryId)) {
return new Response('Already processed', { status: 200 });
}
// Process event
await handleEvent(event);
// Mark as processed
processedDeliveries.add(deliveryId);
return new Response('OK', { status: 200 });
}Database-Based Idempotency
For production, use database to track processed deliveries:
async function isDeliveryProcessed(deliveryId: string): Promise<boolean> {
const result = await db.webhookDeliveries.findUnique({
where: { delivery_id: deliveryId },
});
return !!result;
}
async function markDeliveryProcessed(deliveryId: string) {
await db.webhookDeliveries.create({
data: { delivery_id: deliveryId, processed_at: new Date() },
});
}Best Practices
- Process Asynchronously: Return 200 immediately, process in background
- Implement Idempotency: Handle duplicate deliveries gracefully
- Log All Events: Keep audit trail of all webhook events
- Handle Errors: Don't let webhook processing crash your application
- Monitor Delivery: Track delivery success rates
- Poll for Live Progress: There is no per-commit progress event — poll
GET /api/v1/sessions/{sessionId}when you need up-to-the-moment progress
Related Documentation
- Webhook Setup - How to set up webhooks
- Webhook Security - Security best practices
- API Reference - Complete API documentation