Registration & Checkout

Registration & Checkout

Everything from "I want to sign my kid up" through to "I need to swap which week we booked": tickets and capacity, payments, add-ons, coupons, refunds, rosters, and the parent's ongoing self-service. Whichever way you integrate, we own the money, the records and the compliance.

Overview

What it covers

What changes between integration options is who renders the pages and where the buyer is standing when they pay. What doesn't change is everything underneath.

Two checkout options: single camp or multi-camp. Preview both experiences, then customize the optional cart to match your site.

We handle these no matter which option you pick

Payments, refunds and payouts
Capacity, holds and waitlists
Rosters and registration records
Clear2Join clearance collection
Receipts and transactional email
Recurring memberships and their billing
The Customer Portal for your staff
Card data — it never reaches your page or servers

Your options

Three ways in. If you'd rather see them as complete paths than as a menu, skip to the examples — or see all three side by side on Ways to integrate.

Way Status What you get What you build
Hosted Live A server-rendered storefront and booking flow on our domain, plus the org-scoped parent portal for everything after the sale. Links from your site.
Embedded / SDK Live The checkout drawer as an overlay on your page, server-rendered event-card HTML you inject, and chrome-free iframe pages. Your site and its layout; a button.
API Live — reads Public, unauthenticated JSON for events, pricing options, custom questions and checkout configuration. Your entire discovery UI. The transaction still runs through the drawer — see why.

We render it

Hosted

You send people to us. We host the pages and own the experience and the buyer's account — your integration is a link.

Hosted

Storefront & booking Live

Server-rendered, crawlable pages on our domain, scoped to your organization. Link to them from your navigation and you have a working registration business.

my-v2.thegoodgame.com/s/org/…
Your events and your logo, on our page. Everything inside the pink outline is rendered by us.
Route What it is
{WEB_BASE_URL}/s/org/{organization_id} Your event listing — every published event across every coach in your org.
{WEB_BASE_URL}/s/org/{organization_id}/event/{event_id} Event detail: schedule, venue, pricing, description, FAQs, and a Book CTA.
{WEB_BASE_URL}/s/e/{slug} The same detail page on a vanity slug, for clean marketing URLs.
{WEB_BASE_URL}/s/event/{event_id}/book The hosted booking handoff the Book CTA leads to — sign-in, athlete selection, clearance and payment. You never link this directly; the detail page does.

These render without JavaScript and are indexable — but the SEO credit lands on our domain. If you want it on yours, use event-card fragments or the event API instead.

Hosted

Customer Portal — your team's console Live

The operator side, and the part partners most often don't realise they're getting. Your own staff sign in at app-v2.thegoodgame.com and run the whole business from there. It comes with every integration option — you never build admin screens.

Build and publish events, with pricing, capacity and registration windows
Event calendar across every coach in the organization
Participant rosters, check-in, team and dorm assignments
Create and manage memberships
Coupons, promotions and add-ons
Athletes and their Clear2Join Passport status
Coaches, staff, org members and role-based permissions
Earnings, payouts, bank accounts and disbursements
Locations and venues
Organization branding and event-portal settings
Operator dashboard with revenue targets and performance
Oversight and safe-sport review queues

Everything your team creates here is what the public API and the checkout drawer then expose to your customers — one source of truth, two audiences. See it on Ways to integrate.

Hosted

Org-Scoped Parent Portal Live

Checkout is the transaction. The portal is everything after it — and that's where most of the support burden actually lives. A parent who bought from you gets a self-service account, branded as your organization, that only ever shows them your events.

GET {WEB_BASE_URL}/org/{organization_id}/manage

Add it to your account menu as "Manage my registrations" and you're done. No hosting, no build step, no data sync. It pairs with either the hosted storefront or the embedded drawer.

Status

