Pricing & checkout
Pricing turns a published course into an offer a learner can buy and enroll into. This page covers the commerce surface — setting a price and currency, free versus priced content, the buyer's checkout, coupons, seat limits, unlisted-but-sellable, and pay-first approval. The payout, Stripe Connect, installments, and refund plumbing behind it lives in Payments & payouts.
Every monetary value is stored and transmitted in the currency's smallest unit (cents), the same format Stripe uses. A price of 9990 in BRL means R$99.90.
Setting a price
Price is set on the content's Pricing tab, in the currency's smallest unit. A null or zero price means free. The normalization rules are:
- The price must be finite and between
0and the configured ceiling (it rejectsNaN/Infinityand column overflow). - The currency must be in ISO-4217 form: exactly three letters, for example
BRL,USD. An empty or arbitrary string is refused. - A positive price is rounded;
0or null storesnull(free).
Currency can't change after the first sale
A content's currency can be changed freely as long as there are no sales. From the first purchase on, the currency is frozen.
The lock is enforced on both write paths — setPrice (the Pricing tab) and updateSettings (the API PATCH) — each under the same per-content lock the purchase record uses, so no sale can confirm itself in the window between the check and the write.
Free versus priced content
The price is the single switch that decides how a learner reaches the content:
- Free (null or zero price) — the content is read without checkout. If it also requires approval, the learner requests access themselves and fills the intake form.
- Priced — the public page shows a sales preview, and the learner has to complete checkout before any access is granted.
A free child inside a priced path acts as a sample: it stays readable on its own so a prospective buyer can taste the material before purchasing the composition around it. Bundling several courses into one offer is a matter of composition, not a separate product type.
The buyer checkout flow
A priced course sells through a one-time Stripe Checkout session in payment mode, charging the content's price already adjusted for any coupon.
- The buyer starts checkout from the course's public page and is redirected to Stripe.
- On return, Cursiva re-verifies the session with the provider — the browser is never trusted — and only records the purchase once the session reads as paid.
- The purchase is written as a
purchaserow withkind: "one_time", the learner is enrolled (respecting the seat limit and the cohort chosen at checkout), the coupon redemption is counted, and the enrollment email is sent.
If enrollment can't be completed after a successful payment — for example a time-based course whose cohort filled up during the checkout window — the charge is refunded automatically and the purchase is marked refunded, so money is never taken without delivering access. The mechanics of that refund are covered in Payments & payouts.
Coupons
Coupons are discount codes for paid content, managed on the Pricing tab. A coupon belongs to a single piece of content; the code is unique per organization and always normalized to uppercase.
| Field | Rule |
|---|---|
code | 3 to 24 characters, letters, numbers, and hyphens only; stored uppercase; unique within the organization |
percentOff | Whole number from 1 to 100 |
amountOff | Positive value in cents, up to 100,000,000 |
| Percentage or amount | Exactly one of the two must be set — never both, never neither |
maxRedemptions | Optional; integer of at least 1, up to 10,000,000; null = unlimited |
- Charge floor. The discounted price can never drop below
50(smallest unit) — the minimum Stripe charges in most currencies. A coupon that would take the price below this floor is refused at checkout. - Calculation. Percentage:
round(price × (100 − percentOff) / 100). Fixed amount:price − amountOff. - Who manages them. Creating, pausing/resuming (
setActive), and deleting require pricing rights (finance/admin). Quoting (quote) and redeeming are public at checkout. - Deleting a coupon does not undo past sales — purchases already made remain.
Seat limits
A course can cap how many learners it accepts with a seat limit (content.seat_limit). When the cap is reached, checkout and enrollment stop letting new learners in — the seat check runs when a purchase is finalized and when an approval turns a request into an active learner.
Only active learners occupy a seat. A learner who is refunded or delinquent keeps their row and history but frees the seat, so the limit tracks real, current access rather than the raw number of rows.
Unlisted but sellable
Whether a course appears in the public catalog is separate from whether it can be sold. A course can be unlisted — hidden from the catalog and search — yet still fully purchasable through its direct link.
This is the pattern for a private cohort, an early-access offer, or a course you share only with a specific audience: turn off the Listed toggle in the editor top bar, keep a price on it, and hand out the link. See Publishing & access for how listing and access levels combine.
Pay-first enrollment approval
When paid content requires approval (requiresApproval), the buyer pays first and lands in the request queue — the money doesn't buy access directly.
- The buyer completes checkout; the return creates the enrollment request through the
paidpath, which skips the intake form because the payment is its form. - The team approves — the buyer becomes a learner, respecting the seat limit and the cohort chosen at checkout — or rejects.
- A rejection refunds the buyer's purchase automatically: the rejection resolves the purchase by
(user, content)and the refund is fired by the corresponding action.
For free content with approval, the learner requests access themselves and the intake form is required. The coupon redemption is still counted on the pending-approval path, as long as the purchase was actually recorded. How approvals, cohorts, and the request queue behave is detailed in Learners & enrollments.