Webhook Setup Guide
Learn how to configure webhooks to receive real-time notifications from the SCORM API.
Table of Contents
- Overview
- What are Webhooks?
- Setting Up Webhooks
- Webhook Events
- Security
- Testing Webhooks
- Troubleshooting
Overview
Webhooks allow the SCORM API to send real-time notifications to your application when events occur, such as:
- Package launched, archived, or a new version activated
- Session completed, failed, or suspended
- Usage threshold reached (70%, 90%, 100%)
In-progress commits do not emit webhooks by design. Live progress is not pushed on every CMI commit — poll
GET /api/v1/sessions/{sessionId}when you need up-to-the-moment progress for an active session.
What are Webhooks?
Webhooks are HTTP callbacks that the SCORM API sends to your application when specific events occur. Instead of polling the API for updates, webhooks push notifications to your server.
Benefits
- Real-time Updates: Get notified immediately when events occur
- Reduced API Calls: No need to poll for status updates
- Better Performance: Lower latency than polling
- Event-Driven Architecture: Build reactive systems
Setting Up Webhooks
Step 1: Create a Webhook Endpoint
Create an HTTP endpoint in your application that can receive POST requests:
// Example: Next.js API route
// app/api/webhooks/scorm/route.ts
export async function POST(request: Request) {
const body = await request.json();
// Process webhook event
console.log('Webhook received:', body);
return new Response('OK', { status: 200 });
}
Step 2: Register Webhook with SCORM API
Via Dashboard
- Sign in to your SCORM API dashboard
- Navigate to Dashboard → Webhooks
- Click "Create Webhook"
- Enter your webhook URL
- Select an event scope (
packages,sessions,usage, orall) - Add a signing secret for signature verification
- Click "Create"
Via API
The tenant is resolved from your API key, so no tenant_id is needed. Subscribe
with eventScope (packages, sessions, usage, or all) and provide a
signing secret (secret or its alias signingSecret, 16–512 characters).
curl -X POST https://app.allureconnect.com/api/v1/webhooks \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/api/webhooks/scorm",
"secret": "replace-with-at-least-16-random-characters",
"label": "Production LMS",
"eventScope": "sessions"
}'
Response:
{
"webhook": {
"id": "whk_abc123",
"tenantId": "tnt_your_tenant",
"label": "Production LMS",
"url": "https://your-app.com/api/webhooks/scorm",
"status": "active",
"eventScope": "sessions",
"signingSecretConfigured": true
}
}
Step 3: Verify Webhook Endpoint
Your endpoint should:
- Accept POST requests
- Return 200 status code for successful processing
- Respond within 30 seconds
- Handle errors gracefully
Webhook Events
Available Event Types
Each endpoint subscribes to a scope (packages, sessions, usage, or
all) and receives every delivered event within that scope:
| Event Type | Scope | When Triggered |
|---|---|---|
package.launched |
packages |
A package is launched for a learner |
package.archived |
packages |
A package is archived |
package.version_activated |
packages |
A new package version becomes active |
session.completed |
sessions |
A learner completes a course |
session.failed |
sessions |
A session ends in a failed state |
session.suspended |
sessions |
A session is suspended (learner exits mid-course) |
usage.threshold_reached.70 |
usage |
Tenant usage crosses 70% of quota |
usage.threshold_reached.90 |
usage |
Tenant usage crosses 90% of quota |
usage.threshold_reached.100 |
usage |
Tenant usage reaches 100% of quota |
In-progress commits do not emit webhooks. There is no
session.progressevent — pollGET /api/v1/sessions/{sessionId}for live progress on an active session.
Event Payload Structure
Every delivery uses the same envelope. event is one of the event types above,
data carries the event-specific fields, and delivery_id is unique per
delivery attempt. Every data object also includes baseline audit fields
(audit_event_id, event_plane, entity_type, entity_id, severity); see
Webhook Events for the canonical per-event field reference:
{
"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"
}
For session events, data.updated_at is the session detail timestamp. The
top-level timestamp is the source webhook/audit event timestamp.
Package Launched
Triggered when a package is launched for a learner. The data payload carries
only the baseline audit fields — per-package or per-session detail (learner,
score, progress) is delivered on the session.* events below, not here.
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 Completed
Triggered when a learner completes a course. Session events carry the baseline
audit fields plus the session detail. The learner is identified by
data.learner_id:
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"
}
Usage Threshold Reached
Triggered when tenant usage crosses a quota threshold (70%, 90%, or 100%).
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"
}
Security
Signature Verification
Always verify webhook signatures to ensure requests come from the SCORM API.
How It Works
- Connect creates an HMAC-SHA256 signature of the raw request body using your endpoint's signing secret
- The signature is sent in
X-Connect-Signature: sha256=<hex>(the primary header) - For legacy compatibility, the same hex digest is also sent in
X-Allure-SignaturewithX-Allure-Signature-Algorithm: sha256 - Your endpoint recomputes the HMAC over the raw body and compares it in constant time
Prefer X-Connect-Signature; strip the sha256= prefix before comparing. Fall
back to X-Allure-Signature only if the primary header is absent.
Implementation
import crypto from 'crypto';
export async function POST(request: Request) {
// Prefer X-Connect-Signature (format: "sha256=<hex>"); fall back to legacy.
const connectSignature = request.headers.get('X-Connect-Signature');
const provided = connectSignature
? connectSignature.replace(/^sha256=/, '')
: request.headers.get('X-Allure-Signature');
const body = await request.text(); // raw body — do not parse first
// Get your endpoint's signing secret (from environment or database)
const secret = process.env.WEBHOOK_SECRET!;
// Compute expected signature over the raw body
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
// Verify signature in constant time. Check lengths first:
// timingSafeEqual throws when buffer lengths differ, which would turn a
// malformed signature into a 500 instead of a clean 401.
const providedBuffer = provided ? Buffer.from(provided) : null;
const expectedBuffer = Buffer.from(expectedSignature);
if (
!providedBuffer ||
providedBuffer.length !== expectedBuffer.length ||
!crypto.timingSafeEqual(providedBuffer, expectedBuffer)
) {
return new Response('Invalid signature', { status: 401 });
}
// Process webhook
const event = JSON.parse(body);
await handleWebhookEvent(event);
return new Response('OK', { status: 200 });
}
Python Example
import hmac
import hashlib
import json
def verify_webhook_signature(signature, body, secret):
expected_signature = hmac.new(
secret.encode('utf-8'),
body.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
@app.route('/webhooks/scorm', methods=['POST'])
def webhook_handler():
# Prefer X-Connect-Signature ("sha256=<hex>"); fall back to legacy header.
connect_signature = request.headers.get('X-Connect-Signature')
if connect_signature:
signature = connect_signature.removeprefix('sha256=')
else:
signature = request.headers.get('X-Allure-Signature', '')
body = request.get_data(as_text=True) # raw body — do not parse first
secret = os.getenv('WEBHOOK_SECRET')
if not verify_webhook_signature(signature, body, secret):
return 'Invalid signature', 401
event = json.loads(body)
# Process event
return 'OK', 200
Best Practices
- Always Verify Signatures: Never process webhooks without signature verification
- Use HTTPS: Always use HTTPS for webhook endpoints
- Store Secrets Securely: Never commit webhook secrets to version control
- Idempotency: Handle duplicate webhook deliveries gracefully
- Rate Limiting: Implement rate limiting on your webhook endpoint
Testing Webhooks
Using ngrok (Local Development)
- Install ngrok:
npm install -g ngrok - Start your local server:
npm run dev - Expose with ngrok:
ngrok http 3000 - Use ngrok URL as webhook URL:
https://abc123.ngrok.io/api/webhooks/scorm
Manual Testing
# Test webhook endpoint directly with the same raw body you sign.
WEBHOOK_URL="https://your-app.com/api/webhooks/scorm"
WEBHOOK_SECRET="replace-with-your-webhook-secret"
BODY=$(cat <<'JSON'
{
"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"
}
JSON
)
SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | sed 's/^.* //')
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-H "X-Connect-Signature: sha256=$SIGNATURE" \
--data-raw "$BODY"
Viewing Delivery History
Check webhook delivery status in the dashboard:
curl -X GET "https://app.allureconnect.com/api/customer/webhooks/webhook_abc123/deliveries" \
-H "X-API-Key: your-api-key"
Troubleshooting
Webhook Not Receiving Events
Check:
- Webhook is active in dashboard
- Webhook URL is accessible (not behind firewall)
- Webhook endpoint returns 200 status
- Event type matches subscribed events
Signature Verification Fails
Check:
- Webhook secret matches in both systems
- Request body is read as raw text (not parsed JSON)
- Signature algorithm matches (sha256)
- No body modifications before verification
Delivery Failures
Common Causes:
- Endpoint timeout (>30 seconds)
- Endpoint returns non-200 status
- Network connectivity issues
- SSL certificate problems
Solutions:
- Process webhooks asynchronously
- Return 200 immediately, process in background
- Check delivery history for error details
- Implement retry logic on your side
Related Documentation
- Webhook Events Reference - Complete event reference
- Webhook Security - Security best practices
- API Reference - Complete API documentation
Last Updated: 2026-07-07