Agent skill

LearnUpon Webhooks Skill

Receive and verify LearnUpon webhooks (version 2.0). Use when setting up a LearnUpon (learning management system) webhook endpoint, verifying the X-Webhook-Signature header (hex HMAC-SHA256 of the raw body), debugging 401s, or handling events like user_created, course_completion, course_enrollment, module_complete, badge_awarded and learning_path_completion.

Install this skill

npx skills add hookdeck/webhook-skills --skill learnupon-webhooks


LearnUpon (the learning management system) sends a JSON POST to your endpoint when something happens on your portal: a user is created, a learner is enrolled or completes a course, a module is completed, a badge is awarded, a live-learning session changes. Each request is signed with HMAC-SHA256 in the X-Webhook-Signature header.

Only Webhooks v2.0 are supported. Ask LearnUpon support to turn them on.

Version 1 is a separate, older scheme with a different verification process. This skill covers v2.0 only.

When to Use This Skill

  • How do I receive LearnUpon webhooks?
  • How do I verify the LearnUpon X-Webhook-Signature header?
  • How do I handle course_completion, course_enrollment or user_created events?
  • Why does my LearnUpon webhook signature verification fail (401)?
  • How do I read the data array and X-Webhook-Type header?

Verification (core)

const crypto = require('crypto');

// rawBody: Buffer of the request body exactly as received (express.raw({ type: '*/*' })).
function verifyLearnuponSignature(rawBody, signature, secret = process.env.LEARNUPON_WEBHOOK_SECRET) {
  if (!secret || !Buffer.isBuffer(rawBody)) return false;
  // Validate the format first: exactly 64 hex characters (case-insensitive).
  if (typeof signature !== 'string' || !/^[0-9a-fA-F]{64}$/.test(signature)) return false;
  const expected = crypto.createHmac('sha256', Buffer.from(secret, 'utf8')).update(rawBody).digest();
  const received = Buffer.from(signature.toLowerCase(), 'hex'); // always 32 bytes after validation
  return crypto.timingSafeEqual(expected, received);
}
  • Header: X-Webhook-Signature: <64 hex characters>. The whole value is the digest: no prefix, no list.
  • Signed content: the raw request body bytes only. No timestamp, id or other header is signed (X-LearnUpon-TimeStamp is sent but not signed), so there is no replay window to check.
  • Key: the secret you set in the portal's webhook configuration, used as its UTF-8 bytes (not hex- or base64-decoded). Env var: LEARNUPON_WEBHOOK_SECRET.
  • Python: hmac.new(secret.encode(), raw_body, hashlib.sha256).digest(), validate the header with re.fullmatch(r"[0-9a-fA-F]{64}", sig), then hmac.compare_digest on bytes.
  • Never JSON.parse/JSON.stringify before verifying. Read the body raw, verify, then parse.
  • Missing, empty or malformed header, or a mismatch: return 401.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types

The body is {"data": [...]} with up to 10 events of one type. The X-Webhook-Type header names the type; loop over data. Dedupe on the X-Webhook-ID header. The webhook must accept POST.

Event (X-Webhook-Type)Sent When
user_created, user_updatedA user account is created or updated
course_cloning_completeAn admin finishes cloning a course
course_completionA learner completes a course
course_updatedA course is updated
module_completeA learner completes a module within a course
purchase_completionA customer completes a purchase of courses on the portal
badge_awarded, badge_revokedA badge is awarded to or removed from a learner
course_enrollment, course_unenrollmentA learner is enrolled on or unenrolled from a course
learning_path_enrollment, learning_path_updated, learning_path_completionA learning path enrollment, update or completion
event_session_creation, event_session_updated, event_session_canceledA live-learning event session is created, updated or cancelled
learning_journey_enrollment, learning_journey_completion, learning_journey_blocked, learning_journey_deleted, learning_journey_archivedA learning journey enrollment, completion, blocked enrollment, deletion or archive

Delivery

Answer 2xx, ideally within 2 seconds. No response within 7 seconds counts as a failure. Failed webhooks are resent for 72 hours (at 10 minutes, 3 hours, 12 hours, 24 hours and 48 hours), and LearnUpon retains webhooks for 45 days. Acknowledge fast and process asynchronously.

Environment Variables

LEARNUPON_WEBHOOK_SECRET=your-webhook-secret   # the secret set in the portal's webhook configuration

Local Development

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

npx hookdeck-cli listen 3000 learnupon --path /webhooks/learnupon

Use the printed URL as the webhook URL in your LearnUpon portal.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 12, 2026

View on GitHub →