# 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: ```json { "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: 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: ```json { "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: 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`): ```json { "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: ``` 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: ```json { "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).