rec.us API

Download OpenAPI specification:

API for rec.us, a parks & recreation booking platform.

Reverse engineered from the frontend app and public API responses. Subject to change without notice. Please submit pull requests for any additions or corrections to this documentation on jakajancar/recus.

Entity Model

  • Sport — a sport or activity (e.g. Tennis, Pickleball, Yoga). Referenced by sites, instructors, and sections.
  • Region — a geographic area (e.g. "San Francisco Area", "East Bay"). Used to discover locations across multiple organizations.
  • Organization — a municipal parks & rec department (e.g. "San Francisco Rec & Park"). Configures which features are enabled.
    • Location — a park or facility with bookable sites. Has address, hours, and a reservation window.
      • Site — a bookable unit within a location (court, field, room, picnic table, etc.). Has pricing and allowed durations. Some are walk-up only. Note: The API uses "site" in paths but many response keys use the legacy name courts/courtNumber.
    • Instructor — a coach who offers private lessons. Has sport-specific hourly rates and available time slots.
    • Section — a group class or lesson pack. Has a facilitator, capacity, and multiple sessions.
      • Session — an individual meeting within a section, with a specific date/time.
  • Facility Rental — a site booking created via POST /v1/facility-rentals. Wraps a reservation and an order.
    • Reservation — the time slot hold on a site.
    • Order — the transactional record. Created with pending status and ~10 min expiration. Must be paid (even if $0) to finalize.
      • Payment — settles an order. Has a method type (e.g. free, cardOnline) and amount in cents.

Pagination

Some list endpoints are paginated (default page size 25) — callers must iterate pages to get all results. Others return the full dataset in a single response.

Parameters

All paginated endpoints accept either of these equivalent param styles:

Style Params
Bracket (qs) pg[num]=1&pg[size]=25
Flat page=1&pageSize=25

Both styles are interchangeable on every paginated endpoint. Page numbers are 1-indexed.

Response envelopes

The response shape is fixed per endpoint (does not depend on which param style you use). Two envelope styles exist:

data/meta envelope:

{
  "data": [ ... ],
  "meta": { "pg": { "num": 1, "size": 25, "totalResults": 49 } }
}

More pages exist while pg.num * pg.size < pg.totalResults.

results/total envelope:

{
  "results": [ ... ],
  "total": 157
}

More pages exist while page * pageSize < total. The response does not echo back the current page number.

Authentication

Authenticated endpoints (marked with a lock icon) require a Firebase Auth ID token passed as a Bearer token.

Firebase project: rec-prod Firebase Web API key: AIzaSyCp6DCwnx-6GwkMyI2G1b8ixYs4AXZc-7s

Sign in with email/password

POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=AIzaSyCp6DCwnx-6GwkMyI2G1b8ixYs4AXZc-7s

Request body:

{
  "email": "user@example.com",
  "password": "password",
  "returnSecureToken": true
}

Response:

{
  "localId": "1N78WJBXjdU1avTCTx9z1JkFWMv1",
  "email": "user@example.com",
  "displayName": "",
  "idToken": "eyJhbGciOi...",
  "refreshToken": "AMf-vBx...",
  "expiresIn": "3600"
}

The idToken is used as the Bearer token for all authenticated API calls:

Authorization: Bearer {idToken}

Tokens expire after 1 hour. Use the refreshToken with the Firebase token refresh endpoint to obtain a new idToken.

Examples

Find Available Pickleball Sites

# 1. List organizations
curl -s 'https://api.rec.us/v1/organizations' | jq '.data[].slug'

# 2. Get org detail to find location IDs (in config.banners.pages)
curl -s 'https://api.rec.us/v1/organizations/san-francisco-rec-park' | jq '.config.banners.pages | keys[]'

# 3. Get location detail
curl -s 'https://api.rec.us/v1/locations/ad9e28e1-2d02-4fb5-b31d-b75f63841814' | jq '.location.name, .location.courts[].courtNumber'

# 4. Check schedule for available slots
curl -s 'https://api.rec.us/v1/locations/ad9e28e1-2d02-4fb5-b31d-b75f63841814/schedule?startDate=2026-03-01' | \
  jq '.dates["20260301"][] | select(.sports[].name == "Pickleball") | {court: .courtNumber, slots: [.schedule | to_entries[] | select(.value.referenceType == "RESERVABLE") | .key]}'

Find Available Tennis Instructors

# 1. List instructors with open lesson slots
curl -s 'https://api.rec.us/v1/instructors/cards/lessons?organizationSlug=san-francisco-rec-park' | \
  jq '.[] | {name: .fullName, sport: .sports[0].name, rate: .sports[0].hourlyRate, nextLesson: .lessons[0].reservationTimestampRange}'

# 2. Get full instructor profile
curl -s 'https://api.rec.us/v1/instructors/{instructorId}' | jq '{name: .user.firstName + " " + .user.lastName, bio: .longBio, locations: [.instructorLocations[].locationId]}'

Find Group Classes

