List Boats
Search and filter active, published boats with pagination
/boatsRetrieve a paginated list of boats with optional filters for search, type, capacity, price, and more. Only published boats are returned, and only boats with a price: without d, a boat needs a current price_per_day above zero; with d, a priced week in the range. A published boat whose seasons have lapsed keeps its detail page but is not listed here (the fallback tiers below are the exceptions).
Authentication
Send your API key in the X-API-Key header, as on every Operator API call — see Authentication.
Undated responses are cached for 60 seconds; requests with d are computed fresh.
Query Parameters
Pagination
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Results per page (max 50) |
offset | integer | 0 | Number of results to skip |
Search
| Parameter | Type | Description |
|---|---|---|
q | string | Boat NAME filter — matched against title only, literally (% and _ are ordinary characters, not wildcards). For a make or a model, use makes. When it reads as a boat NAME (3+ characters, at least one letter), published boats carrying that name are also appended after the strict results — see name_match_start_index. |
d | string | Availability date filter. Single date YYYY-MM-DD or range YYYY-MM-DD,YYYY-MM-DD. Matches weeks where the boat has any availability when the range spans more charter weeks than the stay needs -- i.e. over 14 days, or when the start is not a changeover day (see Availability). |
Location Filters
| Parameter | Type | Description |
|---|---|---|
location_id | integer | Filter by exact location ID |
location_type | string | One of country, state, city, municipality, text. Used with location_value. |
location_value | string | The value to match for the given location_type |
country_code | string | Narrow state/city/municipality location searches by country code, lowercase (hr, gr) |
With location_type=text the value is resolved as a free-text place name, which also accepts named sailing regions (Dalmatia, the Dodecanese). The other types look the value up directly and fall back to that same resolver when the direct lookup finds nothing, so a place is only reported unknown once both have failed. location_value is matched literally — % and _ are ordinary characters, not wildcards.
Whatever the type, a value that resolves to no known place returns location_unresolved (the value you sent) with an empty boats array and total: 0. The location is never silently dropped, so a response is never a worldwide search wearing the label of a place.
Boat Filters
| Parameter | Type | Description |
|---|---|---|
type | string | Boat type. Comma-separated for multiple: yacht, catamaran, sailboat, motorboat, rib, other |
guests | integer | Minimum guest capacity |
cabins | integer | Minimum number of cabins |
berths | integer | Minimum number of berths |
toilets | integer | Minimum number of toilets |
priceMin | number | Minimum daily price |
priceMax | number | Maximum daily price |
lengthMin | number | Minimum length in feet |
lengthMax | number | Maximum length in feet |
yearMin | integer | Minimum build year (values 2000 or under are ignored) |
yearMax | integer | Maximum build year (values of the current year or later are ignored) |
rating | number | Minimum average rating |
rentalTypes | string | Comma-separated: bareboat, skippered, no_licence_needed. no_licence_needed means the operator asks no licence of the charterer — it does not mean a skipper comes with the boat. Unknown values are ignored. A boat whose skipper is an obligatory paid extra counts as skippered, not bareboat — the guest cannot sail it themselves. |
manufacturerSlug | string | Filter by one manufacturer slug. makes does the same and more, and is the one to use |
makes | string | Comma-separated make/model filter, OR semantics across entries. An entry is either a make slug (bali — the whole make) or slug:model (bali:4.2). A make with no model stays unconstrained; models only narrow their own make. Model names are matched across every spelling the fleet stores for them (4.2 also matches boats filed as Bali 4.2); the valid values per make come from GET /api/manufacturers/{slug}/models, which returns { models: [{ id, label, values }] } — pass the label. Naming only unknown makes returns zero boats rather than the whole fleet. |
features | string | Comma-separated equipment/amenity terms, AND semantics. Accepts canonical slugs (watermaker), namespaced attribute tags (dinghy:hypalon), and loose synonyms (ac resolves to air_conditioning). The canonical vocabulary is served by GET /api/features. A term that resolves to nothing is ignored rather than rejected, so check terms against that vocabulary. An attribute tag the vocabulary doesn't know, under a concept it does (dinghy:carbon), matches as the concept itself (dinghy) — a broader answer, never a false one. |
Equipment Facets
| Parameter | Type | Description |
|---|---|---|
facet_counts | 1 | Adds a facets object to the response: per-feature boat counts (concept slugs and attribute tags) aggregated over the full matched result set — contextual to every filter in the request, never global. Features no boat in the set carries are omitted. Combine with limit=0 for a cheap counts-only request (boats comes back empty). |
Sorting
| Parameter | Type | Default | Description |
|---|---|---|---|
sort or sortBy | string | recommended | Sort order: recommended, price-low, price-high, rating (aliases price_asc/price_desc accepted). With d, newest (build year, newest first) is also accepted; without d it sorts as recommended. Unknown values sort as recommended. |
Company Filters
| Parameter | Type | Description |
|---|---|---|
company_slug | string | Show only boats from this company |
priority_company | string | Company slug whose boats are floated to the top of the results |
ids | string | Comma-separated boat IDs to fetch specific boats |
Request
Response
Response Fields
| Field | Type | Description |
|---|---|---|
boats | array | Array of boat objects |
total | integer | Total number of matching boats |
limit | integer | Page size used |
offset | integer | Offset used |
next_offset | integer | Offset to pass for the next page |
hasMore | boolean | Whether more results exist beyond this page |
location_unresolved | string | Only when the location you named resolved to no known place: echoes the value. boats is then empty and total is 0 |
backfill_start_index | integer|null | Index in boats where alternatives begin, or null. When the first page's strict results don't fill it, the shared ladder (the AI tool's alternatives come from the same ladder) loosens ONE thing at a time and stops as soon as the page is made up: the dates (the week either side), then the place (nearby bases), then the model (the make kept), then the name (any one of its words, whole words only). Type, guests, cabins, berths, length, year, budget, features and the referral operator are never loosened. Each of these rows carries relaxed — the list of what it gave up — and none is counted in total |
name_match_start_index | integer|null | Index in boats where name matches begin, or null. Only on a name-shaped q, only on the first page, and only when the strict page is not full. These are published boats whose title carries the searched name as a whole word (Bella matches Bella Luna, never Marbella) that the filters could not offer — unavailable for the dates, or with no sellable season at all. They sit after the strict results and before any backfill, are capped at 6, and are not counted in total. |
unavailable_start_index | integer|null | Index in boats where a last-resort listing begins, or null. Only when a plain search by a single exact location (+ optional dates, no other filters) returns nothing at all — no strict matches, no name matches, no backfill. These are published boats at that exact location with no sellable offer right now: their price_per_day and related pricing fields are forced to null. Capped at 6, not counted in total. |
priority_company | string | Echoes the priority_company query value when provided |
facets | object | Only with facet_counts=1: map of feature slug/attribute tag → number of boats in the full matched set carrying it. Zero-count features are omitted. |
Known limitation — paging. With some filter combinations total (and so hasMore) is counted more loosely than the rows are filtered, so it can promise more boats than the pages deliver. Keep paging until a page brings back fewer strict results than limit (or none), rather than relying on hasMore alone.
Boat Object
| Field | Type | Description |
|---|---|---|
id | string | Unique boat identifier (UUID) |
slug | string | URL-friendly identifier |
title | string | Boat name |
boat_type | string | Type of boat |
capacity | integer | Maximum guests |
length_ft | number | Length overall (LOA) in feet |
year | integer | Year built |
price_per_day | number|null | Effective daily rate. With a d filter this is the rate for the week named in charter_dates, which is not always the week you searched — see Changeover days. Without one it is the boat's cheapest week of the year, i.e. a "from" price. Null when the boat has no current pricing — a listing stays published on its specs, images and equipment even when its seasons lapse, so treat this as optional rather than guaranteed. |
price_from | number|null | Lowest seasonal price |
price_to | number|null | Highest seasonal price |
discount_percentage | number|null | Active discount percentage |
discount_label | string|null | Discount label text |
original_price_per_day | number|null | Price before discount |
average_rating | number|null | Average rating (1-5) |
review_count | integer | Number of reviews |
location_id | integer|null | Location ID — null for a boat not yet placed on a marina |
manufacturer_id | string|null | Manufacturer UUID |
company_id | string | Operator company UUID |
avail_weeks | integer | Packed availability bitmask (see Availability) |
hero_image_url | string|null | Primary image, as the un-resized master — the web app rewrites it to a size variant client-side. Don't hotlink the URL as returned: serve the image from https://media.charter.boats, keeping the path from /boats/… onward, and insert a variant suffix before the extension — _card (400×300), _hero (1200×800), _thumb (150×150), all WebP behind a .jpg filename. The master itself can be pruned once variants exist, so the bare path can 404 where a variant won't. /api/ai/boats and /api/agent/boats return already-resolved variants. |
has_valid_name | boolean|null | Whether the boat has a human-readable name |
model | string|null | Boat model name |
manufacturer | object|null | { id, name, slug, logo_url } |
location | object|null | { id, city, name, country }; null when location_id is |
rental_type | string|null | bareboat, skippered, or no_licence_needed — the operator's product class. no_licence_needed means the operator requires no licence of the charterer, which is not the same as a skipper coming along. Read it alongside skippered: a false skippered covers bareboat, no-licence-needed and boats that state neither, and only rental_type tells those apart |
skippered | boolean|string | true (skipper included in the charter price), "required" (the boat comes with a skipper billed as a mandatory fee — see its fees), "optional", or false. ⚠️ true is checked against the boat's live fee set, not read off rental_type: a boat whose operator sells a crewed product AND bills an obligatory skipper fee reports "required", because the charter price does not cover him |
currency | string | Currency code (e.g. EUR) |
internal_discount_pct | number|null | Internal discount percentage when applicable |
required_extras | object|null | The mandatory extras that come with the price on this row — { per_day, per_booking, per_person, per_person_per_day, from }, per charter week, in currency. With a date filter (d) these are the quoted stay's, averaged per week, and from is false. Without one — or where the boat has no sellable week for the dates, so price_per_day falls back to the from-price — they are the fees of the cheapest PRICED week that states a fee (the same basis as price_per_day, which is the minimum over the boat's seasons and likewise ignores whether that week is already booked; an all-zero fee window is unknown, not free, so it is skipped while any priced week charges). from is true only when that set is also the lowest of any priced week and another week charges more — i.e. it marks a floor, and where the cheapest week is not the cheapest to equip, the figure is stated flat. null only when the boat has no sellable week at all |
charter_week_shifted | boolean | true when charter_dates names a different week from the one you searched — the boat is sold that week and not yours. false when it is the week you asked for, including when the changeover day moves its start date (which happens on nearly every result of an off-changeover search and is therefore not per-boat news). |
charter_dates | string|null | The charter week this row is quoted for, as YYYY-MM-DD,YYYY-MM-DD on this boat's changeover day — not the dates you searched. null when the boat has no sellable week in the range at all, and whenever there is no week to name: no d filter, a d outside the 52-week horizon, or a week that has already begun. See Changeover days. |
Availability
avail_weeks is a packed bitmask of bookable weeks relative to an anchor date. To query availability, pass the d date filter rather than decoding the mask yourself.
A week's bit is set when that week's Sunday–Friday nights are all unblocked and at least one is priced. A charter occupies the nights from its check-in up to but not including its check-out day — the boat is handed back in the morning and can sail again the same afternoon — so one guest disembarking and the next embarking share that day, and a boat whose bookings sit either side of a free week is bookable for that week.
Note the asymmetry: a charter's check-in day is occupied (the guest sleeps aboard), only its check-out day is free. The bit does not attest the week's own Saturday, so confirm exact dates with GET /boats/availability before quoting them.
Changeover days
The bitmask is anchored to Saturday, but boats set their own changeover day (charter_day on the boat). A one-week stay that starts on any other day therefore touches two anchor weeks, only one of which it actually needs. When d spans more weeks than the stay requires, the filter matches boats free in any of them rather than all — so a Sunday-to-Sunday search returns the fleet available that week instead of only boats free for a fortnight.
Results are consequently a discovery set, not a quote: a returned boat may only be sellable for the adjacent charter week. Confirm the sellable dates with GET /boats/availability before presenting them.
charter_dates settles that per boat, and price_per_day is quoted from the same week. Two things move it off the dates you asked for:
- The changeover day. A Tuesday search returns Saturday boats reading
2026-09-19,2026-09-26and Sunday boats reading2026-09-20,2026-09-27— each on the day that boat actually turns around. - The week itself. Where the range covers more than the stay needs, a boat is matched if it is free in any of those weeks, so a hull booked for the week you typed but open the week after is returned for the week after — and quoted for it.
So pass charter_dates back as d when deep-linking, and name that range to a guest rather than the one you searched. A null means the boat has no sellable week in the range at all — it is here as a near miss, and its price_per_day is whatever the week you searched happens to say (the yearly "from" rate when that week has no price of its own). Neither is a quote for a real charter; get its dates from GET /boats/availability.
⚠️ priceMin and priceMax are applied to the week you searched, while price_per_day may describe the week the boat is actually sold in. A row quoted from a neighbouring week is dropped when it falls outside your cap, so every counted match respects the filter — but the totals in total and the price-low / price-high ordering are still computed on the searched week, so a capped search can report more matches than it returns. The two fallback tiers marked by name_match_start_index and unavailable_start_index are exempt from price filters by design, as documented above.
Even when populated this is a discovery answer, not a quote. The week's bit does not attest its own check-in day and the price is a cached weekly aggregate, so confirm both against GET /boats/availability (and the price against Resolve Pricing) before presenting them.