Live in production. The portal and its /pp/org/:orgId/* API are both deployed — open {WEB_BASE_URL}/org/{organization_id}/manage with your own org id and you'll be redirected to your organization's own branded sign-in, logo and all. You need an organization id from us and at least one published event for it to have anything to show.

What a parent can do

Page Path What's there
Home /manage An "action needed" hero with outstanding requirements, upcoming events, and per-athlete status.
My Bookings /manage/bookings Every booking with your org — upcoming, past, filterable by athlete.
Booking detail /manage/bookings/{booking_id} Per-athlete registrations, outstanding requirements with a "complete now" drawer, live add-on purchase, payment summary and history, transfer requests, and cancellation requests.
My Athletes /manage/athletes Every athlete the parent has registered with your org.
Athlete detail /manage/athletes/{athlete_id} Tabs for Profile, Bookings, Clear2Join Passport, Membership, and Forms. Emergency contacts, documents, medical questions, and consent all live here.
My Memberships /manage/memberships Active membership subscriptions with self-service cancel. Hidden entirely unless your org publishes memberships.
My Wallet /manage/wallet Saved payment methods — add, remove, set default.
My Profile /manage/profile Name, email and phone changes (both verified), and account deletion.
Sign in /manage/signin Your-branded sign-in. Accepts ?redirect= so you can deep-link straight to a page behind auth.

Getting a buyer from checkout into the portal

The checkout and the portal are both served from {WEB_BASE_URL} — the same origin, no matter whose site the checkout was embedded on. So the account a buyer creates while checking out is their portal account. There's no user record to sync, no SSO handshake to implement, no identifier to map, and you never hold a credential.

Under the hood a login on either surface sets an HttpOnly refresh cookie (tgg_rt), and the other surface silently redeems it on boot via POST /auth/session — both halves are live in production, so a buyer who signed in during checkout normally lands in the portal already authenticated.

Best-effort, not guaranteed. The cookie is third-party from your page, so browsers that partition storage may withhold it until the drawer is granted Storage Access. Then the parent signs in once at the org sign-in screen. Treat a sign-in prompt as a normal outcome, not a bug — the deep links work either way.

URLs
# The portal home — put this in your account menu
{WEB_BASE_URL}/org/{organization_id}/manage

# Straight to one booking — use after a checkout completes
{WEB_BASE_URL}/org/{organization_id}/manage/bookings/{booking_id}

# Behind auth, surviving the sign-in bounce
{WEB_BASE_URL}/org/{organization_id}/manage/signin?redirect=%2Forg%2F{organization_id}%2Fmanage%2Fbookings

To get {booking_id} at the moment of purchase, listen for booking-complete on the drawer — see the JavaScript API. Branding and feature toggles are set on your organization record.

Org scope is a view filter, not the security boundary

Every read is filtered by the signed-in parent's own user id and your organization id. A parent cannot see another parent's data, and your org's portal cannot surface another organization's bookings — even if someone edits the URL. The organization id in the path is not a credential and grants nothing on its own.

Hosted

Memberships Live

Recurring revenue alongside one-off bookings. A membership is a subscription your organization publishes; families buy it once and it then covers the events you say it covers. Billing, renewals and cancellation all run on our side.

What you configure Effect
Coverage All of your events, a named list of events, or every event of a given type.
Price & billing Backed by a Stripe product; renewals and failed payments are handled for you.
Multi-athlete discount A percentage off for families enrolling more than one athlete.
Booking cap Optional maximum bookings per billing period.
Requirements & questions Its own participation requirements and custom questions, collected at signup.
Venue / activity scope Restrict a membership to one venue or activity where that makes sense.

Where each side manages it

  • Your team creates and publishes memberships in the Customer Portal.
  • Families see and cancel theirs in the Org-Scoped Parent Portal. The page is hidden entirely for organizations that don't publish any, so it never shows an empty screen.

Memberships are configured in the portal rather than over the API today — there's no public membership endpoint yet. Tell us if you need one.


Our UI in your page

Embedded / SDK

Our UI runs inside your page. You add a script tag; we render the flow. Still no payment handling and no card data in your stack, but the visitor never leaves your site.

Embedded / SDK

Checkout drawer Live

One script tag and one button attribute puts our entire registration and payment flow on your site. It opens as a full-viewport overlay on top of your existing page, so you don't need a dedicated event-detail page — trigger it from a listing, a hero CTA, a card, anywhere.

Because everything inside the drawer is served and processed by us, you stay out of payment handling entirely — card data never touches your page or your servers.

Where the card actually goes

Card fields are Stripe Elements rendered inside our cross-origin iframe, on our domain. Your page can't read them, and nothing card-shaped reaches your servers. The charge is created on our Stripe account, so we are the merchant of record on every transaction and the settlement, chargeback and card-data obligations are ours.

What that means for your own PCI paperwork depends on your setup, so confirm it with your acquirer or assessor rather than taking a number from us. We'll give them whatever they need about our side.

https://riversidesports.com/summer-camps
Your markup, your styling Rendered by The Good Game

What's inside the drawer

Ticket and session selection with live remaining capacity and per-option quantity caps
Sold-out handling with waitlist join where the event allows it
Registration windows enforced (opens-at, closes-at, manually closed)
Account step: email lookup → sign in or register, plus Google and Apple sign-in
Email OTP verification and SMS phone verification
Participants: pick saved athletes or add new ones, assigned per ticket
Eligibility gating on age range and gender policy
COPPA parental-consent flow for under-13 athletes
Clear2Join requirements inline — see Safety & Clearance
Per-event custom questions
Policy acceptance — informational, checkbox, or drawn signature
Per-athlete add-ons and coupon codes
Payment: card, Apple Pay, Google Pay, Klarna pay-in-4, and the parent's saved cards
Capacity hold with a visible countdown — 15 minutes by default, configurable per event
About and Venue tabs: description, what athletes receive, sponsors, FAQs, venue and map
Confirmation screen and emailed receipt
Your org's logo and name throughout; localized copy
Meta Pixel purchase conversion, with fbclid and _fbp forwarded from your page

Install

HTML
<!-- once, before </body> -->
<script src="https://my-v2.thegoodgame.com/embed.js" defer></script>
Easiest — declarative
<!-- auto-wired on DOMContentLoaded -->
<button data-tgg-checkout data-event-id="{event_id}">
  Register Now
</button>
Or programmatic
<button onclick="TGGCheckout.open({ eventId: '{event_id}' })">
  Register Now
</button>

Testing on staging

Swap the script src to https://staging.my-v2.thegoodgame.com/embed.js and use a staging event ID. The loader derives everything else from its own src.

Checkout must be enabled

The event needs a checkout configuration in TGG. Without one the drawer shows a "registration isn't open" message. Ask us to enable checkout for your events.

Rendering your own pricing first

Read the same bootstrap the wizard uses via the checkout-config endpoint — sessions, prices, remaining capacity, sold-out and free flags, and the registration window.

Embedded / SDK

Two checkout options. Choose what fits your organization.

Use single-camp checkout, or enable multi-camp checkout with a customizable cart. Select an option below to see the buyer experience.

These buttons only switch the demo. Your real checkout mode is controlled by the organization’s Multi-Camp Cart setting in TGG admin, not a code flag. See the full settings and cart reference.

Embedded / SDK

Drawer JavaScript API Live

The loader exposes window.TGGCheckout.

Method Description
open({ eventId }) Opens the checkout drawer for the given event. eventId is the only required input.
close() Closes the drawer and returns the page to click-through. Checkout state is preserved, so re-opening resumes where the buyer left off.
init({ eventId, portalOrigin }) Optional pre-warm — creates the overlay and loads checkout assets ahead of the first click, so the drawer opens instantly. Pass eventId when the page is about one event. portalOrigin overrides the origin derived from the script src (rarely needed).
on('booking-complete', cb) Fires when a booking is created. Callback receives { bookingId } — use it to fire a pixel, record a conversion, or deep-link into the portal.
on('close', cb) Fires when the buyer dismisses the drawer.
on('view-registration', cb) Fires when the buyer clicks View registration on the confirmation screen. Callback receives { bookingId }, and the drawer closes itself first — so this is your hook for routing them somewhere. Send them to your own account page, or straight into the parent portal booking. If you don't listen for it, the drawer simply closes and the buyer is left on your page.
cart.add(eventId, optionId?, qty?, title?) Adds a camp to the multi-camp cart. optionId is optional — a camp with one ticket option resolves itself, and a camp with several asks the buyer in the drawer. qty defaults to 1; adding the same camp and option again raises its quantity. title is what the cart shows; omit it and the cart shows Camp. Returns the new seat count.
cart.remove(eventId, optionId?) / cart.clear() Removes one option, a whole camp (no optionId), or empties the cart. cart.remove returns the new seat count.
cart.items() / cart.count() The cart's lines, as { eventId, optionId, qty }, and its seat count. The cart stores IDs and quantities only — never prices, so nothing on your page can disagree with what the card is charged.
cart.widget({ … }) Restyles the cart we draw after init — the same options as init({ cartWidget }). It cannot switch the cart on or off.
openCart() Opens the drawer on the whole cart: several camps, one checkout, one charge. Returns false and opens nothing when the cart is empty (and fires cart with empty: true).
on('cart', cb) Fires on every cart change with { items, count } — use it to drive your own cart badge.

data-org / open({ org }) is accepted and reserved for multi-org hosts, but does not yet change behavior — pass eventId only.

Choose your checkout mode

Your organization can use either of these two checkout modes:

Mode What the buyer can do
Single-camp checkout (default) Register and pay for one camp per checkout.
Multi-Camp Cart (optional) Put several camps from your organization in one cart, then check out once — one drawer and one card charge.

In Multi-Camp Cart mode, the buyer picks a session and tickets for each camp, then registers each participant for the camp they are attending. If your organization has a multi-camp discount, the cart earns it.

This is an organization setting in TGG admin. The Multi-Camp Cart setting is off by default: off means single-camp checkout; on means Multi-Camp Cart. It controls the whole organization — our cart button, your Add to cart buttons and multi-camp checkout together. It is not a per-event setting or a code flag. Ask your TGG contact to turn this setting on or off. Once enabled, your existing embed.js shows the cart with no deploy on your side.

The no-JavaScript integration. Tag your camp cards and we do the rest — we bind the clicks, hold the cart across pages, draw the cart, and open checkout:

HTML
<!-- One per camp card. data-option-id, data-qty and data-title are optional. -->
<button data-tgg-add-to-cart data-event-id="{event_id}" data-title="{camp_name}">
Add to cart
</button>

<!-- Optional: your own "View cart" entry point and a count to keep in step. -->
<button data-tgg-open-cart>Checkout</button>
<span data-tgg-cart-count>0</span>
Attribute What it does
data-tgg-add-to-cart A click adds the camp named by data-event-id (required). data-option-id picks the ticket option, data-qty the number of seats (default 1), and data-title the name the cart shows — without it we use the nearest heading in the card. Hidden until we know your organization has the cart, and for good if it does not.
data-tgg-open-cart A click calls openCart().
data-tgg-cart-count Its text is kept equal to the cart's seat count.

Cards that mount after the script (any single-page storefront) are handled — the clicks are delegated, so a camp card rendered on a later route works with no re-binding. The cart lives in your site's localStorage, so it follows the buyer from page to page on your domain.

The cart we draw. When the setting is on and the cart has something in it, a round cart button with a count badge appears on your page; it opens a panel listing the camps with a Remove link and a Check out button. You can restyle it to match your site — but not switch it on or off, which stays with the organization setting:

JavaScript
TGGCheckout.init({
cartWidget: {
  accent: '#0077FF',      // count badge + Check out button
  buttonColor: '#111',    // the round cart button
  iconColor: '#fff',      // the cart icon; omit it and we pick white or near-black to suit buttonColor
  position: 'auto',       // the default — or bottom-right / bottom-left / top-right / top-left
                          // / top-center / bottom-center
  label: 'Your camps',    // the panel's heading
  checkoutLabel: 'Check out',
},
});

// Or restyle it later, after init:
TGGCheckout.cart.widget({ buttonColor: '#ffffff', position: 'bottom-left' });

position: 'auto' starts bottom-right and moves out of the way of whatever your page already pins to its corners — a chat launcher, a cookie banner, a sticky header — trying bottom-right, bottom-left, then the top two corners. Next to a chat launcher it lines up above it. A position you name is kept: we only lift the button within it, never move it elsewhere. Colours must be plain CSS colours; anything else is ignored.

Prefer your own cart UI? Drive it from cart.* and on('cart', …) and call openCart(). Our cart button still appears while the setting is on — it cannot be hidden from your code — so restyle and place it to sit alongside yours.

Opening a cart from a link. There is no URL parameter on your page that fills the cart — the loader does not read one. To send a buyer straight into a multi-camp checkout, read your own parameter on page load and call cart.add(…) for each camp, then openCart().

When your organization does not have the cart
  • Our cart button is never drawn, and every data-tgg-add-to-cart button is hidden.
  • cart.* still stores camps, and on('cart', …) still fires — so a cart built by your own code can still reach the drawer.
  • The drawer refuses a cart of two or more camps: it says {organization} takes one camp per checkout, holds Continue, and the Remove link on each camp gets the buyer back to one camp, which checks out as normal.
  • The order API answers 403 to an order of two or more camps.
Camps from more than one organization

One checkout is always one organization's camps. If a cart mixes organizations, the cart panel warns before checkout that the camps cannot be paid for together, the drawer refuses with These camps are run by different organizations and holds Continue until the buyer removes camps, and the order API answers 400.

Discounts in a cart

Automatic discounts are set up on your organization's Discounts page (Organization → Money → Discounts in your TGG console, or by your TGG contact) and on each event's Discounts section in the event builder. Nothing in your code sets them. They come off in a fixed order, and the summary lists them in that order: Early bird, Sibling discount, Package discount, Multi-camp discount, then any coupon.

  • Multi-camp discount. Money off for booking several camps in one checkout — for example 2 camps → 5% off, 3 camps → 10% off. The best tier the buyer reaches applies. It counts camps by default (two children in one camp are already served by the sibling discount), a free camp does not count, and it can be limited to named camps or event types. It is taken off either the full ticket price or the price after the discounts above it (sibling and package). It can run between two dates; outside them it does not apply. The drawer shows it as its own line, such as 2 camps — 5% off, and as Multi-camp discount on the confirmation.
  • Early bird and sibling discounts apply per camp, exactly as they do in a single-camp checkout: an early bird shows as an Early bird line under the ticket price, a sibling discount as Sibling discount.
  • The next rung. When the cart is below a higher rung, the payment step says so — Add 1 more camp and save 10% — with an Add a camp button. The button closes the drawer: the cart lives on your page, so the buyer is back at your camps with the cart intact, and your cart button reopens it.
  • Coupons. A code applies only to the camps it is valid for, and the summary names the camps it came off. Each coupon is set, when it is created, to apply Once per checkout (one use; a fixed amount comes off once, split across the camps it covers) or Each camp (one use per camp; a fixed amount comes off each). A percentage comes off each camp it covers either way.
Payment, confirmation and email

The buyer authorizes one charge for the whole cart. When a camp is sold on a deposit or instalment plan, that charge takes its deposit or first instalment alongside the full price of the other camps, and the rest of that camp's plan is billed on its own schedule. After payment the drawer shows an order summary for the cart — subtotal, discounts, fees, total, paid today and any balance due later — with a link to each camp's registration.

booking-complete fires once for the whole cart, with the first camp's booking, and the cart is emptied. The buyer gets one order confirmation (email, SMS, push and in-app) once every camp is confirmed, listing each camp with its dates, location and add-ons, each discount on its own line, the order total and what was charged today. It replaces the per-camp booking confirmation for those bookings; each camp's coach still gets their own booking notification.

JavaScript
// embed.js is deferred, so wait for load before touching TGGCheckout.
window.addEventListener('load', function () {
  TGGCheckout.on('booking-complete', function (e) {
    dataLayer.push({ event: 'registration_complete', booking_id: e.bookingId });
  });

  TGGCheckout.on('close', function () {
    console.log('checkout dismissed');
  });

  // Optional: pre-warm on an event page so the first open is instant.
  TGGCheckout.init({ eventId: '{event_id}' });
});

How the embed behaves on your page

Useful when you're debugging z-index, CSP, or analytics on a host page.

Detail Behavior
Overlay A single fixed, full-viewport, transparent <iframe id="tgg-ec-iframe"> at z-index: 2147483000, with allow="payment". The drawer and its dimming scrim are rendered inside the iframe.
When closed The iframe stays in the document but is pointer-events: none, so your page remains fully usable. It is never display: none-toggled — that's what keeps the open animation smooth.
Layout impact None. The overlay never resizes, never height-syncs, and never scrolls your page.
Messaging postMessage handshake: the embed sends tgg:ready, the loader sends tgg:open, and the embed replies with tgg:close / tgg:booking-complete. Both sides pin the exact peer origin — nothing is broadcast to *.
Redirect payments Klarna redirects away and back. The loader sets the return URL to your page, then re-opens the drawer straight onto the confirmation and scrubs its own query params via replaceState. Your URLs stay clean.
Ad attribution fbclid (from your page URL) and the _fbp cookie are forwarded into the iframe so the Purchase conversion attributes correctly.
Embedded / SDK

Event-card fragments Live

The most interesting option if you care about search: we render just the event-card grid, with no page chrome, and you inject it server-side into your own template. Google crawls your URL and sees fully-rendered listings.

https://riversidesports.com/camps?city=Fort+Worth
Your header, your hero, your filter form — our card grid dropped into the middle of it, served from your domain.
GET {WEB_BASE_URL}/s/org/{organization_id}/events/fragment
  1. Fetch it server-side when your page renders
    cURL
    curl "{WEB_BASE_URL}/s/org/{organization_id}/events/fragment?city=Fort%20Worth&sport=Tennis&size=12"
  2. Extract and inject the grid

    The response is an HTML document; the piece you want is the single element marked data-tgg-fragment="events". It carries data-tgg-total, data-tgg-page and data-tgg-size so you can render your own pagination.

    HTML (returned)
    <div data-tgg-fragment="events" data-tgg-total="192" data-tgg-page="0" data-tgg-size="12">
      <div class="grid …">
        <!-- one rendered event card per event -->
      </div>
    </div>
  3. Style it and wire the buttons

    Scope your CSS to [data-tgg-fragment], then add data-tgg-checkout data-event-id="…" to the Register buttons so they open the drawer.

Fragment filters (this is the SSR fragment vocabulary, not the JSON API's — see Event list for that one): city, market, zipCode, sport, activityId, eventType, q, minPriceCents, maxPriceCents, minAge, maxAge, genderPolicy, tag, dateFrom, dateTo, sort, page, size. Status is checked against an allowlist, so ?status= cannot reach drafts or cancelled events; ?sort=past and ?sort=all widen it to include completed. Visibility is a separate axis — the fragment forces public-only, unlike the JSON list above.

Embedded / SDK

Chrome-free iframe pages Live

If you'd rather embed whole pages than build them, the server-rendered routes have an /embed variant with our site chrome stripped, sized to drop into an iframe on your layout.

Route What it renders
{WEB_BASE_URL}/s/embed/org/{organization_id} Your event listing, no header or footer.
{WEB_BASE_URL}/s/embed/org/{organization_id}/event/{event_id} One event's detail page, no header or footer.
{WEB_BASE_URL}/s/embed/e/{slug} The same, addressed by vanity slug.

An iframe is opaque to crawlers. If search matters, prefer fragments, which put the markup in your own document.


You render it

API

Read-only, unauthenticated, no key to request — so you can call them from a server-rendered page with no secret management. Call them from your server, not the browser: they send no CORS headers, so a fetch from your own origin fails. Read the callout below before you forward any part of a response onward: today these endpoints return more than the catalogue.

Select fields server-side — never proxy a raw payload

These endpoints are unauthenticated and currently return three things beyond the catalogue. Participant identities: participantsPreview on event detail, and enrolledAvatars on the list, carry enrolled athletes' first and last names. Commercial totals: event detail returns revenue plus per-option collectedCents, chargedCents, coachRevenueCents, payoutCents, netCollectedCents and refundedCents. Private events: the list includes events marked private unless you pass isPrivate=false.

So build an explicit allowlist of the fields you need, server-side, and send only those to the browser. Proxying the object through — or fetching it from client-side JavaScript — publishes your enrolled families' names and your own per-event takings. We are tightening these responses; the allowlist is what makes your integration safe either way.

Exposure is configurable per organization, so a documented field can be present for one org and absent for another. Treat every field as optional — see the response envelope notes.

API

Event list Live

Every published event across every coach in your organization. Note the default includes events marked private — pass isPrivate=false for only the publicly-listed ones.

GET {API_BASE_URL}/api/v1/organizations/{organization_id}/events
cURL
curl "{API_BASE_URL}/api/v1/organizations/{organization_id}/events?size=20&sportName=Tennis"
Query param Notes
page, size Zero-indexed pagination. Defaults 0 and 10.
status Event status filter.
type Event type, e.g. Camp.
title Substring match on the event title.
sportName Sport / activity name.
location, zip Location filters.
isPrivate Include or exclude private events.

Two things about this response that will trip you up

The list is nested one level deeper than you expect, and its total is a string, not a number:

200 OK — shape only
{
  "data": {
    "total": "20",          // string — parse it before you paginate
    "data": [ /* events */ ] // note: data.data, not data
  },
  "timestamp": "2026-08-13T16:44:41.360Z"
}

