Documentation menu

API reference

Four routes make up the Embed API — webinar create/list, webinar registration, course enrollment, and course embed-token minting. All are versioned under /api/v1, same as the Send API, and share its bearer-key auth.

Authentication

Authorization: Bearer mia_live_...

A missing or invalid key returns 401 \{ "error": "Missing or invalid Authorization: Bearer <apiKey>" \} on every route below. Keys are managed under Developers in the dashboard — see the quickstart.

POST /api/v1/webinars

Creates a webinar. accessMode is always forced to "registration" — the Embed API only ever deals in public-registration sessions, never subscriber-only live sessions.

| Field | Type | Required | Notes | | --- | --- | --- | --- | | title | string | yes | Trimmed; empty after trimming returns 400. | | scheduledAt | number | no | Unix ms timestamp. | | courseId | string | no | Ties the webinar to an existing course. | | capacity | number | no | Must be >= 0 if present. Registrations past capacity are waitlisted, not rejected. |

Returns 201 \{ "webinar": LiveSession \}.

GET /api/v1/webinars

No body. Returns 200 \{ "webinars": LiveSession[] \} — every session on the account with accessMode: "registration", subscriber-only sessions are filtered out.

POST /api/v1/webinars/[id]/register

Registers one end user for a webinar and sends the confirmation/waitlist email.

| Field | Type | Required | Notes | | --- | --- | --- | --- | | email | string | yes | Must match a basic email pattern, else 400 \{ "error": "A valid \email` is required" }. | | name| string | yes | Trimmed, max 200 characters, else400 { "error": "A `name` is required" }`. |

Errors: 404 if the webinar doesn't exist or isn't yours; 400 if it isn't accessMode: "registration"; 409 if it has already ended.

Response:

{ "status": "confirmed", "joinUrl": "https://mailinapp.com/webinar/<id>/join?token=..." }

or, once capacity is reached:

{ "status": "waitlisted", "joinUrl": null }

A confirmed registration fires the webinar's registered lifecycle email; a waitlisted one gets the plain waitlist notice regardless of any binding.

POST /api/v1/courses/[id]/enroll

Grants or revokes course access for an email address, keyed to your own entitlement decision rather than a MailInApp checkout.

| Field | Type | Required | Notes | | --- | --- | --- | --- | | email | string | yes | Same validation as registration; lowercased and trimmed before use. | | active | boolean | no | Defaults to true. false revokes access. |

404 if the course doesn't exist or isn't yours. Response: 200 \{ "subscriberId": "...", "active": true \}.

A fresh grant (active: true on a subscriber who wasn't already entitled) fires the course's enrolled lifecycle email. Revoking access never emails the end user on your behalf.

POST /api/v1/courses/[id]/embed-token

Mints a short-lived (5-minute) signed token for embedding the course portal, after re-checking the subscriber is actually entitled.

| Field | Type | Required | Notes | | --- | --- | --- | --- | | email | string | yes | Same validation as above. |

Errors: 404 if the course doesn't exist, isn't yours, or isn't published; 409 if the account hasn't claimed a /learn/<slug> membership URL yet; 403 if this email isn't currently entitled (call enroll first).

Response: 200 \{ "portalUrl": "https://mailinapp.com/learn/<slug>/courses/<courseId>/embed?token=..." \}. Redirect your app's iframe or a new window at portalUrl — it signs the visitor in and lands them in the ordinary course portal.

Lifecycle email bindings

A course or webinar can bind any of its lifecycle events to a studio project instead of the platform's plain confirmation copy:

| Resource | Events | | --- | --- | | Course | enrolled, completed (reminder accepted for parity, no automatic trigger — courses have no natural due date to fire one against) | | Webinar (LiveSession) | registered, reminder |

Bindings are set from the course/webinar's own Lifecycle emails panel in the dashboard, not through this API. They take effect on the next event regardless of whether it was triggered by the Embed API or the equivalent dashboard/public-form action. An event with no binding (or one pointing at a deleted/foreign project) falls straight back to today's plain email — this never breaks a send.

A bound event renders through renderSingleRecipientEmail exactly like the Send API's template mode: full fallback engine, interactive blocks, and a personal signed live-view link. The project's own webhook/Responses tracking picks it up too, attributed to that specific lifecycle event rather than a stored contact row.

Rate limits

Two independent caps apply, per route:

  • Per API key: 60 requests/minute. Exceeding it returns 429 \{ "error": "Rate limit exceeded" \} for that key specifically.
  • Per account, Embed API quota: your plan includes a number of Embed API calls per calendar month, shared across all four routes above. 0 on Free/Starter returns 403 \{ "error": "The Embed API isn't included in your plan" \}; exceeding a paid tier's quota returns 429 \{ "error": "Monthly Embed API quota for your plan exceeded" \} until it resets on the 1st.

Webinar creation additionally re-checks your live-streaming quota (checkLiveSessionQuota) — the same broadcast-minutes/course gate the dashboard's own create flow enforces — and returns 403 with that check's own message if it fails.

Error codes

| Status | Meaning | | --- | --- | | 400 | Malformed JSON or a missing/invalid required field — see each route's own table above. | | 401 | Missing or invalid Authorization header, or the key has been revoked. | | 403 | Embed API not on your plan, quota check failed, live-session quota exceeded, or (embed-token) the email isn't currently entitled. | | 404 | The webinar/course doesn't exist or isn't owned by your account — deliberately the same response as "doesn't exist," same reasoning as the Send API's template-mode 404. | | 409 | (Webinar) the session has already ended. (Embed-token) the account hasn't claimed a membership URL yet. | | 429 | Per-key rate limit or monthly quota exceeded. |