Webhooks Build and above
Webhooks let Coassemble send server-to-server POST requests to your endpoint when important course events happen.
If you are embedding Coassemble in an iframe and want browser-side events instead, see the Course Player embeddable documentation.
Events you can subscribe to
| Event | Fires when |
|---|---|
course.created | A new course is created. |
course.commenced | A learner first commences a course tracking record. When the tracking belongs to a collection, data.collection is { id, key }. |
course.completed | A learner first completes a course tracking record. When the tracking belongs to a collection, data.collection is { id, key }. |
quiz.completed | A learner passes a standalone Screen Player quiz. |
quiz.failed | A learner fails a standalone Screen Player quiz. |
migration.progress | One course in a Classic migration batch finishes (migrated, blocked, or failed). |
migration.completed | A Classic migration batch reaches completed or completed_with_errors. |
Delivery format
Coassemble sends webhooks as POST requests with Content-Type: application/json.
Request headers
| Header | Description |
|---|---|
X-Coassemble-Event | Event name, e.g. course.completed. |
X-Coassemble-Delivery | Unique delivery ID (UUID). Use this for idempotency. |
X-Coassemble-Timestamp | Unix timestamp (seconds) used for signature verification. |
X-Coassemble-Signature | HMAC SHA-256 signature in the format sha256=<hex>. |
X-Coassemble-Test-Mode | true or false, matching the signed body's testMode. |
Request body
{
"id": "17fd9df8-c77a-4b7d-a281-267b74f8cbf3",
"type": "course.completed",
"occurredAt": "2026-02-22T10:15:30.000Z",
"workspaceId": 1234,
"testMode": false,
"data": {
"course": {
"id": 4321,
"title": "Security Basics",
"key": "security-basics",
"clientIdentifier": "course_abc"
},
"collection": {
"id": 42,
"key": "onboarding-path"
},
"tracking": {
"id": 8888,
"identifier": "user_123",
"email": "user@example.com",
"commenced": "2026-02-22T10:01:00.000Z",
"completed": "2026-02-22T10:15:30.000Z",
"totalTime": 870
}
}
}course.commenced and course.completed include optional data.collection ({ id, key }) when the tracking was taken inside a collection. It is omitted when the tracking is not from a collection.
Every newly queued event includes the boolean testMode. Course and quiz learner events use the mode saved on the learner tracking record, so upgrading the workspace does not change it. course.created uses the issuing key's mode; ordinary authoring without an API key uses false. Classic migration batch events use the issuing key's mode stored on the batch jobs; a dashboard session without a key uses false. Manual test deliveries always use true and also include data.test: true.
migration.progress and migration.completed fire only for HTTP 202 batches (two or more unique courses). A one-course HTTP 200 migration does not emit them. The payload data matches GET /v1/headless/course-migrations/{id}. Progress events also include the finished course as data.item.
{
"id": "17fd9df8-c77a-4b7d-a281-267b74f8cbf3",
"type": "migration.completed",
"occurredAt": "2026-09-23T10:15:30.000Z",
"workspaceId": 1234,
"testMode": false,
"data": {
"id": "batch-id",
"status": "completed",
"created": "2026-09-23T10:01:00.000Z",
"completed": "2026-09-23T10:15:30.000Z",
"counts": {
"total": 2,
"pending": 0,
"processing": 0,
"succeeded": 2,
"failed": 0
},
"items": [
{
"jobId": 1,
"courseId": 123,
"title": "Course A",
"status": "migrated",
"total": 3,
"changed": 1,
"blocked": [],
"screens": [],
"error": null,
"reason": null
},
{
"jobId": 2,
"courseId": 456,
"title": "Course B",
"status": "migrated",
"total": 2,
"changed": 0,
"blocked": [],
"screens": [],
"error": null,
"reason": null
}
]
}
} Test events are delivered normally, including in Sandbox. Verify the signature and return 2xx, then route testMode: true events away from production actions. Deliveries queued before this field was introduced retain their original body and may omit the field and header; treat those as unclassified.
Signature verification
Each webhook endpoint has its own signing secret generated when the endpoint is created. Signatures are created with:
sha256 = HMAC_SHA256(secret, "<timestamp>.<raw_request_body>")Example Node.js
import crypto from 'crypto';
function verifyWebhookSignature({ rawBody, timestamp, signatureHeader, secret }) {
const expected = `sha256=${crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')}`;
if (!signatureHeader || signatureHeader.length !== expected.length) return false;
return crypto.timingSafeEqual(
Buffer.from(signatureHeader, 'utf8'),
Buffer.from(expected, 'utf8')
);
}Use the raw request body string when verifying signatures (before JSON parsing).
Retry and failure behaviour
- A delivery is considered successful only when your endpoint returns a
2xxstatus. - Coassemble uses a
10srequest timeout. - Failed deliveries are retried up to 6 total attempts (the first delivery plus five retries).
- Backoff delays after a failed attempt are approximately 1 minute, 5 minutes, 30 minutes, 2 hours, then 6 hours (about 8.5 hours from the first attempt).
- Each delivery stores its own attempt budget when it is queued. Deliveries already pending when the schedule changed keep 4 attempts; new deliveries get 6. Manual replay remains available after automatic retries stop.
- Delivery attempts include the same
X-Coassemble-DeliveryID so you can deduplicate safely. - Retries and manual replays preserve the original payload and its
testMode, regardless of later plan changes.
Setup
- Build an HTTPS endpoint in your app that accepts
POSTJSON and returns2xxquickly. - In Coassemble authoring, go to Developer → Webhooks.
- Create a webhook endpoint URL and select one or more events.
- Send a test delivery to confirm your receiver logic (
testModeanddata.testare bothtrue). - Validate signatures, deduplicate using
X-Coassemble-Delivery, then process asynchronously.
Managing webhooks
Webhook endpoints can be managed in the Coassemble app or via the API.
- In the app: go to Developer → Webhooks to create, edit, test, enable/disable, or delete endpoints.
- Via the API: use the Webhooks API to list, create, and delete endpoints — useful for integrations (like an LMS plugin) that register their own receiver during setup. The signing secret is returned once, on creation.
- Webhook endpoint URLs must use
https://.