List items are a summary shape, not the detail shape. They carry snake_case fields built for cards — start_date, price_cents, max_number_of_athletes, eventAlias, bannerImageUrl, sportName — and not the nested occurrences, venue or pricingPlan you get from event detail. Render listings from the list; fetch the detail endpoint for an event page.

API

Event detail Live

GET {API_BASE_URL}/api/v1/events/{event_id}

Schedule occurrences, venue, the pricing plan with every purchase option, participation requirements, registration features, organizer, activity, FAQs, and the vanity slug. Accepts a UUID or a legacy numeric id.

200 OK — application/json (abbreviated)
{
  "data": {
    "event": {
      "id": "9f8e7d6c-0000-4111-a222-333344445555",
      "title": "Summer Tennis Camp — Session I",
      "eventType": "Camp",
      "status": "published",
      "registrationModel": "per_event",
      "timeZone": "America/Chicago",
      "pricingCurrency": "USD",
      "occurrences": [
        { "date": "2026-08-03", "startTime": "14:00:00", "endTime": "21:00:00", "status": "scheduled" }
      ],
      "venue": {
        "name": "Riverside Varsity Courts",
        "address": { "line1": "123 Main Street", "city": "Fort Worth", "region": "TX", "postalCode": "76109" }
      },
      "pricingPlan": {
        "purchaseOptions": [
          {
            "id": "dddd1111-2222-3333-4444-555566667777",
            "name": "Full Day Ticket",
            "priceCents": 64500,
            "priceCentsWithFees": 70950,
            "audienceType": "participant",
            "capacity": 50,
            "registrationCount": 12
          }
        ]
      },
      "participationRequirements": { "minAge": 5, "maxAge": 18, "genderPolicy": "any" },
      "registrationFeatures": { "capacityLimit": 100, "capacityMode": "hard_cap", "registrationOpen": true },
      "slug": "summercamp"
    }
  },
  "timestamp": "2026-08-06T17:57:02.785Z"
}

