Charter Boats
Charter Boats API
AI Integrations

Search Boats (AI)

Search charter boats with fuzzy location matching, availability checking, and deal ranking

Showing API v1.1 — versionsShowing API v1.0 — versions

GET/ai/boats

Search available charter boats by location, type, dates, capacity, and budget. Optimized for AI consumption with fuzzy location matching, availability bitmask filtering, and batch price resolution.

You are reading API v1.1 — send api_version=1.1 to get it. A call to /api/ai/* that names no version gets 1.0; switch the picker at the top to v1.0 to read that shape.

You are reading API v1.0, what a call to /api/ai/* gets when it names no api_version. A search row carries discounted_price_per_day (the charter per day, our auto-applied offer already taken off), discount_percentage, discount_label, required_extras, required_fees and the "~… EUR total for the week of …" price_summary in place of list_price, card, rental_type and has_multi_port. The detail carries pricing.ranges[].price_per_day, its deposits inside fees.required, slots without prices, and no stay_price. New integrations should send api_version=1.1 — see Versions.

Authentication

No authentication required. A key is optional — see identifying yourself. A valid API key records the call against your account, and when that account has an active affiliate slug every boat url in the response carries it as your referral (see Linking to a boat). An invalid key is ignored, never a 401. All /ai/* endpoints share one rate-limit budget of up to 60 requests per 10 minutes (30 for Custom GPT clients) without a key — a ceiling to stay under rather than an exact allowance, and a valid API key has its own, larger allowance; past it you get 429 with a Retry-After header. See Rate limits.

Query Parameters

ParameterTypeRequiredDescription
locationstringNoDestination — marina, city, island, sailing region, or country, written freely (Lefkada, Greece is fine). See Location Matching.
searchstringNoA boat's NAME (Papillon), matched against the boat's name and nothing else, as a WHOLE WORD (Bella matches Bella Luna, never Marbella; send the whole name, not a prefix) — it is never read as a make or a model (search=Oceanis finds the boats called Oceanis, not the Beneteau range). Make and model belong in makemodel; send both to find a named boat of one make. A name search needs no location, and also returns named_matches or name_not_found.
makemodelstringNoMake and/or model, written the way a guest says it: Bali 4.2, FP 41, Fountaine Pajot, Lagoon, Lucia 40. Make alone is fine, and so is a model alone — a model with no make in front resolves against the model catalogue. Do not split it or add punctuation — the split is resolved against the catalogue, not by spelling. Acronyms resolve through manufacturer aliases (FP, IY). See Make and model in one term.
mstringNoAn already-resolved make/model filter, slug or slug:model (bali, bali:4.2); comma-separate to combine (bali:4.2,lagoon). Taken verbatim and never re-parsed — use it to reproduce a search_url exactly. Prefer makemodel for anything a person typed; sending both ignores makemodel.
boat_typestringNoOne of: sailboat, catamaran, yacht, motorboat, gulet, rib, other
manufacturerstringNoManufacturer name or alias (fuzzy matched, e.g. Bali, Beneteau, FP). A make resolved from makemodel replaces it; makemodel covers this for most callers.
featuresstringNoComma-separated equipment the boat must have — all of them. Canonical slugs (air_conditioning, watermaker) or loose synonyms (ac); see Features for the vocabulary. What resolved is echoed in features_applied; anything unrecognised is ignored and listed in features_unmatched.
guestsintegerNoMinimum guest capacity
date_fromstringNoCharter start date (YYYY-MM-DD). Enables availability filtering and a price summary for the whole stay. Past dates are auto-corrected to the next matching future date.
date_tostringNoCharter end date (YYYY-MM-DD). Typical charters are 7 days (Saturday to Saturday).
price_minnumberNoMinimum price per day in currency (EUR when omitted) for the boat, all fees in — a boat's list figure (list_price.per_day): the grand total at max guests (charter, every required fee now or at pickup, the 45 EUR platform fee, our discounts; deposits never) ÷ days. Undated, the boat's lowest; with dates, the searched week's.
price_maxintegerNoMaximum price per day in currency (EUR when omitted) for the boat, on the same list figure as price_min. Decimals are floored, and 0 means "not asked" — the same for every number below.
total_maxnumberNoMaximum grand total for the stay in currency (EUR when omitted) — screened on the total price_summary states (list_price.total: the boat page's own Total for the stay it names, for the guests given, else max guests); a month-wide range on the weekly list_price.per_day × the stay's days at max guests — the charter, every required fee now or at pickup and the flat 45 EUR platform fee; deposits never included. Requires date_from/date_to.
cabinsintegerNoMinimum number of guest cabins (crew cabins are not counted)
length_minintegerNoMinimum length in feet
year_minintegerNoMinimum build year
sortstringNoSort order: best_value (default), deals, price_low, price_high, newest
limitintegerNoResults to return (default: 5, max: 25)
currencystringNoEUR (default) or USD, API version 1.1: every amount in the answer — list_price, price_summary, inventory.per_day — is written in it, at the rate the site's currency selector uses, and price_min, price_max and total_max are read in it. search_url and alert_query stay in euros, as the website's own links carry them. Version 1.0 answers in euros.

Request

# Search catamarans in Croatia for 8 guests
curl "https://charter.boats/api/ai/boats?location=Split&boat_type=catamaran&guests=8&sort=deals&api_version=1.1"
 
# Search with dates for availability + a whole-stay price summary
curl "https://charter.boats/api/ai/boats?location=Split&date_from=2026-09-26&date_to=2026-10-03&limit=2&api_version=1.1"
# Search catamarans in Croatia for 8 guests
curl "https://charter.boats/api/ai/boats?location=Split&boat_type=catamaran&guests=8&sort=deals"
 
# Search with dates for availability + a whole-stay price summary
curl "https://charter.boats/api/ai/boats?location=Split&date_from=2026-09-26&date_to=2026-10-03&limit=2"

With no api_version the answer is v1.0; api_version=1.0 asks for it by name.

Response

{
  "boats": [
    {
      "id": "05d26e2d-473c-4d30-b9eb-05c447c835ac",
      "url": "https://chrtr.me/n8wxFd0DG7",
      "title": "Sailing Point",
      "boat_type": "catamaran",
      "manufacturer": "Lagoon",
      "model": "40",
      "location": "ACI Marina Split, Croatia",
      "year": 2020,
      "length_ft": 38.5,
      "capacity": 12,
      "cabins": 6,
      "toilets": 4,
      "skippered": "optional",
      "discount_percentage": 50,
      "discount_label": "Charter discount",
      "price_rank_percentile": 3,
      "currency": "EUR",
      "price_summary": "3040 EUR total for the week of Sep 26–Oct 3, 12 guests. 47% discount applied.",
      "list_price": { "per_day": 435, "total": 3040, "original_per_day": 822, "discount_pct": 47, "from": false, "week": 3, "week_start": "2026-09-26", "units": 7, "unit": "night", "party": 12, "label": "for 7 nights · all fees in", "deposits": 1748 },
      "checkinout_days": ["Saturday"],
      "min_charter_days": 7,
      "instant_book": true,
      "hero_image_url": "https://media.charter.boats/boats/05d26e2d-473c-4d30-b9eb-05c447c835ac/images/1772320506123_ffe0cd6a_card.jpg"
    }
  ],
  "inventory": {
    "total_boats": 122,
    "per_day": { "min": 150, "median": 520, "max": 3600 },
    "capacity": { "min": 2, "max": 26 },
    "length_ft": { "min": 21, "max": 180.4 },
    "by_type": { "gulet": 15, "yacht": 38, "sailboat": 22, "catamaran": 27, "motorboat": 20 }
  },
  "location_matched": {
    "id": "21157",
    "name": "Split, West Coast (Zapadna obala)",
    "country": "Croatia"
  },
  "search_url": "https://charter.boats/search?l=text%3ASplit&ln=Split%2C+West+Coast&d=2026-09-26%2C2026-10-03&t=catamaran&g=8",
  "alert_query": "location_type=text&location_value=Split&d=2026-09-26%2C2026-10-03&type=catamaran&guests=8",
  "alert_horizon": { "from": "2026-09-05", "to": "2027-09-03" },
  "results_ordered_by": "Best first — already ranked by value for money. Present them in this order; do not re-sort. …",
  "total_matching": 122
}

A real v1.0 response, GET /ai/boats?location=Lefkada&limit=1&date_from=2027-06-05&date_to=2027-06-12 with no api_version:

{
  "boats": [
    {
      "id": "8d1f81ba-c8e3-468c-9d03-fce534dd3393",
      "url": "https://chrtr.me/Y6GA8F0HK7",
      "title": "Luna Rossa II",
      "boat_type": "catamaran",
      "manufacturer": "Fountaine Pajot",
      "model": "Lucia 40",
      "location": "D-Marin Lefkas, Municipal Unit of Lefkada, Greece",
      "year": 2019,
      "length_ft": 38.5,
      "capacity": 10,
      "cabins": 4,
      "toilets": 4,
      "skippered": "optional",
      "price_rank_percentile": 50,
      "currency": "EUR",
      "price_summary": "~3361 EUR total for the week of Sat 5 Jun 2027 → Sat 12 Jun 2027, 10 guests. 25% discount applied. Charter: 2926 EUR (418/day × 7 nights). Required per-booking fees: 390 EUR. Platform fee: 45 EUR (flat, per booking)",
      "checkinout_days": [
        "Saturday"
      ],
      "min_charter_days": 7,
      "instant_book": true,
      "hero_image_url": "https://media.charter.boats/boats/8d1f81ba-c8e3-468c-9d03-fce534dd3393/images/46605e1f-d033-44b4-b69f-b7c071c398a7_card.jpg",
      "discounted_price_per_day": 418,
      "discount_percentage": 25,
      "discount_label": "Charter discount",
      "required_extras": {
        "per_day": 0,
        "per_booking": 390,
        "per_person": 0,
        "per_person_per_day": 0,
        "from": false
      },
      "required_fees": {
        "total": 435,
        "unit": "stay",
        "nights": 7,
        "charter_unit": "night",
        "from": false,
        "at_capacity": false
      }
    }
  ],
  "inventory": {
    "total_boats": 364,
    "capacity": {
      "min": 2,
      "max": 14
    },
    "length_ft": {
      "min": 31.7,
      "max": 65
    },
    "by_type": {
      "sailboat": 223,
      "catamaran": 140,
      "motorboat": 1
    },
    "discounted_price_per_day": {
      "min": 152,
      "median": 367,
      "max": 5194
    }
  },
  "location_matched": {
    "id": "9442",
    "name": "Nikiana Marina",
    "country": "Greece"
  },
  "alert_horizon": {
    "from": "2026-09-26",
    "to": "2027-09-24"
  },
  "search_url": "https://charter.boats/search?l=text%3ALefkada&ln=Nikiana+Marina&d=2027-06-05%2C2027-06-12",
  "alert_query": "location_type=text&location_value=Lefkada&d=2027-06-05%2C2027-06-12",
  "total_matching": 364
}

When no dates are passed, price_summary states the boat's lowest week ("From 3040 EUR total a week, 12 guests. …") and inventory.by_type is only present when boat_type was not filtered. list_price is still sent: the boat's lowest list figure, marked from.

There is deliberately no available flag. It read as "available for the dates you asked for" while price_summary quoted a different week, and callers relayed the flag. The week a boat can actually be sold is stated in price_summary and nowhere else, so there is one answer to repeat.

A dated row is the boat page's own quote for the stay asked — the dates asked where the boat takes them, else from its changeover day for the nights asked, or its minimum stay where that is longer — for the guests given (else max guests): list_price.total is the page's Total and per_day that total ÷ the stay's days. price_summary names that stay and its total ("the week of Sep 26–Oct 3" for 7 nights; any other stay its dates and length, "Sep 26–29 (3 nights)", "Sep 26–Oct 4 (8 days)" on a day-clock boat); a month-length range states the range of its weeks' totals ("X–Y EUR total a week across Oct 2026, 12 guests."). For the price of the actual stay — Due now, Paid at pickup, the Total and the deposits — call get_boat_details with the dates.

Charter weeks are derived from our stored per-week rates, which the partners quote per 7 nights. MMK and Nausys price the trip itself and a longer booking often carries a discount those rates cannot express, so a multi-week total here runs slightly high; the exact figure is re-quoted from the partner at reservation and the difference reaches the guest as a reduction.

Everything in boats carries a price, with one exception: a search with no dates and no price filter (min, max or trip total) also lists the published boats we hold no price for, as a tail after the priced ones (see unpriced_start_index) — each with bookable_as_searched: false and every price field null. Any dated search, or one with a price filter, excludes them, since there is nothing to quote: undated searches then require a current daily rate, dated searches an available week that is priced. alternatives never carries an unpriced boat.

The other exception is a search by boat name, where "we have nothing" is the wrong answer about a hull that plainly exists — those boats come back in a separate named_matches array, never in boats.

Response Fields

FieldTypeDescription
boatsarrayMatching boats ranked by the selected sort order
boats[].idstringBoat UUID
boats[].urlstringShare link to the boat page (https://chrtr.me/…). On a dated search it carries the week price_summary quotes; with your key it carries your affiliate referral. Treat it as opaque — see Linking to a boat
boats[].titlestringBoat name
boats[].boat_typestringType of boat
boats[].manufacturerstring|nullManufacturer name
boats[].modelstring|nullModel name
boats[].locationstringFormatted location string (marina, city, country)
boats[].yearinteger|nullBuild year
boats[].length_ftnumber|nullLength in feet
boats[].capacityinteger|nullMaximum guests
boats[].cabinsinteger|nullNumber of guest cabins (crew cabins are not counted)
boats[].toiletsinteger|nullNumber of heads/toilets
boats[].skipperedboolean|stringtrue (a skipper comes with the boat, inside the charter price), "required" (a skipper comes with the boat but is billed as a mandatory fee on top — get_boat_details lists it under required fees; never say the price includes him), "optional" (one can be added, at a price), or false (bareboat). Derived from the boat's live fee set, not from rental_type alone
boats[].discount_percentagenumberActive discount percentage. 0 if none.
boats[].discount_labelstring|nullDiscount description (e.g. "50% discount"). null when no discount.
boats[].price_rank_percentilenumber|nullWhere this boat's daily rate sits within the matched inventory (0 = cheapest, 100 = most expensive)
boats[].currencystringCurrency code (e.g. EUR)
boats[].price_summarystring|nullThe one sentence to quote: "T EUR total for the week of Sep 26–Oct 3, N guests[. P% discount applied.]" — the boat page's Total for the stay, for the guests given (else max guests), all fees in, deposits never; any other stay "T EUR total for Sep 26–29 (3 nights), N guests. …"; the percent is list_price.discount_pct, not stated when there is none. Dateless: "From T EUR total a week, N guests. …" (the lowest week, off the precomputed weekly figures). Changed 2026-10-02 (was "P EUR per day for the week of …"; null without dates).
boats[].list_priceobject|nullThe row's ONE list figure: per_day — the grand total (the charter less the boat's auto discount, every required fee charged now or at pickup, the 45 EUR platform fee; deposits never) ÷ its days, per day for the boat, a whole euro rounded up: dated, the boat page's Total for the stay and party asked; dateless, the lowest week of the default package at max guests; total — that grand total (dated only, null without a week; per_day × units may exceed it by up to units − 1 EUR, since only per_day is rounded up); original_per_day — the same with the partner's discount undone and no coupon, per day (null without a discount); discount_pct — derived from per_day and original_per_day, null under 1%; from is true when no week was quoted and it is the boat's lowest; week, week_start, units, unit (night/day) say what total covers (week_start the stay's check-in, units its length); party the guests the fees are priced for — the guests given, else max guests; label the words beside the stay total (dated "for 3 nights · all fees in", dateless "7 nights · all fees in", "8 days" on a day-clock boat). For the complete price — what is due now, paid at pickup, the total and the deposits — call get_boat_details with the dates (stay_price).
boats[].checkinout_daysarrayAllowed check-in/turnover weekday(s), e.g. ["Saturday"]
boats[].min_charter_daysinteger|nullThe boat's minimum charter length in nights for the season in force at the searched check-in (today without dates), when known — on a day-clock boat that is one more charter day, and named_matches[].reasons states it that way ("minimum charter is 8 days")
boats[].instant_bookbooleanWhether a guest can book this boat INSTANTLY — the operator's instant-book mode AND something currently bookable. false on an instant-book boat with no open dates, because the guest would have to inquire; it is not a statement about the operator's settings alone.
boats[].hero_image_urlstring|nullPrimary boat image, as the 400×300 WebP card variant — hotlink it directly; the un-resized original is not a public URL.
total_matchingintegerHow many boats match the whole search before limit, so you can say "top 5 of 122" and know whether a bigger limit is worth it. Omitted when it cannot be known exactly — a total_max budget or a stay shorter than 7 nights filters rows after counting.
results_ordered_bystringPresent when boats holds more than one boat — the list is already in presentation order; do not re-sort it
dates_movedobjectPresent when date_from/date_to was in the past and the search ran on the same day next year instead — { requested, searched, note }. The pair moves together and keeps its length. Tell the guest their dates have passed; never that they have no availability yet
dates_notestringPresent when date_from is not a changeover day (stays up to 14 nights) — each price_summary then names the charter week actually on offer, and those are the dates to give the guest
features_appliedarrayPresent when features was sent — the equipment slugs actually filtered on
features_unmatchedarrayPresent when some features terms were not recognised — they were ignored
inventoryobject|nullAggregate stats for the searched area. null when location did not resolve.
inventory.total_boatsintegerThe number of boats matching the search — the same set boats is drawn from, and always equal to total_matching. Every filter you sent is applied, dates included. Priced boats only, except on a search with no dates and no price filter (min, max or trip total), where it also counts the unpriced tail (unpriced_start_index).
inventory.per_dayobject|null{ min, median, max } of the boats' dateless list figures (per day for the boat, the grand total at max guests, whatever the dates). median is the true median, rounded.
inventory.capacityobject|null{ min, max } guest capacity
inventory.length_ftobject|null{ min, max } length
inventory.by_typeobjectBoat counts per type (only when boat_type not filtered)
location_matchedobject|nullThe location the query resolved to (id, name, country). A sailing-region match reports id as "region:<Name>" (e.g. "region:Dalmatia") and the region's own name.
search_urlstringThe same search on charter.boats — every filter you sent, so the page shows the answer you were given. Absent when the search used something the website cannot express (see search_url_unavailable); when location did not resolve it is a plain text search for the term you sent.
alert_querystringThe same search as /api/boats query params — what an availability alert stores and replays, derived from search_url so the two cannot describe different searches. Absent when location did not resolve, or when there is no link.
search_url_unavailablestringPresent INSTEAD of search_url when no website link can reproduce this answer: a make/model term the catalogue could not resolve (matched here as model text), a make matched loosely (Benet → Beneteau, which has no picker value), a boat_type the website's filter does not offer (gulet, other), a total_max whole-trip budget (the page has only a per-day cap, so its link would show boats over the budget), or an explicit sort (the page carries no ordering in its URL, so the first screen would be different boats). Do not build a link yourself: it would show more boats than you were given. Link the individual boats instead.
alert_horizonobject{ from, to } — the date window availability is known for (the current anchor week plus 52 weeks). Alerts cannot watch dates past to. Absent when location did not resolve.
alternativesarrayPresent when boats is shorter than limit and something could be loosened — the closest boats, in the same shape as boats plus relaxed. See Alternatives.
alternative_inventoryobjectPresent when a place was searched and boats is empty — the same spec across the widened area: total_boats (matching at any date), available_nearby_or_within_7_days (how many of those are free in the searched week or the week either side), by_type, and ranges for per_day, capacity, length_ft. total_boats 0 means this coast does not have that boat; a high total_boats with 0 available means the week is the problem, not the place.
search_interpretedobjectPresent only when a term was read as a make, optionally plus a model, or when a boat's name was folded off the end of makemodel — { make, model, note }. model is "" when the term was a make alone; make is "" when a folded term's head matched no make or model and ran as model text. See below.
named_matchesarrayPresent only on a name search — published boats carrying that name this search could not offer. Same boat shape as boats, plus bookable_as_searched, not_offered_because, availability_note. See Searching by boat name.
named_matches_notestringPresent alongside named_matches — how to present them
name_not_foundstringPresent when a name search matched nothing anywhere in the fleet. Mutually exclusive with named_matches.
listed_unavailablearrayPresent only when a plain search by a single exact location (+ optional dates, no other filters) returns nothing at all — no boats, no alternatives, no named_matches. Published boats at that exact location with no sellable offer right now. Same boat shape, bookable_as_searched: false, not_offered_because, availability_note — but pricing fields are forced null. See Listed but unavailable.
listed_unavailable_notestringPresent alongside listed_unavailable — how to present them
no_resultsobjectPresent only when the response has nothing to hand over — no boats, alternatives, named_matches or listed_unavailable. { searched, why, try_next[] }: what was actually searched after resolution, which constraint emptied it, and the concrete calls to try next.
location_unresolvedstringPresent when the location you sent matched no place we hold. Echoes the term that failed; no search is run rather than a worldwide one.
notestringOne-sentence rendering of no_results, generated from it so the two can never disagree.
FieldTypeDescription
boats[].crew_cabinsinteger|nullNumber of crew cabins, as the operator gives it (null when not given)

In v1.0 a row has no list_price, card, rental_type or has_multi_port, and inventory.per_day is not there. In their place:

FieldTypeDescription
boats[].discounted_price_per_daynumber|nullThe charter per day for the quoted week, our auto-applied offer already taken off — no fees inside
boats[].discount_percentagenumber|nullThe charter discount, percent
boats[].discount_labelstring|nullAlways present: the discount's name, e.g. "Charter discount"
boats[].required_extrasobjectThe required fees beside the day rate: per_day, per_booking, per_person, per_person_per_day (EUR), from
boats[].required_feesobjectThose fees for the quoted stay: total, unit, nights, charter_unit, from, at_capacity
boats[].price_summarystring|null"~T EUR total for the week of Sat 5 Jun 2027 → Sat 12 Jun 2027, N guests. P% discount applied. Charter: … Required per-booking fees: … Platform fee: 45 EUR" — the same total as v1.1, with its parts spelled out
inventory.discounted_price_per_dayobject|null{ min, median, max } — the figures v1.1 calls inventory.per_day

Linking to a boat

boats[].url is a share link to the boat's page on chrtr.me. Treat it as opaque — don't parse it, shorten it or rebuild it. It can take three shapes, and which one you get depends on the call:

  • a bare 6-character boat code (https://chrtr.me/R58ZDk) — nothing carried;
  • the code plus a charter week, and your referral when you called with a key (https://chrtr.me/n8wxFd0DG7) — on a dated search, when the boat can be sold for the week(s) you asked about;
  • the code plus your referral only (https://chrtr.me/rkt5rx-yourslug) — a keyed call whose link carries no week.

The week a link carries is exactly the week price_summary quotes. A month-long range, or a boat that is only sellable on a neighbouring week, gets a link with no dates. A carried week is dropped automatically once its check-in date has passed. Passing the link on unchanged is what keeps a booking attributed to you: the referral is captured on arrival and held in a first-party cookie for your attribution window, so it survives the guest browsing on before they book.

The boat page reads three params, and each one you append overrides what the link carries:

ParamMeaning
dDates, YYYY-MM-DD,YYYY-MM-DD. The second is a check-out date, so 2026-10-17,2026-10-24 is seven nights. Always send a range — a lone date skips the minimum-nights check and quotes a one-day stay the booking path then refuses.
gThe number of guests. The boat page prices per-person fees for that party (capped at the boat's capacity); without it the page prices at capacity.
refAn affiliate slug. You rarely need it: a keyed call already puts yours in the link.
https://chrtr.me/n8wxFd0DG7?d=2026-10-17,2026-10-24&g=6

Any other param you append is passed through to the boat page unchanged. A bare code with no params redirects 301; a link carrying dates, a referral or any appended param redirects 302.

The carried week is the quoted week, not a verified check-in. It comes from the same weekly availability data as price_summary, which does not attest the check-in day itself, so roughly one link in twenty-five lands on a start date the boat page rejects and the guest sees an empty calendar. When you need a date the guest can book, read /ai/boats/{id} — it accepts the link you already have — and take a slot's checkin and checkout from bookable_slots (both YYYY-MM-DDTHH:MM, so slice the date off each), then append them as ?d=. That list is the only thing that has checked the check-in day against the boat's real turnover days, its quoted charter length, and live availability. Never reconstruct d from checkinout_days. Or call /ai/boats/{id} with the guest's date_from and date_to: its url carries that stay when we sell it, and no dates otherwise — your referral either way.

When nothing matches

An empty boats array on its own is not an answer — it leaves you unable to tell "there are no boats in Lefkada" from "none of them sleep 99". So a response that has nothing to offer always carries no_results, and the two cases read differently:

The place did not resolve. No search is run at all, rather than a worldwide one wearing the label of a place. location_unresolved echoes the term that failed, inventory and location_matched are null, alert_query/alert_horizon are absent, and search_url is a plain text search for the term.

The place resolved and the constraints emptied it. searched names the resolved place and the filters applied — dates, guests, cabins, type, a resolved model, price, length, year and equipment — and try_next lists them in the order a guest would relax them, the widest first, since that is the one most likely to be doing the emptying on its own:

{
  "boats": [],
  "location_matched": { "id": "11335", "name": "Lefkas Main Port", "country": "Greece" },
  "no_results": {
    "searched": "Lefkas Main Port with 99 guests",
    "why": "The place resolved and the search ran; every boat was excluded by one or more of these constraints together. It is NOT that Lefkas Main Port has no boats — say the combination found nothing, never that the place is empty.",
    "try_next": [
      "lower `guests` — capacity is the constraint that empties a place fastest",
      "search_locations with `q` for a nearby base and repeat there"
    ]
  },
  "note": "Nothing matched Lefkas Main Port with 99 guests. …"
}

It deliberately does not guess which single filter did it: the query is one conjunction, so the endpoint cannot tell, and a confident wrong cause is worse than the listed set.

no_results is absent whenever there is something to offer — an alternative, a name match or a listed-unavailable hull is an answer, and each carries its own note explaining how to present it. It can appear beside name_not_found; when it does, name_not_found is the answer to give.

Alternatives

boats is always the true answer to the question asked — never padded, never widened. When it comes up short of limit, alternatives carries the closest boats we do have, and each one says what it gave up in relaxed:

relaxedWhat was loosened
datesThe week either side of the ones asked for
placeA nearby base (the nearest marinas holding ~30 boats of the type, up to 60 nm)
modelAnother model of the same make — the make is kept
nameThe boat carries ONE word of the searched name (Lyra Astra → Lyra, Astra Blue)

One thing is loosened at a time, in that order, and the ladder stops as soon as the page is made up — so a boat that gave up one thing always comes before one that gave up two. Nothing else is ever loosened: type, guests, cabins, berths, length, year, budget, equipment and the operator behind a referral all still hold for every row.

{
  "boats": [],
  "total_matching": 0,
  "alternatives": [
    { "title": "Mademoiselle", "relaxed": ["dates"],  "location": "Marina Frapa, Croatia" },
    { "title": "Private Bay",  "relaxed": ["place", "model"], "location": "Marina Kremik, Croatia" }
  ],
  "alternative_inventory": { "total_boats": 1, "available_nearby_or_within_7_days": 1 }
}

Say which: an alternative is not what the guest asked for, and it counts toward no total — total_matching describes boats alone. The website's own search pads a short page from the same ladder, so the link in search_url shows the guest the same boats.

Searching by Boat Name

A boat is only in boats if it can be sold as searched — priced, free for the dates, at the base asked for. For a search by NAME that is the wrong answer: "Sunny" either is or is not our boat, and an empty list reads as "no such boat" to the guest asking and to the owner checking their own listing.

So a name search additionally returns every published boat carrying that name that the filters could not offer, in its own named_matches array — never merged into boats, which stays exactly what it was: bookable, priced, quotable.

curl "https://charter.boats/api/ai/boats?location=Split&search=Sunny&date_from=2026-09-26&date_to=2026-10-03&limit=3"
{
  "boats": [],
  "named_matches": [
    {
      "id": "b6a82d5f-313d-4441-acdb-975b9a591c56",
      "url": "https://chrtr.me/LghbOG0DG7",
      "title": "Sunny",
      "boat_type": "sailboat",
      "manufacturer": "Bavaria",
      "model": "C45 Style",
      "location": "Marina Punat, Krk, Croatia",
      "year": 2023,
      "length_ft": 45.9,
      "capacity": 8,
      "discount_percentage": 35,
      "currency": "EUR",
      "price_summary": "2238 EUR total for the week of Sep 26–Oct 3, 8 guests. 34% discount applied.",
      "bookable_as_searched": false,
      "not_offered_because": [
        "based at Marina Punat, Krk, outside Split, West Coast (Zapadna obala)"
      ],
      "availability_note": "Listed, not bookable as searched — see not_offered_because. price_summary names the nearest week it CAN be sold; call get_boat_details with its id for the full calendar."
    },
    {
      "id": "72fb0d8e-5401-4d17-bee0-88d1cfba08be",
      "url": "https://chrtr.me/R58ZDk",
      "title": "Sunny",
      "manufacturer": "Bali",
      "model": "4.6",
      "location": "Marina Trogir (SCT), Grad Trogir, Croatia",
      "price_summary": "4770 EUR total for the week of Oct 3–10, 12 guests. Nearest week this boat can be sold — not the requested Sep 26–Oct 3",
      "bookable_as_searched": false,
      "not_offered_because": [
        "based at Marina Trogir (SCT), outside Split, West Coast (Zapadna obala)",
        "not free for the dates asked"
      ],
      "availability_note": "Listed, not bookable as searched — see not_offered_because. …"
    }
  ],
  "named_matches_note": "Boats named \"Sunny\" that this search could NOT offer — every bookable one is already in `boats` … A boat whose only reason is the base or the dates can still be sold — say which it is and offer to check: call get_boat_details with the boat's id for its priced date ranges and bookable slots for the next year.",
  "location_matched": { "id": "21157", "name": "Split, West Coast (Zapadna obala)", "country": "Croatia" }
}

Read the two boats above as they are meant: the first is free that very week and only fails on the base (Punat, not Split); the second fails on both. That distinction is the whole point of not_offered_because — it is derived per boat from the row, not a single blanket sentence.

FieldMeaning
bookable_as_searchedAlways false here. Named as searched on purpose: a boat whose only problem is the base is perfectly bookable once the guest accepts that base.
not_offered_becauseOne or more concrete reasons: a Lagoon 46, not the Lagoon 55 asked for (a make/model was searched beside the name), based at X, outside Y, not free for the dates asked, not on sale at the moment — no priced season loaded…, or a spec/budget miss (takes 6 guests, fewer than the 12 asked for).
unpriced_start_indexPresent only on a search with no dates and no price filter (min, max or trip total): the index in boats from which every entry is a published boat we hold no prices for. They are matches — inside total_matching and inventory.total_boats — but each carries bookable_as_searched: false, not_offered_because: ["not on sale at the moment — no priced season loaded, so there is no price to quote"] and null for every price field. unpriced_note says the same in a sentence. Never quote or estimate a price for one; name it, link it and offer an availability alert. Added 2026-09-21.
availability_noteWhat may and may not be quoted for this boat, matched to what the object actually carries — on an undated search price_summary is the boat's lowest week ("From …"), and the note says it is not a quote.

Never counted. total_matching and inventory.total_boats describe the searched area and exclude named_matches entirely — five same-named boats elsewhere must not become "five boats in Split".

What does NOT produce named matches

The array is for a hull's NAME, so it is skipped when the term is something else:

  • Terms under 3 characters, or with no letter in them.
  • Boats that merely fell past limit — a boat with no reason to be excluded is one the search could have offered, so it is dropped from named_matches rather than mislabelled. Raise limit to see it in boats.

Matching is on whole words, so Bella returns Bella, Bella Luna and Isola Bella — never Marbella.

A make or model beside the name (makemodel, m or manufacturer) does not skip it: the name still filters boats, the fleet-wide lookup runs as well, and a named hull of another make or model says so in not_offered_because (a Lagoon 46, not the Lagoon 55 asked for) instead of being absent.

When the name exists nowhere

If a name search matches nothing across the entire fleet, the response says so explicitly rather than leaving an empty boats to be read as "nothing here":

curl "https://charter.boats/api/ai/boats?location=Croatia&search=Zzyzxq&date_from=2026-09-26&date_to=2026-10-03&limit=2"
{
  "boats": [],
  "location_matched": { "id": "21164", "name": "Croatia", "country": "Croatia" },
  "name_not_found": "No published boat named \"Zzyzxq\" exists — checked across the whole fleet, not just Croatia. Say that plainly first. Anything in `alternatives` is a DIFFERENT boat, never that one under another name.",
  "no_results": { /* … what was searched, why it was empty, what to call next … */ }
}

