Get Boat
Get detailed information about a specific boat
/boats/:idRetrieve complete details for a single boat, including location, images, equipment, reviews, seasonal pricing, and calculated ratings.
Authentication
Send your API key in the X-API-Key header, as on every Operator API call — see Authentication.
Responses are cached for 60 seconds (served stale while refreshing), so a change can take up to a minute to show.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The boat's UUID or slug. Store the UUID: a slug can change when a listing is renamed, and an old slug then answers 404 |
Request
Response
Trimmed: long arrays are shortened and some location fields omitted.
Note on the
companyblock. The live response carries acompanyobject. The documented API contract exposes only the currency, cancellation- and guest-terms fields shown above (base_currency,cancel_refund_days,cancel_refund_percent,terms_guest,terms_faq). Operator identity fields are not part of the documented contract and should not be relied upon.
Images.
images[].urlandhero_image_urlare un-resized masters (the example shortens their host). Don't hotlink them as returned: serve each image fromhttps://media.charter.boats, keeping the path from/boats/…onward, and insert a size suffix before the extension —_card(400×300),_hero(1200×800) or_thumb(150×150), all WebP behind a.jpgfilename. Masters can be pruned once the variants exist, so a bare path can 404 where a variant won't. (A URL whose path does not start/boats/is not stored with us — typically an imported boat's source image.)imagesarrive in no particular order: sort bysort_order(and useis_primaryfor the cover).
Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Unique boat identifier (UUID) |
slug | string | URL-friendly identifier |
title | string | Boat name |
boat_type | string | yacht, catamaran, sailboat, motorboat, rib, or other |
mmsi | string|null | Maritime MMSI number, if known |
capacity | integer | Maximum guests |
length_ft | number | Length overall (LOA) in feet — see the note below |
year | integer | Year built |
cabins | integer|null | Number of cabins |
crew_cabins | integer|null | Number of crew cabins |
berths | integer|null | Number of berths |
toilets | integer|null | Number of toilets |
specs | object|null | Extra specs (beam, draft, engine_desc, fuel_capacity, water_capacity, transit_log, …). Metres for beam/draft |
specs_from_model | array | Which specs keys were filled from the manufacturer's model rather than sent for this hull — [], ["beam"] or ["beam","draft"]. Beam is a model constant, but the same model ships with more than one keel, so an inherited draft is typical rather than measured on this boat |
ais | object|null | What this hull has been observed doing, from its AIS track. null for most boats. 🚨 Not utilisation — AIS records movement, and a fully booked boat can sit in one bay all week |
ais.speed | object|null | { low, high, median } knots, the p25–p75 of this boat's speed when under way. Always a range: the spread within one boat (skipper, sea state, wind) exceeds the spread between boats, so a single figure would read as a promise. Not comparative — a boat's number reflects where it sails as much as how |
ais.sailingDaysPerYear | number|null | Days a year this hull was recorded on the move, normalised over ais.trackedYears. A day counts when that day's fixes span at least a kilometre — displacement, never speed: a yacht sailing around its own anchor in a blow logs 1–4 knots without going anywhere, so speed cannot separate a passage from a windy night at anchor. Deliberately a rate and not a "moved on X of Y days" fraction — that division reads as an idle boat, which is invalid here, since a fully chartered week can be spent at anchor. null under a full year of track — scaling a single season to a year assumes winter looks like August, which for a charter yacht is false |
ais.trackedYears | number|null | Years of AIS track behind that rate — calendar time from the first fix to the last, including the stretches the hull sent nothing. Silence over the winter is evidence the boat was not sailing, so it counts; a denominator of days-that-have-a-fix would converge on the numerator for any yacht that switches AIS off on the berth, and push every rate toward 365 |
ais.placesVisited | number|null | Distinct harbours and bays it was recorded stopping at |
description | string|null | Full description: the platform's own text, else our generated one (description_ai), else the operator's highlights |
description_ai | string|null | The generated description on its own, when there is one |
pricing_source | string | Active pricing source: direct, nausys, or mmk |
price_per_day | number|null | Effective daily rate. 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. |
original_price_per_day | number|null | Price before discount |
discount_percentage | number|null | Active discount percentage |
discount_label | string|null | Discount label text |
price_from | number|null | Lowest seasonal price |
price_to | number|null | Highest seasonal price |
charter_day | integer|null | Preferred charter start weekday (0=Sun … 6=Sat) |
check_in_periods | array|null | Check-in/out windows from the source platform. minimalReservationDuration is the shortest stay the boat is sold for in that window. For boats priced from MMK it is the length the current price was quoted for (7 nights, or 6 on boats that check in and out on the same clock), refreshed on every pricing run — the same number the booking flow enforces. |
checkin_time | string|null | Check-in time (e.g. 17:00:00) |
checkout_time | string|null | Check-out time (e.g. 09:00:00) |
min_charter_days | integer|null | The platform's own yacht-level minimum, as sent. 0 means the operator set none, not that single nights are sold — read check_in_periods[].minimalReservationDuration for the enforced minimum. |
instant_book | boolean | Whether instant booking is enabled |
security_deposit | number|null | Security deposit amount |
deposit_with_waiver | number|null | Reduced deposit when a damage waiver is taken |
seo_released_at | string|null | Timestamp the detail page was released for indexing |
company_id | string | Operator company UUID |
location_id | integer|null | Location ID |
model | string|null | Boat model name, as the source platform spells it |
updated_at | string | ISO 8601 last-updated timestamp |
features | string[] | Canonical equipment slugs and attribute tags this boat carries — the values the features filter on List Boats matches (vocabulary) |
manufacturer_id | string|null | Manufacturer UUID |
rental_type | string | bareboat, skippered, or no_licence_needed — the operator's product class. skippered means a skipper comes with the boat, not merely that one is available, and no_licence_needed means the operator requires no licence of the charterer (which is not the same as a skipper coming along). It does not say who pays: read skipper_charged for that; see Skipper |
skipper | integer|null | Per-boat skipper flag: 1 = the boat comes with a skipper (included, or an obligatory paid extra), null = a skipper is available on request, any other value = you sail it yourself. Read it together with rental_type; see Skipper |
skipper_charged | boolean | Whether the obligatory skipper is billed: true = a required, live, non-zero skipper fee applies to this boat under its active pricing source, so the charter price does not cover him and the charge appears in List Boat Fees. false = nothing is billed for him. Derived from the fee set on every read, so it follows the operator's current schedule rather than a stored flag; see Skipper |
crew | object|null | The crew that comes with the boat, when the operator names one: { count, roles[] }. Roles are the operator's own words. null when no roster is published — which is not a statement that there is no crew; see Skipper |
average_rating | number|null | Average review rating (1-5) |
review_count | integer | Total number of reviews |
import_source | string|null | Source platform the boat was imported from |
is_published | boolean | Always true here: an unpublished listing answers 404, not a row (see Errors). Publication is not a guarantee of bookability — a published boat may have no current pricing or availability; check price_per_day. |
photo_descriptions | object|null | AI-generated per-category photo captions |
seo_variant | string|null | SEO copy variant label |
has_valid_name | boolean|null | Whether the boat has a human-readable name |
has_multi_port | boolean | Whether the boat rotates between marinas seasonally |
hero_image_url | string|null | Primary image master — see the Images note above |
tour_360 | string|null | Link to a 360° virtual tour, when the platform provides one |
avail_weeks | integer | Packed availability bitmask, relative to avail_weeks_anchor. A set bit means the hull is free and sellable that week somewhere — it does not assert the boat is at location, since a week may be sold from a second base (see Where each week departs from) |
best_internal_discount_pct | number | Best internal discount percentage available |
discounted_price_per_day | number[] | 52 weekly entries indexed from avail_weeks_anchor (entry w = the week starting w × 7 days after it): the per-day price for that week. 0 means the boat is not sold that week — never free |
discount_pct | number[] | Same indexing: the discount percentage behind that week's price |
extras_per_day, extras_per_booking, extras_per_person, extras_per_person_per_day | number[] | Same indexing: required extras for that week, by how they are charged. A cached weekly aggregate for charts and "from" prices — quote a real stay with Resolve Pricing and List Boat Fees |
company | object | Cancellation/guest-terms fields only (see note above) |
manufacturer | object|null | { id, name, slug, logo_url } |
location | object|null | Marina/location details (see below); null when the boat has no location |
geo_crumbs | array | Breadcrumb trail up the place hierarchy, [{ label, href }] — country, region, city. href is a charter.boats path (prefix https://charter.boats). Only indexable hub pages are included, so the trail can be shorter or empty |
images | array | Active images with id, url, category, is_active, is_primary, sort_order, in no particular order — sort by sort_order |
videos | array | [{ id, url, storage_path, thumbnail_url, duration_seconds, sort_order }], ordered by sort_order; usually empty |
equipment | array | The boat's equipment in display sections: [{ group, items: [{ name, value, rare?, pct?, hidden? }] }]. value is a detail such as a count or type ("Halyard"), null when none. rare: true with pct (the share of the fleet carrying it) marks unusual kit. hidden: true marks an item the operator hid — don't show those to guests |
rare_features | array | Up to three of the boat's rarest items, rarest first, [{ label, pct }] — the "rare find" highlight. Safety and miscellaneous items are left out |
reviews | array | Reviews with id, rating, comment, created_at |
prices | array | Seasonal pricing rows for the active pricing source. Each row names its departure marina; one-way delivery legs and implausible quotes (partner-side typos priced under €10/day) are excluded — see Where each week departs from |
has_day_trips | boolean | The operator also quotes day charters for this boat. They are not sold on charter.boats yet and never appear in prices |
best_price | number|null | Same value as price_per_day; null when the boat has no current pricing |
best_source | string | Pricing source for best_price |
best_currency | string | Currency code (e.g. EUR) |
internal_discount_pct | number|null | Internal discount percentage when applicable |
lowest_price_per_day | number|null | Lowest effective per-day price after any internal discount; null when the boat has no current pricing |
mmk_products | array|null | MMK product variants with name and isDefault (MMK boats only) |
avail_weeks_anchor | string|null | Anchor date (YYYY-MM-DD) for decoding avail_weeks |
length_ft is length overall (LOA), the figure charter platforms and manufacturers advertise — not hull length, which runs shorter. For boats priced from a connected platform it is the length of the boat's model, so sister ships of the same model report the same figure rather than each platform's per-hull number. Boats you manage directly keep whatever length you set.
Where each week departs from
Every row in prices carries the marina that week starts from:
| field | type | description |
|---|---|---|
prices[].departs_from | object|null | { id, name, city } of the departure marina. The boat's own marina for most weeks; a different one where the operator sells that week from a second base. null when several marinas share the week — see below |
prices[].departs_from_count | integer | How many marinas that week can start from. 1 normally; higher when the operator offers the same week from several bases |
A boat is not always in one place. Around 8% of the MMK fleet is quoted from more than
one base across a year — some relocate seasonally, others are offered from several ports
in the same week — so a week's departure marina is not always the marina on location.
Where departs_from_count is above 1, departs_from is null on purpose. We hold one
quote for that week but cannot say which of the bases it belongs to, and naming one would
be a guess. Show the count, or the boat's own marina, rather than picking.
One-way legs are still excluded. MMK also quotes delivery legs that start and end at
different bases. Those are dropped from prices entirely: there is no single marina to
name, and a booking attempt against one is rejected. Such a quote is often a fraction of
the round-trip rate, so including it would make the boat look cheaper than anything you
could book.
price_per_day, price_from/price_to and best_price are still derived from
home-base weeks only, while prices now includes weeks sold from a second base. The
lowest value in prices can therefore be below price_from. Use prices when you need
what a specific week costs, and the scalar fields when you need what the boat costs from
its own marina.
Skipper
rental_type: "skippered" means the charter comes with a skipper — it is not a
statement that one can be arranged. A boat that is sold bareboat but also offers a
crewed option reports rental_type: "bareboat", and the skipper shows as optional
(skippered: "optional" on the list endpoint). When the operator makes the skipper an
obligatory paid extra — the fee is marked required, so the guest cannot decline
him — the boat counts as skippered for search purposes but reports
skippered: "required": the boat cannot be sailed by the charterer, and the
skipper's cost appears as a mandatory fee on top of the charter price rather than
inside it. rental_type may still read "bareboat" on such boats.
⚠️ A crewed product and an obligatory skipper fee are not exclusive, and this is
the case to get right: an operator can sell the boat as rental_type: "skippered"
and bill "Skipper (+ food) — 160 EUR per day" as a required fee. The product class
alone therefore cannot tell you whether the price covers him — skipper_charged can,
and it is what the list endpoints report as skippered: "required" rather than
true. Never present such a boat as "skipper included"; the skipper is obligatory and
the guest pays for him on top.
We take this from the operator's own product setup rather than inferring it: whichever product they mark as the default is what the boat is, and a crewed product sitting alongside a bareboat default makes the skipper an add-on, not an inclusion.
A crewed charter is not always just a skipper, so the wording follows the source: the
operator's product is named Crewed, and where they publish an actual roster we return
it as crew — { "count": 4, "roles": ["Captain", "Chef", "Steward", "Deckhand"] }.
Roles are reproduced as written, not mapped onto a vocabulary of ours, so expect
free text and the occasional operator typo. crew is null on most crewed boats: a
roster is published for a minority of them, and its absence says nothing about whether
a crew is aboard — rental_type remains the answer to that.
Names, ages, photographs and biographies are deliberately not part of this response, though operators supply them.
Whether a licence is required is a separate question, and one we do not always know — it is absent from this response rather than guessed.
Company Object
| Field | Type | Description |
|---|---|---|
base_currency | string | Default currency (e.g. EUR) |
cancel_refund_percent | number|null | Refund percentage under the cancellation policy |
cancel_refund_days | number|null | Days before charter that the refund window applies |
terms_guest | object|null | Guest-facing terms: insurance_requirements, damage_liability, license_requirements |
terms_faq | array|null | Guest questions answered from the operator's terms, [{ q, a }] |
Location Object
| Field | Type | Description |
|---|---|---|
id | integer | Location ID |
slug | string|null | URL slug of the location page (https://charter.boats/locations/{slug}) |
name | string | Marina/location name |
city | string|null | City |
municipality | string|null | Municipality |
state | string|null | State/region |
country | string|null | Country name |
country_code | string|null | ISO country code (lowercase) |
lat | number|null | Latitude |
lon | number|null | Longitude |
website | string|null | Marina website |
phone | string|null | Contact phone |
image_url | string|null | Marina image |
rating | number|null | Marina rating |
capacity | number|null | Berth capacity |
max_draft | number|null | Maximum draft (m) |
max_length | number|null | Maximum vessel length (m) |
vhf_channel | string|null | VHF hailing channel |
neighbors | object|null | Nearby location IDs grouped by distance band: keys like "12nm", "20nm" (nautical miles), each a comma-separated string of location IDs |
description | string|null | Marina description |
Errors
| Status | Message |
|---|---|
400 | Boat ID is required |
404 | Boat not found, or not published |
A listing that is not published — a draft, one an operator withdrew, or one belonging to a
deactivated company — answers 404 exactly as a boat that never existed does, for every caller
including your own boats. Read and edit your unpublished listings through the dashboard, not
here.
When the boat you asked for — by either slug or UUID — has since been merged into another, the same hull listed twice and reconciled, the 404 carries the survivor so you can follow it instead of dropping the boat:
data.moved_to is a slug (or, for a survivor with no slug yet, a UUID) for this same
endpoint. Treat it as a permanent move and update any stored id. It is absent — a plain
404 — when the boat never existed or when it was merged before this was recorded, so
branch on its presence rather than assuming it.