MastriaDocs

Integrations & exports

Mastria connects to the rest of your stack in four places, all of them plain and portable: courses can live in your GitHub repo, completion and certificate events flow out through signed webhooks, your product's own metrics flow in through one small API, and every table exports as CSV. Nothing here needs a native app or a marketplace.

Courses as code

Keep courses in a GitHub repo and review changes as pull requests: connect the repo in Admin → Settings (owner/name, branch, the folder holding course folders; a token only for private repos), and enable publish on merge so a merged PR publishes itself — or press Sync now. Each course is a folder with a course.yaml manifest (structure, stable section/lesson slugs, titles, order — plus optional keys image, level, duration, and tags for the course card, seo for search & social overrides, and next for the recommended next course, so the storefront look versions with the content) and markdown lesson files. Synced lessons validate and publish exactly like admin publishes — invalid labs are reported, never shipped — and every version lands in the same history, so a bad merge rolls back like any other publish.

Merging is releasing. Every sync ships the lessons that changed as one release in course history, with the commit message as the release note — your course versions read like your changelog. A sync reads every file at the commit it records, so a push landing mid-sync never mixes two versions under one release. The course's Review & publish screen doubles as the sync report: what the last sync published, what failed validation, and a Sync now button.

Publish on merge is a GitHub webhook: enabling it in Settings gives you a signing secret; add a webhook on the repo (Settings → Webhooks) with payload URL https://mastria.dev/api/github-webhook, content type application/json, push events only, and that secret. Pushes to other branches are ignored; several academies can sync from one repo.

For synced courses the repo is the source of truth: the next sync overwrites admin edits to them. Every published version stays in history; unpublished draft edits are lost — to keep a change, put it in the repo. Access and completion settings stay editable in the admin: who may see a course and whether it certifies are operational calls, not content. Version diffs are built in: compare any snapshot, the draft, and the live version from the lesson's history.

Removing a synced course. A sync never deletes or restores a course: deleting a folder from the repo only stops its updates (the course stays as it was), and lessons dropped from a manifest are reported as orphaned, never removed. To retire a synced course, delete it on the Courses page (or in its settings) — it moves to Deleted like any other course and can be restored from there. While it sits in Deleted, syncs skip it and say so in the sync report; remove its folder from the repo when you're done with it (a never-published course deletes for good, and a folder still in the repo would recreate it as a draft). To take it off the academy without deleting anything, unpublish it from its sync report or hide it in Access.

Outbound webhooks

Set an https endpoint in Admin → Settings → Outbound webhook. The first save mints a signing secret (shown on the page; rotate it any time). From then on Mastria POSTs one JSON event per occurrence:

EventWhen
lesson.completedA learner completes a lesson (button or passing labs).
course.completedThat completion was the course's last missing published lesson.
certificate.issuedA certificate is issued — once per learner and course (or learning path), ever.

Every payload carries occurred_at, learner.email, and course.slug/course.title (a learning-path certificate carries path.slug/path.title instead); lesson events add lesson, certificate events add certificateurl is the verification page, credential_url the Open Badges 3.0 document beside it:

JSON
{
  "event": "certificate.issued",
  "occurred_at": "2026-09-06T18:41:48.703Z",
  "learner": { "email": "ada@datacraft.io" },
  "course": { "slug": "sql-fundamentals-duckdb", "title": "SQL Fundamentals with DuckDB" },
  "certificate": {
    "id": "cd66f288-98da-4e61-a3b0-31b052f79850",
    "url": "https://demo.mastria.dev/certificates/cd66f288-98da-4e61-a3b0-31b052f79850",
    "credential_url": "https://demo.mastria.dev/certificates/cd66f288-98da-4e61-a3b0-31b052f79850/credential",
    "credential": "SQL Fundamentals with DuckDB",
    "tier": "assessed",
    "issued_at": "2026-08-16T18:01:01.859Z"
  }
}

Verify authenticity with the X-Mastria-Signature header: the hex-encoded HMAC-SHA256 of the raw request body, keyed with your secret. Compare it in constant time before trusting the payload.

JavaScript
import { createHmac, timingSafeEqual } from "node:crypto";

const expected = createHmac("sha256", process.env.MASTRIA_WEBHOOK_SECRET)
  .update(rawBody)
  .digest("hex");
const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));

Deliveries are fire-and-forget with a five-second timeout: a slow or dead endpoint never slows a learner down, and there are no automatic retries yet — the same facts are always available from the CSV exports below if a delivery is missed.

Metric ingest

The impact reports ("learners who completed the onboarding course activated at twice the rate of those who didn't") compare your product's own metrics across trained and untrained learners. Send those metrics keyed by user email; Mastria matches them to learners, including addresses a learner has since changed away from.

Generate a token in Admin → Analytics → Impact, then POST to the platform host:

Shell
curl -X POST https://mastria.dev/api/ingest \
  -H "Authorization: Bearer <your ingest token>" \
  -H "Content-Type: application/json" \
  -d '{"email":"dev@customer.com","metric":"activated"}'
  • Send one event as an object, or a batch as an array of up to 1000.
  • email and metric are required. Metric names are letters, digits, _, ., - (up to 100 characters) — activated, pipelines.created, tickets_filed.
  • value (a number) and occurred_at (ISO 8601 with offset) are optional; a missing value counts as an occurrence, a missing time is "now".
  • Responses: {"ingested": N} on success, 401 for a missing or unknown token, 400 for invalid JSON, 422 with the field errors for a malformed event.

No engineering time to spare? The same page accepts a CSV paste — one event per line, email,metric[,value[,occurred_at]], header row optional — for a monthly export from your warehouse or helpdesk. Regenerating the token invalidates the old one immediately.

CSV exports

Every table walks out the door as plain CSV, staff-only, from the admin:

ExportWhereContents
learners.csvSettings → ExportsEvery learner with account, lessons started and completed, last active, joined.
certificates.csvSettings → Exports, CertificatesEvery credential: holder, name at issue, course or path, credential, tier, issued/refreshed/emailed dates, verification URL, Open Badges document URL.
metric-events.csvSettings → ExportsEvery ingested metric event (newest 100k).
Course funnelThe course's Analytics pagePer-lesson opened and completed counts.
WaitlistCourse settings (coming-soon courses)Signups with dates.

Full content export — your entire academy as markdown, assets, and structure in one click — is on the roadmap; courses as code already gives repo-synced academies that portability today.