Gareth Wilson Gareth Wilson

Guide to Northpass Webhooks: Features and Best Practices

Published


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_endpoints, 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

FeatureDetails
AvailabilityWebhooks app, Premium plan
ConfigurationWebhooks app, or POST /v2/webhook_endpoints
Limit10 subscriptions per school
Signaturebase64 HMAC-SHA256
Signature headerx-northpass-hmac-sha256
Signed contentThe raw request body
KeyThe school's shared secret, shown in the Webhooks app
BodyJSON:API-style JSON
Event routingBody field data.type
RetriesHourly for 24 hours
After 24 hours of failureAll of the school's webhooks are disabled

Events and payloads

data.typeTriggered WhenCommon Use Cases
learner_created_eventsA learner is createdCreate the user in your CRM or app
person_invited_eventsA person is invitedTrack invitations
person_activated_eventsA person activates their accountMark the learner active
learner_enrolled_to_course_eventsA learner is enrolled in a courseRecord enrolments
learner_started_course_eventsA learner starts a courseProgress tracking
activity_completed_eventsA learner completes an activityFine-grained progress
quiz_completed_eventsA learner completes a quizStore scores
course_completed_eventsA learner completes a courseIssue credit, update the CRM
course_published_eventsA course is publishedRefresh your catalogue
learner_registered_to_ilt_session_eventsA learner registers for an ILT sessionSync session rosters
learning_path_learner_enrolled_eventsA learner is enrolled in a learning pathRecord enrolments
learning_path_started_eventsA learner starts a learning pathProgress tracking
learning_path_item_completed_eventsA learner completes a learning path itemProgress tracking
learning_path_completed_eventsA learner completes a learning pathIssue credit

A few details from the docs: learner_started_course_events 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.id is the event ID, and data.type is the subscription name.
  • data.included carries the related people, courses, activities or quizzes, though not on every event.
  • data.attributes is specific to the event; a quiz completion, for example, includes the score value and the minimum_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_enrolled_to_learning_path_events rather than learning_path_learner_enrolled_events. 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

  1. In the Webhooks app, add your endpoint URL (for example https://example.com/webhooks/northpass).
  2. Choose the subscriptions to send there, such as learner_created_events and course_completed_events.
  3. Save. Northpass checks that the URL responds with 2xx.

Or call POST /v2/webhook_endpoints 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_enrolled_to_learning_path_events 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 200 first.

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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.