API reference

Webhooks

Receive course.created, course.failed, and course.exported events on a per-request callbackUrl. Payload shapes and security model.

Webhooks let you skip polling. When you supply callbackUrl on a POST /api/v1/courses or POST /api/v1/courses/:id/export request, we POST a typed event to your URL when the job completes.

How it works

  • Per-request, not per-account. Pass callbackUrl on each request to control delivery. Every customer brings their own URL on every call.
  • Single delivery. Each event is delivered once with a 10-second timeout. Build idempotency into your handler — keying off jobId (for course.created / course.failed) or courseId + exportedAt (for course.exported) is enough.
  • Typed payloads. Three event types: course.created, course.failed, course.exported. Each has a stable shape (below). Dates are ISO 8601 strings; Content-Type is application/json.

Receiver requirements

  • Public, HTTPS endpoint. Your callbackUrl must resolve to a public IP. HTTPS is recommended.
  • Direct response. The endpoint must terminate at the URL you provide; redirects aren't followed.
  • Return 2xx on success. Anything else counts as a delivery failure.

Events

course.created

Fired when a course-generation job reaches completed.

{
  "event": "course.created",
  "jobId": "8aF2k...Xq",
  "course": {
    "id": "cou_abc123",
    "name": "Workplace Safety Basics",
    "headline": "A practical guide for new hires",
    "shareUrl": "https://learningstudioai.com/go/cou_abc123",
    "createdAt": "2026-04-28T17:14:08.000Z"
  }
}

The course object matches the GET job status response.

course.failed

Fired when a job reaches failed instead of completed.

{
  "event": "course.failed",
  "jobId": "8aF2k...Xq",
  "error": {
    "message": "Course outline could not be generated for this subject.",
    "code": "GENERATION_FAILED"
  }
}

error.code is best-effort — present for known failure modes, omitted for unknown ones.

course.exported

Fired when POST /api/v1/courses/:id/export completes and a callbackUrl was supplied.

{
  "event": "course.exported",
  "courseId": "cou_abc123",
  "format": "scorm12",
  "downloadUrl": "https://storage.googleapis.com/.../course.zip",
  "shareUrl": "https://learningstudioai.com/go/cou_abc123",
  "exportedAt": "2026-04-28T17:15:42.000Z"
}

Receiving webhooks

A minimal Node receiver:

import express from 'express';

const app = express();
app.use(express.json());

app.post('/hooks/learningstudio', async (req, res) => {
  const event = req.body;

  switch (event.event) {
    case 'course.created':
      await ingestNewCourse(event.course);
      break;
    case 'course.failed':
      await alertOps(event.jobId, event.error);
      break;
    case 'course.exported':
      await uploadToLms(event.courseId, event.downloadUrl);
      break;
    default:
      // Forward-compat: unknown events should still 200.
      console.log('unknown event', event);
  }

  res.sendStatus(200);
});

app.listen(3000);

Best practices

  • Respond quickly. If your handler does heavy work (e.g. download the SCORM zip and upload to an LMS), enqueue a background job and return 200 immediately. The 10-second timeout is enforced on our side.
  • Be idempotent. Build your handler to safely process the same event twice — network blips can cause occasional duplicates.
  • Forward-compat. Future events may be added. Fall through unknown event types with a 200 — never reject with a 4xx.

Security

Authenticate the webhook by URL secrecy: include a hard-to-guess token as a path segment or query parameter on your callbackUrl (e.g. /hooks/learningstudio/<random-32-char-token>). Reject requests on any other path.