New: PHASE9_COMMERCE_AND_LAUNCH.md, WEBSITE_REGISTRATION_AND_CHECKOUT.md, STRIPE_INTEGRATION_RUNBOOK.md, WINTER_BREAK_LAUNCH_CHECKLIST.md. Updated: ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md (commerce boundary + PHASE 9 BUILT rows), OFFERING_AND_REGISTRATION_MODEL.md (pricing, the payment/commerce boundary, three-status principle), WEBSITE_ENROLLMENT_INTEGRATION.md (price fields, real agreement/checkout/webhook sections replacing Phase 8 placeholders), WINTER_BREAK_CAMP_OPERATIONS.md (real payment step replacing the manual placeholder).
11 KiB
Website → RootNode Registration & Checkout
Audience: whoever builds rootnodeacademy.com's registration/checkout flow for a paid offering
(Winter Break Camp and anything after it). This is the Phase 9 companion to
docs/WEBSITE_ENROLLMENT_INTEGRATION.md — read that document first for the shared trust
boundary, authentication header, and the base /api/integrations/registrations contract. This
document covers everything Phase 9 adds on top: pricing display, the agreement step, checkout,
the success page, and status lookup.
The end-to-end flow
1. GET /api/public/offerings/:slug — display program, price, availability (no auth, browser-safe)
2. POST /api/integrations/registrations — submit the registration (server-to-server)
3. POST /api/integrations/registrations/:id/agreements — accept the required agreement(s)
4. POST /api/integrations/registrations/:id/checkout — create a hosted checkout session
5. redirect the customer's browser to the returned checkout_url
6. RootNode's webhook confirms payment — NOT something the website calls
7. GET /api/integrations/registrations/:id/status — poll from the success page
Steps 2-4 and 7 are server-to-server only, using the same X-RootNode-Integration-Key header as
every other integration route. Steps 1 and 5 are the only things that ever touch the customer's
browser directly.
1. Price and availability display
GET /api/public/offerings/:slug
No auth needed. As of Phase 9, the response includes:
{
"price_amount": 24900,
"price_currency": "usd",
"requires_payment": true
}
price_amount is an integer in minor currency units (cents) — divide by 100 and format with
price_currency for display ($249.00). price_amount: null means a free offering —
requires_payment is false and you should skip steps 3/4/5 entirely (call checkout on a free
offering and you'll get 409 NO_PRICE_CONFIGURED; there's nothing to check out).
This price is display-only. The actual amount charged always comes from the SAME offering row, read server-side at checkout-creation time (step 4) — there is no field anywhere in this contract for the website to submit an amount. A browser (or a compromised website backend) cannot pay a different price than what's configured on the offering.
2. Submit the registration
Unchanged from docs/WEBSITE_ENROLLMENT_INTEGRATION.md §2 — adult or minor/guardian flow, same
event_id idempotency. You'll get back a registration_id; hold onto it for every step below.
3. Accept the agreement(s)
POST /api/integrations/registrations/:id/agreements
Header: X-RootNode-Integration-Key: <secret>
Body: { "agreement_id": "winter-camp-waiver-2026" }
Ask RootNode for the current agreement_id(s) configured on the offering — there is no
"list required agreements" endpoint on this public contract yet; if your flow needs to render the
agreement text/checkbox dynamically, ask the RootNode team before building a workaround.
Which party is recorded as accepting is server-derived from the registration's own
is_minor (set at submission in step 2) — you never send who's accepting. 201 on success:
{ "accepted": true, "agreement_id": "winter-camp-waiver-2026", "accepted_version": 1 }
404 AGREEMENT_NOT_FOUND if the id is wrong or not configured on this offering.
Checkout is blocked until every required agreement is accepted (409 AGREEMENTS_INCOMPLETE
from step 4). Call this before checkout, always.
4. Create the checkout session
POST /api/integrations/registrations/:id/checkout
Header: X-RootNode-Integration-Key: <secret>
Body: { "success_url": "https://rootnodeacademy.com/enroll/success", "cancel_url": "https://rootnodeacademy.com/enroll/cancelled" }
Both URLs are yours — RootNode's payment provider redirects the customer's browser back to
one of them after checkout. success_url and cancel_url are required; nothing else is. There
is no amount field — see "Price and availability display" above.
Success (201):
{ "checkout_url": "https://checkout.stripe.com/c/pay/cs_test_...", "transaction_id": "..." }
Redirect the customer's browser to checkout_url immediately (a 302, or a client-side
window.location). In local/staging use with the deterministic test provider (see
docs/STRIPE_INTEGRATION_RUNBOOK.md), checkout_url is a fake URL pointing straight back at
your own success_url with a ?test_session=... param appended — there is no real hosted page
to visit, which is fine for automated testing but means you can't manually click through a
"real" checkout page against the test provider.
Errors:
| Status | error |
Meaning |
|---|---|---|
401 |
unauthorized |
Missing/wrong integration key. |
400 |
VALIDATION |
success_url/cancel_url missing. |
404 |
NOT_FOUND / OFFERING_NOT_FOUND |
Bad registration id, or its offering vanished. |
409 |
NO_PRICE_CONFIGURED |
This offering is free — don't call checkout for it. |
409 |
AGREEMENTS_INCOMPLETE |
Call step 3 first. |
409 |
INVALID_STATE |
The registration is already past the point where checkout makes sense (e.g. already confirmed). |
409 |
OFFERING_NOT_OPEN / OFFERING_CANCELLED / OFFERING_COMPLETED / OFFERING_FULL / REGISTRATION_NOT_YET_OPEN / REGISTRATION_CLOSED |
The offering/registration window state changed since step 2. |
503 |
PROVIDER_NOT_CONFIGURED |
RootNode's payment provider isn't configured yet — a RootNode-side problem, not yours. Show a generic "checkout is temporarily unavailable" message and alert RootNode. |
5. Payment confirmation — never trust the redirect
RootNode's own webhook is the only thing that ever confirms payment. When the payment
provider redirects the customer back to your success_url, that redirect proves nothing on its
own — a customer can reload, bookmark, or manually construct that URL without ever paying, and a
successful redirect can race ahead of the webhook actually being processed. Never render "you're
enrolled" or grant anything based on the URL/query string alone.
6. Building a safe success page
GET /api/integrations/registrations/:id/status
Header: X-RootNode-Integration-Key: <secret>
Call this from your OWN backend (never expose the integration key to the browser) when the
customer lands on your success page, and poll it every couple of seconds for a short window (the
webhook is normally processed within a second or two of the customer completing checkout — see
docs/STRIPE_INTEGRATION_RUNBOOK.md for real-world Stripe delivery latency). Response:
{
"registration_id": "6dd6d898-f895-477f-97d8-1e5ce89c27cd",
"status": "confirmed",
"payment_status": "paid",
"enrollment_activated": false
}
Deliberately minimal — no name, no email, no amount, no provider reference. Enough to render one of three states on your success page:
payment_status: "paid"(orstatus: "enrollment_activated") — "You're all set — check your email for your RootNode Academy activation link."payment_status: "pending"or missing,statusstill pre-confirmation — "Processing your payment — this page will update automatically" (keep polling, or tell the customer to check back / check email shortly).payment_status: "failed"— "Your payment didn't go through. You can try again;" — for a priced offering, a failed payment does NOT release the seat, so the SAME registration can retry checkout (back to step 4) without losing its place.
A cancelled/expired checkout (customer closed the tab, or the provider's own session timeout)
shows up as status: "cancelled" once RootNode's reservation reaper or the provider's own
"expired" webhook processes it — at that point the seat has been released and the customer needs
to start a new registration if they still want in.
401 (bad/missing key) and 404 NOT_FOUND (bad registration id) are the only error responses.
7. What you must NOT do
- Never store, log, or transmit card numbers, CVVs, or any other payment-card data — this contract has no field for it anywhere, on purpose. RootNode never touches card data either; Stripe's hosted Checkout page collects it directly.
- Never send an
amount/pricein the checkout request — there is no such field, and if you add one to your own request body it is silently ignored (the price always comes from the offering). - Never build your own "payment succeeded" email — RootNode's activation/confirmation email
fires automatically on webhook-confirmed conversion (same machinery
docs/ WEBSITE_ENROLLMENT_INTEGRATION.mdalready documents). A second email from your side would duplicate or conflict with it. - Never poll the status endpoint indefinitely/aggressively — a reasonable few-second interval for a short window (a minute or two) covers real webhook latency; beyond that, tell the customer to check their email and stop polling.
Accessibility and loading-state notes
- The redirect to
checkout_urlin step 4 is a full navigation, not a modal — treat it like any other outbound link (announce the navigation to assistive tech, don't trap focus expecting a return). - The success page in step 6 has three real states to design for (processing/confirmed/failed), not two — see step 6's own state list. Design a genuine "still processing" state; don't leave the customer looking at a page that appears frozen while you poll.
- Every checkout/agreement error in the tables above has a distinct, human-meaningful cause — map each to a real, specific message rather than one generic "something went wrong."
Test-mode flow (what to actually test against)
Local/staging integration testing uses PAYMENT_PROVIDER=test (RootNode's own deterministic
provider, no Stripe account needed) — see docs/STRIPE_INTEGRATION_RUNBOOK.md for how to drive a
full checkout → webhook → confirmation cycle against it, including simulating a failed payment
and an abandoned checkout. Once RootNode has real Stripe credentials, staging can switch to
PAYMENT_PROVIDER=stripe in Stripe's OWN test mode (fake test cards, e.g. 4242 4242 4242 4242)
for a true end-to-end rehearsal before anything goes live with real money.
Privacy note
The only personal data this contract ever sees is what docs/WEBSITE_ENROLLMENT_INTEGRATION.md
already documents for registration (names, emails, phone) — nothing payment-specific is added.
Whatever billing/contact information Stripe's hosted Checkout collects from the customer (name,
email, card) stays with Stripe; RootNode never receives or stores it beyond the transaction
metadata docs/PHASE9_COMMERCE_AND_LAUNCH.md describes (amount, currency, status, timestamps,
provider reference — never card data).