Gareth Wilson Gareth Wilson

Guide to Absorb LMS Webhooks: Features and Best Practices

Published


Absorb LMS webhooks tell your other systems what is happening in your learning portal: a user is created, a learner completes a course, a certificate or competency is awarded, an admin enrols someone in a session. If you run training on Absorb LMS, webhooks are how that activity reaches your HR system, CRM or reporting tools as it happens, without polling the Absorb Integration API.

This guide covers how Absorb LMS webhooks are configured and signed, the events you can subscribe to, what the lean payloads contain, how failed deliveries are retried, and the practices that keep an Absorb receiver reliable.

What are Absorb LMS webhooks?

An Absorb LMS webhook has a name, a listener URL and one or more events. When one of those events happens in the portal, Absorb sends an HTTP request with a JSON body to the URL. Only a System Administrator can configure webhooks, and if one listener handles several events, Absorb asks you to configure a single webhook with several events rather than one webhook per event.

The payloads are deliberately small. In Absorb's words, they "indicate that a change has occurred, and provide keys to learn more about that change". A receiver usually takes the IDs from the payload and calls the Absorb Integration API for the full record.

Each request carries an absorb-signature header: an HMAC-SHA256 of the raw request body, keyed with the webhook's Secret Key. Absorb's article doesn't name the encoding in text, but its screenshot of a real delivery shows a bare 64-character lowercase hex value.

Absorb LMS webhook features

FeatureDetails
ConfigurationAccount > Client Settings, then the Webhooks tile. System Administrators only
AvailabilityThe Webhooks page is enabled per portal. Ask your Client Success representative if the tile is missing
SubscriptionOne or more events per webhook
PausingTurn off the Enable webhook toggle
Signature headerabsorb-signature
SchemeHMAC-SHA256, shown as bare lowercase hex
Signed contentThe raw request body, nothing else
SecretOne Secret Key per webhook, generated after the webhook is saved
Test requestsTest Webhook sends a random signature that never verifies
RetriesTwo immediate automatic retries, then up to five manual retries per delivery from the Events tab
Delivery historyDelivered and failed deliveries, with the payload and error kept for failures only
Replay windowNone. No timestamp is signed

Absorb documents no response timeout, no ordering guarantee and no source IP list for webhooks.

Common events

Absorb lists 26 events in two groups. The Administrator events follow admin actions, and the Learner events follow what learners do.

AreaEvents
UsersUser Created
Online coursesOnline Course Activated, Online Course Deactivated
Instructor-led coursesInstructor Led Course Activated, Instructor Led Course Deactivated
Curriculums and bundlesCurriculum Activated, Curriculum Deactivated, Course Bundle Activated, Course Bundle Deactivated, Course Bundle Deleted
Departments and groupsDepartment Created, Department Deleted, Group Created, Group Deleted
Admin enrolmentsUser Enrolled in Course by Admin, User Enrolled in Session by Admin, Enrollment Completed by Admin
Learner enrolmentsUser Enrolled in Course by Learner, User Enrolled in Session by Learner, Enrollment Completed by Learner, Session Enrollment Completed
AwardsCertificate Awarded, Competency Awarded, Course Credits Awarded
SkillsSelf-Assessment Completed, Learning Path Completed (both need a Skills subscription)

Absorb's article describes the payload in prose. A typical payload contains the event name, the user ID of the learner who acted or was acted on, the ID of the admin for admin events, and the transaction's client ID and correlation ID.

The JSON key names appear only in the article's screenshots of two real deliveries. They show a body with timestamp, eventName and details, where eventName is the display name with the spaces removed (UserCreated, CertificateAwarded) and details holds the IDs. Absorb has not published the wire names of the other 24 events. Following the same rule, "Enrollment Completed by Admin" would be EnrollmentCompletedbyAdmin, but check the first real delivery of each event before relying on its exact spelling and casing, and compare names case-insensitively.

Setting up Absorb LMS webhooks

Absorb recommends building and testing in a sandbox portal first, then recreating the webhook in production.

  1. Log in to the Admin Experience, open the Account menu at the top right, and click Client Settings.
  2. Click the Webhooks tile, then Add new webhook. Older portals show a New Webhook button on the Client Settings page instead.
  3. Check that Enable webhook is on, then enter a name, your listener URL and the events you want.
  4. Click Save.
  5. Reopen the webhook and copy its Secret key. The field is empty until the webhook has been saved. Store the value server-side, for example as ABSORB_WEBHOOK_SECRET.

Each webhook has its own Secret Key, so your sandbox and production webhooks have different keys.

The Test webhook button, available before you save, sends a request to your URL and shows your listener's response. It doesn't sign that request with the Secret Key. Absorb says "test requests use a randomly generated signature, so the signature for a test event cannot be validated against the Secret Key". A receiver that verifies signatures correctly rejects every test request. To test end to end, save the webhook and trigger a real event in a sandbox portal, such as creating a user.

If you receive Absorb LMS webhooks through Hookdeck, create a source with the Absorb LMS source type, register its URL as the listener URL, and paste the Secret Key into the source. Test Webhook requests still arrive in Hookdeck, which accepts them and then marks them as failed verification because of the random signature. Real deliveries are signed with the Secret Key and verify normally.

Securing Absorb LMS webhooks

Compute an HMAC-SHA256 of the raw request body with the Secret Key as the key, hex-encode it, and compare it with absorb-signature in constant time. The Secret Key is used as a plain string: don't hex- or base64-decode it. Absorb warns that adding or removing spaces, line breaks or other formatting changes the signature, so verify the bytes exactly as received.

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

function verifyAbsorbSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const received = Buffer.from(signatureHeader.trim().toLowerCase(), 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/absorb-lms', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.ABSORB_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyAbsorbSignature(req.body, req.get('absorb-signature'), secret)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Route on event.eventName, then fetch full records from the Integration API.
  res.sendStatus(200);
});

The same check in Python:

import hashlib
import hmac


def verify_absorb_signature(raw_body: bytes, signature_header, secret: str) -> bool:
    if not signature_header or not secret:
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    # Compare bytes: compare_digest raises TypeError on a str with non-ASCII characters.
    return hmac.compare_digest(
        signature_header.strip().lower().encode("utf-8"), expected.encode("utf-8")
    )


# In FastAPI: raw_body = await request.body()
# verify_absorb_signature(raw_body, request.headers.get("absorb-signature"),
#                         os.environ["ABSORB_WEBHOOK_SECRET"])

To check your verifier, use the secret absorb-guide-test-secret and this exact body:

{"eventName":"UserCreated"}

The absorb-signature value should be:

29f85a72f38e4b06bc4cfcd2e3d939329b465213d0a1ac1b11ada7af11375802

Absorb also sends a User-Agent header and suggests using it to help authenticate, log or monitor requests. Its value isn't documented as a contract, so treat it as a logging aid rather than a credential.

Make Absorb LMS webhooks production-ready. Hookdeck Event Gateway verifies the absorb-signature header on every delivery, stores each admin and learner event, and retries delivery to your service on a schedule you control.

Absorb LMS webhook limitations and pain points

Test requests never verify

The Problem: You click Test webhook, your receiver returns 401, and it looks as though verification is broken.

Why It Happens: Absorb signs test requests with a random value, not the Secret Key. The Secret Key doesn't exist yet at that point either, because it is generated when you save.

Workarounds:

  • Use Test Webhook only to check that the URL is reachable.
  • Save the webhook, copy the Secret Key, and trigger a real event in a sandbox portal to test verification.

How Hookdeck Can Help: Hookdeck records every request, including the test ones it marks as failed verification, so you can see that Absorb reached your source and then confirm a real event verifies.

Two quick retries, then manual work

The Problem: A short outage on your side leaves a list of failed deliveries that someone has to retry by hand.

Why It Happens: Absorb retries a failed delivery twice, immediately, and then marks it as Failed. After that, an admin has to open the Events tab and retry each delivery one at a time, up to five times. Bulk retry isn't available.

Workarounds:

  • Keep the receiver highly available, and return 200 as soon as the request is verified and stored.
  • After an outage, filter the Events tab by Failed and retry each delivery, or reconcile through the Integration API.

How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives, so Absorb's two retries aren't spent on your downtime. Hookdeck then delivers to your service with retries you configure, and lets you replay events in bulk.

Thin payloads and undocumented field names

The Problem: The webhook tells you what happened but not enough to act on, and the exact keys aren't in the docs.

Why It Happens: Absorb keeps payloads lean by design, and publishes the key names only in screenshots. The wire spelling of most event names is inferred from two examples.

Workarounds:

  • Treat the webhook as a signal: read the IDs from details and fetch the record from the Integration API.
  • Log the first delivery of each event type and build your handler against what actually arrives.

How Hookdeck Can Help: Hookdeck keeps the full body and headers of every request, so you can see exactly which fields each event carries before you write code against them.

No replay protection

The Problem: A captured request still verifies if it is sent again later.

Why It Happens: Only the body is signed. Absorb sends no signed timestamp, so there is no freshness check to make.

Workarounds:

  • Deduplicate on the correlation ID in details, combined with eventName.
  • Keep the listener on HTTPS.

How Hookdeck Can Help: Hookdeck's deduplication can drop repeated requests based on the body fields you choose.

Best practices

Verify against the raw body

In Express, mount express.raw({ type: '*/*' }) on the webhook route and parse only after verification. In Next.js, use Buffer.from(await request.arrayBuffer()). In FastAPI, read await request.body(). Re-serialised JSON won't match the signature.

Keep one webhook per listener

Absorb recommends one webhook with several events for a listener that handles several events. That gives you one Secret Key per listener, and one place to pause deliveries.

Route on eventName, case-insensitively

Switch on eventName, compare it without regard to case, and log and acknowledge any name you don't recognise. Several event names contain lowercase words such as "by" and "in", and Absorb hasn't published how they appear on the wire.

Fetch details from the Integration API

Use the learner, admin and correlation IDs in the payload to look up what you need. For a UserCreated event, Absorb's own example is to GET the new learner from the API.

Acknowledge fast and process asynchronously

Absorb doesn't publish a timeout, and it only retries twice. Verify, store the event, return 200, and do the API calls and downstream work afterwards. See why to process webhooks asynchronously and our guide to webhook idempotency.

Conclusion

Absorb LMS signs each webhook with absorb-signature, an HMAC-SHA256 of the raw body keyed with the webhook's Secret Key. Copy the key after saving the webhook, expect Test Webhook requests to fail verification, route on eventName, fetch full records from the Integration API, and answer quickly, because Absorb retries a failed delivery only twice before it needs an admin.

Hookdeck Event Gateway sits in front of your receiver, checks every Absorb signature, and holds each event until your service has processed it, so an outage on your side doesn't turn into a list of manual retries.

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