Developer documentation

Athletes App Public API

A read-only JSON API for embedding your roster, teams, and event registration links on any external site — your school's homepage, a club marketing page, or a fan portal.

Getting started

All endpoints live under https://athletes.app/api/public/* and return JSON. They're designed for read-only embedding — list your events on a school site, render a roster on a club page, link straight to a hosted registration checkout — without ever needing to authenticate or store credentials in client-side code.

Find your account ID

Every endpoint requires an account_id query parameter. Find yours in the dashboard under Settings → Account. It's a UUID and it's safe to expose publicly — it acts as a tenant identifier, not a secret.

Base URL

https://athletes.app/api/public

Authentication

None. The public API is unauthenticated by design. Only data explicitly marked public in the dashboard is returned:

  • Events with is_published = true
  • Teams with is_public = true, is_active = true, and level = varsity
  • People with is_public = true on a public team

Anything else stays private even if you have the right account_id. Toggle visibility per-record from the dashboard.

CORS & embedding

All public endpoints respond with Access-Control-Allow-Origin: * and handle OPTIONS preflight requests. You can call them directly from any browser on any domain — no proxy server needed.

Caching

Responses are served with Cache-Control: public, s-maxage=60, stale-while-revalidate=300. CDNs and browsers cache results for 60 seconds, and may serve stale-but-fresh-enough responses for up to 5 minutes while the cache revalidates in the background. Updates you make in the dashboard typically appear within a minute.

Errors

Errors return a JSON body with an error field and a standard HTTP status code:

{
  "error": "account_id query parameter is required"
}
StatusMeaning
400Missing or invalid query parameter (usually account_id).
404Requested resource not found (account, event, or player slug doesn't exist or isn't public).
500Server-side error. Retry with backoff.

Endpoints

GET/api/public/events#

Returns published events for an account, with a precomputed register_url that points at a hosted Stripe-powered checkout page on the account's branded domain.

Query parameters

ParameterTypeRequiredDescription
account_idstring (UUID)requiredYour account identifier.
slugstringoptionalIf provided, returns a single event under an event key. Returns 404 if not found.
include_pastbooleanoptionalPass true to include events whose ends_at is in the past. Defaults to upcoming only.

Response shape

{
  "events": [
    {
      "id": "uuid",
      "slug": "summer-camp-2026",
      "name": "Summer Skills Camp",
      "description": "Three-day skills intensive…",
      "location": "Main Gym",
      "starts_at": "2026-07-15T09:00:00Z",
      "ends_at": "2026-07-17T16:00:00Z",
      "registration_opens_at": "2026-05-01T00:00:00Z",
      "registration_closes_at": "2026-07-10T23:59:59Z",
      "capacity": 50,
      "fee_amount": 15000,
      "fee_description": "Per camper",
      "image_url": "https://…/event-images/{account_id}/{uuid}.jpg",
      "registration_open": true,
      "register_url": "https://yourname.athletes.app/register/summer-camp-2026"
    }
  ]
}

Notes

  • fee_amount is in the smallest currency unit (cents for USD). 15000 = $150.00.
  • registration_open is computed server-side based onregistration_opens_at and registration_closes_at. Use it to disable buttons in your UI.
  • register_url resolves to the account's custom domain when one is configured, falling back to {subdomain}.athletes.app. Always link to it as-is — don't reconstruct the URL yourself.
  • image_url is the event's cover image — a public URL hosted on Supabase Storage. null if the event has no cover. Safe to use directly in <img> tags or CSS background-image. Recommended display ratio is 16:9.
  • When slug is provided the response key is event (singular) instead of events.

Example

curl 'https://athletes.app/api/public/events?account_id=YOUR_ACCOUNT_ID'
GET/api/public/players#

Returns players currently rostered on public, active, varsity teams, including their profile data, awards, and season stats.

Query parameters

ParameterTypeRequiredDescription
account_idstring (UUID)requiredYour account identifier.
slugstringoptionalIf provided, returns a single player under a player key. Returns 404 if not found.
awardstringoptionalFilter to players who hold an award with this slug (e.g. all-conference).

Response shape

{
  "players": [
    {
      "id": "uuid",
      "slug": "marcus-johnson",
      "first_name": "Marcus",
      "last_name": "Johnson",
      "name": "Marcus Johnson",
      "photo": "https://…",
      "height": "6'3\"",
      "weight_lbs": 185,
      "grad_year": 2027,
      "hometown": "Provo, UT",
      "bio": "Three-year varsity guard…",
      "instagram": "marcus.j",
      "twitter": "marcusj",
      "hudl_url": "https://hudl.com/…",
      "teams": [
        {
          "id": "uuid",
          "name": "Boys Varsity Basketball",
          "slug": "boys-varsity",
          "icon": "https://…",
          "jersey_number": 23,
          "position": "Guard",
          "grade": "Junior",
          "season_bio": "…",
          "photo": "https://…",
          "awards": [
            { "title": "All-Conference First Team", "slug": "all-conference", "category": "individual" }
          ]
        }
      ],
      "stats": [
        {
          "season_label": "2025-26",
          "class_label": "Junior",
          "gp": 24,
          "ppg": 18.4,
          "rpg": 6.1,
          "apg": 4.2,
          "spg": 1.8,
          "bpg": 0.4,
          "fg_pct": 0.487,
          "three_pct": 0.382,
          "ft_pct": 0.812,
          "is_career_total": false
        }
      ]
    }
  ]
}

