New: ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md (canonical business/platform bridge — course families, delivery modes, BUILT NOW/PHASE 7 BUILT/FUTURE), OFFERING_AND_ REGISTRATION_MODEL.md (lifecycle diagrams, capacity mechanics), STUDENT_GUARDIAN_DATA_ MODEL.md (data minimization, RBAC, legal-review items), SCHOOL_AND_COMMUNITY_DELIVERY_ ARCHITECTURE.md (organization/location mapping, explicitly not multi-tenancy), PHASE7_PRODUCT_ARCHITECTURE.md (phase summary). Updated: WEBSITE_ENROLLMENT_INTEGRATION.md (documented future offering/registration flow, current live contract unchanged), platform/README.md (test list). Co-Authored-By: Claude Sonnet 5 <[email protected]>
248 lines
13 KiB
Markdown
248 lines
13 KiB
Markdown
# Website → RootNode Academy Enrollment Integration
|
|
|
|
Audience: whoever builds `rootnodeacademy.com` (marketing, pricing, checkout) and needs to turn
|
|
a successful purchase/signup into a real RootNode Academy student enrollment. This document is
|
|
meant to be enough on its own — you should not need to read the Platform API's source to
|
|
integrate against it.
|
|
|
|
## Responsibility boundary
|
|
|
|
**The website is responsible for:** marketing, program selection, pricing, checkout/payment (now
|
|
or in the future), collecting enrollment/customer information, and sending ONE verified
|
|
enrollment event to the endpoint below once payment/signup is confirmed.
|
|
|
|
**RootNode's platform is responsible for everything after that:** student identity, program
|
|
entitlement, account activation, the entire password lifecycle, all operational enrollment
|
|
email (activation, enrollment confirmation, password reset — see
|
|
`docs/PHASE5C_ENROLLMENT_COMMUNICATIONS.md`), learning, Labs, progress, assessment, and
|
|
completion. **The website does not send email for platform activation or any other account
|
|
lifecycle event — RootNode owns that entirely, as of Phase 5C.** Do not build a parallel
|
|
"welcome" or "here's how to log in" email on the website side; it would either duplicate or
|
|
conflict with what RootNode already sends automatically.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
rootnodeacademy.com (marketing, pricing, checkout — built separately, not in this repo)
|
|
|
|
|
| your backend verifies payment/signup with your payment provider
|
|
v
|
|
your backend calls the RootNode Enrollment API (below) — server-to-server only
|
|
v
|
|
RootNode Academy Platform API
|
|
creates/matches a student account
|
|
creates or reuses (idempotently) an enrollment
|
|
v
|
|
the student logs in at the RootNode Academy platform and sees their program
|
|
```
|
|
|
|
**RootNode never talks to your payment provider, and your payment provider never talks to
|
|
RootNode.** Your backend is the only thing that calls this endpoint, after it has already
|
|
confirmed payment (or whatever your enrollment trigger is) on its own. This endpoint knows
|
|
nothing about Stripe, PayPal, or any other provider's object model — it only understands a
|
|
small, generic enrollment event described below.
|
|
|
|
## The endpoint
|
|
|
|
```
|
|
POST /api/integrations/enrollments
|
|
```
|
|
|
|
Reachable at the RootNode Academy platform's public origin (the same host your students'
|
|
browsers already use), via HTTPS in any real deployment. This is the **only** endpoint your
|
|
backend needs.
|
|
|
|
## Authentication
|
|
|
|
A pre-shared secret, sent as a header:
|
|
|
|
```
|
|
X-RootNode-Integration-Key: <the secret>
|
|
```
|
|
|
|
- This is **not** a student or admin login. It has nothing to do with cookies, sessions, or
|
|
browser auth of any kind.
|
|
- **The secret must live only in your backend's server-side configuration.** It must never be
|
|
embedded in any browser-served JavaScript, mobile app bundle, or client-side config — anyone
|
|
who obtains it can create enrollments.
|
|
- A request with a missing or wrong key gets `401 { "error": "unauthorized" }` and nothing else
|
|
happens — no student is looked up, nothing is created.
|
|
- RootNode will provide you with the production secret out of band (not over email/chat in
|
|
plaintext — ask the RootNode team for the current secure-sharing process). Rotate it by
|
|
asking RootNode to issue a new one; there is no self-service rotation yet.
|
|
|
|
## Request
|
|
|
|
```json
|
|
{
|
|
"event_id": "order_9f8e7d6c5b4a",
|
|
"source": "website",
|
|
"program_slug": "root-kubernetes-foundations",
|
|
"status": "paid",
|
|
"student": {
|
|
"email": "[email protected]",
|
|
"login_identifier": null,
|
|
"display_name": "Jamie Rivera",
|
|
"contact_email": null
|
|
}
|
|
}
|
|
```
|
|
|
|
| Field | Required | Notes |
|
|
|---|---|---|
|
|
| `event_id` | **yes** | A string, unique per real-world event, from *your* system (an order ID, a payment intent ID — anything stable). This is the idempotency key — see below. Not a RootNode-generated value. |
|
|
| `source` | no | Free text, e.g. `"website"`. Recorded for audit only, defaults to `"integration"` if omitted. |
|
|
| `program_slug` | **yes** | The RootNode program's id, e.g. `"root-kubernetes-foundations"`. Ask RootNode for the current list — there is no public program-catalog endpoint. |
|
|
| `status` | **yes** | One of: `"paid"`, `"active"`, `"confirmed"` (all mean "grant access now") or `"pending"`, `"awaiting_payment"` (means "record the enrollment but don't grant access yet"). This is a small, fixed, provider-neutral vocabulary — translate whatever your payment provider says into one of these five values before calling. Anything else is rejected. |
|
|
| `student.email` | one of email/login_identifier required | The student's own email, if they have one. |
|
|
| `student.login_identifier` | one of email/login_identifier required | Use this instead of/alongside email for a student who doesn't have their own email address (a minor enrolled by a parent/guardian — see below). Any stable, unique string you control — e.g. an internal user ID from your own system works well. |
|
|
| `student.display_name` | no | Shown in the RootNode instructor portal and, once the student logs in, to themselves. Falls back to the email or login_identifier if omitted. |
|
|
| `student.contact_email` | no | **Only** for a student with no email of their own (§ below) — where to send THIS enrollment's activation/confirmation email (typically a guardian's address collected at checkout). Never treated as the student's own identity/login. |
|
|
|
|
### What you must NOT send
|
|
|
|
- No card numbers, CVVs, bank details, or any other payment-card data. This endpoint has no
|
|
field for it and never will — RootNode never touches payment-card data at all.
|
|
- No raw provider webhook payloads (Stripe event objects, PayPal IPN bodies, etc.) — translate
|
|
to the shape above first. This endpoint is deliberately provider-neutral.
|
|
- No password. RootNode issues its own credential/activation flow (see "What the student
|
|
experiences" below) — you cannot set a student's password through this endpoint, and should
|
|
never try to invent one.
|
|
|
|
## Response
|
|
|
|
**Success — a new enrollment was created** (`201`):
|
|
```json
|
|
{ "enrollment_id": "6dd6d898-f895-477f-97d8-1e5ce89c27cd", "student_id": "student-042", "program_id": "root-kubernetes-foundations", "status": "active", "created": true }
|
|
```
|
|
|
|
**Success — this exact `event_id` was already processed** (`200`, idempotent replay — see below):
|
|
```json
|
|
{ "enrollment_id": "6dd6d898-f895-477f-97d8-1e5ce89c27cd", "student_id": "student-042", "program_id": "root-kubernetes-foundations", "status": "active", "created": false }
|
|
```
|
|
|
|
**Errors:**
|
|
|
|
| Status | Body `error` | Meaning |
|
|
|---|---|---|
|
|
| `401` | `unauthorized` | Missing/wrong `X-RootNode-Integration-Key`. |
|
|
| `400` | `event_id is required` | — |
|
|
| `400` | `program_slug is required` | — |
|
|
| `400` | `status must be one of: paid, active, confirmed, pending, awaiting_payment` | — |
|
|
| `400` | `student.email or student.login_identifier is required` | — |
|
|
| `404` | `PROGRAM_NOT_FOUND` | `program_slug` doesn't exist. |
|
|
| `409` | `PROGRAM_INACTIVE` | The program exists but isn't currently open for enrollment. |
|
|
| `500` | `internal_error` | Something unexpected broke server-side. Safe to retry (see below). |
|
|
|
|
## Idempotency and retries
|
|
|
|
**`event_id` is the whole mechanism.** Send the exact same `event_id` again (your payment
|
|
provider retried a webhook, your own backend retried a timed-out request, anything) and you get
|
|
back the **same enrollment**, `created: false`, `200` instead of `201` — never a duplicate. This
|
|
is enforced at the database level (a unique constraint), not just "best effort," so it's safe
|
|
under concurrent/parallel retries too, not just sequential ones.
|
|
|
|
**Retry policy:** on a `5xx` or a network error/timeout, retry with the *same* `event_id`
|
|
(standard exponential backoff is fine — a few attempts over a couple of minutes is plenty). On a
|
|
`4xx`, do not blindly retry — fix the request (see the error table above) or, for `401`, check
|
|
your key.
|
|
|
|
**Never generate a new `event_id` for a retry of the same real-world event.** Doing so would
|
|
create a second enrollment. Reserve a fresh `event_id` only for a genuinely new enrollment event
|
|
(e.g. a renewal, a second purchase).
|
|
|
|
## Student matching rules
|
|
|
|
RootNode looks for an existing student by `email` (case-insensitive) or `login_identifier`
|
|
(exact match), in that priority. If found, that student is enrolled. If not found, a new student
|
|
account is created automatically — this is a trusted server-to-server call from your backend,
|
|
equivalent to an instructor adding a student through the admin portal, not a public signup form.
|
|
|
|
## Minor / no-email students
|
|
|
|
RootNode explicitly supports students without their own email address (younger students,
|
|
enrolled by a parent/guardian). Send `student.login_identifier` instead of (or in addition to)
|
|
`email` — any stable string you control, such as an internal account/order reference. You must
|
|
send **at least one** of `email` or `login_identifier`; sending neither is rejected.
|
|
|
|
If the enrolled student has no email at all, send `student.contact_email` too (e.g. the
|
|
guardian's address collected at checkout) so RootNode has somewhere to deliver the activation
|
|
email — otherwise the enrollment is created but nobody is notified automatically, and an
|
|
instructor has to relay the activation link manually. `contact_email` is stored only against
|
|
that specific enrollment, never copied into the student's own identity record.
|
|
|
|
**As of Phase 7, a real guardian/parent record now exists** (`guardians`/`student_guardians` —
|
|
see `docs/STUDENT_GUARDIAN_DATA_MODEL.md`), but this endpoint's contract is UNCHANGED and does
|
|
not yet create or link one — `student.contact_email` still works exactly as described above,
|
|
stored only on the enrollment. If you need "a parent manages multiple children's enrollments" as
|
|
a first-class concept in this endpoint, talk to the RootNode team before building around a
|
|
workaround; the schema now has the real tables for it, they're just not wired into this
|
|
endpoint yet.
|
|
|
|
## Future flow: offerings and registrations (Phase 7, not yet live on this endpoint)
|
|
|
|
RootNode Academy is now (rootnodeacademy.com, live separately from this repository) building
|
|
toward representing specific purchasable deliveries — camps, cohorts, self-paced access — as
|
|
first-class **offerings**, with a **registration** step before final enrollment (see
|
|
`docs/OFFERING_AND_REGISTRATION_MODEL.md`). The eventual intended flow:
|
|
|
|
```
|
|
Website: learner selects an offering (e.g. "Winter Break Linux & Systems Camp")
|
|
-> Website collects registration info (student/guardian, readiness, agreements)
|
|
-> Website checkout/payment (future — not built yet)
|
|
-> Verified commerce event -> RootNode confirms the registration
|
|
-> RootNode activates the enrollment (reusing this exact endpoint's machinery)
|
|
-> Student receives their existing activation email, unchanged
|
|
-> Student/camp access begins
|
|
```
|
|
|
|
**This is documentation of an intended future shape, not a live contract change.** The endpoint
|
|
below is unaffected — it continues to work exactly as documented, program-slug-based, with no
|
|
concept of an offering. A future phase will either extend this endpoint with an optional
|
|
`offering_slug` field or add a parallel endpoint once the website's actual checkout flow is
|
|
designed; that decision has not been made. Idempotency (`event_id`) will remain the mechanism
|
|
either way.
|
|
|
|
## What the student experiences after you call this (as of Phase 5C)
|
|
|
|
If `status` mapped to "active": RootNode sends the student (or their enrollment's
|
|
`contact_email`) a real email automatically —an activation email with a one-time setup link if
|
|
they're new, or an enrollment confirmation if they already have a RootNode account — and they
|
|
can log in as soon as they act on it. **You do not need to send any email yourself for this** —
|
|
see "Responsibility boundary" above. If `status` mapped to "pending": the enrollment exists but
|
|
the student can't start until a RootNode instructor activates it — useful if your flow needs a
|
|
manual review step before access is granted; no email goes out until activation happens.
|
|
|
|
If the student has neither an `email` nor a `contact_email` on file, RootNode still creates the
|
|
account and enrollment, but nothing gets emailed automatically — an instructor has to relay the
|
|
activation link by hand. Send whichever address you have whenever you have one.
|
|
|
|
## Local/staging testing
|
|
|
|
Set `INTEGRATION_SHARED_SECRET` in your test environment's config (in local dev,
|
|
`platform/.env`; example dev value used by this repo's own test suite:
|
|
`dev-integration-secret-not-for-production` — never use this value anywhere real). Then:
|
|
|
|
```bash
|
|
curl -s -X POST http://localhost:4200/api/integrations/enrollments \
|
|
-H 'Content-Type: application/json' \
|
|
-H 'X-RootNode-Integration-Key: dev-integration-secret-not-for-production' \
|
|
-d '{
|
|
"event_id": "test-order-001",
|
|
"source": "website",
|
|
"program_slug": "root-kubernetes-foundations",
|
|
"status": "paid",
|
|
"student": { "email": "[email protected]", "display_name": "Test Student" }
|
|
}'
|
|
```
|
|
|
|
All example data on this page is fake — do not reuse these values against a real deployment.
|
|
|
|
## Production secret handling
|
|
|
|
- The production `X-RootNode-Integration-Key` value must be requested from the RootNode team
|
|
and stored in your backend's secret manager/environment configuration — never in source
|
|
control, never in a browser-reachable config, never logged.
|
|
- RootNode's own logs and audit trail never record the secret's value, only that an
|
|
authenticated call happened (or a `401` if it didn't).
|