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.
| Endpoint | Description |
|---|---|
| POST /api/v1/organizations/{organizationId}/webhooks | Register (url, eventTypes, description). The signing secret is returned only in this response. |
| GET /api/v1/organizations/{organizationId}/webhooks | List registered webhooks |
| DELETE /api/v1/organizations/{organizationId}/webhooks/{subscriptionId} | Delete |
| POST /api/v1/organizations/{organizationId}/webhooks/{subscriptionId}/test | Send a test ping event |
| GET /api/v1/organizations/{organizationId}/webhooks/{subscriptionId}/deliveries | Delivery history (status, attempts, last HTTP status and error) |
| POST /api/v1/organizations/{organizationId}/webhooks/deliveries/{deliveryId}/replay | Retry a delivery (resets the attempt count and sends again) |
| GET /api/v1/webhook-events | List 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
| Event | Description |
|---|---|
| lesson.completed | A learner completed a lesson |
| course.started | A learner started a course |
| certificate.issued | A certificate was issued |
| member.added | A member was added to the organization |
| subscription.activated | A paid subscription became active |
| session.started / session.answered / session.hint / session.revealed / session.ended | A practice session started, an answer was given, a hint or the answer was shown, or the session ended |
| mastery.updated | A learner's mastery was updated |
| session.deleted / learner.data_deleted | A practice session, or a learner's learning data, was deleted |
| ping | Test 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.