# List all programs for SF Rec Park in the next 90 days
curl -s 'https://api.rec.us/v1/discovery/programmed?organizationId=17380e28-7e02-4b52-82c5-fab18557fd7a&startDate=2026-02-27&endDate=2026-05-27' | \
  jq '.[] | {name: .name, sport: .sportName, location: .locationName, spots: (.capacity - .participantCount), sessions: (.sessions | length)}'

Book a Free Site (Full Flow)

End-to-end example booking a free pickleball site at Granada Park.

# 1. Authenticate
TOKEN=$(curl -s -X POST \
  'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=AIzaSyCp6DCwnx-6GwkMyI2G1b8ixYs4AXZc-7s' \
  -H 'Content-Type: application/json' \
  -d '{"email":"user@example.com","password":"password","returnSecureToken":true}' \
  | jq -r '.idToken')

# 2. Find the location — search by region or org
#    Granada Park is in the San Francisco Area region
curl -s 'https://api.rec.us/v1/locations/availability?regionId=51cad8df-4985-4c09-ba5e-c7893f672c26' | \
  jq '.[] | select(.location.name == "Granada Park") | .location | {id, name, courts: [.courts[] | {id, court: .courtNumber, pricing: .config.pricing.default}]}'

# 3. Check the schedule for available pickleball slots on a specific date
LOCATION_ID="38a201f0-4fb1-4991-8e72-db8a9495319e"
curl -s "https://api.rec.us/v1/locations/$LOCATION_ID/schedule?startDate=2026-03-05" | \
  jq '.dates["20260305"][] | select(.sports[].name == "Pickleball") | {court: .courtNumber, slots: [.schedule | to_entries[] | select(.value.referenceType == "RESERVABLE") | .key]}'

# 4. Verify the site supports instant booking
SITE_ID="99b7129e-5ed4-4fd8-aba2-fee1683310bb"   # Site A (court)
curl -s "https://api.rec.us/v1/sites/$SITE_ID" \
  -H "Authorization: Bearer $TOKEN" | \
  jq '.data | {id, courtNumber, isInstantBookable, capacity}'

# 5. Create the reservation
ORDER_ID=$(curl -s -X POST 'https://api.rec.us/v1/facility-rentals' \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"data\": {
      \"reservation\": {
        \"timestampRange\": \"[2026-03-05 13:00:00, 2026-03-05 14:00:00)\",
        \"locationId\": \"$LOCATION_ID\",
        \"courtIds\": [\"$SITE_ID\"]
      }
    }
  }" | jq -r '.data.order.id')

echo "Order created: $ORDER_ID"

# 6. Complete the order (required even for $0 reservations)
curl -s -X POST "https://api.rec.us/v1/orders/$ORDER_ID/pay" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"data":{"payments":[{"paymentMethodType":"free","amountCents":0}]}}' | \
  jq '.data | {status, settledAt}'

# 7. Verify — the slot should now show as RESERVATION
curl -s "https://api.rec.us/v1/locations/$LOCATION_ID/schedule?startDate=2026-03-05" | \
  jq '.dates["20260305"][] | select(.courtNumber == "A") | .schedule["13:00, 14:00"]'
# → { "referenceType": "RESERVATION", "referenceId": "..." }

Key points:

  • After POST /v1/facility-rentals, the order is "pending" with a ~10 minute expiration. You must call POST /v1/orders/{orderId}/pay to finalize it.
  • For free sites, use paymentMethodType: "free" with amountCents: 0.
  • The timestampRange uses PostgreSQL range syntax: [start, end) — inclusive start, exclusive end.
  • Times in timestampRange are in the location's local timezone (e.g. America/Los_Angeles).

Book a Paid Site (Full Flow)

Same as the free site flow above, but diverges at step 4 — the site has a fixed-slots booking policy, and payment requires a Stripe confirmation step.

# Steps 1-3 are the same as "Book a Free Site" (authenticate, find location, check schedule).

# 4. Get site detail — check booking policy and pricing
SITE_ID="040a5a1a-443a-4641-b079-ed7e650bd5ac"   # J.P. Murphy site "Court 3"
curl -s "https://api.rec.us/v1/sites/$SITE_ID" \
  -H "Authorization: Bearer $TOKEN" | \
  jq '.data | {id, courtNumber, isInstantBookable, noReservationText,
    pricing: .config.pricing.default,
    bookingPolicy: .config.bookingPolicies[0].type,
    fixedSlots: [.config.bookingPolicies[0].slots[] | select(.dayOfWeek == 7) | {start: .startTimeLocal, end: .endTimeLocal}]}'
# → bookingPolicy: "fixed-slots", fixedSlots: [{start: "16:30:00", end: "18:00:00"}, ...]
# With fixed-slots, your timestampRange MUST match a slot exactly.

# 5. Create the reservation (timestampRange matches the fixed slot 16:30-18:00)
LOCATION_ID="7a8ef25a-dc20-4046-8aab-7212a9a41d20"
RESULT=$(curl -s -X POST 'https://api.rec.us/v1/facility-rentals' \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"data\": {
      \"reservation\": {
        \"timestampRange\": \"[2026-03-01 16:30:00, 2026-03-01 18:00:00)\",
        \"locationId\": \"$LOCATION_ID\",
        \"courtIds\": [\"$SITE_ID\"]
      }
    }
  }")
ORDER_ID=$(echo "$RESULT" | jq -r '.data.order.id')
ITEM_ID=$(echo "$RESULT" | jq -r '.data.order.items[0].id')
TOTAL=$(echo "$RESULT" | jq -r '.data.order.total')
echo "Order: $ORDER_ID, Item: $ITEM_ID, Total: $TOTAL cents"
# → Total: 750 (= $7.50 for 1.5 hours at $5/hour)

# 6. Look up saved payment methods
USER_ID=$(echo "$RESULT" | jq -r '.data.order.customer.id')
ORG_ID=$(echo "$RESULT" | jq -r '.data.order.organization.id')
curl -s -X POST "https://api.rec.us/v1/users/$USER_ID/payment-method-setup" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"data\":{\"organizationId\":\"$ORG_ID\"}}" | \
  jq '.data.paymentMethods[] | {id, brand: .card.brand, last4: .card.last4}'
# → { "id": "pm_1Sygm4CMyY4UUjhBOhNeoApv", "brand": "visa", "last4": "7510" }

# 7. Submit payment (use "ACH" type — "cardOnline" is rejected by the API)
PAY_RESULT=$(curl -s -X POST "https://api.rec.us/v1/orders/$ORDER_ID/pay" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"data\":{\"payments\":[{\"paymentMethodType\":\"ACH\",\"amountCents\":$TOTAL}]}}")

# Extract Stripe PaymentIntent details from the response
PI_ID=$(echo "$PAY_RESULT" | jq -r '.included.payments[0].gatewayData.paymentIntentId')
CLIENT_SECRET=$(echo "$PAY_RESULT" | jq -r '.included.payments[0].gatewayData.clientSecret')
echo "PaymentIntent: $PI_ID"

# 8. Confirm the PaymentIntent with Stripe using the saved card
PM_ID="pm_1Sygm4CMyY4UUjhBOhNeoApv"  # from step 6
STRIPE_PK="pk_live_51MPUx4CMyY4UUjhBlgalg5uPiGdXHOWbOTEOioIXfReEeAuLviTRXhdTGvZtTnYtDm2eZonv8buTf73YKIzJHV4i00YikF7WiB"
curl -s -X POST "https://api.stripe.com/v1/payment_intents/$PI_ID/confirm" \
  -u "$STRIPE_PK:" \
  -d "payment_method=$PM_ID" \
  -d "client_secret=$CLIENT_SECRET" | \
  jq '{status, amount, currency}'
# → { "status": "succeeded", "amount": 750, "currency": "usd" }

# 9. Verify — order should show totalAmountRemaining: 0
curl -s "https://api.rec.us/v1/orders/$ORDER_ID" \
  -H "Authorization: Bearer $TOKEN" | \
  jq '.order | {status, total, totalAmountRemaining}'

Key differences from the free site flow:

  • Fixed-slots sites require timestampRange to exactly match a pre-defined slot boundary (check config.bookingPolicies). Using a custom duration like [16:30, 17:30) will fail with "The reservation violates the site's booking policy".
  • Card payment is a two-step process: (1) call /pay with paymentMethodType: "ACH" to create a Stripe PaymentIntent, then (2) confirm it via the Stripe API with a saved payment_method ID.
  • The Stripe publishable key authenticates the /confirm call (passed as HTTP basic auth username with empty password).
  • After Stripe confirmation, rec.us is notified via webhook and the order settles automatically.
  • Daily booking limits exist per location/date (e.g. 1 reservation per day). Expired unpaid orders may still count against the limit temporarily. The error is E_INELIGIBLE_SITE_BOOKING with a message like "Daily booking limit of 1 reached for March 1, 2026".

Guides

Availability & Schedule — Endpoint Comparison

Four endpoints return time slot / availability data. They overlap significantly but each has unique fields.

GET /v1/locations/{id} GET /v1/locations/availability GET /v1/sites/{id}/availability GET /v1/locations/{id}/schedule
Intent "What can I book here?" — single location with bookable start times per site "Where can I play?" — cross-location discovery of bookable start times "How long can I book?" — per-slot duration options for a specific site "What does the day look like?" — full slot grid with bookings, open hours, and reservable slots
Frontend usage Location detail page (/locations/[id]) — site metadata + inline booking flow. Uses publishedSites=true. Location search/map page (/locations) /sites/[siteId] page — facility rentals only (non-court types). Reached via org's Facility Rentals tab → GET /v1/sites?organizationId={id}. Court booking never calls this endpoint. Location detail page (/locations/[id]) — visual schedule grid
Scope Single location, all sites Multi-location (filter by org, region, or lat/lng) Single site Single location, all sites
Site ID (UUID) Yes, per site (in courts[].id) Yes, per site (in courts[].id) N/A (you provide it) Only indirectly, via reservations[].courts[] (booked slots only; key name is misleading)
Available start times Yes (courts[].availableSlots) Yes (courts[].availableSlots) Yes (keys of data[date]) No (has RESERVABLE ranges, not individual start times)
Sport names No (only sportId) No (only sportId) No Yes (sports[].name)
All slot states No — available start times only No — available start times only No — available start times only Yes — RESERVABLE, RESERVATION, OPEN
Who booked each slot No No No Yes — users dict with names, skill levels
Per-slot available durations No (can be computed client-side — see below) No (can be computed client-side — see below) Yes (availableDurationsMinutes per time) No
Booking policies Yes (courts[].config.bookingPolicies) Yes (courts[].config.bookingPolicies) No No
allowedReservationDurations Yes, per site Yes, per site No No
Pricing Yes, per site (config.pricing) Yes, per site (config.pricing) No Only on booked reservations (reservationCost)
isInstantBookable Yes, per site Yes, per site No No
Detailed location info (e.g. playGuidelines) Yes No No No
Multi-location No Yes No No
Date range Automatic (reservation window, typically 7 days) Automatic (reservation window, typically 7 days) Automatic (reservation window, typically 7 days) Explicit startDate/endDate params

Computing per-slot durations client-side

GET /v1/sites/{id}/availability returns availableDurationsMinutes per time slot, but requires one request per site — impractical for multi-site views. Both GET /v1/locations/{id} and GET /v1/locations/availability return enough data to compute equivalent durations client-side (this is what the rec.us frontend does for court reservations on the location detail page).

Each site (in courts[]) provides three relevant fields:

  • availableSlots — bookable start times as "YYYY-MM-DD HH:MM:SS" strings
  • allowedReservationDurations.minutes — e.g. [30, 60, 90]
  • config.bookingPolicies[] — optional; when present with type: "fixed-slots" and isActive: true, the site uses pre-defined time blocks

Fixed-slot sites (active bookingPolicies with type: "fixed-slots"):

The bookingPolicies[].slots array defines the valid time blocks per day of week (dayOfWeek 1=Mon…7=Sun). Each slot has startTimeLocal and endTimeLocal. For each block whose startTimeLocal (truncated to HH:MM) appears in availableSlots on a matching day-of-week, the only available duration is endTimeLocal - startTimeLocal. The allowedReservationDurations field is ignored — it contains a stale superset.

Note: availableSlots for fixed-slot sites includes every 30-minute tick within the open range (e.g. 07:30, 08:00, 08:30 for a 07:30–09:00 block), but only the block start times are valid booking starts.

Flexible-booking sites (no active bookingPolicies, or isActive: false):

For each start time in availableSlots, check which durations from allowedReservationDurations.minutes are feasible by verifying that all consecutive 30-minute sub-slots exist:

for each duration in allowedReservationDurations.minutes (ascending):
    feasible = true
    for offset in 0, 30, 60, ... (duration - 30):
        if (startTime + offset) not in availableSlots for that date:
            feasible = false; break
    if feasible: include duration

For example, with allowedReservationDurations: [30, 60, 90] and availableSlots containing 17:00, 17:30, 18:00 but not 18:30:

  • At 17:00: [30, 60, 90] (17:00, 17:30, 18:00 all present)
  • At 17:30: [30, 60] (17:30, 18:00 present; 18:30 missing)
  • At 18:00: [30] (18:00 present; 18:30 missing)

This computation produces results identical to GET /v1/sites/{id}/availability.

Sports

Sport and activity types

List all sports

Returns all sports/activities on the platform.

Notes:

  • Sport IDs are stable and referenced by courts[].sports[].sportId in location/availability responses (note: the courts key contains all site types, not just courts) and sports[].id in schedule responses.
  • Includes non-court activities (e.g. Guitar, Yoga, Day Camp) used by the programs/sections system.

Responses

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Regions

Geographic regions

List all regions

Returns all geographic regions.

Responses

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Organizations

Municipal parks & rec departments

List all organizations

Returns all organizations on the platform. Paginated (data/meta envelope — see Pagination).

query Parameters
pg[num]
integer >= 1
Default: 1

Page number (bracket style). 1-indexed. Interchangeable with flat page param.

pg[size]
integer >= 1
Default: 25

Page size (bracket style). Interchangeable with flat pageSize param.

page
integer >= 1
Default: 1

Page number (flat style). 1-indexed. Interchangeable with bracket pg[num] param.

pageSize
integer >= 1
Default: 25

Page size (flat style). Interchangeable with bracket pg[size] param.

Responses

Response samples

Content type
application/json
{
  • "data": [],
  • "meta": {
    }
}

Get organization detail

Returns full organization detail including configuration.

config.tabs controls which features are enabled for the organization. A tab with "enabled": false is disabled. Possible tabs:

  • locations — court reservations (renamed per org, e.g. "Court Reservations", "Sport Facility Rentals")
  • coaching — private lessons with instructors
  • programs — group classes / lesson packs
  • facilityRentals — picnic/event space rentals
  • membershipsAndPasses — membership programs
path Parameters
organizationSlugOrId
required
string

Organization slug (e.g. "san-francisco-rec-park") or UUID.

Responses

Response samples

Content type
application/json
{
  • "id": "17380e28-7e02-4b52-82c5-fab18557fd7a",
  • "slug": "san-francisco-rec-park",
  • "name": "San Francisco Rec & Park",
  • "description": "Official SF organization",
  • "logo": "https://...",
  • "fullLogo": "https://...",
  • "config": {
    },
  • "activityOrCategoryOrder": [ ]
}

List organization membership groups

Authorizations:
firebaseAuth
path Parameters
organizationId
required
string <uuid>

Responses

Response samples

Content type
application/json
{ }

Locations

Parks and facilities with bookable sites

Get location detail

Returns location detail with all sites.

Note: The response nests sites under a courts key, but this array contains all site types (courts, fields, rooms, picnic tables, etc.), not just courts. Similarly, courtNumber is the display name for any site type (e.g. "Court 1", "Picnic Table A", "Room 201").

Key fields on each site in courts[]:

  • courts[].courtNumber — the display name for any site, despite the field name.
  • courts[].noReservationText — if set (e.g. "Not Reservable"), the site is walk-up only.
  • courts[].availableSlots — bookable start times as "YYYY-MM-DD HH:MM:SS" strings in the location's timezone. Covers the upcoming reservation window.
  • courts[].allowedReservationDurations — the set of durations this site supports.
  • courts[].config.bookingPolicies — optional. When present with type: "fixed-slots" and isActive: true, the site uses pre-defined time blocks. See "Computing per-slot durations client-side" in the API description.
  • courts[].isInstantBookable — whether the site can be booked directly.
  • courts[].sports[].sportId — references a global sport. The sport name is not included here; it appears in the schedule endpoint.
  • defaultReservationWindow — how many days in advance reservations open (e.g. 7 = one week ahead).
  • reservationReleaseTimeLocal — the local time when new reservation slots become available (e.g. "08:00:00" = 8 AM).
  • playGuidelines — detailed markdown with reservation rules, site assignments, hours, etc.
path Parameters
locationId
required
string <uuid>
query Parameters
publishedSites
string
Value: "true"

Set to "true" to filter to only published (bookable) sites. Without this, all sites are returned including unpublished/inactive ones.

Responses

Response samples

Content type
application/json
{
  • "location": {
    }
}

Get schedule

Returns the schedule for all sites at a location for the given date range.

Schedule slot keys are formatted as "HH:MM, HH:MM" (start time, end time in 24h local time).

referenceType values:

Value Meaning
RESERVABLE Open slot — can be booked
RESERVATION Already booked — referenceId links to the reservations dict
OPEN Not reservable — referenceLabel explains why (e.g. "Not Reservable" for walk-up courts or outside bookable hours)

reservations dict: Keyed by reservation UUID. Contains cost (in cents), who booked it (users[]), which site (courts[] — key name is misleading), and whether it's a regular reservation, lesson, or class.

users dict: Keyed by user UUID. Only shows first initial and last initial for privacy. Includes skillLevel (e.g. "first-timer", "beginner", "intermediate", "advanced").

instructors, classes, sessions, facilityRentals dicts: Populated when the schedule includes instructor lessons, group classes, or facility rentals. Empty for pure reservation schedules.

Note: The schedule response uses courtNumber as the display name for each site entry, even for non-court site types. There is no site UUID in the schedule's per-site entries — only courtNumber and sports. Site UUIDs appear only in reservations[].courts[] for booked slots.

path Parameters
locationId
required
string <uuid>
query Parameters
startDate
required
string <date>

Start date (YYYY-MM-DD).

endDate
string <date>

End date (YYYY-MM-DD). Defaults to same as startDate.

Responses

Response samples

Content type
application/json
{
  • "dates": {
    },
  • "reservations": {
    },
  • "users": {
    },
  • "instructors": { },
  • "classes": { },
  • "sessions": { },
  • "facilityRentals": { }
}

Get available slots across locations

Multi-location equivalent of GET /v1/locations/{locationId} — returns the same courts[].availableSlots structure but across many locations at once. Also the only way to list locations for a specific organization (the single-location endpoint does not support org filtering).

Response is a subset of the location detail endpoint — same courts[] structure (see its docs for field descriptions) but without playGuidelines, organization, regions, schedule, and a few other location/court fields.

  • distance — populated when filtering by lat/lng, otherwise null.
  • Only locations with at least one site are returned. Locations with zero available slots are still included (check availableSlots length).
query Parameters
regionId
string <uuid>

UUID of a region. At least one of regionId, organizationSlug, or latitude/longitude is required.

organizationSlug
string

Organization slug (e.g. san-francisco-rec-park).

latitude
number

Latitude for geo filter.

longitude
number

Longitude for geo filter.

publishedSites
string
Value: "true"

Set to "true" to filter to only published (bookable) sites. Without this, all sites are returned including unpublished/inactive ones.

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Instructors

Private lesson instructors

List instructors with available lessons

Returns instructors with their upcoming available lesson time slots.

lessons[] — upcoming available time slots for booking a private lesson with this instructor. Each has a time range, sport, and location.

query Parameters
organizationSlug
string

Filter by organization. At least one of organizationSlug or locationId is required.

locationId
string <uuid>

Filter by location.

regionId
string <uuid>

Filter by region.

latitude
number

Latitude for geo filter.

longitude
number

Longitude for geo filter.

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Get instructor detail

Returns full instructor profile.

instructorLocations[] — which locations this instructor teaches at, with per-location hourly rates (may differ from the base rate in sports[]).

path Parameters
instructorId
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "id": "415856ca-7fe0-4473-aa7e-5ea145bd871c",
  • "userId": "5312b05a-230b-4d74-a30e-aa3f5f62f8cc",
  • "shortDescription": "Usually available: Weekdays 10:30AM-6PM...",
  • "longBio": "Markdown string with full bio, availability, pricing...",
  • "canTeachPrivateLessons": true,
  • "isPro": false,
  • "tags": [ ],
  • "certifications": [ ],
  • "images": {},
  • "config": {
    },
  • "user": {
    },
  • "sports": [
    ],
  • "instructorLocations": [
    ]
}

Discovery

Program and group class discovery

List available programs and group classes

Returns available programs and group classes for a given organization, location, or region within a date range.

Notes:

  • capacity — max participants. participantCount — currently enrolled. Available spots = capacity - participantCount.
  • sessions[] — individual meeting dates/times. Timestamps are in the section's timezone.
  • recommendedLevel — skill level (e.g. "all", "beginner", "intermediate", "advanced").
  • Types of sections include lesson packs (1:1, capacity=1), small group lessons (capacity=4), and other programs.
query Parameters
organizationId
string <uuid>

Organization UUID. At least one of organizationId, locationId, regionId, or instructorId is required.

locationId
string <uuid>

Location UUID.

regionId
string <uuid>

Region UUID.

instructorId
string <uuid>

Instructor UUID.

sportIds
string

Filter by sport ID.

startDate
required
string <date>

Start date (YYYY-MM-DD).

endDate
required
string <date>

End date (YYYY-MM-DD).

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Sites

Bookable units (courts, fields, rooms, picnic tables, etc.)

List sites for an organization

Returns facility-rental sites for a given organization. Paginated (results/total envelope — see Pagination).

Only returns non-court site types (picnic tables, bounce houses, gyms, outdoor event spaces, etc.). Courts, fields, and rinks are not included — those are accessed exclusively through the location endpoints (GET /v1/locations/{id} and GET /v1/locations/availability). Organizations that only have courts (e.g. san-francisco-rec-park) return total: 0.

Response items have the same shape as GET /v1/sites/{siteId} .data.

query Parameters
organizationId
required
string <uuid>

Organization UUID.

pg[num]
integer >= 1
Default: 1

Page number (bracket style). 1-indexed. Interchangeable with flat page param.

pg[size]
integer >= 1
Default: 25

Page size (bracket style). Interchangeable with flat pageSize param.

page
integer >= 1
Default: 1

Page number (flat style). 1-indexed. Interchangeable with bracket pg[num] param.

pageSize
integer >= 1
Default: 25

Page size (flat style). Interchangeable with bracket pg[size] param.

Responses

Response samples

Content type
application/json
{
  • "results": [
    ],
  • "total": 0
}

Get site detail

Returns detailed information about a specific site, including whether it supports instant booking.

Notes:

  • courtNumber — the display name (e.g. "A", "Court 1", "Picnic Table 3"), despite the field name.
  • capacity — 0 means no cap on attendees.
  • isInstantBookable — if true, the site can be booked directly via the facility-rentals endpoint. If false, the site may require a request/approval flow.
  • noReservationText — if set (e.g. "Not Reservable"), the site is walk-up only.
  • config.bookingPolicies[] — optional. When present with type: "fixed-slots", the site uses pre-defined time blocks instead of flexible durations. The timestampRange in a facility-rental request must exactly match one of these slot boundaries, or the API will reject with "The reservation violates the site's booking policy". dayOfWeek uses 1=Monday through 7=Sunday. If no bookingPolicies are present, the site uses flexible booking with allowedReservationDurations.
path Parameters
siteId
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Get site availability

Returns available dates and time slots for a specific site.

Notes:

  • Keys are dates (YYYY-MM-DD), values are objects keyed by start time (HH:MM:SS).
  • Each time slot lists which durations (in minutes) are available starting at that time.
  • This endpoint is used by the frontend to populate the time and duration pickers.
path Parameters
siteId
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Get site instant booking configuration

Returns the instant booking configuration for a site, if one exists. Returns 404 if not configured.

path Parameters
siteId
required
string <uuid>

Responses

Response samples

Content type
application/json
{ }

Get site add-ons

Returns add-ons available when booking a specific site.

path Parameters
siteId
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Users

User profiles and account management

Get current user profile

Returns the authenticated user's profile. The id field is the rec.us user UUID (not the Firebase UID).

Notes:

  • The id (rec.us UUID) is needed for user-scoped endpoints like /v1/users/{userId}/bookings. This is not the same as firebaseUserId.
Authorizations:
firebaseAuth

Responses

Response samples

Content type
application/json
{
  • "id": "9101112a-3912-4cda-8814-8cd29586bd0f",
  • "householdId": "7a8da9f0-93ce-4578-bfc8-61cc36fbfbf0",
  • "firebaseUserId": "1N78WJBXjdU1avTCTx9z1JkFWMv1",
  • "recId": "EID223",
  • "email": "user@example.com",
  • "role": "user",
  • "firstName": "John",
  • "lastName": "Doe",
  • "phone": "2234445555",
  • "formattedAddress": "455 Vallejo St., San Francisco, CA 94133, USA",
  • "skillLevel": "first-timer",
  • "isInstructor": false,
  • "memberships": [ ],
  • "organizationRoles": { },
  • "profile": {
    }
}