Companion reads

Endpoint Returns
/api/v1/organizations/{organization_id}/events/{event_id} The same detail, org-scoped.
/api/v1/organizations/{organization_id}/events/{event_id}/products Purchasable options for the event.
/api/v1/organizations/{organization_id}/events/{event_id}/questions The event's custom registration questions.
/api/v1/organizations/{organization_id}/events/{event_id}/addons The event's purchasable add-ons.
/api/v1/events/by-slug/{slug} Resolve a vanity slug to an event. The slug is the eventAlias field on a list item; a slug that doesn't exist returns 404.

description fields contain sanitized HTML — render as rich text or strip tags. Every monetary field is an integer in cents.

API

Pricing & availability Live

The bootstrap the checkout drawer itself uses. Read it so your own page can't advertise a session the drawer will refuse.

GET {API_BASE_URL}/api/v1/ec/events/{event_id}/checkout-config
Field What it tells you
sessions[] Each purchase option with priceCents, a human timeLabel, remainingCapacity (null = unlimited), maxQuantity, and whether it can be waitlisted.
soldOut, eventCapacityRemaining Event-level availability, so a stepper can't offer more seats than exist.
free Every session is $0, so the payment step is skipped.
registrationOpen, windowOpensAt, windowClosesAt Whether registration is open right now, and the window if one is set.
ageMin, ageMax, genderRequirement Eligibility rules — enforce them in your UI to avoid a dead end at checkout.
policies[] Each policy with its full text and how it must be accepted (informational, checkbox, or drawn signature).
hasAddons, hasCustomQuestions Whether those steps will appear in the drawer.
org, description, whatAthletesReceive, sponsors, faqs, venue Everything the drawer's About and Venue tabs show, if you'd rather render it.
waitlist Whether a waitlist is offered and which occurrence a join targets.
API

