Files
rootnode-academy/docs/WEBSITE_ENROLLMENT_INTEGRATION.md
T
ced4568 777efd168d docs(phase5c): enrollment communications documentation + Google Workspace runbook
- PHASE5C_ENROLLMENT_COMMUNICATIONS.md: the full design doc.
- RUNBOOK_GOOGLE_WORKSPACE_EMAIL.md: every REQUIRES GOOGLE WORKSPACE ADMIN ACTION step
  (mailbox, service account, domain-wide delegation scoped to gmail.send only, SPF/DKIM/DMARC),
  environment variables, testing procedure, production secret handling, rotation, revocation,
  troubleshooting, quotas.
- RUNBOOK_STUDENT_ACTIVATION.md: day-to-day operation of the pipeline from /admin.
- WEBSITE_ENROLLMENT_INTEGRATION.md: explicit website-vs-RootNode responsibility boundary,
  the new contact_email field, and closes the 'RootNode doesn't send activation email yet'
  known limitation this doc previously documented.
- RUNBOOK_SELF_PACED_ENROLLMENT.md, RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md: point at the new
  automatic-email behavior and the Resend activation control.
- REMOTE_AND_SELF_PACED_ARCHITECTURE.md, ENGINEERING_SOP.md: close the enrollment-
  communication gap; new ENGINEERING_SOP §16.
- PRODUCTION_VALIDATION_CHECKLIST.md, README.md, platform/README.md: cross-references and a
  Phase 5C production-validation section.
2026-08-25 14:45:18 -05:00

221 lines
12 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.
There is no guardian/parent account system yet — the enrollment is created directly against the
student identity you provide. If you need to support "a parent manages multiple children's
enrollments" as a first-class concept later, talk to the RootNode team before building around a
workaround; the schema has room for it but it isn't implemented.
## 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).