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.
We handle these no matter which option you pick
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. |
Hosted
You send people to us. We host the pages and own the experience and the buyer's account — your integration is a link.
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.
| 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.
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.
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.
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.
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.
# 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.
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.
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.
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.
What's inside the drawer
fbclid and _fbp forwarded from your page
Install
<!-- once, before </body> -->
<script src="https://my-v2.thegoodgame.com/embed.js" defer></script>
<!-- auto-wired on DOMContentLoaded -->
<button data-tgg-checkout data-event-id="{event_id}">
Register Now
</button>
<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.
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.
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.
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:
<!-- 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:
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-cartbutton is hidden. -
cart.*still stores camps, andon('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
403to 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.
// 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.
|
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.
-
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" -
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 carriesdata-tgg-total,data-tgg-pageanddata-tgg-sizeso 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> -
Style it and wire the buttons
Scope your CSS to
[data-tgg-fragment], then adddata-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.
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.
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.
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.
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:
{
"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.
Event detail Live
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.
{
"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.
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.
| 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. |
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.
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.
Link to us
LiveThe fastest possible launch. You add one link to your navigation and we handle browsing, registration, payment, clearance and the parent's account.
-
Get your organization ID from us
A UUID. Everything below is scoped to it.
-
Link your navigation at your storefront
HTML
<a href="https://my-v2.thegoodgame.com/s/org/{organization_id}">Camps</a> -
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> -
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.
Checkout on your own site
LiveThe most common integration. Your pages, your brand, your traffic — visitors never leave, and you still never touch payments.
-
Add the loader once, before
</body>HTML<script src="https://my-v2.thegoodgame.com/embed.js" defer></script> -
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.
-
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.
JavaScriptconst 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>`; }); });
Our event cards, inside your page
LiveExample B without building the listing. We render the cards; you inject them into your own template, so the crawl credit lands on your domain.
-
Your visitor submits your filter form
Sport, location, ZIP, dates, price — whatever your UI offers.
-
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". -
Wire the cards to the drawer
Add the loader from example B and put
data-tgg-checkout data-event-id="…"on the Register buttons.
Build the whole storefront on our API
LiveFull 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.
-
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.
-
Pull one event and render your own detail page
cURL
curl "{API_BASE_URL}/api/v1/events/{event_id}" -
Render live pricing and availability
So your page can't advertise a session the drawer will refuse.
cURLcurl "{API_BASE_URL}/api/v1/ec/events/{event_id}/checkout-config" -
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> -
Send buyers to the portal afterwards
Listen for
booking-completeand deep-link into/org/{organization_id}/manage/bookings/{booking_id}, as in example B.