Get Boat
Get detailed information about a specific boat
Showing API v1.1 — versionsShowing API v1.0 — versions
/boats/:idRetrieve complete details for a single boat, including location, images, equipment, reviews, seasonal pricing, and calculated ratings.
You are reading API v1.1. Send api_version=1.1 to get it: a call with your API key, or to /api/ai/*, that names no version gets 1.0 — switch the picker at the top to v1.0 to read that shape.
You are reading API v1.0, what a call with your API key gets when it names no api_version. There is no list_price: price_per_day and best_price are the charter day rate and lowest_price_per_day the same, with our auto-applied offer already taken off and no fees inside. New integrations should send api_version=1.1 — see Versions.
Authentication
Send your API key in the X-API-Key header, as on every Operator API call. It reads only boats your company operates; another operator's boat is a 403 — sell those through the Discovery API. 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.
The price fields of a real v1.1 response (GET /boats/55467c1d-bfab-484f-b0c0-bb9352d4d46d?api_version=1.1):
The price fields of a real v1.0 response, the same boat with api_version=1.0 — best_price and lowest_price_per_day are in v1.0 only, list_price in v1.1 only:
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 is not part of it — including the operator's id, which was removed from this response on 2026-09-15: a boat never names the company that runs it.
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, gulet, 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 as stored, crew cabins included — the record an operator edits. Guest cabins are cabins − crew_cabins, the figure the boat page, the cabins filter and the AI and agent APIs state |
crew_cabins | integer|null | Number of crew cabins, counted in 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. null when the boat has none or when we have withheld it — a moderator marks a listing's description inaccurate or carrying personal details, and no rung of that ladder is then published |
description_ai | string|null | The generated description on its own, when there is one — null too whenever description is withheld |
pricing_source | string | Active pricing source: direct, nausys, or mmk |
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. For boats from Nausys it is Nausys's own figure, 0 or null where it names none (no minimum in that window). |
checkin_time | string|null | The boat's own check-in time (e.g. 17:00:00) — the hour the season in force states, or failing that the one its charter base states. null means nobody has stated one — it is not a default; the season's check_in_periods[].checkInTime answers first where one covers the date, and where neither does, show "TBD" rather than assuming an hour. |
checkout_time | string|null | The boat's own check-out time (e.g. 09:00:00), read the same way, with the same meaning for null. |
min_charter_days | integer|null | The platform's own yacht-level minimum, as sent; on Nausys boats, the lowest minimum among the check-in periods not yet ended (null when none states one). 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 |
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 |
skipper_for_hire | boolean | Whether a skipper is offered as a priced optional extra on a boat with no named crew: true = a live, non-zero, non-required skipper fee applies under the active pricing source and crew is null, so the charter price does not cover him and he is tickable in List Boat Fees. On a rental_type: "skippered" boat with skipper_charged: false this is what turns skippered into "optional". Derived from the fee set on every read; 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 |
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 |
images[].tag | object | null | The place the photo also shows, when the uploader named one: { href, label, icon } — icon is pin; label is the place, its town and its country (Lakka Osmos, Paxos, GR). null for most photos |
videos | array | [{ id, url, thumbnail_url, sort_order }], ordered by sort_order; usually empty. thumbnail_url is a poster frame grabbed from the clip at upload, null on a video uploaded before that existed — fall back to the boat's own hero image. The bucket key is internal and is not returned |
equipment | array | The boat's equipment in display sections: [{ group, items: [{ name, value, rare?, pct?, hidden?, standard? }] }]. 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; kit standard on the boat's model follows the same rule, and is listed even when the operator did not list it — standard: true marks such a line, which no equipment row of the boat names. 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_source | string | Where the boat's price comes from: mmk, nausys or direct |
best_currency | string | The currency every amount in this response is in: EUR |
price_currency | string | The currency the operator prices the boat in (EUR or USD); a guest charged in it pays the operator's own figure |
internal_discount_pct | number|null | Internal discount percentage when applicable |
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 |
Price fields in v1.1:
| Field | Type | Description |
|---|---|---|
price_per_day | number|null | The boat's dateless LIST figure: the lowest grand total at max guests — charter less the auto discount, every required fee charged now or at pickup, the 45 EUR platform fee; deposits never — per day for the boat. Null when the boat has no current pricing — a listing stays published on its specs, images and equipment even when its seasons lapse |
list_price | object|null | The row's ONE figure, the boat's stored lowest (from: true): per_day — the grand total at max guests (the charter less the boat's auto discount, every required fee charged now or at pickup, the 45 EUR platform fee; deposits never) of the default package over 7 nights (8 days on a day-clock boat) ÷ those days, per day for the boat, a whole euro rounded up; total — that week's grand total (dated only, null without a week; per_day × units may exceed it by up to units − 1 EUR, since only per_day is rounded up); original_per_day — the same week with the partner's discount undone and no coupon, per day (null without a discount); discount_pct — derived from per_day and original_per_day, null under 1%; from is true when no week was quoted and it is the boat's lowest; week, week_start, units, unit (night/day) say what it covers; party the max guests the fees are priced for; label the words beside the stay total (dated "for 7 nights · all fees in", dateless "7 nights · all fees in", "8 days" on a day-clock boat). The fees are inside it — there is no separate fee figure. null when nothing is priced |
Price fields in v1.0:
| Field | Type | Description |
|---|---|---|
price_per_day | number|null | The charter day rate, our auto-applied offer already taken off, no fees inside. Null when the boat has no current pricing |
best_price | number|null | Same value as price_per_day; null when the boat has no current pricing |
lowest_price_per_day | number|null | Lowest effective per-day price after any internal discount; null when the boat has no current pricing |
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 v1.0's 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.
The other way round is skipper_for_hire: a crewed product that bills no obligatory
skipper but lists him as a priced optional extra (The Moorings' "Powered" cats:
"Skipper - HR (subject to availability)", 280 a day) does not include him either. Such a
boat reports skippered: "optional" and skipper_for_hire: true — unless the operator
names a crew (crew non-null), in which case the priced row is a surcharge beside a crew
that is aboard anyway and the boat stays true. The list endpoints derive the same fact
from the same fee set, so a list row and its detail report one word.
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 operator's cancellation policy; null when none applies. A boat sold directly on charter.boats (not through MMK or Nausys) is always null: no refund after the first 24 hours from payment |
cancel_refund_days | number|null | Days before charter that the refund window applies; null when no policy applies, as above |
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.
A 404 for a listing that is no longer published may instead carry data.suggest: the
search terms for the boat, never the boat. It is what you would type to look for one
like it, and it is deliberately not a redirect — a hull withdrawn by one operator is often
listed by another, but matching on a name asserts an identity we cannot prove (Merry
Fisher is a model, so two different boats carry it).
It appears only for hulls listed through a booking partner, whose name, model and base
that partner publishes itself. A listing an owner manages directly answers a bare 404
whether it exists or not, so suggest is never a way to test whether an unpublished
listing is there.
Every field is optional and any of them may be missing (a listing with no usable name
carries no name; one with no manufacturer carries neither make_slug nor model_label;
and a model label containing a comma is dropped, since that is the separator makes=
uses). Feed them to GET /api/boats as
q=<name>&makes=<make_slug>:<model_label>, falling back to
makes=<make_slug>:<model_label>&location_id=<location_id> when that is empty. Nothing
identifying is in it — no boat id, no operator, no price — and the response is still a
404, so do not treat it as a row.