A one-word name has nothing to loosen to, so this answer carries no alternatives. A name of two or more words does: Lyra Astra comes back with the boats named Lyra and Astra Blue, each marked relaxed: ["name"] — and name_not_found still says, in the same response, that no boat carries the whole name.

named_matches and name_not_found never appear together: the first means the name exists and could not be sold as searched, the second that it does not exist.

Listed but Unavailable

A plain search for one exact marina (no name, make, budget or other filter — just a place, and optionally dates) can still come back with nothing to sell: no strict match, no nearby/date alternative, no name match. That location page still visibly lists a published boat, so an empty boats array would read as "nothing is there" about a hull the guest can see exists.

listed_unavailable carries a few of those published boats — outside boats and total_matching, with bookable_as_searched: false and every pricing field forced null. Never quote a price from one; the only correct actions are naming it, giving its reason, and offering an availability alert.

{
  "boats": [],
  "listed_unavailable": [
    {
      "id": "9f1e2a3b-...",
      "url": "https://chrtr.me/xYzAbc",
      "title": "Il Mare",
      "boat_type": "sailboat",
      "location": "ACI Marina Split, Split, Croatia",
      "list_price": null,
      "price_summary": null,
      "bookable_as_searched": false,
      "not_offered_because": ["not on sale at the moment — no priced season loaded, so there is no price to quote"],
      "availability_note": "Published at the searched marina, but not currently bookable. Quote no price. You may name and link this boat, explain why it is unavailable, and offer an availability alert."
    }
  ],
  "listed_unavailable_note": "No boat was bookable as searched and no nearby/date alternative was found. These published boats are listed at the exact searched marina, but they are NOT bookable results and count toward neither `boats` nor `total_matching`. Name and link them only as currently unavailable; quote no price and offer an availability alert."
}

This is scoped tight on purpose: it only fires for a search that named ONE unambiguous place. A city with several marinas (Split, Palma) or a country/state/region match never triggers it — pointing "the one boat listed here" at an arbitrary member of a multi-marina city would misname where it actually is.

Make and Model in One Term

Bali 4.2 finds nothing on a literal reading: the make lives in a separate manufacturers table and the fleet stores the model on its own (4.2), so no single field ever holds that string. Send it as makemodel and the endpoint splits it — longest make prefix first, so Fountaine Pajot Helia 44 resolves as one make and one model.

Send it as one string. Deciding where a make ends is a question about the catalogue, not about language: Arno Leopard 23 is a two-word make, Custom Made is a make with no model, and FP is an alias for Fountaine Pajot. The endpoint owns that mapping, so pass the phrase through unedited.

The split is only applied when that make actually has a boat matching the remainder. This matters because some model lines share a name with a manufacturer: there is a manufacturer row called "First" that owns no boats, so First 40 is never redirected to it.

A term with no make in front is read as a model. When no split applies, makemodel is looked up in the model catalogue on its own — Lucia 40, or First 40, which resolves to the boats listed as exactly that model (a First 40, not a First 40.7). Model names are not shared between builders, so no make is needed to pin one down. This step belongs to makemodel only: search is a boat's name and is never looked up as a model.

A term that resolves to nothing may carry the boat's NAME on its tail. A guest folds the hull's name into the phrase — "How much is the RAND Play 24 Diva?" — and the whole string matches no make and no model. When no search was sent beside it, the trailing words are tried as a boat's name before the words fall back to model text: the longest run first (Okea Iris before Iris), as whole words, against published boats, with at least one leading word kept for the make or model. When a run names a boat the request continues exactly as makemodel=RAND Play 24&search=Diva would — the head is read again through the same split and catalogue lookup, the name reaches the strict filter and named_matches, and search_interpreted says what was read:

"search_interpreted": {
  "make": "RAND",
  "model": "Play 24",
  "note": "Read \"RAND Play 24 Diva\" as the boat name \"Diva\" after the make + model \"RAND Play 24\", searched together with boat name \"Diva\", dates 2027-12-04 to 2027-12-11. If nothing matched, say no RAND Play 24 matched those together — never that the make or the model is missing here; `named_matches` and `alternatives` say what came closest."
}

A term that resolves to nothing still filters — when no trailing run names a boat either, it is matched as model TEXT, so it finds nothing honestly rather than falling back to no filter at all (Nonsense Brand 9 used to come back as five arbitrary boats, which reads as "here is what you asked for"), and the no_results line says that no catalogue make or model matches it and its trailing words name no published boat. The website has no param for that match, so such an answer carries search_url_unavailable instead of a link.

