Agent skill

Lessonly Webhooks Skill

Receive and authenticate Lessonly (now Seismic Learning) webhooks. Use when setting up a Lessonly lesson-completed callback endpoint, configuring lesson_completed_callback_authorization, checking the Authorization header, or debugging 401s on a Lessonly webhook.

Install this skill

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


Lessonly is a training platform, now part of Seismic and branded Seismic Learning; the API and docs still use the Lessonly name. It sends one webhook: a callback when a learner completes a lesson (lesson completed). Payloads are not signed. The only credential is an optional Authorization header whose value you choose.

When to Use This Skill

  • How do I receive Lessonly lesson-completed webhooks?
  • How do I set lesson_completed_callback_url and lesson_completed_callback_authorization?
  • Does Lessonly sign webhook payloads? (No: shared Authorization value only)
  • Why does my Lessonly (Seismic Learning) webhook endpoint return 401?

Verification (core)

You set the credential yourself with PUT /api/v1/company_settings (lesson_completed_callback_authorization, for example Tm93IGlzIHRo). Lessonly sends it verbatim as Authorization: <value>; it adds no scheme. If you included Bearer or Basic in the value, that prefix is part of it. Compare the whole header to LESSONLY_AUTHORIZATION; don't strip, parse or base64-decode anything:

const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyLessonlyRequest(headers, expected = process.env.LESSONLY_AUTHORIZATION) {
  if (!expected) return false;                 // never accept when unconfigured
  const received = headers['authorization'];   // full value, exactly as configured
  return Boolean(received) && safeEqual(received, expected);
}

Return 401 when the header is missing or does not match, 400 for a body that isn't a JSON object, and 200 once handled. Read the body raw (express.raw({ type: '*/*' })) so any content type works, and parse it only after the check. Python and Next.js equivalents are in references/verification.md.

What this does and doesn't give you: a static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Anyone who captures a request can resend it. Use HTTPS only, make the handler idempotent, and rotate the credential (set a new lesson_completed_callback_authorization) if it leaks.

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

Webhook Events

EventTriggered When
lesson completedA learner completes a lesson (delivered to lesson_completed_callback_url)

There is no event-type field to switch on: every delivery to the callback URL is a lesson completion. Lessonly's API reference doesn't document the payload, so the JSON body in this skill's examples (lesson_id, user_id, completed_at) is illustrative. Capture a real delivery (the Hookdeck CLI makes that easy) before depending on field names.

Setup Note (Hookdeck)

Hookdeck's Lessonly source (type LESSONLY, labelled "Lessonly") compares the whole Authorization header to the key you configure. Paste the Authorization value exactly as set in Lessonly, including any "Bearer " or "Basic " prefix.

Environment Variables

LESSONLY_AUTHORIZATION=Bearer 00000000-0000-0000-0000-000000000000   # full Authorization value, as set in Lessonly

Local Development

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

No account required. Use the printed URL as lesson_completed_callback_url.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 9, 2026

View on GitHub →