Guide to Northpass Webhooks: Features and Best Practices
Northpass, now Gainsight Customer Education, is a learning platform for teaching customers and partners how to use a product. Companies run an "academy" of courses and learning paths, and the training data is most useful outside the LMS: a completed onboarding course can update a customer's health score in the CRM, and a new learner can be matched to an account. Northpass webhooks deliver those learner and course events as they happen.
This guide covers Northpass's 14 webhook subscriptions, the JSON:API-style payload, verifying the base64 X-Northpass-Hmac-SHA256 header, and the retry rule that can switch off every webhook in your school.
What are Northpass webhooks?
Webhooks come with Northpass's Webhooks app, available on the Premium plan. A school can have up to 10 subscriptions, created in the app or through the v2 API (POST /v2/webhook_, with a url and a list of subscriptions). Northpass only saves a subscription if the URL answers with a 2xx, as a reachability check.
Deliveries are JSON POST requests shaped like JSON:API documents: a data object with the event's id, its type (the subscription name, ending in _events), attributes, links and included related records.
Every request is signed with the school's shared secret. X-Northpass-Hmac-SHA256 holds a base64 HMAC-SHA256 of the raw body, the same scheme Shopify uses.
Northpass webhook features
| Feature | Details |
|---|---|
| Availability | Webhooks app, Premium plan |
| Configuration | Webhooks app, or POST /v2/webhook_ |
| Limit | 10 subscriptions per school |
| Signature | base64 HMAC-SHA256 |
| Signature header | x-northpass-hmac-sha256 |
| Signed content | The raw request body |
| Key | The school's shared secret, shown in the Webhooks app |
| Body | JSON:API-style JSON |
| Event routing | Body field data.type |
| Retries | Hourly for 24 hours |
| After 24 hours of failure | All of the school's webhooks are disabled |
Events and payloads
data.type | Triggered When | Common Use Cases |
|---|---|---|
learner_created_events | A learner is created | Create the user in your CRM or app |
person_invited_events | A person is invited | Track invitations |
person_activated_events | A person activates their account | Mark the learner active |
learner_ | A learner is enrolled in a course | Record enrolments |
learner_ | A learner starts a course | Progress tracking |
activity_ | A learner completes an activity | Fine-grained progress |
quiz_completed_events | A learner completes a quiz | Store scores |
course_completed_events | A learner completes a course | Issue credit, update the CRM |
course_published_events | A course is published | Refresh your catalogue |
learner_ | A learner registers for an ILT session | Sync session rosters |
learning_ | A learner is enrolled in a learning path | Record enrolments |
learning_ | A learner starts a learning path | Progress tracking |
learning_ | A learner completes a learning path item | Progress tracking |
learning_ | A learner completes a learning path | Issue credit |
A few details from the docs: learner_ also fires when a learner restarts a course, course_published_events references the person who published, and person_invited_events applies to academies that use preregistration.
Event Payload Structure
data.idis the event ID, anddata.typeis the subscription name.data.includedcarries the related people, courses, activities or quizzes, though not on every event.data.attributesis specific to the event; a quiz completion, for example, includes the scorevalueand theminimum_passing_score.
Two of Northpass's own examples disagree with the subscription list. The person-invited example has data.type set to learner_created_events, and the learning-path enrolment example uses learner_ rather than learning_. Route on both spellings for learning-path enrolment, and log any unknown data.type instead of rejecting it.
Setting up Northpass webhooks
Prerequisites
- A Northpass school on the Premium plan, with the Webhooks app enabled
- Admin access to the school
- A publicly reachable HTTPS endpoint that already returns
2xx
Get Your Shared Secret
Open the Webhooks app; the school's shared secret is shown there. Store it as WEBHOOK_SECRET and use its UTF-8 bytes as the HMAC key, without decoding. One secret signs every webhook for the school.
Register Your Endpoint
- In the Webhooks app, add your endpoint URL (for example
https://example.com/webhooks/northpass). - Choose the subscriptions to send there, such as
learner_created_eventsandcourse_completed_events. - Save. Northpass checks that the URL responds with
2xx.
Or call POST /v2/webhook_ with a url and a list of subscriptions. Either way, keep to the 10-subscription limit.
Test a delivery
Start a local receiver and forward requests with the Hookdeck CLI:
npx hookdeck-cli listen 3000 northpass --path /webhooks/northpass
Start the CLI before saving the subscription so the reachability check passes. Then create a test learner and enrol them in a course.
Securing Northpass webhooks
Compute HMAC-SHA256 of the raw body with the shared secret, base64-encode the digest, and compare it with X-Northpass-Hmac-SHA256 in constant time. Northpass's Ruby and Python samples use Base64.encode64(...).strip and base64.b64encode(...). See Webhook verification.
const crypto = require('node:crypto');
function decodeBase64(value) {
if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
const bytes = Buffer.from(value, 'base64');
return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
? bytes : null;
}
function verifySignature(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
const signature = headers['x-northpass-hmac-sha256'];
if (typeof signature !== 'string') return false;
const key = Buffer.from(secret, 'utf8');
const message = rawBody;
const candidates = [signature];
const expected = crypto.createHmac('sha256', key).update(message).digest();
return candidates.some((candidate) => {
const received = decodeBase64(candidate);
if (!received) return false;
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
The handler deduplicates on data.id and accepts both spellings of the learning-path enrolment type.
const express = require('express');
function createNorthpassApp({ seen, crm, progress }) {
const app = express();
app.post('/webhooks/northpass', express.raw({ type: '*/*' }), async (req, res) => {
const secret = process.env.WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500);
if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
let parsed;
try {
parsed = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return res.sendStatus(400);
const { data } = parsed;
if (!data || typeof data !== 'object' || !data.id || typeof data.type !== 'string') {
return res.sendStatus(400);
}
try {
if (await seen(data.id)) return res.sendStatus(200);
switch (data.type) {
case 'learner_created_events':
case 'person_activated_events':
await crm.upsertLearner(data);
break;
case 'course_completed_events':
case 'learning_path_completed_events':
await crm.recordCompletion(data);
break;
case 'learning_path_learner_enrolled_events':
case 'learner_enrolled_to_learning_path_events':
case 'learner_enrolled_to_course_events':
await progress.recordEnrolment(data);
break;
default:
await progress.record(data.type, data);
}
} catch {
return res.sendStatus(503);
}
return res.sendStatus(200);
});
return app;
}
No timestamp is signed, so a captured delivery verifies forever. data.id is the guard against processing it twice.
For a Northpass source, Hookdeck computes the base64 HMAC-SHA256 of the raw body with your shared secret and compares it with the header. Northpass always receives 200 from Hookdeck, so your school's webhooks aren't at risk of being disabled because of your outages; a mismatch is logged as failed verification and the request isn't forwarded. Hookdeck applies no timestamp tolerance.
Make Northpass webhooks production-ready. Hookdeck Event Gateway acknowledges every Northpass delivery, so an outage on your side can't trigger the 24-hour rule that disables all of your school's webhooks.
Northpass webhook limitations and pain points
One failing endpoint disables everything
The Problem: A single broken receiver stops every webhook in your school, including ones going to healthy systems.
Why It Happens: After a delivery has failed hourly for 24 hours, Northpass disables all of the school's webhooks and notifies the admin.
Workarounds:
- Alert on failures well before 24 hours pass, and fix or remove the failing subscription.
- After re-enabling, use the List all sent webhooks endpoint to find failed messages.
How Hookdeck Can Help: Hookdeck answers Northpass with 200 and holds the events, so a downstream outage never counts as a Northpass failure.
Only 10 subscriptions
The Problem: You can't give every consumer its own set of subscriptions.
Why It Happens: Northpass caps a school at 10 subscriptions.
Workarounds:
- Send events to one receiver and fan out from there.
How Hookdeck Can Help: One Hookdeck source can receive all subscriptions and route each data.type to as many destinations as you need.
Example payloads that don't match the list
The Problem: Routing on the documented subscription names misses some deliveries.
Why It Happens: Northpass's examples use learner_created_events for an invitation and learner_ for a learning-path enrolment. Several other examples contain comments or invalid JSON.
Workarounds:
- Match both learning-path spellings and log unknown types.
- Build fixtures from real deliveries rather than the docs.
How Hookdeck Can Help: Hookdeck stores each delivery's real body, so you can see which data.type values your school actually sends.
Reachability check on save
The Problem: You can't create a subscription before the receiver is deployed.
Why It Happens: Northpass requires the URL to return 2xx when the subscription is saved.
Workarounds:
- Deploy a minimal endpoint that returns
200first.
How Hookdeck Can Help: A Hookdeck source URL answers 200 from the moment it's created, so subscriptions can be set up before your service exists.
Best practices
Compare the base64 string carefully
Decode and compare bytes, or compare the exact string Northpass sends after validating its format. Base64 values with stray trailing bits can decode to the same bytes, so the verifier above checks that the value round-trips.
Keep the key as text
The shared secret is used as UTF-8 bytes. Don't base64- or hex-decode it, even if it looks encoded.
Acknowledge, then call the CRM
Northpass doesn't state a timeout, but CRM and HRIS calls are slow. Store the event and return 2xx, then process in the background as the asynchronous processing guide describes.
Record data.id with each change
Hourly retries over a day mean the same event can arrive many times. Keep data.id alongside the update, per our idempotency guide.
Conclusion
Northpass webhooks are well suited to syncing training activity into customer systems, with a familiar Shopify-style signature and an event ID on every delivery. The 10-subscription cap, mismatched examples and the rule that disables every webhook after 24 hours of failures are what to design around.
Hookdeck Event Gateway takes the pressure off that 24-hour rule and lets one subscription feed many consumers.
Get started with Hookdeck for free and handle Northpass webhooks reliably in minutes.