When the make and the model half both resolve to a catalogue entry, the search filters on every stored spelling of that model (the fleet writes both 4.1 and Bali 4.1), and the returned search_url carries it as m=bali:4.2. That URL reproduces the same result set on the website — the API answer and the link agree by construction, which the test suite checks by following every link back to the API. A bare model resolves the same way: Oceanis 46 and Lipari 41 are catalogue models and travel as m=beneteau:Oceanis 46, never as free text — q is a boat's NAME on both surfaces. Only a term the catalogue cannot place at all has no link at all.

When a make is read out of the term, search_interpreted reports it. Use it to phrase an empty result accurately — when the model was the only constraint, no match means that specific model is unavailable here, not that the make is missing:

"search_interpreted": {
  "make": "Bali",
  "model": "4.2",
  "note": "Read \"Bali 4.2\" as make + model. If nothing matched, say the Bali 4.2 specifically is not available here — do not say the make is missing."
}

When a name, dates or any other constraint ran beside it, the query is one AND and the endpoint cannot tell which emptied it, so the note names the set instead and never pins the zero on the model (makemodel=Lagoon 55&search=Iris: five Lagoon 55s exist, none named Iris — the two Lagoon 46 "Iris" come back in named_matches with "a Lagoon 46, not the Lagoon 55 asked for"):

"search_interpreted": {
  "make": "Lagoon",
  "model": "55",
  "note": "Read \"Lagoon 55\" as make + model, searched together with boat name \"Iris\", dates 2027-08-07 to 2027-08-14. If nothing matched, say no Lagoon 55 matched those together — never that the make or the model is missing here; `named_matches` and `alternatives` say what came closest."
}

It is also present when the term was a make and nothing else (Bali), with model: "". Only makemodel is read this way — search is always a boat's name, so a make or model belongs in makemodel, and a boat's name in search. Model words that match no catalogue entry are matched against the model text of that make's boats, never against boat names.

A make resolved from makemodel replaces the manufacturer you sent. Pass m when you already hold a resolved filter — it is never parsed.

Location Matching

location is written freely — by a guest or by a model — and resolved in this order:

  1. Sailing region. Istria, Kvarner, Dalmatia, the Ionian, the Saronic, the Cyclades, the Dodecanese and the Sporades (with common spellings such as Dalmacija or Kyklades, a leading "the" and a trailing country) resolve geographically, to every boat-carrying location inside the region's area and in that region's own country — the Dodecanese never returns boats from the Turkish coast beside it. location_matched then reports id: "region:<Name>".
  2. Place names. Otherwise the term is matched against location names, cities, states and countries, trying progressively looser forms and stopping at the first that finds something:
    • the whole term, then the part before the first comma — anything after a comma is a hint that narrows the match (Lefkada, Greece, Nydri, Lefkada, Greece);
    • either half of a parenthetical name (Yachtclub Seget (Marina Baotić));
    • the term with a trailing geographic word removed (Amalfi Coast → Amalfi; also region, islands, riviera, gulf, bay and similar);
    • the term with up to two trailing words dropped — accepted only when the rows it finds confirm the dropped words, so Marina Agios Kosma never degrades to every marina in the world.
  3. Ranking. Among the matches: exact name > city (administrative prefixes such as "Municipal Unit of" or "Općina" are ignored) > name starting with the term > city starting with the term > state > country.

What gets searched depends on where the best match landed:

  • A state, a country, or only a loose substring match — every matched location in it.
  • A marina or city — every location in that city, and, where the city has five locations or fewer, every location within roughly 5 km, because boats moor at the nearest port rather than at the named spot.

