Learners and enrollments
An enrollment is the link between a person and a piece of content: it is born from a self-enrollment, an approved request, a purchase, or a grant from the team, and its state controls who has access to the course. This page explains each entry path, the enrollment lifecycle, intake forms, and how to track, restart, and refund learners in Cursiva.
Learner, enrollment, and purchase
Three records work together, and it's worth telling them apart:
- Learner (enrollment): the row that ties a user to a piece of content. It stores progress, intake answers, and the
status. It is what grants — or withholds — access. - Purchase: a financial record for paid content. It is only written after the Stripe checkout is confirmed. It outlives the content and has no foreign key to the enrollment.
- Member: someone on the organization's staff. Don't confuse them with a learner — members produce and manage content; learners consume it. See
/docs/teamsfor the staff roles.
Ways to enroll
The entry path depends on how the content is configured — whether it is free or paid and whether it requires approval. Price is always the access boundary: content with a price requires a verified purchase before enrollment.
| Path | When it applies | How it happens |
|---|---|---|
| Self-enrollment (free and open) | Free content, no approval | The learner clicks enroll and joins instantly; intake is validated on the spot |
| Request + approval | Free content marked "requires approval" | The learner submits a request with their intake answers; the team approves or declines |
| Purchase (checkout) | Content with a price | The learner pays first; the enrollment is created after the charge is confirmed |
| Purchase + approval (pay first) | Priced content with "requires approval" | Pays first; the payment becomes a request the team approves (declining triggers a refund) |
| Grant by the team/API | Any published content | An admin enrolls another person without requiring a purchase, respecting the seat limit |
A few important details for each path:
- Free self-enrollment: only works when the content is free and doesn't require approval. If the content requires approval, self-enrollment is blocked and the person must submit a request.
- Enrollment request: learner-initiated requests are for free courses that require approval. The request holds the intake answers until the team decides. On approval, the answers travel to the learner's row and the request is removed.
- Purchase: Cursiva never talks directly to Stripe in the core — the purchase is recorded only when the checkout returns verified as paid. Only then is the enrollment created. Details on pricing, coupons, and seats live in
/docs/pricing-checkout. - Purchase + approval (pay first): the checkout creates the request after the charge. This path skips both the "free only" lock and intake validation — for the buyer, the payment is the form. If the team declines, the buyer must be refunded.
- Grant by the team/API: an admin with edit rights on the content can enroll another person (comp), without requiring a purchase. The content must be published, and the seat limit still applies.
Enrollment lifecycle (status)
Every enrollment carries a status. Only active grants access — the refunded and past_due rows are kept to preserve history and progress, but every access predicate excludes them.
| Status | Grants access? | Meaning |
|---|---|---|
active | Yes | Valid enrollment. It's the only status that unlocks the content |
refunded | No | The purchase was refunded. The row remains (history and progress), but access is revoked |
past_due | No | Delinquent installment plan. Access freezes after a failed charge and returns on the next paid invoice |
Transitions are driven by payment events and by team actions:
- A refund moves the enrollment to
refundedand revokes access, but keeps the row and the progress. - A failed installment moves the enrollment to
past_due(frozen); the next paid invoice returns it toactive; a canceled plan becomesrefunded. - Repurchasing after a refund reactivates the same row instead of creating another — the previous enrollment and progress are restored.
Intake forms (enrollment questions)
Intake forms collect information before the learner begins. There are two ways to define the questions:
- Questions embedded in the content: configured directly in the content editor.
- Reusable forms (Settings → Intakes): a named set of questions that the organization attaches to multiple pieces of content by live reference. Any member can pick a form; creating, editing, or deleting one requires organization admin.
When a reusable form is deleted, the content that referenced it automatically falls back to its embedded questions — the reference is ON DELETE SET NULL, so no content is left without a form.
Field types
An intake form accepts the field types below. Each answer is normalized (trimmed) and validated according to its type before being stored.
| Type | Field | Validation applied |
|---|---|---|
text | Short text | Free text, up to 2000 characters |
textarea | Long text | Free multi-line text, up to 2000 characters |
email | Must be a valid email format | |
phone | Phone | Permissive: digits plus the usual separators (formats vary by country) |
number | Number | Integer or decimal (negatives allowed) |
date | Date | ISO format YYYY-MM-DD |
select | Choice (list) | The value must be one of the options; 2 to 20 options per field |
file | File | The answer is the upload URL (/uploads/… or an http(s) address) |
Any field can be marked required, and an optional description appears as help text under the label. The form limits protect the enrollment flow:
| Limit | Value |
|---|---|
| Questions per form | 10 |
| Options per choice field | 2 to 20 |
| Question label | 200 characters |
| Help text (description) | 300 characters |
| Learner answer | 2000 characters |
| Reusable form name | 80 characters |
Who fills it in and where the answers show up
Not every learner goes through intake — whoever pays is excused from the form, because the payment is already the approval.
| Entry path | Fills in the intake? | Note |
|---|---|---|
| Free self-enrollment | Yes | Required fields are enforced before joining |
| Free request + approval | Yes, when submitting the request | The answers stay with the request and migrate to the enrollment on approval |
| Purchase (checkout) | No | The buyer skips the intake; required fields don't block a paid enrollment |
| Grant by the team/API | No | Staff don't fill it in; answers from an approved request travel along |
Answers are written to the learner's enrollment (or to the request, while it's pending). The team consults them in the Learners tab of the content editor and in the pending-request queue. Only ask for data with a clear use, and explain why each piece of information is needed.
Seat limits
A piece of content can have a seat limit. The course page shows the remaining seats before the enroll click (in the style of "3 seats left"), and the limit is enforced in a serialized way at the moment of enrollment, so that concurrent sign-ups don't blow past capacity.
- The limit only counts new, active enrollments. A reactivated row (one that was
refundedorpast_due) already occupied a seat and doesn't count again. - In dated courses, the seat limit is per cohort, not per content.
- Team grants also respect the seat limit.
Tracking learners
There are two main places to keep track of who is enrolled:
- The content's Learners tab: lists that content's learners with who they are, their progress on the path (steps completed out of the total, including referenced modules), the result of the last quiz, and the active purchase (which enables the refund).
- The organization's learners page: a paginated view of all enrollments, newest to oldest. Search matches by name or email directly in the database (not just the loaded page), and there's a filter by status (in progress or completed).
Tags and teams help segment the operation. Prefer stable criteria — role, cohort, or program — over temporary groups created for each task. For aggregate metrics (enrollments, completions, and quiz performance), see /docs/insights.
Restarting the course
Restarting resets a learner's progress on a piece of content. It's a team action that requires edit rights on the content.
- Erases the entire progress map of the enrollment — the root and every referenced module — and reopens completion.
- Quiz history is kept: it feeds the mastery model and the insights, so it isn't erased on restart.
- It doesn't change the enrollment's
statusor the purchase link — only the progress.
Removing an enrollment and refunds
Removing the enrollment (unenroll) is a team action with edit rights. It removes the learner's row and their membership in any cohort at the same time and, if there is an active installment plan, signals a cancellation of the Stripe subscription — removing access never leaves the billing running.
Refunding is distinct from removing. The refund is executed after the Stripe reversal is confirmed, and then it:
- Marks the purchase as refunded (a purchase refunds only once).
- Revokes access by moving the enrollment to
refunded— the row and the progress remain for history. - Uncounts the coupon that the sale had consumed, so that a limited-use code isn't eroded by refunded purchases.
Refunding moves money, so it requires billing rights (finance/admin on the owning team, or owner/admin on the organization). The rules for amounts, installments, and coupons — including the automatic refund when an upfront payment is declined at approval — are in /docs/payments-payouts.