Search Boats (AI)
Search charter boats with fuzzy location matching, availability checking, and deal ranking
/ai/boatsSearch 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
| Parameter | Type | Required | Description |
|---|---|---|---|
location | string | No | Destination — marina, city, island, sailing region, or country, written freely (Lefkada, Greece is fine). See Location Matching. |
search | string | No | A 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. |
makemodel | string | No | Make 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. |
m | string | No | An 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_type | string | No | One of: sailboat, catamaran, yacht, motorboat, gulet, rib, other |
manufacturer | string | No | Manufacturer name or alias (fuzzy matched, e.g. Bali, Beneteau, FP). A make resolved from makemodel replaces it; makemodel covers this for most callers. |
features | string | No | Comma-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. |
guests | integer | No | Minimum guest capacity |
date_from | string | No | Charter 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_to | string | No | Charter end date (YYYY-MM-DD). Typical charters are 7 days (Saturday to Saturday). |
price_min | number | No | Minimum 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_max | integer | No | Maximum 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_max | number | No | Maximum whole-trip total (charter + required extras) for the requested stay — every week of it, not just the first. Requires date_from/date_to. |
cabins | integer | No | Minimum number of cabins |
length_min | integer | No | Minimum length in feet |
year_min | integer | No | Minimum build year |
sort | string | No | Sort order: best_value (default), deals, price_low, price_high, newest |
limit | integer | No | Results to return (default: 5, max: 25) |
Request
Response
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
| Field | Type | Description |
|---|---|---|
boats | array | Matching boats ranked by the selected sort order |
boats[].id | string | Boat UUID |
boats[].url | string | Share 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[].title | string | Boat name |
boats[].boat_type | string | Type of boat |
boats[].manufacturer | string|null | Manufacturer name |
boats[].model | string|null | Model name |
boats[].location | string | Formatted location string (marina, city, country) |
boats[].year | integer|null | Build year |
boats[].length_ft | number|null | Length in feet |
boats[].capacity | integer|null | Maximum guests |
boats[].cabins | integer|null | Number of cabins |
boats[].toilets | integer|null | Number of heads/toilets |
boats[].skippered | boolean|string | true (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_day | number | Daily 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_percentage | number | Active discount percentage. 0 if none. |
boats[].discount_label | string|null | Discount description (e.g. "50% discount"). null when no discount. |
boats[].price_rank_percentile | number|null | Where this boat's daily rate sits within the matched inventory (0 = cheapest, 100 = most expensive) |
boats[].currency | string | Currency code (e.g. EUR) |
boats[].required_extras | object|null | Mandatory 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_summary | string|null | Human-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_days | array | Allowed check-in/turnover weekday(s), e.g. ["Saturday"] |
boats[].min_charter_days | integer|null | The boat's minimum charter length in nights, when known |
boats[].instant_book | boolean | Whether the boat supports instant booking |
boats[].hero_image_url | string|null | Primary boat image, as the 400×300 WebP card variant — hotlink it directly; the un-resized original is not a public URL. |
total_matching | integer | How 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_by | string | Present when boats holds more than one boat — the list is already in presentation order; do not re-sort it |
dates_note | string | Present 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_applied | array | Present when features was sent — the equipment slugs actually filtered on |
features_unmatched | array | Present when some features terms were not recognised — they were ignored |
inventory | object|null | Aggregate stats for the searched area. null when location did not resolve. |
inventory.total_boats | integer | The 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_day | object|null | { min, median, max } of the boats' current day rates (the undated rate, whatever the dates). median is the true median, rounded. |
inventory.capacity | object|null | { min, max } guest capacity |
inventory.length_ft | object|null | { min, max } length |
inventory.by_type | object | Boat counts per type (only when boat_type not filtered) |
location_matched | object|null | The 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_url | string | The 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_query | string | The 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_unavailable | string | Present 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_horizon | object | { 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. |
alternatives | array | Present 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_inventory | object | Present 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_interpreted | object | Present 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_matches | array | Present 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_note | string | Present alongside named_matches — how to present them |
name_not_found | string | Present when a name search matched nothing anywhere in the fleet. Mutually exclusive with named_matches. |
listed_unavailable | array | Present 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_note | string | Present alongside listed_unavailable — how to present them |
no_results | object | Present 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_unresolved | string | Present when the location you sent matched no place we hold. Echoes the term that failed; no search is run rather than a worldwide one. |
note | string | One-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:
| Param | Meaning |
|---|---|
d | Dates, 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. |
ref | An affiliate slug. You rarely need it: a keyed call already puts yours in the link. |
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:
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:
relaxed | What was loosened |
|---|---|
dates | The week either side of the ones asked for |
place | A nearby base (the nearest marinas holding ~30 boats of the type, up to 60 nm) |
model | Another model of the same make — the make is kept |
name | The 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.
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.
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.
| Field | Meaning |
|---|---|
bookable_as_searched | Always 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_because | One 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_note | What 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,mormanufacturer). The name still filtersboats— 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, andalternativesanswers 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 fromnamed_matchesrather than mislabelled. Raiselimitto see it inboats.
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":
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.
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:
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:
- Sailing region.
Istria,Kvarner,Dalmatia,the Ionian,the Saronic,the Cyclades,the Dodecaneseandthe Sporades(with common spellings such asDalmacijaorKyklades, 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_matchedthen reportsid: "region:<Name>". - 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 Kosmanever degrades to every marina in the world.
- the whole term, then the part before the first comma — anything after a comma is a hint that narrows the match (
- 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
| Value | Description |
|---|---|
best_value | Best price-per-person value first, available boats before unranked ones (default) |
deals | Best discounts first, then lowest price |
price_low | Cheapest per-day rate first |
price_high | Most expensive first |
newest | Newest build year first |
Boat Details
/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
Response
Response Fields
| Field | Type | Description |
|---|---|---|
url | string|null | Share link to the boat page — no dates; your referral when called with a key |
boat.id | string | Boat UUID |
boat.title | string | Boat name |
boat.description | string|null | Description (trimmed to ~500 chars) |
boat.operator_notes | string|null | The operator's own practical notes, where the partner supplies them (fuel policy, check-in conditions and the like) — unedited text |
boat.boat_type | string|null | Type of boat |
boat.manufacturer | string|null | Manufacturer name |
boat.year | integer|null | Build year |
boat.length_ft | number|null | Length in feet |
boat.capacity | integer|null | Maximum guests |
boat.cabins | integer|null | Number of cabins |
boat.berths | integer|null | Number of berths |
boat.toilets | integer|null | Number of heads/toilets |
boat.instant_book | boolean | Whether instant booking is supported |
boat.rental_type | string|null | The operator's product class: e.g. skippered, bareboat. It does not say who pays for the skipper — read boat.skipper for that |
boat.skipper | string | Who 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_schedule | string|null | Human-readable check-in day/time and minimum nights |
boat.security_deposit | number|null | Security deposit amount |
boat.location | object | { name, city, country } (each may be null) |
boat.equipment | array | The 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_days | integer | Look-ahead window (365) |
pricing.currency | string | Currency code |
pricing.ranges | array | Contiguous available date windows with from, to, price_per_day (discount already applied). At most 30, soonest first |
pricing.truncated | boolean | true when more than 30 ranges existed — the later ones were dropped |
pricing.note | string | Guidance on how to read pricing |
bookable_slots.note | string | Guidance on quoting slots |
bookable_slots.slots | array | Authoritative { 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.truncated | boolean | true when the 60-slot cap cut the list |
fees.required | array | Mandatory { 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.optional | array | Optional add-on fees, same shape |
discounts.note | string | Guidance on which discounts are pre-applied |
discounts.operator_seasonal | array | Seasonal discounts already baked into pricing.ranges |
discounts.operator_conditional_offers | array | Coupon-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_grants | array | Agent-granted discounts (NOT pre-applied) |
company.has_members | boolean | Whether the operator has staff members responding |
company.cancellation_policy | string | Plain-language cancellation policy |
Errors
| Status | Body |
|---|---|
400 | Boat 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 |