{
  "$schema": "./_mapping.schema.json",
  "provider": "fareharbor",
  "name": "FareHarbor (External API v1)",
  "vertical": "tours_rentals",
  "verified_on": "2026-10-13",
  "docs": [
    {
      "title": "FareHarbor Integration Center: home (who can get access, keys, rate limits)",
      "url": "https://developer.fareharbor.com/api/external/v1/home",
      "note": "Public. API access is for qualified Affiliates (distribution); webhook access is for Software Partners. User keys are issued per currency; the supplier must approve the partner."
    },
    {
      "title": "FareHarbor External API reference",
      "url": "https://developer.fareharbor.com/api/external/v1/api-reference",
      "note": "Public Redoc page. Every field below is quoted from its OpenAPI 3.1 source."
    },
    {
      "title": "FareHarbor External API OpenAPI spec (api-reference)",
      "url": "https://developer.fareharbor.com/api/external/v1/FareHarbor-External-API.yaml/api-reference",
      "note": "Public YAML behind the reference page. Item, Availability, Booking, Company and Book-request schemas."
    },
    {
      "title": "FareHarbor Webhooks (Booking, Item, Crew member; best practices)",
      "url": "https://developer.fareharbor.com/api/external/v1/webhooks",
      "note": "Public. Spec at https://developer.fareharbor.com/api/external/v1/FareHarbor-External-API.yaml/webhooks. Webhooks are configured by FareHarbor support, carry the full booking JSON, and are not signed."
    }
  ],
  "access": {
    "public_docs": true,
    "how_to_get_access": "Gated. 'External API access is granted on a case by case basis': email api-support@fareharbor.com (or the account manager) to apply as an Affiliate (API, to book suppliers' inventory) and/or Software Partner (webhooks). FareHarbor issues an App key and one User key per currency, the supplier (venue) must approve SplitClub before SplitClub can book for it, and there is a certification step. Webhook URLs (one production, one test) are set by support@fareharbor.com.",
    "auth": "Two headers on every request: X-FareHarbor-API-App: <app key> and X-FareHarbor-API-User: <user key>. Rate limits are per IP.",
    "base_url": "https://fareharbor.com/api/external/v1"
  },
  "splitclub": {
    "integration_provider": null,
    "recommended_path": "poll_and_post",
    "note": "SplitClub's inbound hook does not accept a 'fareharbor' provider yet. Path: a SplitClub-side worker with Affiliate keys pulls GET /companies/{shortname}/items/ and, per item, availabilities in 7-day windows (FareHarbor recommends: next 90 days weekly, next 30 days daily, today every 2 hours), and POSTs Offerings with inline tiers and slots to SplitClub. When a group Checkout funds, the worker books it back with POST /companies/{shortname}/availabilities/{availability.pk}/bookings/ (one customers[] entry per guest with that availability's customer_type_rate pk, external_id = the checkout's external_booking_id, amount_paid = what SplitClub collected in cents). A booking webhook (Software Partner access) can later replace polling for changes once 'fareharbor' is added to the inbound hook."
  },
  "objects": {
    "Offering": {
      "source": "GET /companies/{shortname}/items/{item.pk}/ (plus GET /companies/{shortname}/ for timezone and currency)",
      "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Items-Endpoints/operation/get-item",
      "note": "Pointers are rooted at the SplitClub Offering (tiers and availability inline). FareHarbor items have no type field, so kind is chosen per item when the venue is onboarded: boat_day (charters, boat tours), rental (jet ski, kayak, bike), package or ride. Item tags[] (shortname) may help suggest it.",
      "fields": [
        { "to": "/external_id", "from": "item.pk", "transform": "int → string, prefix fh_item_<company shortname>_", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Items-Endpoints/operation/get-item" },
        { "to": "/kind", "from": "const:boat_day | rental | package | ride", "transform": "set per item at onboarding; not in FareHarbor's data" },
        { "to": "/title", "from": "item.name", "transform": "truncate to 120 chars", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Items-Endpoints/operation/get-item" },
        { "to": "/description", "from": "item.description_text", "transform": "plain text; truncate to 4000. (description / description_safe_html are markdown / HTML)", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Items-Endpoints/operation/get-item" },
        { "to": "/media/0/url", "from": "item.images[].image_cdn_url (or item.image_cdn_url for the primary)", "transform": "up to 10; type image; must be https", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Items-Endpoints/operation/get-item" },
        { "to": "/tiers", "from": "item.customer_prototypes[]", "transform": "one tier per customer prototype, see PriceTier", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Items-Endpoints/operation/get-item" },
        { "to": "/availability", "from": "availabilities for the item", "transform": "see Availability", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Availabilities-Endpoints" },
        { "to": "/policy/cancellation", "from": "item.effective_cancellation_policy {type, cutoff_hours_before}", "transform": "type hours-before-start → [{hours_before: cutoff_hours_before, refund_pct: 100}]; never → []; always → [{hours_before: 0, refund_pct: 100}]; hours-before-midnight → approximate as hours_before = cutoff_hours_before + hours from midnight to the slot start, or leave to policy.notes. The docs say when a booking can be cancelled, not the refund percentage, so refund_pct 100 is an assumption", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Items-Endpoints/operation/get-item", "unverified": true },
        { "to": "/policy/notes", "from": "item.cancellation_policy", "transform": "markdown → plain text; truncate to 2000", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Items-Endpoints/operation/get-item" },
        { "to": "/policy/min_age", "from": "item.structured_description.min_age", "transform": "number|null → 18 or 21 when it equals those, 0 when null; other ages go to policy.notes", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Items-Endpoints/operation/get-item" },
        { "to": "/metadata/fareharbor_shortname", "from": "company.shortname", "transform": "needed to call back for bookings", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Company-Endpoints/operation/get-company" }
      ]
    },
    "PriceTier": {
      "source": "item.customer_prototypes[] (defaults) and availability.customer_type_rates[] (per slot)",
      "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Items-Endpoints/operation/get-item",
      "note": "Pointers are rooted at the Offering; /tiers/0 is each customer type (Adult, Child, Private charter…). Tier ids use the customer prototype pk, which is stable across availabilities; each availability has its own customer_type_rate pk, which is what a booking must send, so keep availability.pk → {customer_prototype.pk → customer_type_rate.pk} for the write-back. Money: integer minor units of the company's currency. The rate's `total` description says only 'The price of a single customer of this type' (example 20000); the unit is confirmed by the same docs' amount_paid ('USD cents… 1425 means $14.25') and the booking pair receipt_total 38776 ↔ receipt_total_display \"387.76\".",
      "fields": [
        { "to": "/tiers/0/external_id", "from": "customer_prototypes[].pk", "transform": "prefix fh_cp_", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Items-Endpoints/operation/get-item" },
        { "to": "/tiers/0/label", "from": "customer_prototypes[].display_name", "transform": "truncate to 80", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Items-Endpoints/operation/get-item" },
        { "to": "/tiers/0/amount", "from": "customer_prototypes[].total", "transform": "integer minor units, copy as is (20000 = $200.00). Use total (pre-tax) and declare tax separately, or total_including_tax with a tax fee included: true", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Items-Endpoints/operation/get-item" },
        { "to": "/tiers/0/currency", "from": "company.currency", "transform": "already lower case ISO 4217 (enum aed…usd)", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Company-Endpoints/operation/get-company" },
        { "to": "/tiers/0/unit", "from": "const:per_person", "transform": "a customer type is priced per customer. For a private-charter type whose one 'customer' is the whole boat, send per_group instead (venue confirms at onboarding)" },
        { "to": "/tiers/0/min_party", "from": "availability.customer_type_rates[].minimum_party_size", "transform": "number|null; null → omit. Applies only when at least one customer of that type is in the party", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Availabilities-Endpoints" },
        { "to": "/tiers/0/max_party", "from": "availability.customer_type_rates[].maximum_party_size", "transform": "number|null; null → omit; clamp to 50", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Availabilities-Endpoints" },
        { "to": "/tiers/0/fees/0", "from": "item.tax_percentage", "transform": "{type: tax, percent: tax_percentage, included: false} when > 0 and amount came from total", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Items-Endpoints/operation/get-item" }
      ]
    },
    "Availability": {
      "source": "GET /companies/{shortname}/items/{item.pk}/minimal/availabilities/date-range/{start-date}/{end-date}/?detailed=yes (max 7 days per call), or GET /companies/{shortname}/availabilities/{availability.pk}/ for full prices",
      "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Availabilities-Endpoints/operation/get-minimal-availabilities-by-date-range",
      "note": "Pointers are rooted at the Offering. Times: 'Availability start and end times are in the company's local timezone' and the documented example carries a colon-less offset, \"2023-08-18T00:30:00-0600\". SplitClub needs RFC 3339 offsets, so insert the colon (-0600 → -06:00); if a value arrives without any offset, attach the company timezone's offset for that local time. The minimal endpoint's customer_type_rates carry customer_prototype.total but not the rate's own total, so read per-slot prices from the full availability.",
      "fields": [
        { "to": "/availability/timezone", "from": "company.timezone", "transform": "IANA name, e.g. America/Los_Angeles", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Company-Endpoints/operation/get-company" },
        { "to": "/availability/slots/0/external_id", "from": "availabilities[].pk", "transform": "prefix fh_av_", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Availabilities-Endpoints/operation/get-minimal-availabilities-by-date-range" },
        { "to": "/availability/slots/0/starts_at", "from": "availabilities[].start_at", "transform": "\"2023-08-18T00:30:00-0600\" → \"2023-08-18T00:30:00-06:00\"", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Availabilities-Endpoints/operation/get-availability" },
        { "to": "/availability/slots/0/ends_at", "from": "availabilities[].end_at", "transform": "same; start_at may equal end_at for 'point' availabilities → omit ends_at then", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Availabilities-Endpoints/operation/get-availability" },
        { "to": "/availability/slots/0/capacity", "from": "availabilities[].capacity", "transform": "customers that can still book (changes as others book). 0 → drop the slot (SplitClub capacity minimum is 1). Skip slots whose online_booking_status is closed or cancelled, or call", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Availabilities-Endpoints/operation/get-minimal-availabilities-by-date-range" },
        { "to": "/availability/slots/0/tier_overrides/0/tier", "from": "availability.customer_type_rates[].customer_prototype.pk", "transform": "prefix fh_cp_ (matches the tier external_id)", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Availabilities-Endpoints/operation/get-availability" },
        { "to": "/availability/slots/0/tier_overrides/0/amount", "from": "availability.customer_type_rates[].total", "transform": "integer minor units; only when it differs from the tier's default", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Availabilities-Endpoints/operation/get-availability" },
        { "to": "/availability/cutoff_minutes", "from": "availabilities[].effective_booking_cutoff.cutoff (?detailed=yes)", "transform": "a company-local datetime, not a duration: minutes = slot start − cutoff. Varies per slot; use the most common value or leave unset", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Availabilities-Endpoints" }
      ]
    },
    "Checkout": {
      "source": "Booking: GET /companies/{shortname}/bookings/{Booking.uuid}/, or the booking webhook body {\"booking\": {…}}",
      "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Bookings-Endpoints/operation/get-booking",
      "note": "Pointers are rooted at checkout_create. Two directions. (1) SplitClub-first (preferred): the party funds a Checkout, then the worker POSTs the booking with external_id = the checkout's external_booking_id, customers[] = one {customer_type_rate: pk} per guest, amount_paid = collected cents (null = full payment created; must not exceed the total). (2) Booking-first: a booking that already exists in FareHarbor is split, mapping as below.",
      "fields": [
        { "to": "/external_booking_id", "from": "booking.uuid", "transform": "prefix fh_bk_ (the UUID is how the API addresses a booking; display_id like \"#6876876\" is for humans)", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Bookings-Endpoints/operation/get-booking" },
        { "to": "/amount", "from": "booking.receipt_total", "transform": "integer minor units (38776 ↔ \"387.76\"); null → compute Σ customers[].customer_type_rate.total_including_tax", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Bookings-Endpoints/operation/get-booking" },
        { "to": "/currency", "from": "booking.company.currency", "transform": "already lower case", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Bookings-Endpoints/operation/get-booking" },
        { "to": "/party_size", "from": "booking.customer_count", "transform": "must be 2..50 for a split", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Bookings-Endpoints/operation/get-booking" },
        { "to": "/offering_external_id", "from": "booking.availability.item.pk", "transform": "prefix fh_item_<shortname>_", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Bookings-Endpoints/operation/get-booking" },
        { "to": "/description", "from": "booking.availability.item.name + booking.availability.start_at", "transform": "e.g. \"Jet Ski Tour for 4, Sat 8 Nov 09:00\"", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Bookings-Endpoints/operation/get-booking" },
        { "to": "/pay_by", "from": "booking.availability.start_at", "transform": "fix the offset colon; SplitClub deadline = start minus the venue's cutoff. Booking-first: also before the FareHarbor cancellation cutoff (effective_cancellation_policy) so an unfunded booking can still be cancelled", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Bookings-Endpoints/operation/get-booking" },
        { "to": "/metadata/fareharbor_display_id", "from": "booking.display_id", "source_url": "https://developer.fareharbor.com/api/external/v1/api-reference#tag/Bookings-Endpoints/operation/get-booking" }
      ]
    }
  },
  "webhooks": {
    "supported": true,
    "events": [
      {
        "event": "Booking notification",
        "maps_to": "booking.status booked → link/create Checkout; cancelled → cancel the linked Checkout; rebooked → follow rebooked_to and move the Checkout",
        "source_url": "https://developer.fareharbor.com/api/external/v1/webhooks#tag/Webhooks/Webhook-Schemas/Booking",
        "fields": [
          { "to": "/external_booking_id", "from": "booking.uuid", "transform": "prefix fh_bk_", "source_url": "https://developer.fareharbor.com/api/external/v1/webhooks#tag/Webhooks/Webhook-Schemas/Booking" },
          { "to": "/party_size", "from": "booking.customer_count", "source_url": "https://developer.fareharbor.com/api/external/v1/webhooks#tag/Webhooks/Webhook-Schemas/Booking" },
          { "to": "/amount", "from": "booking.receipt_total", "transform": "integer minor units", "source_url": "https://developer.fareharbor.com/api/external/v1/webhooks#tag/Webhooks/Webhook-Schemas/Booking" }
        ]
      },
      {
        "event": "Item notification",
        "maps_to": "GET item.external_api_url → re-sync that Offering",
        "source_url": "https://developer.fareharbor.com/api/external/v1/webhooks#tag/Webhooks/Webhook-Schemas/Item"
      },
      {
        "event": "Crew member notification",
        "maps_to": "ignore",
        "source_url": "https://developer.fareharbor.com/api/external/v1/webhooks"
      }
    ],
    "note": "There are no event-name strings: FareHarbor POSTs to the URL(s) support configured, and a booking webhook can be set to fire on all changes, creations only, or cancellations only. The body is the full, current booking as {\"booking\": {…}} (status booked | cancelled | rebooked tells you what happened); the data type is chosen at setup (Bookings only, Bookings + payments, Optimized variants, or a custom schema), and payments[] amounts are minor units with *_display strings. Item webhooks send {\"item\": {pk, name, company, external_api_url, dashboard_url}}. Companies in demo mode do not trigger webhooks. Failures are retried with long pauses and too many failures can get the webhook deactivated. No signature: FareHarbor 'does not provide a way to incorporate custom headers', recommends a secret in the URL path or query string (e.g. ?key=…) and re-fetching the booking with GET /companies/{shortname}/bookings/{uuid}/ before trusting it, and advises against IP allowlists."
  },
  "gaps": [
    "Access is gated: API keys only for approved Affiliates, webhooks only for Software Partners, certification required, and each venue (supplier) must approve SplitClub. Until then nothing can be tested.",
    "Items have no category/type field, so Offering.kind (boat_day, rental, package, ride) must be chosen per item at onboarding.",
    "Item has no duration field (structured_description.duration is free text); slot end_at is the reliable duration.",
    "The customer_type_rate `total` field does not state its unit in its own description; minor units are inferred from the same spec's amount_paid ('USD cents') and receipt_total ↔ *_display example pairs.",
    "Cancellation policy says when a booking may be cancelled, not how much is refunded; policy.cancellation refund_pct is assumed 100. The delete-booking docs mention a 48-hour / 100% refund rule for API cancellations that SplitClub should confirm with FareHarbor.",
    "start_at/end_at offsets are documented by example as ±HHMM (no colon); verify on live data and normalise to ±HH:MM.",
    "Webhooks are not signed and cannot carry custom headers; the inbound hook needs a URL-secret check plus a re-fetch, and 'fareharbor' is not an accepted integration_provider yet.",
    "Whether amount_paid recorded by SplitClub as an affiliate settles to the venue the way SplitClub's payouts expect (invoice_price, affiliate payments) needs FareHarbor's partner team."
  ]
}
