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-Signatureheader? - How do I handle
course_completion,course_enrollmentoruser_createdevents? - Why does my LearnUpon webhook signature verification fail (401)?
- How do I read the
dataarray andX-Webhook-Typeheader?
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-TimeStampis 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_, validate the header withbody, hashlib.sha256).digest() re.fullmatch(r"[0-9a-fA-F]{64}", sig), thenhmac.compare_digeston bytes. - Never
JSON.parse/JSON.stringifybefore 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_updated | A user account is created or updated |
course_cloning_complete | An admin finishes cloning a course |
course_completion | A learner completes a course |
course_updated | A course is updated |
module_complete | A learner completes a module within a course |
purchase_completion | A customer completes a purchase of courses on the portal |
badge_awarded, badge_revoked | A badge is awarded to or removed from a learner |
course_enrollment, course_unenrollment | A learner is enrolled on or unenrolled from a course |
learning_path_enrollment, learning_path_updated, learning_path_completion | A learning path enrollment, update or completion |
event_session_creation, event_session_updated, event_session_canceled | A live-learning event session is created, updated or cancelled |
learning_, learning_, learning_journey_blocked, learning_journey_deleted, learning_ | A 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
- references/overview.md - What LearnUpon webhooks are, events and payload shape
- references/setup.md - Enable webhooks and configure the endpoint and secret
- references/verification.md - Signature verification details, published test vector, debugging
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests