# Northpass Webhooks

Northpass (now Gainsight Customer Education) is a customer education LMS. Its Webhooks app
(Premium plan) sends a JSON `POST` when learners are created, invited or activated, courses are
published, learners enroll, start or complete courses, activities and quizzes, register for ILT
sessions, or progress through learning paths.

Each request carries `X-Northpass-Hmac-SHA256`: the standard padded base64 HMAC-SHA256 of the raw
request body, keyed with your school's shared secret. Only the body is signed: there is no
timestamp, so there is no replay window to check.

## When to Use This Skill

* How do I receive Northpass webhooks?
* How do I verify the `X-Northpass-Hmac-SHA256` signature?
* How do I handle `learner_created_events` or `course_completed_events`?
* Why does my Northpass webhook endpoint return 401?
* Why did Northpass disable all my school's webhooks?

## Verification (core)

```javascript
const crypto = require('crypto');

const SIGNATURE_FORMAT = /^[A-Za-z0-9+/]{43}=$/; // padded base64 of 32 bytes, nothing else

function verifyNorthpassSignature(rawBody, signature, secret) { // rawBody: Buffer
  if (!secret || typeof signature !== 'string' || !SIGNATURE_FORMAT.test(signature)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('base64');
  const a = Buffer.from(signature, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

```

* Header: `X-Northpass-Hmac-SHA256`. The whole value is one bare digest: no `sha256=` prefix, no list.
* Signed content: the **raw body bytes only**. Read them with `express.raw({ type: '*/*' })`
  (Next.js: `await request.arrayBuffer()`, FastAPI: `await request.body()`), verify, then parse.
* Key: the school's shared secret as UTF-8 bytes, used as-is (not base64- or hex-decoded).
* Compare the base64 **string** exactly (case-sensitive). Do not decode it first: a value whose
  last character carries stray low bits decodes to the same bytes but is not the canonical digest.
* Python: `hmac.compare_digest(received_bytes, base64.b64encode(digest))` after the same
  `[A-Za-z0-9+/]{43}=` format check (never compare `str`).
* Missing, empty, malformed or mismatched signature: `401`. Otherwise `200` once handled.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/northpass-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/northpass-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/northpass-webhooks/examples/fastapi/).

## Event Types

The payload is JSON:API style. Dispatch on `data.type` (no header names the event); `data.id` is
the event id and `data.included` holds the related people, courses and so on.

| `data.type` | Sent When |
| --- | --- |
| `learner_created_events` | A learner is created |
| `person_invited_events` | A person is invited |
| `person_activated_events` | A person activates their account |
| `learner_enrolled_to_course_events` | A learner is enrolled in a course |
| `learner_started_course_events` | A learner starts a course |
| `activity_completed_events` | A learner completes an activity |
| `quiz_completed_events` | A learner completes a quiz |
| `course_completed_events` | A learner completes a course |
| `course_published_events` | A course is published |
| `learner_registered_to_ilt_session_events` | A learner registers for an ILT session |
| `learning_path_learner_enrolled_events` | A learner is enrolled in a learning path |
| `learning_path_started_events` | A learner starts a learning path |
| `learning_path_item_completed_events` | A learner completes a learning path item |
| `learning_path_completed_events` | A learner completes a learning path |

The examples handle the first learner, course and quiz events and log the rest. See
[references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/northpass-webhooks/references/overview.md) for payload fields.

## Delivery

Return a 2xx quickly. A failed delivery is retried every hour for 24 hours; if it still fails,
**all** of the school's webhooks are disabled and the admin is notified. Failed messages can be
listed with the List all sent webhooks API endpoint.

## Environment Variables

```bash
NORTHPASS_WEBHOOK_SECRET=your_school_shared_secret   # shown in the Webhooks app

```

## Local Development

Northpass can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 northpass --path /webhooks/northpass

```

Give the printed URL to the Northpass Webhooks app as the endpoint.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/northpass-webhooks/references/overview.md) - What Northpass webhooks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/northpass-webhooks/references/setup.md) - Enable the Webhooks app, get the shared secret, register the endpoint
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/northpass-webhooks/references/verification.md) - Signature details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/northpass-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests