{
  "$schema": "./_mapping.schema.json",
  "provider": "chronogolf",
  "name": "Lightspeed Golf (Chronogolf)",
  "vertical": "golf",
  "verified_on": "2026-10-13",
  "docs": [
    {
      "title": "Lightspeed Golf Partner API (V2) documentation",
      "url": "https://partner-api.docs.chronogolf.com/",
      "note": "Public Postman documenter. Sections used: Conventions (read this first), Authentication, Club Setup & Pricing, Tee Times, Reservations & Booking."
    },
    {
      "title": "Same docs as a Postman collection (machine-readable)",
      "url": "https://partner-api.docs.chronogolf.com/api/collections/21108/2sBXwqqAKY?segregateAuth=true&versionTag=latest"
    }
  ],
  "access": {
    "public_docs": true,
    "how_to_get_access": "Credentials are issued by hand ('There's no self-service developer portal yet'). Email golf.api@lightspeedhq.com for staging and production credentials, and to connect each club. A newly connected club can take up to 5 minutes to show in GET /organizations.",
    "auth": "OAuth 2.0, refresh-token grant only. POST /oauth/token (form-encoded: grant_type=refresh_token, refresh_token, client_id, client_secret) returns a Bearer access_token valid 7200 s and a NEW refresh_token. Refresh tokens are single-use and rotate: persist the new one atomically and never refresh from two processes. Every V2 call needs `Accept: application/vnd.api+json` (otherwise an empty 403). Rate limit 200 requests per minute (429).",
    "base_url": "https://partner-api.chronogolf.com/partner_api/v2 (staging: https://partner-staging.chronogolf.com/partner_api/v2)"
  },
  "splitclub": {
    "integration_provider": null,
    "recommended_path": "poll_and_post",
    "note": "Not mapped to 'lightspeed'. That slug on the inbound hook is for Lightspeed's restaurant/retail POS webhooks. Lightspeed Golf is a separate API (partner-api.chronogolf.com, its own OAuth app, JSON:API) and sends no webhooks ('There are no webhooks today'). A SplitClub worker polls teetimes per course and per day (future-only) and reservations with filter[updated_since], then posts to /v1."
  },
  "objects": {
    "Offering": {
      "source": "GET /organizations (clubs) → GET /organizations/{org_id}/courses. One Offering per course (× bookable holes from GET …/products?filter[product_type][]=green_fee).",
      "kind": "tee_time",
      "source_url": "https://partner-api.docs.chronogolf.com/",
      "note": "Docs: 'Each course has its own tee sheet, its own tee times, and its own pricing… Tee-sheet reads are always per-course.' Courses expose only name, holes, par, distance, online_booking_enabled, cart_mandatory.",
      "fields": [
        { "to": "/external_id", "from": "course.id (+ product.attributes.bookable_holes)", "transform": "compose 'chronogolf:{courseUuid}:{holes}'", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/kind", "from": "const:tee_time" },
        { "to": "/category", "from": "const:golf" },
        { "to": "/title", "from": "course.attributes.name + bookable_holes", "transform": "e.g. 'Blue Course · 18 holes'", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/metadata/chronogolf_org_id", "from": "organization.id", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/metadata/chronogolf_course_id", "from": "course.id", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/metadata/cart_mandatory", "from": "course.attributes.cart_mandatory", "transform": "boolean → 'true'/'false'", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/availability/timezone", "from": "(not in API)", "transform": "Docs: 'The club's timezone is not exposed anywhere in the API.' Get it from the club out of band and store it on the integration.", "source_url": "https://partner-api.docs.chronogolf.com/", "unverified": true }
      ]
    },
    "PriceTier": {
      "source": "GET /organizations/{org_id}/teetimes?filter[date]=…&filter[course]=…&custom_params[with_pricing]=true&custom_params[player_types][]={public type}&custom_params[holes]=… (rates per player type). Pointers are inline on the Offering: /tiers/N/…",
      "source_url": "https://partner-api.docs.chronogolf.com/",
      "note": "Lightspeed Golf exposes no rate sheet (no weekday/twilight rules). Prices are only computed per tee time per player type. Make one tier per player type the club sells publicly (default_player_type on the organization, default_player_role 'public'), with the most common price as amount, and use per-slot tier_overrides for the rest. days / time_window can't be read from the API.",
      "fields": [
        { "to": "/tiers/0/external_id", "from": "rates[].player_type_id + holes", "transform": "compose 'chronogolf-pt:{playerTypeUuid}:{holes}'", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/tiers/0/label", "from": "player_type.attributes.name", "transform": "from GET …/player_types (e.g. 'Public', 'Junior')", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/tiers/0/amount", "from": "teetimes[].attributes.rates[].green_fee (or rates[].subtotal, which includes a mandatory cart)", "transform": "decimal number in the club's currency → integer minor units (× 100, round half up). Pre-tax unless organization.tax_inclusive is true.", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/tiers/0/fees/0", "from": "teetimes[].attributes.rates[].half_cart", "transform": "{type:'other', label:'Cart (shared)', amount: decimal → cents, included:false}; null when the course doesn't price carts that way", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/tiers/0/fees/1", "from": "organizations[].attributes.tax_inclusive", "transform": "if false: {type:'tax', percent:<club rate from the club>, included:false}. The API has 'no tax rate and no tax-amount field anywhere'.", "source_url": "https://partner-api.docs.chronogolf.com/", "unverified": true },
        { "to": "/tiers/0/currency", "from": "(not in API)", "transform": "Docs: 'The club's currency is not exposed on the org, course, product, tee-time, or round.' Configure per club (usd / cad / eur …).", "source_url": "https://partner-api.docs.chronogolf.com/", "unverified": true },
        { "to": "/tiers/0/unit", "from": "const:per_person", "transform": "rates are per player; repeat custom_params[player_types][] per player to price a group", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/tiers/0/max_party", "from": "const:4", "transform": "'A reservation… has 1 to 4 rounds, one per golfer'", "source_url": "https://partner-api.docs.chronogolf.com/" }
      ]
    },
    "Availability": {
      "source": "GET /partner_api/v2/organizations/{org_id}/teetimes?filter[date]=YYYY-MM-DD&filter[course]={course_uuid}&custom_params[with_pricing]=true&custom_params[player_types][]=… (paged, page[size] ≤ 100). Pointers are inline on the Offering: /availability/…",
      "source_url": "https://partner-api.docs.chronogolf.com/",
      "note": "Today and future dates only (past → 422). Querying beyond a player type's booking_range returns 422 for the whole call. Check restrictions before reading subtotal: subtotal is a number when bookable and an array of messages when not.",
      "fields": [
        { "to": "/availability/slots/0/external_id", "from": "teetimes[].id", "transform": "UUID", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/availability/slots/0/starts_at", "from": "teetimes[].attributes.date + teetimes[].attributes.start_time", "transform": "'2026-05-29' + '06:00' are club-local wall clock with no offset. Combine and apply the club's IANA timezone (supplied out of band) → ISO 8601 with offset.", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/availability/slots/0/capacity", "from": "teetimes[].attributes.free_slots", "transform": "integer open spots (max 4, a frozen slot reports 0). Send 0 when blocked is true or restrictions is non-empty.", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/availability/slots/0/tier_overrides/0/amount", "from": "teetimes[].attributes.rates[].green_fee", "transform": "decimal → integer minor units; only when it differs from the tier amount", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/availability/slots/0/tier_overrides/0/tier", "from": "teetimes[].attributes.rates[].player_type_id", "transform": "→ 'chronogolf-pt:{playerTypeUuid}:{holes}'", "source_url": "https://partner-api.docs.chronogolf.com/" }
      ]
    },
    "Checkout": {
      "source": "Reservation: GET /organizations/{org_id}/reservations?filter[updated_since]=…&include=rounds,teetime (read), or the 3-step booking flow POST …/reservation_requests → …/round_requests (one per player) → …/reservations (to book once a group funds), then POST …/payment_confirmations. Pointers are into checkout_create.",
      "source_url": "https://partner-api.docs.chronogolf.com/",
      "note": "payment_confirmations records an external payment: amount must equal Σ round.price within $0.01, and reference holds our id. That's where SplitClub's chk_ id goes after funding. The confirm step (POST /reservations) is safe to retry (201 = booked, 404 = already booked); the earlier steps aren't.",
      "fields": [
        { "to": "/external_booking_id", "from": "reservation.id", "transform": "UUID; prefix 'chronogolf:'. booking_reference ('9M0B-5S0N') is a display label only, so put it in metadata.", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/offering_external_id", "from": "reservation.relationships.teetime → teetime.relationships.course.id + reservation.attributes.holes", "transform": "rebuild 'chronogolf:{courseUuid}:{holes}'. teetime can be null on legacy/removed slots.", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/party_size", "from": "reservation.relationships.rounds.data.length", "transform": "count of rounds (1–4, one per player); SplitClub needs ≥ 2", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/amount", "from": "Σ included[type=round].attributes.price", "transform": "decimal numbers in the club's currency → sum → integer minor units. Pre-tax on tax-exclusive clubs (add tax yourself; the tax-inclusive total isn't exposed).", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/currency", "from": "(not in API)", "transform": "per-club configured currency", "unverified": true },
        { "to": "/description", "from": "teetime.attributes.date + start_time + reservation.attributes.holes", "transform": "e.g. 'Tee time for 4, Sat 8:00, 18 holes'", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/pay_by", "from": "teetime.attributes.date + start_time", "transform": "club-local → offset via the club's timezone, minus SplitClub's pay-by lead time (SplitClub policy; Lightspeed has no hold/expiry on reservations)", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/metadata/booking_reference", "from": "reservation.attributes.booking_reference", "source_url": "https://partner-api.docs.chronogolf.com/" },
        { "to": "/metadata/chronogolf_state", "from": "reservation.attributes.state + cancelled_at", "transform": "treat cancelled_at != null as cancelled (state can still read 'confirmed')", "source_url": "https://partner-api.docs.chronogolf.com/" }
      ]
    }
  },
  "webhooks": {
    "supported": false,
    "events": [],
    "note": "Docs, Conventions → Polling and rate limits: 'There are no webhooks. To stay in sync, poll with filter[updated_since]= on reservations and customers and pull only what changed.' A round change, check-ins included, bumps the reservation's updated_at. Source: https://partner-api.docs.chronogolf.com/"
  },
  "gaps": [
    "Club timezone isn't exposed anywhere. Tee times are club-local wall clock; record timestamps are shown in US Eastern whatever the club's zone. Collect the IANA zone per club at onboarding.",
    "Club currency isn't exposed (bare numbers). Collect it per club.",
    "Tax: only the organization's tax_inclusive boolean; no rate or amount. For tax-exclusive clubs the payable total has to be computed with a rate supplied by the club.",
    "No rate-sheet endpoint. Weekday/weekend/twilight rules (PriceTier days / time_window) can't be read; prices exist only per tee time per player type.",
    "Tee times are future-only and limited by each player type's booking_range (422 beyond it). Inventory further out needs a long-window player type from the club.",
    "Requests don't freeze inventory. A slot can be taken between reservation_request and confirm, so book only after the SplitClub group is funded, and handle a failed confirm.",
    "No webhooks; polling only, at 200 requests per minute.",
    "No cancellation policy, dress code or arrival window in the API, so Offering.policy can't be mapped.",
    "Teetimes can link to an 'event' relationship (filter[including_events]=true), but no event endpoint or fields are documented, so Event isn't mapped.",
    "27-hole and 2x9 courses: an 18-hole booking auto-creates a linked back-nine reservation, and the return nine isn't checked when the list shows a slot as available. Expect confirm failures there."
  ]
}
