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

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/, 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.typeSent When
learner_created_eventsA learner is created
person_invited_eventsA person is invited
person_activated_eventsA person activates their account
learner_enrolled_to_course_eventsA learner is enrolled in a course
learner_started_course_eventsA learner starts a course
activity_completed_eventsA learner completes an activity
quiz_completed_eventsA learner completes a quiz
course_completed_eventsA learner completes a course
course_published_eventsA course is published
learner_registered_to_ilt_session_eventsA learner registers for an ILT session
learning_path_learner_enrolled_eventsA learner is enrolled in a learning path
learning_path_started_eventsA learner starts a learning path
learning_path_item_completed_eventsA learner completes a learning path item
learning_path_completed_eventsA 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 12, 2026

View on GitHub →