Get user's group memberships

Authorizations:
firebaseAuth
path Parameters
userId
required
string <uuid>

Responses

Response samples

Content type
application/json
{ }

Check claimable items

Authorizations:
firebaseAuth
path Parameters
userId
required
string <uuid>

Responses

Response samples

Content type
application/json
{ }

Validate signup data

Request Body schema: application/json
required
object

Responses

Request samples

Content type
application/json
{ }

Response samples

Content type
application/json
{ }

Send verification code

Request Body schema: application/json
required
object

Responses

Request samples

Content type
application/json
{ }

Response samples

Content type
application/json
{ }

Get Stripe setup intent and saved cards

Returns a Stripe SetupIntent and the user's saved payment methods for a given organization. This is how you discover saved card IDs (pm_...) needed for the card payment flow.

Notes:

  • paymentMethods[].id — the Stripe PaymentMethod ID (e.g. pm_...). Used when confirming a PaymentIntent via the Stripe API.
  • setupIntentId / clientSecret — a Stripe SetupIntent for adding new payment methods (not needed for paying with an existing saved card).
  • organizationId is required because payment methods are scoped per Stripe Connect account (each organization has its own).
Authorizations:
firebaseAuth
path Parameters
userId
required
string <uuid>
Request Body schema: application/json
required
required
object

Responses

Request samples