With dates, a location also matches the boats that depart from it in those weeks though based at another marina.

A term that matches nothing is never widened into a worldwide search — see When nothing matches.

Sort Orders

ValueDescription
best_valueBest value first, available boats before unranked ones (default)
dealsBest discounts first, then lowest price
price_lowCheapest per-day rate first
price_highMost expensive first
newestNewest build year first

Boat Details

GET/ai/boats/{id}

Full detail for a single boat: description, specs, equipment, 365-day pricing windows, authoritative bookable check-in/check-out slots, fees, discounts, and cancellation policy. The {id} accepts a boat UUID, the boat's slug (lagoon-40-sailing-point), or the part of a chrtr.me link after the slash — a bare code (n8wxFd) or the longer dated/referral form /ai/boats hands out (n8wxFd0DG7, rkt5rx-yourslug), which names the same boat. Pass that path segment, not the whole URL: a full URL in the path is not matched. Authentication and rate limits are as for the search; a keyed call gets a url carrying your referral.

Optional date_from and date_to (both YYYY-MM-DD, check-out after check-in) name the stay you are quoting. With them, fees are the ones that apply to that stay — a fee's season is picked by the check-in date, as on the boat page — and url opens the boat on those dates when the stay is one we sell — stay_price prices it (total is not null) and, on a boat with no check-in periods, it starts and ends on the boat's changeover day; otherwise it carries no dates. Without them, fees are those in force today and url carries no dates. A check-in already in the past is moved to the next year it is not, the pair together and the stay length kept — the same rule as the search — and the response then carries dates_moved ({ requested, searched, note }). A pair that is not two real calendar dates, or is reversed, is a 400.

Optional currency (EUR, the default, or USD; API version 1.1) writes every amount — stay_price, bookable_slots[], pricing.ranges[], fees, boat.security_deposit and fixed euro offers — in that currency, as the boat page writes it in the currency selector's choice. Version 1.0 answers in euros.

Optional guests (a number, API version 1.1) is the party the stay is priced for: stay_price, every bookable_slots[] price and, with dates, the fees are then priced for that many guests, and per_person_day is per one of them — as the checkout prices a party. Without it, the boat's capacity.

Request

curl "https://charter.boats/api/ai/boats/n8wxFd?api_version=1.1"
curl "https://charter.boats/api/ai/boats/n8wxFd?date_from=2027-08-07&date_to=2027-08-14&api_version=1.1"
curl "https://charter.boats/api/ai/boats/n8wxFd"
curl "https://charter.boats/api/ai/boats/n8wxFd?date_from=2027-08-07&date_to=2027-08-14"

With no api_version the answer is v1.0; api_version=1.0 asks for it by name.

Response

{
  "url": "https://chrtr.me/n8wxFd",
  "boat": {
    "id": "05d26e2d-473c-4d30-b9eb-05c447c835ac",
    "title": "Sailing Point",
    "description": null,
    "operator_notes": null,
    "boat_type": "catamaran",
    "manufacturer": "Lagoon",
    "year": 2020,
    "length_ft": 38.5,
    "capacity": 12,
    "cabins": 6,
    "berths": 12,
    "toilets": 4,
    "instant_book": true,
    "rental_type": "bareboat",
    "skipper": "required",
    "charter_schedule": "Check-in Sat from 17:00 to 09:00, minimum 7 nights",
    "security_deposit": 3000,
    "location": {
      "name": "ACI Marina Split",
      "city": "Split",
      "country": "Croatia"
    },
    "equipment": [
      { "group": "Rigging", "items": ["[31%] Lazy bag main"] },
      { "group": "Deck", "items": ["[0.8%] Jacuzzi", "[64%] Dinghy (Honda 6 HP)", "[12%] Cockpit floor and transoms in teak", "Aft cockpit cushions"] }
    ],
    "equipment_note": "[n%] = n% of boats have it. Lowest n% are the most rare. It is not part of the item's name. Items with no [n%] have no fleet share recorded for their name in the equipment dictionary."
  },
  "pricing": {
    "horizon_days": 365,
    "currency": "EUR",
    "ranges": [
      { "from": "2026-09-27", "to": "2026-10-02", "per_day": 228, "original_per_day": 253 },
      { "from": "2026-10-15", "to": "2026-10-16", "per_day": 174, "original_per_day": null },
      { "from": "2026-10-17", "to": "2026-11-07", "per_day": 180, "original_per_day": null }
    ],
    "truncated": false,
    "note": "`per_day` is the price per day for the boat, all in — charter, every required fee and the platform fee at max 8 guests, the boat's discounts already in it; refundable deposits are not. …"
  },
  "bookable_slots": {
    "note": "The check-in/check-out pairs you may quote, soonest first — never invent others. `total` is the whole price of the stay: `due_now` is paid on booking, `paid_at_pickup` at the base. …",
    "slots": [
      {
        "type": "bareboat",
        "checkin": "2026-09-26T17:00",
        "checkout": "2026-10-03T09:00",
        "nights": 7,
        "base_price": 2464,
        "product": null
      },
      {
        "type": "bareboat",
        "checkin": "2026-10-17T17:00",
        "checkout": "2026-10-24T09:00",
        "nights": 7,
        "base_price": 1918,
        "product": null
      }
    ],
    "truncated": false
  },
  "fees": {
    "required": [
      { "description": "Charter package", "amount": "420 EUR", "period": "per_booking", "paid_at": "booking" },
      { "description": "Platform fee", "amount": "45 EUR", "period": "per_booking", "paid_at": "booking" }
    ],
    "optional": [
      { "description": "Stand up paddle (SUP)", "amount": "130 EUR", "period": "per_week", "paid_at": "base" },
      { "description": "Skipper", "amount": "220 EUR", "period": "per_day", "paid_at": "base" }
    ]
  },
  "discounts": {
    "note": "Every figure in this answer already has `operator_seasonal` and the best public offer in it. A private code or an agent grant is in no figure: …",
    "operator_seasonal": [
      {
        "label": "Charter discount",
        "value": { "type": "percent", "amount": 50 },
        "applies_to": { "from": "2026-09-27", "to": "2026-12-31" }
      }
    ],
    "operator_conditional_offers": [],
    "agent_grants": []
  },
  "company": {
    "has_members": false,
    "cancellation_policy": "Cancel within 24 hours of paying for a refund of the charter price. 50% of the charter price refunded if cancelled 30+ days before arrival. No refund after. Every refund is less card processing, and the platform fee is not refunded. Full details: https://charter.boats/fees-and-pricing"
  }
}

The parts that differ in a real v1.0 response (GET /ai/boats/55467c1d-bfab-484f-b0c0-bb9352d4d46d?date_from=2027-06-05&date_to=2027-06-12, no api_version) — no stay_price, no fees.deposits:

{
  "pricing": {
    "horizon_days": 365,
    "currency": "EUR",
    "truncated": false,
    "note": "Price already includes operator_seasonal discount. Every dat…",
    "ranges": [
      {
        "from": "2026-10-11",
        "to": "2026-10-16",
        "price_per_day": 307
      },
      {
        "from": "2026-10-17",
        "to": "2026-11-20",
        "price_per_day": 324
      }
    ]
  },
  "bookable_slots": {
    "slots": [
      {
        "type": "bareboat",
        "checkin": "2026-10-10T17:00",
        "checkout": "2026-10-17T09:00",
        "nights": 7,
        "charter_length": "7 nights",
        "base_price": 2149,
        "product": "Bareboat"
      }
    ]
  },
  "discounts": {
    "operator_seasonal": [
      {
        "label": "45% discount",
        "value": {
          "type": "percent",
          "amount": 45
        },
        "applies_to": {
          "from": "2026-10-11",
          "to": "2026-11-20"
        },
        "original_price_per_day": 559
      }
    ]
  }
}

Response Fields

