Webhooks

Register a webhook to receive events from your organization (a lesson completed, a certificate issued, a member added and so on) on your server as HTTPS POST requests.


1. Registering and managing

Webhooks are registered through the API. You need the webhooks:manage scope and organization-admin rights.

EndpointDescription
POST /api/v1/organizations/{organizationId}/webhooksRegister (url, eventTypes, description). The signing secret is returned only in this response.
GET /api/v1/organizations/{organizationId}/webhooksList registered webhooks
DELETE /api/v1/organizations/{organizationId}/webhooks/{subscriptionId}Delete
POST /api/v1/organizations/{organizationId}/webhooks/{subscriptionId}/testSend a test ping event
GET /api/v1/organizations/{organizationId}/webhooks/{subscriptionId}/deliveriesDelivery history (status, attempts, last HTTP status and error)
POST /api/v1/organizations/{organizationId}/webhooks/deliveries/{deliveryId}/replayRetry a delivery (resets the attempt count and sends again)
GET /api/v1/webhook-eventsList the events you can subscribe to

The url must start with https://. Leave eventTypes empty to receive every event, including ones added later. Unknown event names are ignored.

2. Events

EventDescription
lesson.completedA learner completed a lesson
course.startedA learner started a course
certificate.issuedA certificate was issued
member.addedA member was added to the organization
subscription.activatedA paid subscription became active
session.started / session.answered / session.hint / session.revealed / session.endedA practice session started, an answer was given, a hint or the answer was shown, or the session ended
mastery.updatedA learner's mastery was updated
session.deleted / learner.data_deletedA practice session, or a learner's learning data, was deleted
pingTest send

There are no events for courses or lessons being added, updated or deleted.

3. Request format

Every event is sent in the same JSON envelope. What's inside data depends on the event.

POST https://your-server.example.com/manabu2-webhook
Content-Type: application/json
X-Manabu-Event: lesson.completed
X-Manabu-Delivery: 3f2a...           (one id per delivery)
X-Manabu-Timestamp: 1767225600       (send time, UNIX seconds)
X-Manabu-Signature: sha256=9c1e...

{
  "id": "b81d...",
  "type": "lesson.completed",
  "createdAt": "2026-10-04T09:00:00+00:00",
  "organizationId": "...",
  "data": {
    "userId": "...",
    "lessonId": "...",
    "courseId": "..."
  }
}

id identifies the event and stays the same across retries. If you receive the same id twice, ignore the second one.

4. Verifying the signature

X-Manabu-Signature is an HMAC-SHA256 keyed with the secret you received at registration. It signs the timestamp, a dot, and the request body, and is sent as lowercase hex prefixed with sha256=.

signature = hex( HMAC_SHA256( secret, "{X-Manabu-Timestamp}.{raw request body}" ) )
■ Verifying in C#
var secret = "whsec_...";
var body = await new StreamReader(Request.Body).ReadToEndAsync();
var timestamp = Request.Headers["X-Manabu-Timestamp"].ToString();

using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var computed = Convert.ToHexString(
    hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{body}"))).ToLowerInvariant();

var received = Request.Headers["X-Manabu-Signature"].ToString().Replace("sha256=", "");

if (!CryptographicOperations.FixedTimeEquals(
        Encoding.ASCII.GetBytes(computed), Encoding.ASCII.GetBytes(received)))
{
    return Unauthorized();
}

The timestamp is part of what's signed, so to block replayed requests, reject requests whose X-Manabu-Timestamp is far from the current time.

5. Responses and retries

  • Return a 2xx within 10 seconds and the delivery counts as successful. Do heavy work after responding.
  • On a non-2xx response, a timeout or a connection error, we retry after 30 seconds, 2 minutes, 10 minutes, 30 minutes and 1 hour (up to 6 attempts including the first).
  • A delivery that fails all 6 attempts stays in the delivery history as failed, and you can replay it.
  • Because of retries, the same event can arrive more than once, and order isn't guaranteed.

Next: Using learning content for RAG →