Skip to main content
Webhooks let your server react to course lifecycle events without polling the status endpoint. Configure one webhook URL per account.

Setup

Webhook URL and secret are configured per account. Contact your account manager with:
  • The HTTPS URL to deliver events to
  • A webhook_secret of your choice (32+ random bytes, base64 or hex)
Self-serve webhook configuration is on the roadmap.

Event types

EventFires when
course.readyCourse generation completes successfully
course.failedCourse generation ends in an unrecoverable error
course.paused_for_reviewGeneration pauses at the first review checkpoint
course.module_readyOne module’s video transitions to approved (fires per module)
course.quota_pausedGeneration pauses because the live cost meter crossed the account billing cap
quota.warningAccount crosses 80% of monthly budget
quota.exceededA course ends early due to budget exhaustion

Payload shape

All course events share the same envelope: top-level metadata plus a nested data object carrying course fields.
{
  "id": "wh_1a2b3c4d5e6f7a8b",
  "event": "course.ready",
  "created_at": "2026-06-23T10:30:00Z",
  "api_version": "v1",
  "data": {
    "course_id": "20260623_103000_abc123",
    "title": "Sales Fundamentals",
    "status": "ready",
    "created_at": "2026-06-23T10:00:00Z",
    "sandbox": false,
    "module_count": 6
  }
}
data always includes course_id, title, status, created_at, and sandbox. Event-specific additions:
  • course.ready: adds module_count
  • course.failed: adds error_message
  • course.module_ready: adds module_index (0-based) and, when available, video_url (signed, 24-hour TTL)
  • quota.exceeded: adds module_count and overage_hours
language and external_ref are NOT included in the payload. If you need them, fetch the course with GET /api/v1/courses/{course_id}.

Signing and verification

Every delivery is signed with HMAC-SHA256 using your webhook_secret. Two headers are included:
HeaderValue
Mahara-Signaturet=<timestamp>,v1=<hex_hmac>
X-Mahara-EventThe event name (e.g. course.ready)
The signed content is <timestamp>.<raw_request_body>. Recompute the HMAC and compare in constant time.

Python verification

import hmac
import hashlib
import time

def verify(request_body: bytes, signature_header: str, secret: str, tolerance_sec: int = 300) -> bool:
    parts = dict(p.split("=") for p in signature_header.split(","))
    t = parts["t"]
    v1 = parts["v1"]

    if abs(time.time() - int(t)) > tolerance_sec:
        return False

    signed = f"{t}.".encode() + request_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

Node.js verification

const crypto = require('crypto');

function verify(rawBody, signatureHeader, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map(p => p.split('='))
  );
  const { t, v1 } = parts;

  if (Math.abs(Date.now() / 1000 - parseInt(t)) > toleranceSec) {
    return false;
  }

  const signed = `${t}.${rawBody}`;
  const expected = crypto.createHmac('sha256', secret).update(signed).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
Always verify the raw request body, not the parsed JSON. Different JSON serializers produce different byte sequences, which breaks the signature.

Retry behaviour

Deliveries that fail to reach your endpoint are not automatically retried in the current API version. If your endpoint is down, you will miss events. Mitigations:
  • Make your webhook handler highly available (queue + background processor pattern).
  • Implement a periodic reconciliation job that calls GET /api/v1/courses to catch missed events.
  • Automatic retries with exponential backoff are on the roadmap.

Idempotency

Your handler must be idempotent. Even with retries off, network reasons may cause duplicate deliveries. Use the top-level id field (e.g. wh_1a2b3c4d5e6f7a8b) as a dedup key in your processing.

Sandbox webhooks

Sandbox runs (with sk_test_ keys) fire webhook events with "sandbox": true in the data envelope. Branch on this field if your downstream effects (e.g. emailing the customer) should not run in sandbox.