Search Locations (AI)
Search sailing destinations by name, or rank an area's locations by real on-the-water popularity
/ai/locationsSearch for sailing locations — marinas, harbours, and anchorages where a boat can berth or drop anchor and stay overnight (charter bases are locations too). Two search modes, freely combinable:
- Name search (
q): normalized matching that handles diacritics and special characters, against the place's name and the town and country text around it — soq=splitalso returns marinas in the Split area, and a multi-word query matches when every word is present. Places with no name are not searchable by name. - Area listing: give
region,country, orcitywithoutqand get the area's locations.
Either way, results are ranked by popularity unless you ask for another sort — observed vessel traffic from EMODnet AIS climatology for the requested month. Locations without traffic coverage rank after covered ones, by fleet size. A name search is therefore not ordered by how closely the name matches: q=split leads with the busiest marinas in the Split area, not the closest name match.
A third mode, reachability (from), lists the places boats sail to from a given location — see from below.
Authentication
No authentication required. A key is optional — a valid API key records the call against your account; an invalid one 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 |
|---|---|---|---|
q | string | No* | Name search (minimum 2 characters). Marina name, city, island, or country. |
region | string | No* | Sailing region as a guest says it: Dalmatia, Istria, Kvarner, the Ionian, the Saronic, the Cyclades, the Dodecanese, the Sporades. An unknown region returns an empty list with a note — never a worldwide result. |
country | string | No* | Country name or 2-letter code |
city | string | No* | City name |
type | string | No | marina | harbour | anchorage | bay | mooring | spot — the same vocabulary locations[].type carries back, so a type read off a result can be sent straight back as a filter. spot is an unclassified stop (beach, spit, headland); a place with no stored category answers type=spot and never type=marina |
month | integer | No | 1–12 — the month popularity is ranked for (default: current month) |
sort | string | No | popularity (default) | boats | rating |
from | string | No* | Origin location id or slug — switches the endpoint into reachability mode: places boats were recorded sailing to from there, most-travelled first. Anything that is not an id or slug shape (spaces, for instance) is a 400; a well-formed id or slug that matches no published location returns an empty list with from_unresolved, never a worldwide result |
within_nm | integer | No | How far, for a from search. One of 10, 20, 35, 55, 100 (default 35); anything else snaps up to the next ring — anything over 100 to 100 — and is echoed as within_nm_requested. A charter boat makes about 6 knots, so these are ~1.7h, ~3.3h, ~5.8h, ~9.2h and ~16.7h |
limit | integer | No | Max results (default: 5, max: 20) |
*At least one of q, region, country, city, or from is required.
Request
Response
Reachability
from answers "where can I sail from here". Places come from, in order: passages boats were actually recorded making from the origin, then computed sea-route neighbours, then — only when those give fewer places than limit — the nearest places inside the ring. basis names what produced the bulk of the list, so an observed list can still end with a few merely-nearby places; those rows carry boats_observed and trips_observed as null and sort after every observed leg.
Reachability rows carry no rating or rating_count, and type applies to them as a filter on the returned places.
Response Fields
| Field | Type | Description |
|---|---|---|
locations | array | Matching locations, in ranking order |
locations[].id | number | Location ID |
locations[].name | string | Location name — the English form where we hold one (Zavraće Cove for Uvala Zavraće). A place with no name of its own shows its type instead (e.g. "Anchorage") — never null |
locations[].city | string|null | City |
locations[].country | string|null | Country |
locations[].type | string | marina | harbour | anchorage | mooring | bay | spot. spot is an unclassified stop — a beach, spit or headland — never a marina |
locations[].lat | number | Latitude |
locations[].lon | number | Longitude |
locations[].boat_count | integer | Number of published boats at this location |
locations[].rating | number|null | Google rating (name and area searches only) |
locations[].rating_count | number|null | Google review count (name and area searches only) |
locations[].busyness | string|null | Traffic tier for the requested month, relative to this location's own peak: very-quiet | quiet | moderate | busy | packed. null without AIS coverage — absolute counts are never exposed |
locations[].url | string | Link to the location page on charter.boats, by slug (https://charter.boats/locations/marina-kastela) |
ranking_note | string | Present on popularity sorts and on every reachability response — explains the ranking basis |
region_matched | string | Present when region resolved — the canonical region name |
region_unresolved | string | Present when region named an area we don't cover; locations is empty and note explains |
no_results | object | Present when a name or area search, an unresolved region or from, or a from ring holding nothing, comes back empty — never alongside results. { searched, why, try_next[] } — what was actually searched (the name and every area/type filter as applied), why it came back empty, and the concrete calls to try next |
note | string | Present alongside no_results — a one-sentence rendering of it, generated from it so the two never disagree |
reachable_from | object | Reachability mode only — the origin that was resolved: id, name, url |
within_nm | integer | Ring actually used |
within_nm_requested | integer | Present only when the request was snapped to a ring |
basis | string | What produced the bulk of the list: observed = boats were tracked making these passages, searoute = computed sea-route neighbours, nearby = straight-line proximity, none = nothing found. Check each row's trips_observed for the evidence behind that row |
total_within_ring | integer | How many places fell inside the ring before limit. 0 comes with no_results — the ring is empty, or type emptied it — never a bare [] |
legs_with_time | integer | How many carry a distance, and therefore a time |
locations[].estimated_nm | number|null | Estimated distance (haversine ×1.3 for most pairs) — never a charted distance |
locations[].hours | number|null | estimated_nm ÷ 6 knots — a planning figure inheriting the distance's uncertainty, not a recorded duration |
locations[].boats_observed / trips_observed | integer|null | How much evidence sits behind the leg. null on a place added only because it is near |
from_unresolved | string | Present when from matched nothing; locations is empty and note explains |
Errors
| Status | Message |
|---|---|
400 | Provide q (min 2 chars), an area filter (region / country / city), or from (sail from a place) |
400 | `from` must be a location id or slug — from was not an id or slug shape |
Location Details
/ai/locations/{id}Full detail for a single location: description, coordinates, facilities, rating and a summary of nearby points of interest — plus what tracked AIS traffic shows about the place, which is the part that exists nowhere else.
Three separate observed layers ride on this response and they come from different data, so they appear and disappear independently:
busyness/busyness_by_month— EMODnet vessel-density climatology (locations.ais_hours). Answers when is it busy here.behaviour— recorded stays from the Datalastic archive. Answers how boats use it: overnight or a lunch stop, how long they stay, what time it fills.sails_to— recorded passages. Answers where boats go from here, with measured door-to-door times.
Each is null (or empty) below its own evidence gate. A place we have barely watched simply says nothing rather than guessing.
Request
The {id} accepts the numeric id or the slug (aci-marina-split).
Response
Response Fields
| Field | Type | Description |
|---|---|---|
id | number | Location ID |
name | string | Location name |
city | string|null | City |
municipality | string|null | Municipality |
country | string|null | Country |
type | string | marina | harbour | anchorage | mooring | bay | spot. spot is an unclassified stop — a beach, spit or headland — never a marina |
description | string|null | Description, cut at 500 characters. May contain Markdown emphasis (**…**, *…*) |
lat | number | Latitude |
lon | number | Longitude |
capacity | number|null | Berth capacity |
max_draft | number|null | Maximum draft in metres |
max_length | number|null | Maximum LOA in metres |
vhf_channel | string|null | Marina VHF channel |
phone | string|null | Contact phone |
website | string|null | Website URL |
rating | number|null | Average rating |
rating_count | integer|null | Number of ratings |
boat_count | integer | Total published boats at this location — not a page of them |
from_price_per_day | number|null | Cheapest bookable day rate here; null where nothing is priced |
amenities | array | List of amenity names |
poi_categories | array | Every POI category present near this location. Derived per category rather than from a distance-ordered sample — a sample of the nearest N misses a category that exists but sits further out, which previously reported a yacht marina as having no chandlery. |
poi_count | integer | Nearby points of interest — the real total. It was previously capped at 200, which reported 200 for a place holding 411. |
busyness | string|null | Traffic tier for the current month, relative to this place's own peak month: very-quiet | quiet | moderate | busy | packed. Same vocabulary as the list response, so the two can never describe one place two ways |
busyness_by_month | array|null | All twelve months as { month, busyness }, month 1 = January |
busyness_note | string | Present only when busyness is — restates that the tier is self-relative |
behaviour | object|null | Recorded stays: kind (overnight | day_stop | mixed), overnight_pct, median_stay_hours, busiest_hour (local hour with the most arrivals and departures), four_fifths_full_by_hour, boats_seen, stays_recorded, quiet_alternative, and the observation window as full dates, observed_from / observed_to (YYYY-MM-DD) |
sails_to | array | Where boats were recorded sailing from here, most-travelled first |
sails_to[].hours | number|null | Passage time, as estimated_nm ÷ 6 knots — the one distance→time rule every AI surface uses. A planning figure inheriting the distance's uncertainty, not a recorded duration. null only where we hold no distance |
sails_to[].estimated_nm | number|null | Estimated distance (haversine ×1.3 for most pairs) — an estimate, not a charted distance |
sails_to[].boats_observed / trips_observed | integer|null | How much evidence sits behind the leg |
sails_to_basis | string|null | observed = boats were tracked making these passages. Anything else means merely nearby |
nearest_bookable_base | object|null | For a place with no fleet of its own — only 621 of 56,104 locations have boats — the nearest one that does: id, name, distance_nm, boat_count, url |
url | string | Link to the location page on charter.boats, by slug |
Errors
| Status | Message |
|---|---|
400 | Location ID required |
404 | Location not found |