# Coassemble Webhooks

## When to Use This Skill

* How do I receive Coassemble webhooks?
* How do I verify the `X-Coassemble-Signature` header?
* How do I handle `course.completed`, `quiz.failed`, or `migration.completed` events?
* 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 `type` field (also sent in `X-Coassemble-Event`). `X-Coassemble-Delivery` is the same across retries and replays, so use it for idempotency.
* Test deliveries have `testMode: true` and are signed like real ones; verify, return `2xx`, then skip production actions.
* Answer a missing, malformed or wrong signature with `401`.

### Verification (Node.js)

```javascript
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)

```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_WEBHOOK_TOLERANCE_SECONDS` 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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/coassemble-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/coassemble-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/coassemble-webhooks/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](https://github.com/hookdeck/webhook-skills/blob/main/skills/coassemble-webhooks/references/overview.md) and [Coassemble's webhook docs](https://developers.coassemble.com/webhooks).

## 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

```bash
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

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 coassemble --path /webhooks/coassemble

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/coassemble-webhooks/references/overview.md) - Coassemble webhook concepts, events, payloads, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/coassemble-webhooks/references/setup.md) - Creating an endpoint and getting the signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/coassemble-webhooks/references/verification.md) - Signature verification details and gotchas