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:
| Event | When |
|---|---|
lesson.completed | A learner completes a lesson (button or passing labs). |
course.completed | That completion was the course's last missing published lesson. |
certificate.issued | A 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 certificate — url is the verification page,
credential_url the Open Badges 3.0 document beside it:
{
"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.
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:
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.
emailandmetricare required. Metric names are letters, digits,_,.,-(up to 100 characters) —activated,pipelines.created,tickets_filed.value(a number) andoccurred_at(ISO 8601 with offset) are optional; a missing value counts as an occurrence, a missing time is "now".- Responses:
{"ingested": N}on success,401for a missing or unknown token,400for invalid JSON,422with 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:
| Export | Where | Contents |
|---|---|---|
learners.csv | Settings → Exports | Every learner with account, lessons started and completed, last active, joined. |
certificates.csv | Settings → Exports, Certificates | Every credential: holder, name at issue, course or path, credential, tier, issued/refreshed/emailed dates, verification URL, Open Badges document URL. |
metric-events.csv | Settings → Exports | Every ingested metric event (newest 100k). |
| Course funnel | The course's Analytics page | Per-lesson opened and completed counts. |
| Waitlist | Course 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.