Content type
application/json
{
  • "data": {
    }
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Facility Rentals

Site reservation creation

Create a site reservation

Creates a new facility rental (site reservation).

Notes:

  • timestampRange — PostgreSQL-style range: [ = inclusive start, ) = exclusive end. Times are in the location's timezone.
  • courtIds — array of site UUIDs to reserve. Despite the name, accepts any site type (court, field, room, etc.). Typically one site per reservation.
  • Do not include attendeeCount for sites with capacity: 0 (uncapped), or the API will reject with "Attendee count exceeds site capacity".
  • The site must have isInstantBookable: true (check via GET /v1/sites/{siteId}).

Important: The order is created with status: "pending" and has an expiration timer (~10 minutes). You must complete the order by calling POST /v1/orders/{orderId}/pay before it expires, even for free ($0) reservations.

Authorizations:
firebaseAuth
Request Body schema: application/json
required
required
object

Responses

Request samples

Content type
application/json
{
  • "data": {
    }
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Orders

Order management and checkout

Get current active order

Authorizations:
firebaseAuth

Responses

Response samples

Content type
application/json
{ }

List orders by booking

Authorizations:
firebaseAuth
query Parameters
bookingId
required
string <uuid>

Filter orders by booking ID.

Responses

Response samples

Content type
application/json
{ }

Get order detail

Wraps response in an order key.

Authorizations:
firebaseAuth
path Parameters
orderId
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "order": {
    }
}

Delete/cancel a pending order

Authorizations:
firebaseAuth
path Parameters
orderId
required
string <uuid>

Responses

Response samples

Content type
application/json
{ }

Get order detail (v2)

Order detail v2 endpoint. Supports include[]=installments for installment plan details.

Authorizations:
firebaseAuth
path Parameters
orderId
required
string <uuid>
query Parameters
include[]
Array of strings
Items Value: "installments"

Sideloaded relations.

Responses

Response samples

Content type
application/json
{ }

Get order line items

Authorizations:
firebaseAuth
path Parameters
orderId
required
string <uuid>

Responses

Response samples

Content type
application/json
{ }

Get payment history

Authorizations:
firebaseAuth
path Parameters
orderId
required
string <uuid>

Responses

Response samples

Content type
application/json
{ }

Submit payment

Submits payment for an order.

Payment method types:

Type Use case API-callable?
free $0 reservations (amountCents must be 0) Yes
ACH Triggers Stripe PaymentIntent for card payment Yes (see below)
cardOnline Used by the web frontend only No — rejected with E_INVALID_BODY
organizationCredit Org credit balance Untested
check Check (also requires checkNumber field) Admin only
cash Cash payment Admin only (E_FORBIDDEN for consumers)
cardPresent In-person card (also requires providerReaderId field) Admin only
giftCard Gift card (also requires storedValueAccountCode field) Untested
scholarship Scholarship (also requires storedValueAccountCode field) Untested

Paying with a saved card (Stripe flow)

The cardOnline payment type is rejected by the API with E_INVALID_BODY when called directly. To pay with a saved card via the API, use paymentMethodType: "ACH" instead — this creates a Stripe PaymentIntent that you then confirm with the Stripe API using a saved card.

Stripe publishable key: pk_live_51MPUx4CMyY4UUjhBlgalg5uPiGdXHOWbOTEOioIXfReEeAuLviTRXhdTGvZtTnYtDm2eZonv8buTf73YKIzJHV4i00YikF7WiB

Step 1: Submit payment with ACH type. The response includes included.payments[] with Stripe PaymentIntent details.

Key fields in gatewayData:

  • paymentIntentId — the Stripe PaymentIntent ID (pi_...)
  • clientSecret — the client secret needed to confirm the PaymentIntent
  • paymentMethods[] — the user's saved cards (same data as from payment-method-setup)

Step 2: Confirm the PaymentIntent via Stripe API:

curl -s -X POST "https://api.stripe.com/v1/payment_intents/${PAYMENT_INTENT_ID}/confirm" \
  -u "${STRIPE_PUBLISHABLE_KEY}:" \
  -d "payment_method=${PAYMENT_METHOD_ID}" \
  -d "client_secret=${CLIENT_SECRET}"

Once Stripe confirms the payment, the rec.us order is automatically settled via webhook. The order's totalAmountRemaining drops to 0 and the payment status changes to succeeded.

Authorizations:
firebaseAuth
path Parameters
orderId
required
string <uuid>
Request Body schema: application/json
required
required
object

Responses

Request samples

Content type
application/json
Example
{
  • "data": {
    }
}

Response samples

Content type
application/json
Example
{
  • "data": {
    }
}

Apply discount/promo code

Authorizations:
firebaseAuth
path Parameters
orderId
required
string <uuid>
Request Body schema: application/json
required
object

Responses

Request samples

Content type
application/json
{ }

Response samples

Content type
application/json
{ }

Remove an item from an order

Authorizations:
firebaseAuth
path Parameters
orderItemId
required
string <uuid>

Responses

Response samples

Content type
application/json
{ }

Bookings

Booking management and cancellation

List all bookings

Returns all bookings for a user. Paginated (data/meta envelope — see Pagination).

Notes:

  • timeStatus"future" for upcoming bookings, "past" for completed ones.
  • canceledAt — non-null if the booking was cancelled.
  • type"facilityRental" for site bookings, "session" for group classes, etc.
Authorizations:
firebaseAuth
path Parameters
userId
required
string <uuid>
query Parameters
pg[num]
integer >= 1
Default: 1

Page number (bracket style). 1-indexed. Interchangeable with flat page param.

pg[size]
integer >= 1
Default: 25

Page size (bracket style). Interchangeable with flat pageSize param.

page
integer >= 1
Default: 1

Page number (flat style). 1-indexed. Interchangeable with bracket pg[num] param.

pageSize
integer >= 1
Default: 25

Page size (flat style). Interchangeable with bracket pg[size] param.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

List upcoming bookings

Returns only upcoming bookings with sideloaded includes. Paginated (data/meta envelope — see Pagination). May return empty if no planned bookings exist.

Authorizations:
firebaseAuth
path Parameters
userId
required
string <uuid>
query Parameters
pg[num]
integer >= 1
Default: 1

Page number (bracket style). 1-indexed. Interchangeable with flat page param.

pg[size]
integer >= 1
Default: 25

Page size (bracket style). Interchangeable with flat pageSize param.

page
integer >= 1
Default: 1

Page number (flat style). 1-indexed. Interchangeable with bracket pg[num] param.

pageSize
integer >= 1
Default: 25

Page size (flat style). Interchangeable with bracket pg[size] param.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Get booking detail

Returns a single booking with sideloaded includes.

Authorizations:
firebaseAuth
path Parameters
bookingId
required
string <uuid>
query Parameters
include[]
Array of strings
Items Enum: "facilityRental" "section" "reservations" "sites" "locations" "reservationSiteIds" "customer" "participant"

Sideloaded relations (repeatable).

Responses

Response samples

Content type
application/json
{
  • "data": {
    },
  • "included": {
    }
}

Check refund eligibility

Returns refund eligibility information. The frontend polls this every 1 second until suggestionGenerated is true. If eligibleUntil is in the future, a refund is available.

Authorizations:
firebaseAuth
path Parameters
bookingId
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Preview refund amount and destinations

Returns refund preview with amount and available destinations.

Notes:

  • refundType"full", "partial", or "zero".
  • applicable: false for free ($0) bookings — no refund preview needed, just cancel directly.
Authorizations:
firebaseAuth
path Parameters
bookingId
required
string <uuid>

Responses

Response samples

Content type
application/json
Example
{
  • "data": {
    }
}

Cancel a booking

Cancels a booking.

Notes:

  • canceledAt is set on success. The status remains "confirmed" (not changed to "cancelled").
  • refundDestination values: "original_payment_methods" or "account_credit".
  • For free bookings, the refund-preview returns applicable: false — skip the refund flow and just POST cancel directly with no body (or empty {}).
Authorizations:
firebaseAuth
path Parameters
bookingId
required
string <uuid>
Request Body schema: application/json
object

Responses

Request samples

Content type
application/json
Example
{
  • "data": {
    }
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Payments

Payment details and cancellation

Get payment detail

Authorizations:
firebaseAuth
path Parameters
paymentId
required
string <uuid>

Responses

Response samples

Content type
application/json
{ }

Cancel a payment

Authorizations:
firebaseAuth
path Parameters
paymentId
required
string <uuid>

Responses

Response samples

Content type
application/json
{ }