FieldTypeDescription
urlstring|nullShare link to the boat page — carries the stay's dates when called with a stay we sell (see date_from), no dates otherwise; your referral when called with a key
boat.idstringBoat UUID
boat.titlestringBoat name
boat.descriptionstring|nullDescription (trimmed to ~500 chars). null when the boat has none or when we have withheld it — a moderator marks a listing's description inaccurate or carrying personal details, and it is then published nowhere. The field is always present.
boat.operator_notesstring|nullThe operator's own practical notes, where the partner supplies them (fuel policy, check-in conditions and the like) — unedited text
boat.boat_typestring|nullType of boat
boat.manufacturerstring|nullManufacturer name
boat.yearinteger|nullBuild year
boat.length_ftnumber|nullLength in feet
boat.capacityinteger|nullMaximum guests
boat.cabinsinteger|nullNumber of guest cabins (crew cabins are not counted)
boat.berthsinteger|nullNumber of berths
boat.toiletsinteger|nullNumber of heads/toilets
boat.instant_bookbooleanWhether a guest can book this boat INSTANTLY — the operator's instant-book mode AND something currently bookable. false on an instant-book boat with no open dates, because the guest would have to inquire; it is not a statement about the operator's settings alone.
boat.rental_typestring|nullThe operator's product class: e.g. skippered, bareboat. It does not say who pays for the skipper — read boat.skipper for that
boat.skipperstringWho sails her and who pays for him, from this boat's own fee set: included (comes with the boat, inside the charter price), required (comes with the boat, billed as a mandatory fee listed under fees.required — never say the price covers him), optional (one can be added, at a price), none (you sail it). The same statement search_boats returns as skippered
boat.charter_schedulestring|nullHuman-readable check-in day, turnover hours and minimum stay — in the unit the boat sells in, "minimum 7 nights" or "minimum 8 days" on a day-clock boat — for the season in force (the one covering today, else the next to open) — e.g. "Check-in Sat from 17:00 to 09:00, minimum 7 nights". 🕐 There is no default hour. Where the operator has not stated one, the line says so instead of inventing it: "Check-in Sat, minimum 7 nights, check-in/check-out times not set by the operator — confirm with the operator before booking", or "…, the check-out time not set by the operator — …" when only one half is missing. null means the boat states no schedule at all. Never present the hours as known when that phrase is there.
boat.security_depositnumber|nullSecurity deposit amount
boat.locationobject{ name, city, country } (each may be null)
boat.equipmentarrayThe boat's equipment grouped by section: { group, items[] }, items as display names (with a value in brackets where one is recorded, e.g. "Dinghy (Honda 6 HP)"). Every item with a fleet share is prefixed [n%] with the share of boats that have it, e.g. "[0.8%] Jacuzzi"; the lowest are the rarest. An item with no share (operator free text) has no prefix, and the prefix is not part of the name. Kit standard on the boat's model is listed even when the operator did not list it, with its share like any item. The same list, order and duplicate-spelling collapse as the boat page. At most 80 items across all groups; within a section the rarest come first, so the cap sheds the commonest kit
boat.equipment_notestringPresent only when an item carries a share prefix: what [n%] means
pricing.horizon_daysintegerLook-ahead window (365)
pricing.currencystringCurrency code
pricing.rangesarrayContiguous available date windows with from, to, per_day — the LIST figure: the lowest grand total at max guests per day for the boat among the boat's weeks the range covers, deposits never in it; null when none of them is priced — and original_per_day, the same week's figure before its discount (null without one; added 2026-10-02). Quote a stay from bookable_slots or stay_price, never from it. At most 30, soonest first
pricing.truncatedbooleantrue when more than 30 ranges existed — the later ones were dropped
pricing.notestringGuidance on how to read pricing
bookable_slots.notestringGuidance on quoting slots
bookable_slots.slotsarrayAuthoritative { type, checkin, checkout, nights, charter_length, due_now, paid_at_pickup, total, deposits, base_price, regular_charter, charter_discounts, auto_discount, auto_discount_label, per_person_day, product } slots (in that order since 2026-10-02: the three prices and the deposits first), soonest first, at most 60. nights is the stay's nights on every boat; charter_length is the same stay in the unit the boat sells in — "7 nights", or "8 days" on a day-clock boat — and is the phrase to quote. base_price is the charter total only, in whole euros as the boat page sells it. due_now (added 2026-10-01) is the boat page's Due now for the slot — the charter, the required fees charged at booking (a deposit charged then included) and the platform fee, less the best auto-applied offer (auto_discount, already taken off due_now, total and per_person_day) — or null where its fees cannot be priced; paid_at_pickup and total (added 2026-10-01) are what is paid at the base — the fees paid there, deposits not included — and the grand total (see the next row); deposits (added 2026-10-01) lists the slot's deposits { label, amount, charged_now }: one charged at booking is inside due_now; none is ever inside total. product names the partner package the slot is priced for (pass it on when reserving), or null.
bookable_slots.slots[].paid_at_pickup, .total, .per_person_daynumber|nullAdded 2026-10-01: what is paid at the base for the slot — the fees paid there, deposits not included — and the grand total, due_now + paid_at_pickup − the deposits charged now (deposits are never in a total); per_person_day is that total ÷ the charter's days ÷ the boat's capacity. Null where a row cannot be priced.
stay_priceobjectOnly with date_from/date_to (added 2026-10-01): the stay's prices as the boat page states them — due_now (charged on booking, a deposit charged now inside it), paid_at_pickup (the fees paid at the base, no deposit), total (the grand total: due_now + paid_at_pickup − the deposits charged now; deposits never in it), per_person_day (total ÷ days ÷ guests priced) — and its deposits (label, amount, charged_now); agent_commission_estimate only when your key's user is an active agent; null figures where the page does not sell those dates.
bookable_slots.truncatedbooleantrue when the 60-slot cap cut the list
fees.requiredarrayMandatory { description, amount, period, paid_at? } fees. amount is a string with its unit — "270 EUR", "9%", "50% of daily price", "3.5% of charter + advance extras" or "included"; a percentage is of the charter total for the stay, once. With date_from/date_to a percentage also states what it comes to for that stay, as the boat page does — "30% (3120 EUR)", "est." before the amount where its base is estimated (added 2026-10-01); a rate with no figure behind it stays the rate alone. paid_at is "booking" (in the online total) or "base" (settled at the marina) and is absent on an included fee. Deposits are not in this list — see fees.deposits. Scoped like the boat page: one package, the standard week, the boat's capacity as crew size
fees.optionalarrayOptional add-on fees, same shape
fees.depositsarrayAdded 2026-10-02 (moved out of fees.required, which carried them marked deposit: true): the required deposits — the APA, a refundable security or other deposit — same shape. Never in any Total: with paid_at: "booking" it is charged inside Due now, with "base" it is paid at pickup.
discounts.notestringEvery figure already has operator_seasonal and the best public offer in it; a private code or agent grant is in none
discounts.operator_seasonalarraySeasonal discounts already baked into pricing.ranges
discounts.operator_conditional_offersarrayCoupon-style offers (NOT pre-applied) with label, value, visibility (public = applied automatically at checkout, private = the guest enters the code), condition, remaining_uses, and rule — at most one coupon applies per booking
discounts.agent_grantsarrayAgent-granted discounts (NOT pre-applied)
company.has_membersbooleanWhether the operator has staff members responding
company.cancellation_policystringPlain-language cancellation policy
FieldTypeDescription
boat.crew_cabinsinteger|nullNumber of crew cabins, as the operator gives it (null when not given)

In v1.0 there is no stay_price, no fees.deposits and guests is not read. These differ:

FieldTypeDescription
pricing.ranges[].price_per_daynumberThe charter day rate for that window, operator_seasonal already taken off, no fees inside (v1.1: per_day, all fees in, with original_per_day)
bookable_slots.slots[]array{ type, checkin, checkout, nights, charter_length, base_price, product } — no due_now, paid_at_pickup, total, deposits or per_person_day
fees.requiredarrayAlso lists the required deposits, after the fees (v1.1 moves them to fees.deposits)
discounts.operator_seasonal[].original_price_per_daynumberThe day rate before that discount

Errors

StatusBody
400Boat id required
404{ "error": "boat_unavailable", "message": "This boat is no longer available. …" } in data — the id did not resolve to a boat, or the boat is no longer published

Compare Boats

GET/ai/boats/compare

