Charter Boats
Charter Boats API
AI Integrations

Search Boats (AI)

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

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.

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: 60 requests per 10 minutes (30 for Custom GPT clients); past it you get 429 with a Retry-After header.

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 — 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 EUR. Undated, compared with the boat's current day rate; with dates, with the searched week's per-day price.
price_maxintegerNoMaximum price per day in EUR. Undated, compared with the boat's current day rate; with dates, with the searched week's per-day price. Decimals are floored, and 0 means "not asked" — the same for every number below.
total_maxnumberNoMaximum whole-trip total (charter + required extras) for the requested stay — every week of it, not just the first. Requires date_from/date_to.
cabinsintegerNoMinimum number of cabins
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)

Request

# 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"

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",
      "discounted_price_per_day": 351,
      "discount_percentage": 50,
      "discount_label": "Charter discount",
      "price_rank_percentile": 3,
      "currency": "EUR",
      "required_extras": {
        "per_day": 0,
        "per_booking": 420,
        "per_person": 0,
        "per_person_per_day": 0,
        "from": false
      },
      "price_summary": "~2877 EUR total for the week of Sat 26 Sep 2026 → Sat 3 Oct 2026, 12 guests. 50% discount applied. Charter: 2457 EUR (351/day × 7 nights). Booking extras: 420 EUR (cleaning etc)",
      "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,
    "discounted_price_per_day": { "min": 89.86, "median": 3623, "max": 38742.86 },
    "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&guests=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
}

When no dates are passed, price_summary is null and inventory.by_type is only present when boat_type was not filtered. required_extras is still sent — a mandatory fee belongs to the boat, not to the dates — quoted from the boat's cheapest priced week that states one, and marked from where that figure is a floor.

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 stay longer than one charter week is priced as every week it covers, at each week's own rate — a fortnight is week 1 + week 2. price_summary names the span it totalled ("the 2 weeks of Sat 5 Sep → Sat 19 Sep") and the nights behind the figure, and total_max screens against that same total, so a boat is never filtered out on a number different from the one quoted back. A month-length range is the exception: it has no single stay to total, so it stays a weekly "from" range.

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. A listed boat whose seasons have run out stays on the website (its page shows specs and an availability alert instead of a rate) but is excluded from boats, since there is nothing to quote: undated searches require a current daily rate, dated searches require an available week that is priced. alternatives follows the same rule.

The one 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 cabins
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[].discounted_price_per_daynumberDaily rate in the boat's currency (already discounted). Over a stay spanning several charter weeks this is the average across those weeks, so × nights gives the stay's charter total — a fortnight is week 1 + week 2, not week 1 twice.
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[].required_extrasobject|nullMandatory extras (per_day, per_booking, per_person, per_person_per_day, from). For a dated search, the quoted stay's, averaged per week the same way, with from: false. Without dates, the cheapest priced week that states a fee — one real week's fee set, never a per-field minimum, and never an empty fee window read as free — with from: true only where that set is a floor (no priced week charges less, one charges more). null only when the boat has no priced week (an explicitly unbookable row).
boats[].price_summarystring|nullHuman-readable total breakdown for the quoted stay — it names the charter weeks on offer ("the week of …" or "the 2 weeks of …") and the nights the total covers. null when no dates provided.
boats[].checkinout_daysarrayAllowed check-in/turnover weekday(s), e.g. ["Saturday"]
boats[].min_charter_daysinteger|nullThe boat's minimum charter length in nights, when known
boats[].instant_bookbooleanWhether the boat supports instant booking
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_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 priced 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.
inventory.discounted_price_per_dayobject|null{ min, median, max } of the boats' current day rates (the undated rate, 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 discounted_price_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 — { make, model, note }. model is "" when the term was a make alone. 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.

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.

You may append two params, and both override 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.
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

Anything else you append is dropped. A bare code with no params redirects 301; a link carrying dates or a referral 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. If no slot matches the guest's window, hand over the url from /ai/boats/{id} instead — it carries no dates, and still carries your referral.

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 AstraLyra, 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,
      "discounted_price_per_day": 264,
      "discount_percentage": 35,
      "currency": "EUR",
      "price_summary": "~2138 EUR total for the week of Sat 26 Sep 2026 → Sat 3 Oct 2026, 8 guests. 35% discount applied. Charter: 1848 EUR (264/day × 7 nights). Booking extras: 290 EUR (cleaning etc)",
      "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",
      "discounted_price_per_day": 600,
      "price_summary": "~4715 EUR total for the week of Sat 3 Oct 2026 → Sat 10 Oct 2026, 11 guests. Nearest week this boat can be sold — not the requested Sat 26 Sep 2026 → Sat 3 Oct 2026. …",
      "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: 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).
