Charter Boats
Charter Boats API
AI Integrations

Search Locations (AI)

Search sailing destinations by name, or rank an area's locations by real on-the-water popularity

GET/ai/locations

Search 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 — so q=split also 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, or city without q and 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

ParameterTypeRequiredDescription
qstringNo*Name search (minimum 2 characters). Marina name, city, island, or country.
regionstringNo*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.
countrystringNo*Country name or 2-letter code
citystringNo*City name
typestringNomarina | 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
monthintegerNo1–12 — the month popularity is ranked for (default: current month)
sortstringNopopularity (default) | boats | rating
fromstringNo*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_nmintegerNoHow 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
limitintegerNoMax results (default: 5, max: 20)

*At least one of q, region, country, city, or from is required.

Request

curl "https://charter.boats/api/ai/locations?q=split&limit=3"
 
# Most popular spots in a region for a given month
curl "https://charter.boats/api/ai/locations?region=Dalmatia&month=8&limit=5"

Response

{
  "locations": [
    {
      "id": 20804,
      "name": "Marina Trogir (SCT)",
      "city": "Grad Trogir",
      "country": "Croatia",
      "type": "marina",
      "lat": 43.51342,
      "lon": 16.24422,
      "boat_count": 181,
      "rating": 4.4,
      "rating_count": 155,
      "busyness": "busy",
      "url": "https://charter.boats/locations/marina-trogir-sct"
    },
    {
      "id": 12132,
      "name": "Marina Kaštela",
      "city": "Grad Kaštela",
      "country": "Croatia",
      "type": "marina",
      "lat": 43.5455422,
      "lon": 16.4032056,
      "boat_count": 307,
      "rating": 2.7,
      "rating_count": 15,
      "busyness": "packed",
      "url": "https://charter.boats/locations/marina-kastela"
    }
  ],
  "ranking_note": "Ordered by observed vessel traffic (AIS) in September; locations without traffic coverage rank after covered ones by fleet size. \"busyness\" compares a location to its own peak month only."
}

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.

curl "https://charter.boats/api/ai/locations?from=aci-marina-split&within_nm=20&limit=2"
{
  "locations": [
    {
      "id": 24394,
      "name": "Zavraće Cove",
      "city": "Općina Milna",
      "country": null,
      "type": "spot",
      "lat": 43.32579,
      "lon": 16.425877,
      "estimated_nm": 10.7,
      "hours": 1.8,
      "boats_observed": 111,
      "trips_observed": 237,
      "boat_count": 0,
      "busyness": null,
      "url": "https://charter.boats/locations/uvala-zavrace"
    }
  ],
  "reachable_from": { "id": 12908, "name": "ACI Marina Split", "url": "https://charter.boats/locations/aci-marina-split" },
  "within_nm": 20,
  "basis": "observed",
  "total_within_ring": 99,
  "legs_with_time": 99,
  "ranking_note": "Places boats sail to from ACI Marina Split within 20nm, most-travelled first. A full 20nm leg is about 3.3h. Hours are the estimated distance at 6 knots; …"
}

Reachability rows carry no rating or rating_count, and type applies to them as a filter on the returned places.

Response Fields

FieldTypeDescription
locationsarrayMatching locations, in ranking order
locations[].idnumberLocation ID
locations[].namestringLocation 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[].citystring|nullCity
locations[].countrystring|nullCountry
locations[].typestringmarina | harbour | anchorage | mooring | bay | spot. spot is an unclassified stop — a beach, spit or headland — never a marina
locations[].latnumberLatitude
locations[].lonnumberLongitude
locations[].boat_countintegerNumber of published boats at this location
locations[].ratingnumber|nullGoogle rating (name and area searches only)
locations[].rating_countnumber|nullGoogle review count (name and area searches only)
locations[].busynessstring|nullTraffic 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[].urlstringLink to the location page on charter.boats, by slug (https://charter.boats/locations/marina-kastela)
ranking_notestringPresent on popularity sorts and on every reachability response — explains the ranking basis
region_matchedstringPresent when region resolved — the canonical region name
region_unresolvedstringPresent when region named an area we don't cover; locations is empty and note explains
no_resultsobjectPresent 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
notestringPresent alongside no_results — a one-sentence rendering of it, generated from it so the two never disagree
reachable_fromobjectReachability mode only — the origin that was resolved: id, name, url
within_nmintegerRing actually used
within_nm_requestedintegerPresent only when the request was snapped to a ring
basisstringWhat 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_ringintegerHow 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_timeintegerHow many carry a distance, and therefore a time
locations[].estimated_nmnumber|nullEstimated distance (haversine ×1.3 for most pairs) — never a charted distance
locations[].hoursnumber|nullestimated_nm ÷ 6 knots — a planning figure inheriting the distance's uncertainty, not a recorded duration
locations[].boats_observed / trips_observedinteger|nullHow much evidence sits behind the leg. null on a place added only because it is near
from_unresolvedstringPresent when from matched nothing; locations is empty and note explains

Errors

StatusMessage
400Provide q (min 2 chars), an area filter (region / country / city), or from (sail from a place)
400`from` must be a location id or slugfrom was not an id or slug shape

Location Details

GET/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).

curl "https://charter.boats/api/ai/locations/aci-marina-split"

Response

{
  "id": 12908,
  "name": "ACI Marina Split",
  "city": "Split",
  "municipality": "Grad Split",
  "country": "Croatia",
  "type": "marina",
  "description": "ACI Marina Split sits in the **southwest corner of Split Harbour**, just north of the Sustipan Peninsula...",
  "lat": 43.5026218,
  "lon": 16.4307491,
  "capacity": null,
  "max_draft": null,
  "max_length": null,
  "vhf_channel": null,
  "phone": "+385 21 398 599",
  "website": "https://www.aci-marinas.com/aci_marina/aci-split/",
  "rating": 3.2,
  "rating_count": 72,
  "boat_count": 276,
  "from_price_per_day": 70.38,
  "amenities": [],
  "poi_categories": [
    "bank", "fuel", "water", "fishing", "grocery", "laundry", "medical",
    "parking", "worship", "pharmacy", "chandlery", "restaurant", "provisioning", "entertainment"
  ],
  "poi_count": 1267,
  "busyness": "packed",
  "busyness_by_month": [
    { "month": 1, "busyness": "moderate" },
    { "month": 2, "busyness": "moderate" },
    { "month": 3, "busyness": "moderate" }
  ],
  "busyness_note": "Busyness compares this place to its own peak month only, from observed vessel traffic (AIS) — it does not compare one place to another.",
  "behaviour": {
    "kind": "overnight",
    "overnight_pct": 91,
    "median_stay_hours": 24.97,
    "busiest_hour": 9,
    "four_fifths_full_by_hour": 17,
    "boats_seen": 1185,
    "stays_recorded": 3608,
    "quiet_alternative": false,
    "observed_from": "2021-08-09",
    "observed_to": "2026-08-25"
  },
  "sails_to": [
    {
      "id": 24394, "name": "Zavraće Cove", "type": "spot",
      "hours": 1.8, "estimated_nm": 10.7,
      "boats_observed": 111, "trips_observed": 237, "boat_count": 0,
      "url": "https://charter.boats/locations/uvala-zavrace"
    },
    {
      "id": 23224, "name": "ACI Marina Palmižana", "type": "marina",
      "hours": 3.4, "estimated_nm": 20.5,
      "boats_observed": 87, "trips_observed": 155, "boat_count": 0,
      "url": "https://charter.boats/locations/aci-marina-palmizana"
    }
  ],
  "sails_to_basis": "observed",
  "nearest_bookable_base": null,
  "url": "https://charter.boats/locations/aci-marina-split"
}

Response Fields

FieldTypeDescription
idnumberLocation ID
namestringLocation name
citystring|nullCity
municipalitystring|nullMunicipality
countrystring|nullCountry
typestringmarina | harbour | anchorage | mooring | bay | spot. spot is an unclassified stop — a beach, spit or headland — never a marina
descriptionstring|nullDescription, cut at 500 characters. May contain Markdown emphasis (**…**, *…*)
latnumberLatitude
lonnumberLongitude
capacitynumber|nullBerth capacity
max_draftnumber|nullMaximum draft in metres
max_lengthnumber|nullMaximum LOA in metres
vhf_channelstring|nullMarina VHF channel
phonestring|nullContact phone
websitestring|nullWebsite URL
ratingnumber|nullAverage rating
rating_countinteger|nullNumber of ratings
boat_countintegerTotal published boats at this location — not a page of them
from_price_per_daynumber|nullCheapest bookable day rate here; null where nothing is priced
amenitiesarrayList of amenity names
poi_categoriesarrayEvery 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_countintegerNearby points of interest — the real total. It was previously capped at 200, which reported 200 for a place holding 411.
busynessstring|nullTraffic 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_montharray|nullAll twelve months as { month, busyness }, month 1 = January
busyness_notestringPresent only when busyness is — restates that the tier is self-relative
behaviourobject|nullRecorded 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_toarrayWhere boats were recorded sailing from here, most-travelled first
sails_to[].hoursnumber|nullPassage 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_nmnumber|nullEstimated distance (haversine ×1.3 for most pairs) — an estimate, not a charted distance
sails_to[].boats_observed / trips_observedinteger|nullHow much evidence sits behind the leg
sails_to_basisstring|nullobserved = boats were tracked making these passages. Anything else means merely nearby
nearest_bookable_baseobject|nullFor 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
urlstringLink to the location page on charter.boats, by slug

Errors

StatusMessage
400Location ID required
404Location not found

On this page