Notes

  • Players appear once and aggregate every public team they're rostered on under the teams array.
  • stats.is_career_total is true for all-time career totals, false for individual season rows.
  • Shooting percentages (fg_pct, three_pct, ft_pct) are decimals — multiply by 100 to display.
  • When slug is provided the response key is player (singular).

Example

curl 'https://athletes.app/api/public/players?account_id=YOUR_ACCOUNT_ID&award=all-conference'
GET/api/public/teams#

Returns public, active, varsity teams with their full roster and coaching staff embedded.

Query parameters

ParameterTypeRequiredDescription
account_idstring (UUID)requiredYour account identifier.

Response shape

{
  "teams": [
    {
      "id": "uuid",
      "name": "Boys Varsity Basketball",
      "level": "varsity",
      "icon": "https://…",
      "slug": "boys-varsity",
      "awards": [
        { "title": "Region Champions 2025" }
      ],
      "staff": [
        {
          "id": "uuid",
          "slug": "coach-smith",
          "first_name": "John",
          "last_name": "Smith",
          "name": "John Smith",
          "photo": "https://…"
        }
      ],
      "players": [
        {
          "id": "uuid",
          "slug": "marcus-johnson",
          "first_name": "Marcus",
          "last_name": "Johnson",
          "name": "Marcus Johnson",
          "photo": "https://…",
          "height": "6'3\"",
          "weight_lbs": 185,
          "grad_year": 2027,
          "hometown": "Provo, UT",
          "bio": "…",
          "season_bio": "…",
          "jersey_number": 23,
          "position": "Guard",
          "grade": "Junior",
          "awards": [{ "title": "All-Conference" }],
          "teams": [
            { "id": "uuid", "name": "Boys Varsity Basketball", "slug": "boys-varsity" }
          ]
        }
      ]
    }
  ]
}

Notes

  • Only varsity, active, public teams are returned. Sub-varsity and private teams stay hidden.
  • Each player's teams array includes every public team they're also on, so you can show cross-team affiliations.
  • Use /api/public/players instead if you want season stats — the teams endpoint omits stats for payload size.

Example

curl 'https://athletes.app/api/public/teams?account_id=YOUR_ACCOUNT_ID'

Code examples

Browser / JavaScript

const ACCOUNT_ID = "00000000-0000-0000-0000-000000000000"

const res = await fetch(
  `https://athletes.app/api/public/events?account_id=${ACCOUNT_ID}`
)
const { events } = await res.json()

for (const event of events) {
  console.log(event.name, event.register_url)
}

React component

import { useEffect, useState } from "react"

const ACCOUNT_ID = "00000000-0000-0000-0000-000000000000"

export function UpcomingEvents() {
  const [events, setEvents] = useState([])

  useEffect(() => {
    fetch(`https://athletes.app/api/public/events?account_id=${ACCOUNT_ID}`)
      .then((r) => r.json())
      .then((data) => setEvents(data.events))
  }, [])

  return (
    <ul>
      {events.map((event) => (
        <li key={event.id}>
          <strong>{event.name}</strong>
          {event.registration_open && (
            <a href={event.register_url}> Register →</a>
          )}
        </li>
      ))}
    </ul>
  )
}

Server-side (Node)

const ACCOUNT_ID = process.env.ATHLETES_APP_ACCOUNT_ID

const res = await fetch(
  `https://athletes.app/api/public/players?account_id=${ACCOUNT_ID}`,
  { next: { revalidate: 60 } } // Next.js ISR
)
const { players } = await res.json()

Embed a registration link in HTML

For the simplest case — linking to a single event from a static site — skip the API entirely. Use the registration URL directly:

<a href="https://yourname.athletes.app/register/summer-camp-2026">
  Register for Summer Camp →
</a>

Or use /api/public/events?slug=summer-camp-2026 to fetch metadata first if you want to show the price, capacity, or disable the link when registration is closed.

Changelog

  • 2026-04-28Added GET /api/public/events with register_url for hosted checkout embedding.
  • EarlierInitial release of GET /api/public/players and GET /api/public/teams.

Need an endpoint we don't have?

Email us and we'll usually ship it within a week.