{
  "$schema": "./_mapping.schema.json",
  "provider": "foreup",
  "name": "foreUP",
  "vertical": "golf",
  "verified_on": "2026-10-13",
  "docs": [
    {
      "title": "foreUP API (Apiary, rendered)",
      "url": "https://foreup.docs.apiary.io/",
      "note": "JS-rendered. The intro says 'Our API documentation requires approval by our Development team… partners@foreup.com', but the full reference is readable without a login."
    },
    {
      "title": "foreUP API Blueprint (raw source of the same docs, last updated 2024-11-12)",
      "url": "https://foreup.docs.apiary.io/api-description-document",
      "note": "Every field below was read from this file. Sections used: Group Courses, Group Tee Sheets, Group Tee Time Pricing, New Bulk Endpoints (Seasons / Seasonal Timeframes), Group Webhook, Group Webhook Events, Data Structures."
    }
  ],
  "access": {
    "public_docs": true,
    "how_to_get_access": "Email partners@foreup.com. foreUP must approve the partnership and grant per-course access. The API only returns courses the account has permission for (403 otherwise, see FAQ in the docs).",
    "auth": "POST /tokens with {email, password} returns a JWT in data.id. Send it on every call as header `x-authorization: Bearer <jwt>`. Single sign-on tokens from a course can be checked with GET /tokens/validate/{token}.",
    "base_url": "https://api.foreupsoftware.com/api_rest/index.php/"
  },
  "splitclub": {
    "integration_provider": null,
    "recommended_path": "poll_and_post",
    "note": "foreUP is not on the inbound hook's provider list. Its webhooks carry only ids (event, course_id, teesheet_id, id) and have no documented signature, so they can't be posted to /v1 as they are. A SplitClub worker should poll GET …/teetimes per tee sheet and per day and post Offerings with inline tiers and availability. It can use teetime.updated / teetime.deleted deliveries to its own endpoint as a cue to re-fetch."
  },
  "objects": {
    "Offering": {
      "source": "GET /courses?include=teesheets (+ GET /courses/{courseId}/teesheets?include=holes). One Offering per tee sheet per holes option (9 / 18).",
      "kind": "tee_time",
      "source_url": "https://foreup.docs.apiary.io/#reference/courses/tee-sheets/get-all",
      "note": "Tee sheets come in two versions: tee_sheet_version '1.0.0' (old, front/back sides) and '2.0.0'+ (revamped, sides with scheduleSideId, up to 6 sides / 54 holes). On 2.0.0+ you can split per side if that's wanted.",
      "fields": [
        { "to": "/external_id", "from": "courses[].id + teesheets[].id + holes", "transform": "compose 'foreup:{courseId}:{teesheetId}:{holes}'", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/kind", "from": "const:tee_time" },
        { "to": "/category", "from": "const:golf" },
        { "to": "/title", "from": "teesheets[].attributes.title + holes", "transform": "e.g. 'Exec 9 holes'; append '· 18 holes' when the sheet sells both", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/description", "from": "courses[].attributes.course_summary", "source_url": "https://foreup.docs.apiary.io/#reference/courses/courses/list-all" },
        { "to": "/metadata/foreup_course_id", "from": "courses[].id", "source_url": "https://foreup.docs.apiary.io/#reference/courses/courses/list-all" },
        { "to": "/metadata/foreup_teesheet_id", "from": "teesheets[].id", "source_url": "https://foreup.docs.apiary.io/#reference/courses/tee-sheets/get-all" },
        { "to": "/metadata/foreup_tee_sheet_version", "from": "courses[].attributes.tee_sheet_version", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/availability/timezone", "from": "courses[].attributes.state + zip", "transform": "Courses carry no IANA timezone. Look it up from the course address (state/zip), or ask the course. The ±hhmm offset on teetimes[].attributes.time gives the offset for that day only, not the zone.", "source_url": "https://foreup.docs.apiary.io/#reference/courses/courses/list-all", "unverified": true }
      ]
    },
    "PriceTier": {
      "source": "GET /courses/{courseId}/teesheets/{teesheetId}/seasons?include=… and …/seasons/{seasonId}/timeframes (Seasonal Timeframes). Pointers are inline on the Offering: /tiers/N/…",
      "source_url": "https://foreup.docs.apiary.io/api-description-document",
      "note": "foreUP pricing = Price Class (who: member, public, senior…) × Season (date range) × Time Frame (weekday booleans + start/end time). The price sits on the Time Frame. Make one tier per (timeframe, holes) for the public price class only. A tier has no date range, so seasons have to be flattened: post only the tiers of the season in force, and re-post when the season changes. The docs give no currency field.",
      "fields": [
        { "to": "/tiers/0/external_id", "from": "timeframes[].id + holes", "transform": "compose 'foreup-tf:{timeframeId}:{holes}'", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/tiers/0/label", "from": "timeframes[].attributes.name", "transform": "e.g. 'Twilight' / 'Default'; add the price class name when it isn't Public", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/tiers/0/amount", "from": "timeframes[].attributes.18holePrice | 9holePrice", "transform": "decimal dollars → integer cents (round half up). Pick 9holePrice or 18holePrice to match the Offering's holes. Walking price; see the cart fee below.", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/tiers/0/fees/0", "from": "timeframes[].attributes.18cartPrice | 9cartPrice", "transform": "{type:'other', label:'Cart', amount: decimal dollars → integer cents, included:false}. Or fold it into amount for a riding-only tier.", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/tiers/0/currency", "from": "const:usd", "transform": "foreUP returns no currency code; assume the course's local currency (usd for US courses)", "unverified": true },
        { "to": "/tiers/0/unit", "from": "const:per_person", "transform": "Tee Time Slot greenFee is documented as 'The green for for a single person in this slot'", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/tiers/0/days", "from": "timeframes[].attributes.monday … sunday", "transform": "each true boolean → 'mon'…'sun'", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/tiers/0/time_window/from", "from": "timeframes[].attributes.startTime", "transform": "number HHMM (0…2400) → 'HH:MM' (e.g. 1500 → '15:00'); omit time_window when it is 0–2400", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/tiers/0/time_window/to", "from": "timeframes[].attributes.endTime", "transform": "number HHMM → 'HH:MM'; 2400 → '23:59'", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/tiers/0/min_party", "from": "teetimes[].attributes.allowedGroupSizes", "transform": "min(allowedGroupSizes). Only on the revamped tee sheet (2.0.0+). On the old sheet the min/max group size isn't exposed.", "source_url": "https://foreup.docs.apiary.io/#reference/tee-sheets/tee-time-slots/get-all" },
        { "to": "/tiers/0/max_party", "from": "teetimes[].attributes.allowedGroupSizes", "transform": "max(allowedGroupSizes); fall back to 4 ('Each slot can have multiple bookings totalling up to 4 players')", "source_url": "https://foreup.docs.apiary.io/#reference/tee-sheets/tee-time-slots/get-all" }
      ]
    },
    "Availability": {
      "source": "GET /courses/{courseId}/teesheets/{teesheetId}/teetimes?date=YYYY-MM-DD&startTime=HHMM&endTime=HHMM&holes=9|18|Both[&scheduleSideId=all][&priceClassId=…]. Pointers are inline on the Offering: /availability/…",
      "source_url": "https://foreup.docs.apiary.io/#reference/tee-sheets/tee-time-slots/get-all",
      "note": "There's no date-range query: date, startTime and endTime are required, so you poll one day per call. On revamped tee sheets you get only the first side unless scheduleSideId=all.",
      "fields": [
        { "to": "/availability/slots/0/external_id", "from": "teetimes[].id", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/availability/slots/0/starts_at", "from": "teetimes[].attributes.time", "transform": "Documented as ISO 8601 with offset but without a colon ('2017-01-09T06:07:00-0700'). Insert the colon: '-0700' → '-07:00'.", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/availability/slots/0/capacity", "from": "teetimes[].attributes.availableSpots", "transform": "integer, open player spots", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/availability/slots/0/tier_overrides/0/amount", "from": "teetimes[].attributes.greenFee (+ cartFee when rateType='riding')", "transform": "decimal dollars → integer cents. Send it only when it differs from the matching tier (specials: hasSpecial / specialDiscountPercentage).", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/availability/slots/0/tier_overrides/0/tier", "from": "matching tier external_id", "transform": "the tier whose timeframe covers this slot's weekday/time" }
      ]
    },
    "Checkout": {
      "source": "Booking: GET /courses/{courseId}/teesheets/{teesheetId}/bookings/{bookingId}?include=sales,sales.items (read), or POST …/bookings with Bookings Creation (to hold the time once a SplitClub group is funded). Pointers are into checkout_create.",
      "source_url": "https://foreup.docs.apiary.io/#reference/tee-sheets/bookings",
      "note": "'Bookings don't contain any pricing information'. The price lives on the related sale (include=sales) or GET …/bookings/{id}/pricing (per price class, 9/18 hole + cart prices). POST …/bookings takes start without an offset ('2017-05-05T12:30:00'), course-local.",
      "fields": [
        { "to": "/external_booking_id", "from": "bookings.id", "transform": "e.g. 'TTID_1018142720xb6jt'; prefix 'foreup:' for uniqueness", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/offering_external_id", "from": "teesheetId + bookings.attributes.holes", "transform": "rebuild 'foreup:{courseId}:{teesheetId}:{holes}'", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/party_size", "from": "bookings.attributes.playerCount", "transform": "integer; SplitClub needs ≥ 2", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/amount", "from": "sales.attributes.total (include=sales) or Σ booking pricing", "transform": "decimal dollars → integer cents. For an unpaid booking, compute playerCount × (greenFee + cartFee) from the slot, or from GET …/bookings/{id}/pricing (18holePrice/18cartPrice).", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/currency", "from": "const:usd", "transform": "foreUP returns no currency code", "unverified": true },
        { "to": "/description", "from": "bookings.attributes.title + start + holes", "transform": "e.g. 'Tee time for 4, Sat 8:00, 18 holes'", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/pay_by", "from": "bookings.attributes.start", "transform": "ISO with ±hhmm offset → add the colon, then subtract SplitClub's pay-by lead time. foreUP has no pay-by concept; this is SplitClub policy.", "source_url": "https://foreup.docs.apiary.io/api-description-document" },
        { "to": "/metadata/foreup_booking_source", "from": "bookings.attributes.bookingSource", "source_url": "https://foreup.docs.apiary.io/api-description-document" }
      ]
    }
  },
  "webhooks": {
    "supported": true,
    "signature": {
      "header": "One custom header whose value you choose (Webhook.attributes.headerValue, 'The value for the key above'). The header name isn't shown in the docs.",
      "algorithm": "none documented: a static shared value, not an HMAC",
      "signed_payload": "not signed",
      "source_url": "https://foreup.docs.apiary.io/#reference/webhook",
      "unverified": true
    },
    "events": [
      {
        "event": "teetime.updated",
        "maps_to": "re-fetch the booking (GET …/bookings/{id}?include=sales) → update the linked Checkout; re-poll that day's teetimes → update Availability",
        "source_url": "https://foreup.docs.apiary.io/#reference/webhook-events",
        "fields": [
          { "to": "/external_booking_id", "from": "id", "transform": "booking id; payload is only {event, course_id, teesheet_id, id}", "source_url": "https://foreup.docs.apiary.io/#reference/webhook-events" }
        ]
      },
      {
        "event": "teetime.deleted",
        "maps_to": "cancel the linked SplitClub Checkout if still open; re-poll that day's Availability",
        "source_url": "https://foreup.docs.apiary.io/#reference/webhook-events",
        "fields": [
          { "to": "/external_booking_id", "from": "id", "source_url": "https://foreup.docs.apiary.io/#reference/webhook-events" }
        ]
      },
      {
        "event": "sale.created",
        "maps_to": "ignore (POS sale; can reconcile paid bookings via cart_id)",
        "source_url": "https://foreup.docs.apiary.io/#reference/webhook-events"
      },
      {
        "event": "customer.updated",
        "maps_to": "ignore",
        "source_url": "https://foreup.docs.apiary.io/#reference/webhook-events",
        "unverified": true
      }
    ],
    "note": "Register with POST /hooks {type:'api_hooks', attributes:{url, event, headerValue}}. The Webhook object's event enum lists only sale.created, teetime.deleted and teetime.updated, but the events page also documents customer.updated. Payloads carry ids only: 'The only way to know how a tee time has changed is by issueing a seperate call'."
  },
  "gaps": [
    "No currency field anywhere; amounts are bare decimals. Assume the course's local currency.",
    "No IANA timezone on courses. Slot times carry a ±hhmm offset (no colon); POST bookings.start is local with no offset. Get the zone from the course address or from the course.",
    "Webhook authenticity: only a static custom header value, no HMAC and no named header. SplitClub's /v1 hook can't verify it, hence poll_and_post.",
    "Tee-time slots need one call per day (date + startTime + endTime required). There's no range query.",
    "Seasons (date ranges) don't map to PriceTier, which has no date range. Post the tiers of the season in force and re-post at season change.",
    "Old tee sheets (tee_sheet_version 1.0.0) don't return allowedGroupSizes; min/max party isn't exposed for them.",
    "No cancellation policy, dress code or arrival window in the API, so Offering.policy can't be mapped.",
    "Events/leagues/shotguns appear only as booking.type values (league, shotgun, event, tournament) with no event object, so Event isn't mapped.",
    "No documented rate limit except the Seasons/Timeframes endpoints (50 requests per 10 seconds per route and user)."
  ]
}
