Agent skill
Northpass Webhooks Skill
Receive and verify Northpass (Gainsight Customer Education) webhooks. Use when setting up a Northpass webhook endpoint, verifying the X-Northpass-Hmac-SHA256 signature, debugging 401s, or handling learner_created_events, course_completed_events, learner_enrolled_to_course_events and other learner, course and learning path events.
Install this skill
npx skills add hookdeck/webhook-skills --skill 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-SHA256signature? - How do I handle
learner_created_eventsorcourse_completed_events? - Why does my Northpass webhook endpoint return 401?
- Why did Northpass disable all my school's webhooks?
Verification (core)
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: nosha256=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_after the samedigest(received_ bytes, base64.b64encode(digest)) [A-Za-z0-9+/]{43}=format check (never comparestr). - Missing, empty, malformed or mismatched signature:
401. Otherwise200once handled.
For complete handlers with tests, see examples/express/, examples/nextjs/, 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_ | A learner is enrolled in a course |
learner_ | A learner starts a course |
activity_ | 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_ | A learner registers for an ILT session |
learning_ | A learner is enrolled in a learning path |
learning_ | A learner starts a learning path |
learning_ | A learner completes a learning path item |
learning_ | A learner completes a learning path |
The examples handle the first learner, course and quiz events and log the rest. See 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
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):
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 - What Northpass webhooks are, events and payload shape
- references/setup.md - Enable the Webhooks app, get the shared secret, register the endpoint
- references/verification.md - Signature details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests