Guide to Lessonly Webhooks: Features and Best Practices
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_ 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
| Feature | Details |
|---|---|
| Configuration | PUT https://api.lessonly.com/api/v1/company_ |
| Events | Lesson completion only |
| Callback URL | lesson_, one per company |
| Authentication | lesson_, sent verbatim as the Authorization header |
| Signature and timestamp | None |
| Payload and method | Not documented in the API reference |
| Retries and timeout | Not documented |
| Source IPs | Not 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_to a Hookdeck Inspect URL and complete a lesson to see exactly what Lessonly sends, with no account or setup required.completed_ callback_ url
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_. The source has one field, Authorization Header: paste the lesson_ 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_ 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_ 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_, 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.