Files
rootnode-academy/docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md
T
ced4568 80a5f6e457 docs(phase9): document commerce integration and Winter Break launch readiness
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).
2026-08-27 09:06:04 -05:00

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" (or status: "enrollment_activated") — "You're all set — check your email for your RootNode Academy activation link."
  • payment_status: "pending" or missing, status still 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/price in 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.md already 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_url in 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).