Agent skill
Coassemble Webhooks Skill
Receive and verify Coassemble webhooks. Use when setting up Coassemble course and learner webhook handlers, debugging X-Coassemble-Signature verification, or handling events like course.completed, course.commenced, quiz.completed, quiz.failed, and migration.completed.
Install this skill
npx skills add hookdeck/webhook-skills --skill coassemble-webhooks
When to Use This Skill
- How do I receive Coassemble webhooks?
- How do I verify the
X-Coassemble-Signatureheader? - How do I handle
course.completed,quiz.failed, ormigration.completedevents? - Why is my Coassemble webhook signature verification failing?
- How do I sync Coassemble learner completions to my LMS, HRIS or CRM?
Essential Code (USE THIS)
Coassemble signs every delivery with two headers:
X-Coassemble-Signature: sha256=<hex>
X-Coassemble-Timestamp: 1736937000
- The signature is HMAC-SHA256 (lowercase hex) of
<X-Coassemble-Timestamp value>.<raw body>, keyed with the endpoint's signing secret (returned once when you create the endpoint), used as-is (UTF-8, not decoded). - Accept the digest with or without the
sha256=prefix, and compare hex case-insensitively. - The event name is in the body's
typefield (also sent inX-Coassemble-Event).X-Coassemble-Deliveryis the same across retries and replays, so use it for idempotency. - Test deliveries have
testMode: trueand are signed like real ones; verify, return2xx, then skip production actions. - Answer a missing, malformed or wrong signature with
401.
Verification (Node.js)
const crypto = require('crypto');
// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifyCoassemble(rawBody, signatureHeader, timestampHeader, secret) {
if (!secret || !signatureHeader || !timestampHeader) return false;
const hex = signatureHeader.startsWith('sha256=') ? signatureHeader.slice(7) : signatureHeader;
if (!/^[0-9a-fA-F]{64}$/.test(hex)) return false; // validate before decoding
const message = Buffer.concat([Buffer.from(timestampHeader, 'latin1'), Buffer.from('.'), rawBody]);
const expected = crypto.createHmac('sha256', secret).update(message).digest();
const received = Buffer.from(hex, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
Verification (Python)
import hashlib, hmac, re
def verify_coassemble(raw_body: bytes, signature_header, timestamp_header, secret: str) -> bool:
if not secret or not signature_header or not timestamp_header:
return False
digest = signature_header.removeprefix("sha256=")
if not re.fullmatch(r"[0-9a-fA-F]{64}", digest): # validate before decoding
return False
message = b"".join([timestamp_header.encode("latin-1"), b".", raw_body])
expected = hmac.new(secret.encode(), message, hashlib.sha256).digest()
return hmac.compare_digest(bytes.fromhex(digest), expected)
Coassemble's docs show a Node sample that interpolates a string body; the examples sign the raw body bytes instead, since re-serialized JSON can differ from what was sent.
The timestamp age check is off by default, because retries and replays (Coassemble's and Hookdeck's) resend the original X-Coassemble-Timestamp. Set COASSEMBLE_ to a positive number of seconds (for example 300) to opt in. Use X-Coassemble-Delivery for idempotency instead.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Common Event Types
Dispatch on the body's type field.
| Event | Triggered When |
|---|---|
course.created | A new course is created |
course.commenced | A learner first commences a course tracking record |
course.completed | A learner first completes a course tracking record |
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 |
migration.completed | A Classic migration batch reaches completed or completed_with_errors |
For payload shapes and the full list, see references/overview.md and Coassemble's webhook docs.
Important Headers
| Header | Description |
|---|---|
X-Coassemble-Signature | sha256=<hex> HMAC-SHA256 signature |
X-Coassemble-Timestamp | Unix timestamp (seconds) used for signature verification |
X-Coassemble-Event | The event type |
X-Coassemble-Delivery | Unique delivery ID (UUID), stable across retries |
X-Coassemble-Test-Mode | true or false, matching the body's testMode |
Environment Variables
COASSEMBLE_WEBHOOK_SECRET=your_webhook_secret # returned once when the endpoint is created
COASSEMBLE_WEBHOOK_TOLERANCE_SECONDS=0 # optional; a positive value opts in to the replay window
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 coassemble --path /webhooks/coassemble
Reference Materials
- references/overview.md - Coassemble webhook concepts, events, payloads, retries
- references/setup.md - Creating an endpoint and getting the signing secret
- references/verification.md - Signature verification details and gotchas