Booking writes Planned

There is no server-to-server booking or payment endpoint today. This is the one real limit on an API-only registration integration, so it's called out rather than buried.

What to do instead

Read everything you need to render the catalogue, then hand off the transaction to the checkout drawer — two lines of HTML. That's deliberate for now: it's what keeps card data and clearance capture on our side rather than yours. Example D shows exactly this combination.

A partner-key registration API is on the roadmap. Tell us if you need it, because demand is what schedules it.

The parent-facing /ec/* endpoints the drawer calls are authenticated as the parent, not as a partner — they aren't a server-to-server surface and aren't documented as one.


Examples

Complete integrations

The sections above are a menu. These are four finished meals — each one takes you all the way from nothing to a live booking, so you can pick one and follow it end to end. They're alternatives to each other, not steps.

Example A · Hosted

Link to us

Live

The fastest possible launch. You add one link to your navigation and we handle browsing, registration, payment, clearance and the parent's account.

You build
One link. Optionally a second one.
Rough estimate
Under an hour
SEO credit
Our domain
  1. Get your organization ID from us

    A UUID. Everything below is scoped to it.

  2. Link your navigation at your storefront
    HTML
    <a href="https://my-v2.thegoodgame.com/s/org/{organization_id}">Camps</a>
  3. Add a "Manage my registrations" link

    Skipping it means your team fields every change request by email.

    HTML
    <a href="https://my-v2.thegoodgame.com/org/{organization_id}/manage">
      Manage my registrations
    </a>
  4. Send us your branding

    Logo, display name, the noun you use for events, and your Meta Pixel ID. We set them on your organization record.

Example B · Embedded / SDK

Checkout on your own site

Live

The most common integration. Your pages, your brand, your traffic — visitors never leave, and you still never touch payments.

You build
Your pages + a Register button
Rough estimate
Half a day
SEO credit
Your domain
  1. Add the loader once, before </body>
    HTML
    <script src="https://my-v2.thegoodgame.com/embed.js" defer></script>
  2. Mark your Register buttons
    HTML
    <button data-tgg-checkout data-event-id="{event_id}">Register Now</button>

    The attribute is matched at click time, so buttons you render later — from a fetch, a framework, a re-render — work with no extra wiring. The click can also land on an icon or span inside the button.

  3. Handle completion and hand off to the portal

    Fire your analytics, then send the buyer straight into the booking so they can finish waivers immediately instead of abandoning them.

    JavaScript
    const ORG = '{organization_id}';
    const PORTAL = 'https://my-v2.thegoodgame.com';
    
    window.addEventListener('load', function () {
      TGGCheckout.on('booking-complete', function (e) {
        dataLayer.push({ event: 'registration_complete', booking_id: e.bookingId });
    
        document.getElementById('post-checkout-cta').innerHTML =
          `<a href="${PORTAL}/org/${ORG}/manage/bookings/${e.bookingId}">` +
          `Complete your athlete's requirements →</a>`;
      });
    });
Example C · Embedded / SDK

Our event cards, inside your page

Live

Example B without building the listing. We render the cards; you inject them into your own template, so the crawl credit lands on your domain.

You build
A filter form + a server-side fetch
Rough estimate
A day
SEO credit
Your domain
  1. Your visitor submits your filter form

    Sport, location, ZIP, dates, price — whatever your UI offers.

  2. Your server fetches the fragment and injects the grid
    cURL
    curl "{WEB_BASE_URL}/s/org/{organization_id}/events/fragment?city=Fort%20Worth&sport=Tennis&size=12"

    Take the element marked data-tgg-fragment="events".

  3. Wire the cards to the drawer

    Add the loader from example B and put data-tgg-checkout data-event-id="…" on the Register buttons.

Example D · API + embedded checkout

Build the whole storefront on our API

Live

Full control. You call our public endpoints, render your own listings and event pages in your own framework, and only hand off at the payment moment. This is the one for teams who already have a design system.

https://riversidesports.com/camps
Steps 1–2. Every pixel is yours; only the data came from us. Nothing is outlined because nothing here is ours to render.
https://riversidesports.com/camps/summer-tennis
Steps 3–4. Your event page, our drawer for the transaction — the only piece you don't own.
Uses
Event API + drawer
You build
Listings, filters, event pages
Rough estimate
A sprint
SEO credit
Your domain
  1. Pull the event list and render your own listing page
    cURL
    curl "{API_BASE_URL}/api/v1/organizations/{organization_id}/events?size=20&sportName=Tennis"

    Don't want to build cards? Swap this step for example C's fragment and keep the rest unchanged.

  2. Pull one event and render your own detail page
    cURL
    curl "{API_BASE_URL}/api/v1/events/{event_id}"
  3. Render live pricing and availability

    So your page can't advertise a session the drawer will refuse.

    cURL
    curl "{API_BASE_URL}/api/v1/ec/events/{event_id}/checkout-config"
  4. Hand off to the checkout drawer

    The one piece you don't build — and the reason card data never reaches you.

    HTML
    <script src="https://my-v2.thegoodgame.com/embed.js" defer></script>
    <button data-tgg-checkout data-event-id="{event_id}">Register Now</button>
  5. Send buyers to the portal afterwards

    Listen for booking-complete and deep-link into /org/{organization_id}/manage/bookings/{booking_id}, as in example B.

Ready to build?

Getting started tells you exactly what to ask us for — and there's a copy-paste email at the bottom of it. Or just write to us: an organization ID, an API key, a staging event to test against, or something still marked Preview or Planned.

Contact us