Gareth Wilson Gareth Wilson

Guide to Lessonly Webhooks: Features and Best Practices

Published


Lessonly webhooks tell your systems when someone finishes training. When a learner completes a lesson, Lessonly can call a URL you choose, so you can record the completion in an HRIS, unlock access in another tool, or keep a compliance report current without polling the API. Lessonly is now part of Seismic and sold as Seismic Learning, and lessonly.com redirects to Seismic's site, but the API reference at docs.lessonly.com still uses the Lessonly name, and so does Hookdeck's source type.

This guide covers how the lesson completion webhook is configured and authenticated, what Lessonly's documentation does and does not tell you, and how to build a receiver that copes with the gaps.

What are Lessonly webhooks?

Lessonly's API reference describes a single webhook: a callback sent upon lesson completion. There is no dashboard list of subscriptions and no choice of events. Each company has one callback URL, set through the company settings API, and every request to it is a lesson completion.

Lessonly does not sign the callback. The company settings include an optional lesson_completed_callback_authorization value, which Lessonly describes as "the optional value of an Authorization: VALUE header to be sent with webhooks". Lessonly sends the string as it is, without adding Bearer or any other scheme. Your receiver compares the Authorization header with that string.

Lessonly webhook features

FeatureDetails
ConfigurationPUT https://api.lessonly.com/api/v1/company_settings
EventsLesson completion only
Callback URLlesson_completed_callback_url, one per company
Authenticationlesson_completed_callback_authorization, sent verbatim as the Authorization header
Signature and timestampNone
Payload and methodNot documented in the API reference
Retries and timeoutNot documented
Source IPsNot published

The API reference points to separate webhook documentation for the payload, and that documentation is not published alongside it. The docs also carry a notice that Seismic is limiting Lessonly Public API write endpoints for users, groups, lessons and paths as customers move to the upgraded platform. The company settings endpoint is not among those listed.

The payload

Because the body is not documented, capture a real delivery before you write code that depends on field names. Point the callback URL at an inspection tool, complete a lesson as a test learner, and look at the method, headers and body you receive.

Inspect Lessonly webhooks as they arrive. Set lesson_completed_callback_url to a Hookdeck Inspect URL and complete a lesson to see exactly what Lessonly sends, with no account or setup required.

Setting up Lessonly webhooks

You need a Lessonly API key. Sign in, open the company settings page and click API & Webhook. If you do not see the API key link, Lessonly says the feature may need to be activated for your company. API requests use HTTP Basic auth, with your Lessonly subdomain as the username and the API key as the password.

Decide on the full Authorization value first. Generate a long random token, for example with openssl rand -hex 32, and add a scheme yourself if you want one, such as Bearer <token>. Then set both fields:

curl -X PUT https://api.lessonly.com/api/v1/company_settings \
  -H "Content-Type: application/json" \
  -u "SUBDOMAIN:API_KEY" \
  -d '{
    "lesson_completed_callback_url": "https://example.com/webhooks/lessonly",
    "lesson_completed_callback_authorization": "Bearer YOUR_LONG_RANDOM_TOKEN"
  }'

A successful call returns the stored values, so you can confirm both were saved.

To route through Hookdeck, create a Hookdeck source with the Lessonly type and use its URL as lesson_completed_callback_url. The source has one field, Authorization Header: paste the lesson_completed_callback_authorization value exactly as you set it, including any Bearer or Basic prefix. Hookdeck compares the whole value. The source accepts POST. Lessonly's reference does not state the method, so check your first delivery in Hookdeck to confirm it arrived.

Securing Lessonly webhooks

Compare the whole Authorization header with the value you set, in constant time. Do not add or strip a scheme in code: whatever you put in lesson_completed_callback_authorization is the whole credential.

const crypto = require('crypto');
const express = require('express');

