- 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.
221 lines
12 KiB
Markdown
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).
|