One boat set against up to 10 closest published boats of the same type in the same country, as a table of specs and the weekly price for each of the next 52 weeks — the compare_boat tool on the MCP server and in Mermaid, and the compare pages (https://charter.boats/boats/compare, and /boats/{slug}/compare for one boat). For what is better or worse about one boat, see What Stands Out. Authentication and rate limits are as for Boat Details.

Pass id (a boat id, slug or short-link code, resolved as for Boat Details), or boat_type (sailboat, catamaran, motorboat, gulet, yacht, rib, other) and country (ISO code), optionally with region and city, to describe a boat instead. The specs take Search Boats' names: makemodel (as the guest said it), length_min / length_max (feet), year_min / year_max, guests, cabins, berths, toilets. Without id they describe the boat to compare against; with id they filter its peers as the search would (a minimum for guests, guest cabins, berths and toilets; a range for length and year; makemodel keeps the boats whose make or model the words name). With id, lock (comma-separated: region, city, make, model, year, length, guests, berths, cabins, toilets) keeps only the peers the same as the boat in those fields — the fields a peer's same lists.

Closeness: the same city, region, make and model, a build year and a length inside what was asked (with id: within 2 years and 5% of the boat), and the same guests, cabins, berths and toilets, counted only for what the anchor states. A boat with no week it can be sold in over the 52 is left out. With id, and when that boat sells at least one week, each peer's overlap_pct is the share of its open weeks the peer is open too, otherwise null. similar_pct, which the compare page shows, is the share of the stated fields a peer shares. Peers rank by it, then by overlap_pct, then by the nearest length. Each boat carries its image (a photo URL, null when it has none).

Weekly price: each week's all-in total in currency (EUR, the default, or USD) at max guests (charter, every required fee, the platform fee; deposits never) — the figure search compares. null means the boat cannot be sold that week. weeks gives each column's check-in date. Each boat's price_currency is the currency its operator prices in; the figures themselves are in currency, and a peer's delta is the difference of the two figures in it.

Cabins are guest cabins: cabins excludes crew cabins, and crew_cabins gives those (where a boat carries its own specs). Answers are cached for up to an hour.

With id (price_mode: "delta"), anchor.price holds that boat's weekly figures and each peer holds its region, city, same and otherwise only what differs from it — make, model, and signed deltas for year ("+3"), length ("+1.0 m"), guests, berths, cabins, toilets (a peer missing the value shows null) — and its price per week is its figure minus the anchor's, except in absolute_weeks, where the anchor cannot be sold and the peer's own figure stands. With specs only (price_mode: "absolute"), every peer carries its own specs and weekly figures.

Request

curl "https://charter.boats/api/ai/boats/compare?id=n8wxFd"
curl "https://charter.boats/api/ai/boats/compare?id=n8wxFd&guests=10&lock=city"
curl "https://charter.boats/api/ai/boats/compare?boat_type=catamaran&country=HR&city=Split&length_min=40&length_max=44"

Response

{
  "anchor": { "id": "1f8a…", "slug": "lagoon-42-mia", "title": "Mia", "image": "https://…", "type": "catamaran", "country": "HR", "region": "Dalmatia", "city": "Split", "make": "Lagoon", "model": "42", "year": 2020, "length_m": 12.8, "guests": 10, "berths": 10, "cabins": 4, "toilets": 4, "price": [null, 4210, 4390, "…"] },
  "price_mode": "delta",
  "weeks": ["2026-10-10", "2026-10-17", "2026-10-24", "…"],
  "peers": [
    { "id": "77c1…", "slug": "lagoon-42-ana", "title": "Ana", "image": "https://…", "similar_pct": 100, "overlap_pct": 96, "price": [null, 0, -120, "…"], "region": "Dalmatia", "city": "Split", "same": ["region", "city", "make", "model", "year", "length", "guests", "berths", "cabins", "toilets"] },
    { "id": "93d0…", "slug": "lagoon-42-sun", "title": "Sun", "image": null, "similar_pct": 80, "overlap_pct": 100, "price": [null, 310, null, "…"], "region": "Dalmatia", "city": "Trogir", "same": ["region", "make", "model", "guests", "berths", "cabins", "toilets"], "year": "+2", "length": "+0.3 m" }
  ]
}

Errors

StatusBody
400Neither id nor boat_type and country, an unknown boat_type, or a country that is not a country code or name
404Boat not found — the id did not resolve, or the boat is not published

What Stands Out

GET/ai/boats/{id}/standouts

What is better and what is worse about one boat than the boats that cost the same, for one week — the get_boat_standouts tool on the MCP server and in Mermaid — each line stating how it compares, so a model never has to guess what is normal for charter. Anything the answer does not list is ordinary for boats like it. The {id} resolves as for Boat Details; authentication and rate limits are as for the search.

Similar boats are the same boat type priced within ±5% of this boat for that week (its all-in day rate for the week — every required fee and the platform fee in, the figure the search card prints), sold the same way (a day charter is compared only with day charters), in the boat's own country — every country when the country has fewer than 10. With fewer than 10 even then, nothing is compared and note says so. Without date_from the boats' "from" figures — the all-in day rate of each boat's cheapest week from today — are compared instead.

A pro is better than 90% of those boats; a con is worse than 90% of them, so the list does not lean negative. 10% is the default cut-off, and cutoff_pct moves every bar together. What is compared: build year, length, cabins, bathrooms, maximum guests, security deposit, check-in and check-out hour, kit that under 10% of them carry (and is rare across the fleet — all of it, in one line, as [n%] item), kit that 90% of them carry and this boat does not (only when its equipment list is as long as theirs and the model's other hulls mostly lack it too), a changeover day off the one 70% of them use (never Saturday), how many of the model are listed, and the last open weeks of the month or season. When nothing clears those bars, pros still names the one thing that beats the most of them and cons the one thing the most of them beat — past half of them — each with its true share; a con names the typical figure beside the boat's.

Optional date_from (YYYY-MM-DD) names the week. Optional cutoff_pct (default 10, at most 50) sets the cut-off: a pro beats all but that share of the similar boats, a con trails all but it, and rare kit is on fewer than it, across the fleet too. Optional compare_by (default price) picks the similar boats: price — within ±5% of this boat's rate, what else the same money buys; guests — the same guest count; length — within ±2.5% of its length. Each is the same boat type, priced for that week. On guests and length the all-in day rate is compared too (€211 a day, all fees in (better than 99%); without date_from, €211 a day at the cheapest week, all fees in), and the measure that picked the group is not compared with itself. Any other value is a 400. Optional fallback (default true): false leaves out the one line each side that is named when nothing clears the cut-off. Optional currency (EUR, the default, or USD) writes the security deposit and the day rate in it ($230 a day, all fees in).

Request

curl "https://charter.boats/api/ai/boats/n8wxFd/standouts?date_from=2026-10-10"

Response

{
  "boat_id": "1f8a2fa9-94cb-4c82-861d-20686843c94c",
  "week": { "from": "2026-10-10", "to": "2026-10-17" },
  "compared_with": "148 sailboats priced within 5% of her that week in Croatia",
  "compare_by": "price",
  "cutoff_pct": 10,
  "pros": [
    "6 cabins (better than 99%)",
    "Rare kit: [0.7%] Watermaker, [4.1%] Generator",
    "Her only open week in October (1 of 2 taken)"
  ],
  "cons": [
    "Built 2015 (typical 2019)",
    "€3000 security deposit (typical €1800; 147 state one)",
    "Check-out 08:00 (typical 09:00)"
  ],
  "rare_kit": [
    { "name": "Watermaker", "share_pct": 0.7, "useful_score": 4 },
    { "name": "Generator", "share_pct": 4.1, "useful_score": 4 }
  ],
  "note": "Anything not listed here is ordinary for boats like this one. [n%] is the share of them that have that item."
}

Response Fields

FieldTypeDescription
weekobject|null{ from, to } for the week asked about, null without date_from
compared_withstring|nullThe similar boats every line is measured against, said once; null when fewer than 10
compare_bystringThe basis the similar boats were picked by
cutoff_pctnumberThe cut-off the answer used
pros / consstring[]"fact (how it compares)". A percentage is of the boats in compared_with; where fewer of them state the value, the count follows (147 state one)
rare_kitobject[]The kit in the Rare kit pro as data, in the same order — most useful first, the rarer first among equals: { name, share_pct, useful_score }, where share_pct is the share of the boats in compared_with that carry it and useful_score (1-5, null for an item that matches no searchable feature) is how much it changes a charter week for the people aboard. Empty when no kit clears the cut-off or nothing was compared
notestringWhether the absence of a line means "ordinary", or nothing was compared

Errors

StatusBody
400Boat id required, or compare_by is not price, guests or length
404Boat not found — the id did not resolve, or the boat is not published
500The comparison could not be made; the answer is not sent without it