// Exactly the lesson_completed_callback_authorization value.
const EXPECTED = process.env.LESSONLY_AUTHORIZATION;

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/lessonly', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get('authorization');
  if (!EXPECTED || typeof received !== 'string' || !safeEqual(received, EXPECTED)) {
    return res.sendStatus(401);
  }

  // The body format is not documented: keep the raw text if it isn't JSON.
  const raw = req.body.toString('utf8');
  let completion = raw;
  try {
    completion = JSON.parse(raw);
  } catch {}

  // Store the completion, return 200, and read fields defensively in a worker.
  res.sendStatus(200);
});

The same check in Python:

import hmac
import os

EXPECTED = os.environ.get("LESSONLY_AUTHORIZATION", "")


def verify_lessonly_request(headers) -> bool:
    if not EXPECTED:
        return False  # never accept when unconfigured
    received = headers.get("authorization")
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), EXPECTED.encode("utf-8"))

A static Authorization value authenticates the sender, not the body. It cannot detect a modified request, and with no signature or timestamp a captured request can be replayed. Use an HTTPS callback URL and a long random value, and set a new value with the same PUT if the old one might have leaked. The field is optional, so if it was never set Lessonly sends no Authorization header and there is nothing to check.

Make Lessonly webhooks production-ready. Hookdeck Event Gateway checks the Authorization value on every lesson completion, keeps each request with its full body so you can see the undocumented payload, and retries delivery to your service on your schedule.

Lessonly webhook limitations and pain points

The payload is undocumented

The Problem: You have to write a handler for a body you have never seen.

Why It Happens: The API reference documents the settings that configure the webhook, but points elsewhere for the payload, and that page is not published with it.

Workarounds:

  • Capture real deliveries first, then write the handler against what you saw.
  • Read fields defensively and reject nothing just because a field you did not expect is present.

How Hookdeck Can Help: Hookdeck stores every request with its headers and body, so you can see each completion exactly as Lessonly sent it and replay it against your handler while you develop.

No documented retries

The Problem: If your endpoint is down when a learner finishes a lesson, you cannot tell whether that completion will ever arrive.

Why It Happens: Lessonly does not document retries, a timeout, or what happens to a failed callback.

Workarounds:

  • Keep the endpoint simple and highly available, and acknowledge as soon as the request is stored.
  • Reconcile completions against Lessonly's API on a schedule for anything that must be complete, such as compliance training.

How Hookdeck Can Help: Hookdeck accepts each callback and stores it before your service sees it, then delivers with retries you control, so your own downtime does not lose completions.

One callback URL per company

The Problem: Two systems that both need completions cannot each register their own URL.

Why It Happens: lesson_completed_callback_url is a single company setting.

Workarounds:

  • Point the callback at a service you own and fan out from there.

How Hookdeck Can Help: A Hookdeck source can have several connections, so one Lessonly callback can be delivered to every system that needs it.

Best practices

Store the exact Authorization value

Keep the value exactly as you sent it in lesson_completed_callback_authorization, and compare it whole. If you included Bearer , it is part of the value.

Deduplicate on the learner and the lesson

There is no documented event ID. Once you have seen a real payload, build an idempotency key from the learner, the lesson and the completion time, so a repeated request does not record a completion twice. See our guide to webhook idempotency.

Acknowledge fast and process asynchronously

Verify the header, store the request, return 200, and update downstream systems afterwards. See why to process webhooks asynchronously.

Conclusion

Lessonly, now Seismic Learning, sends one webhook, on lesson completion, to a single company callback URL, authenticated only by the Authorization value you set in the company settings. Compare that whole value in constant time over HTTPS, capture real deliveries before relying on the undocumented payload, and make the handler idempotent because retries are not documented either way.

Hookdeck Event Gateway checks the header, keeps every completion with its full body, and retries and replays delivery to your service, so the gaps in Lessonly's documentation do not turn into gaps in your training records.

Get started with Hookdeck for free and handle Lessonly 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.