Enrollments API, developer guide
Enroll a person in a course after a sale outside Aladia (CRM, your Stripe, a management system): POST /v2/integrations/enrollments with the academy API key, idempotency, errors, curl, Node.js and Python examples, the enrollment.created webhook and enrollments from SSO.
07/10/2026
When a course is sold outside Aladia (your CRM closes the deal, payment goes through your Stripe or a bank transfer), you grant access with one call: Aladia creates the account (or finds the existing one), enrolls the person in the course and edition and sends the You're enrolled email. The full flow for academy admins is in How you sell a course.
The enrollment is an external payment: no subscription, instalment or transaction on Aladia, no Aladia revenue in reports. Staff can remove the person from the course like any student (refunds are up to you).
Authentication
An academy API key: Settings › Developers › API keys, in the academy context (API keys and webhooks). Send it in the apikey header of every request, from your server only (never in the browser).
- The key works in the workspace where it was created: it only enrolls in that academy's courses; another academy's course answers
404. - The key has the permissions of whoever created it: it needs Requests: manage (
leads.update), which Owner and Admin have.
The call
POST https://api.aladia.io/v2/integrations/enrollments
apikey: <the academy key>
Content-Type: application/json| Field | Required | Description |
|---|---|---|
email | yes | the person's email: key of the account and of idempotency |
courseId | yes | the course id (it is in the /courses/<id> URL); the course must be published |
cycleId | for live courses with several editions | the edition; with a single edition it can be omitted |
name | no | full name in one field (or firstName and lastName) |
externalId | no | the customer id in your CRM (letters, digits and . _ : @ -, max 120): returned in the response and the webhook. external_id is accepted too |
language | no | it or en: language of the email and of a new account |
notify | no | false = no You're enrolled email (you send your own). Default true |
Response
201 Created the first time (or when it adds a new edition of a live course), 200 OK if the person was already enrolled in that course and edition:
{
"id": "6703f1c2a4b5c6d7e8f90123",
"status": "created",
"courseId": "6ab516ab857fed8289e3f276",
"cycleId": "6ab516ab857fed8289e3f27a",
"profileId": "6703f1c2a4b5c6d7e8f90001",
"email": "mario.rossi@example.com",
"accountCreated": true,
"payment": "external",
"externalId": "hubspot-deal-4242"
}status: created (new enrollment, or a pending invitation turned into one), cycle-added (already enrolled, edition added), existing (nothing to do). accountCreated: true = the account was just created: the email includes Set your password.
Idempotency
The key is email + course + edition. Calling again with the same data creates no duplicates, sends no second email and fires no second webhook: retry safely after a timeout. Two simultaneous calls for the same email and course go through one at a time.
Errors
| Status | errorCode | When |
|---|---|---|
400 | enrollment-invalid | invalid email, externalId with characters not allowed, edition not in the course, missing cycleId on a live course with several editions (the field is in details) |
401 | missing, expired or deleted key | |
403 | the key lacks Requests: manage | |
403 | workspace-sso-required | sign-in from your product set to Product only and the person has no account yet (see below) |
404 | the course does not exist or is not a course of the key's academy | |
409 | lead-course-not-available | the course is not published |
409 | enrollment-account-blocked | the account with that email is blocked |
Examples
curl
curl -X POST https://api.aladia.io/v2/integrations/enrollments \
-H "apikey: $ALADIA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "mario.rossi@example.com",
"name": "Mario Rossi",
"courseId": "6ab516ab857fed8289e3f276",
"cycleId": "6ab516ab857fed8289e3f27a",
"externalId": "hubspot-deal-4242"
}'Node.js (18+)
async function enroll({ email, name, courseId, cycleId, dealId }) {
const response = await fetch('https://api.aladia.io/v2/integrations/enrollments', {
method: 'POST',
headers: { apikey: process.env.ALADIA_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ email, name, courseId, cycleId, externalId: dealId }),
})
const body = await response.json()
if (!response.ok) throw new Error(`Aladia ${response.status}: ${body.errorCode ?? ''} ${body.message ?? ''}`)
/* 201 = enrolled now, 200 = already enrolled: done either way. */
return body
}Python (requests)
import os
import requests
def enroll(email, name, course_id, cycle_id=None, deal_id=None):
response = requests.post(
"https://api.aladia.io/v2/integrations/enrollments",
headers={"apikey": os.environ["ALADIA_API_KEY"]},
json={"email": email, "name": name, "courseId": course_id, "cycleId": cycle_id, "externalId": deal_id},
timeout=30,
)
if response.status_code not in (200, 201):
raise RuntimeError(f"Aladia {response.status_code}: {response.text}")
return response.json()With Make or Zapier: an HTTP › Make a request module with the same method, headers and body, after the "payment received" or "deal won" step.
Webhooks
In the same academy, Settings › Webhooks:
lead.created: a request for information from the site (where a CRM sale starts);lead.updated: the request changes status or assignee (also when Enroll moves it to Enrolled);enrollment.created: a new enrollment (or an added edition) from the API or from Enroll. It does not fire when the response isexisting.
Each delivery is a JSON POST to the webhook URL, with this body:
{
"event": "enrollment",
"data": {
"event": "enrollment.created",
"enrollment": {
"id": "6703f1c2a4b5c6d7e8f90123",
"status": "created",
"course": { "id": "6ab516ab857fed8289e3f276", "title": "Master in Law" },
"cycleId": "6ab516ab857fed8289e3f27a",
"profileId": "6703f1c2a4b5c6d7e8f90001",
"email": "mario.rossi@example.com",
"name": "Mario Rossi",
"externalId": "hubspot-deal-4242",
"payment": "external",
"via": "api",
"accountCreated": true,
"createdAt": "2026-10-07T09:30:00.000Z"
}
}
}via: api or lead (Enroll in Requests).
Enrollments from SSO
If the academy uses sign-in from your product, the JWT courses claim enrolls too: at every sign-in the user joins the listed courses. Like API enrollments they involve no Aladia purchase, but they are marked from your product (SSO), not external payment: the product manages them (replace_courses can remove them), while API enrollments stay until staff removes them. No claim is needed to say "paid": neither counts as Aladia revenue.
Which to use:
| Situation | Way |
|---|---|
| The product knows at every sign-in which courses the user is entitled to | courses claim (and replace_courses) |
| A CRM or an external checkout closes the sale, once | enrollments API |
| Academy in Product only and the person never signed in | courses claim: the API answers 403 workspace-sso-required because it creates no accounts outside the product |
In Product only the API enrolls people who already have an account (they signed in from the product at least once). In Mixed or with SSO off it creates the account as usual.