availability_noteWhat may and may not be quoted for this boat, matched to what the object actually carries — an undated search has no price_summary, and the note says so instead of pointing at a null.

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:

  • A make or model is in play (makemodel, m or manufacturer). The name still filters boats — a Beneteau called Bela — but the fleet-wide lookup of other boats with that name is skipped: it would list other builders' hulls as "boats named that" while the question was about one make, and alternatives answers a class.
  • 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.

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",
      "discounted_price_per_day": 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 still filters — 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"). 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 — 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."
}

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 CoastAmalfi; 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.

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

Sort Orders

ValueDescription
best_valueBest price-per-person 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, and the url here never carries dates.

Request

curl "https://charter.boats/api/ai/boats/n8wxFd"

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": ["Lazy bag main"] },
      { "group": "Deck", "items": ["Dinghy (Honda 6 HP)", "Cockpit floor and transoms in teak", "Aft cockpit cushions"] }
    ]
  },
  "pricing": {
    "horizon_days": 365,
    "currency": "EUR",
    "ranges": [
      { "from": "2026-09-27", "to": "2026-10-02", "price_per_day": 351.22 },
      { "from": "2026-10-15", "to": "2026-10-16", "price_per_day": 258.85 },
      { "from": "2026-10-17", "to": "2026-11-07", "price_per_day": 273.18 }
    ],
    "truncated": false,
    "note": "Price already includes operator_seasonal discount. Every date inside a `ranges[*]` entry IS bookable (subject to `boat.charter_schedule`)..."
  },
  "bookable_slots": {
    "note": "Authoritative list of (check-in, check-out) slots you may quote. `base_price` is the charter total only — ADD fees from `fees.required`...",
    "slots": [
      {
        "type": "bareboat",
        "checkin": "2026-09-26T17:00",
        "checkout": "2026-10-03T09:00",
        "nights": 7,
        "base_price": 2458.54,
        "product": null
      },
      {
        "type": "bareboat",
        "checkin": "2026-10-17T17:00",
        "checkout": "2026-10-24T09:00",
        "nights": 7,
        "base_price": 1912.26,
        "product": null
      }
    ],
    "truncated": false
  },
  "fees": {
    "required": [
      { "description": "Charter package", "amount": "420 EUR", "period": "per_booking", "paid_at": "booking" },
      { "description": "Service 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": "`operator_seasonal` is ALREADY APPLIED to `pricing.ranges[*].price_per_day`. `operator_conditional_offers` and `agent_grants` are NOT applied...",
    "operator_seasonal": [
      {
        "label": "Charter discount",
        "value": { "type": "percent", "amount": 50 },
        "applies_to": { "from": "2026-09-27", "to": "2026-12-31" },
        "original_price_per_day": 702.45
      }
    ],
    "operator_conditional_offers": [],
    "agent_grants": []
  },
  "company": {
    "has_members": false,
    "cancellation_policy": "Free cancellation within 24 hours of booking. Service fees are non-refundable."
  }
}

Response Fields

FieldTypeDescription
urlstring|nullShare link to the boat page — no dates; your referral when called with a key
boat.idstringBoat UUID
boat.titlestringBoat name
boat.descriptionstring|nullDescription (trimmed to ~500 chars)
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 cabins
boat.berthsinteger|nullNumber of berths
boat.toiletsinteger|nullNumber of heads/toilets
boat.instant_bookbooleanWhether instant booking is supported
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 required fees: 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/time and minimum nights
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)"). At most 80 items across all groups; within a section the rarest come first, so the cap sheds the commonest kit
pricing.horizon_daysintegerLook-ahead window (365)
pricing.currencystringCurrency code
pricing.rangesarrayContiguous available date windows with from, to, price_per_day (discount already applied). 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, base_price, product } slots, soonest first, at most 60. base_price is the charter total only. product names the partner package the slot is priced for (pass it on when reserving), or null. A day-trip slot has nights: 0 — it exists only where that package is quoted same-day
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. paid_at is "booking" (in the online total) or "base" (settled at the marina) and is absent on an included fee. Scoped like the boat page: one package, the standard week, the boat's capacity as crew size
fees.optionalarrayOptional add-on fees, same shape
discounts.notestringGuidance on which discounts are pre-applied
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

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