Search POIs
Find points of interest near a location or coordinates
Endpoint
Description
Search for points of interest (restaurants, shops, fuel stations, etc.) near a location. Supports three search modes: by location ID, by coordinates with radius, or by geographic region (country, state, city, or a free-text place such as a sailing region). The three modes return slightly different row shapes — see POI Fields.
Car parks are de-duplicated. OpenStreetMap draws one marina car park as a polygon per aisle, and a second mapper often adds points on top of the same lot — D-Marin Lefkas arrived as 21 rows all reading "Parking". A location returns one entry per car park you could tell from the next: per distinct name, per distinct capacity where there is no name, and never two within 60 m. A car park with neither a name nor a capacity is returned only where it is the nearest one and nothing else at that location speaks for parking.
Authentication
Send X-API-Key: YOUR_API_KEY on every call from outside charter.boats — without it Cloudflare bot protection answers with an HTML 403 page, not JSON. Call it from your server: this route sends no CORS headers. See Authentication.
Query Parameters
Use one of the three search modes. Every search must be anchored to a place — a location ID, a coordinate pair, or a region. There is no mode that searches all POIs everywhere: q narrows results within whichever mode you are in — all three — and is not a search mode on its own. A request with no anchor returns 400. If several anchors are sent, region mode wins, then location ID, then coordinates.
minRating is accepted and ignored — POIs carry no ratings.
Mode 1: By Location ID
| Parameter | Type | Required | Description |
|---|---|---|---|
location_id | number | Yes | Location ID to find nearby POIs. An unknown or unpublished location returns 404 |
category | string | No | Filter by POI category |
q | string | No | Filter by name (matches name or name_en). Matched against the stored name, so a POI that has no name of its own is never returned by q — see the note under name below |
limit | number | No | Max results (default: 50, max: 200) |
Results are the POIs linked to that location, nearest first. category and q are applied in the query, so limit means "this many restaurants called Konoba", not "this many of the nearest POIs, then narrowed".
Mode 2: By Coordinates
| Parameter | Type | Required | Description |
|---|---|---|---|
lat | number | Yes | Latitude |
lon | number | Yes | Longitude |
radius | number | No | Search radius in km (default: 1, max: 10) |
category | string | No | Filter by POI category |
q | string | No | Filter by name (name or name_en). Applied to the whole radius, not to the page: with q the search reads up to 500 candidates in distance order and the name filter decides which limit come back, so a match further out than the nearest limit is still returned. A radius holding more than 500 POIs is the one case where a distant match can be missed — narrow radius there |
limit | number | No | Max results (default: 50, max: 200) |
Results are ordered by distance from the point.
Mode 3: By Region
| Parameter | Type | Required | Description |
|---|---|---|---|
location_type | string | Yes | One of: country, state, city, municipality, or text — a place no column holds, such as a sailing region (Dalmatia, the Dodecanese) or a free-text place name, resolved the same way boat search resolves it. A text value that resolves to nothing returns an empty list |
location_value | string | Yes | Region name or country code (or the free text for text) |
country_code | string | No | ISO country code (narrows state/city/municipality results) |
category | string | No | Filter by POI category |
q | string | No | Filter by name (matches name or name_en). Matched against the stored name, so a POI that has no name of its own is never returned by q — see the note under name below. |
limit | number | No | Max results (default: 50, max: 200) |
offset | number | No | Pagination offset (default: 0, max: 5000). Beyond that the request is rejected rather than clamped, so a caller paging to the end never silently re-reads earlier pages. Narrow with category or a smaller region instead of paging deeper. Results are ordered by POI id, so pages are stable. |
Example Requests
By Location
By Coordinates
By Country
By Sailing Region
Example Response (Location Mode)
Example Response (Coordinate Mode)
Here distance_km is from the point you queried, and distance_meters is from the POI to its nearest charter location (location_name) — not from your point.
Example Response (Region Mode)
The total field is the size of the returned page, not the global count.
Response Fields
| Field | Type | Description |
|---|---|---|
pois | array | List of POIs |
total | number | Number of POIs in this response (page size, not a global count) |
hasMore | boolean | Whether more results exist (region mode with pagination) |
location_id | number | Echoed location ID (location mode) |
radius_km | number | Echoed search radius (coordinate mode) |
location_type | string | Echoed location type (region mode) |
location_value | string | Echoed location value (region mode) |
POI Fields
Location and region modes return the full row below. Coordinate mode returns a reduced row: poi_id (there is no id), name, name_en, category, subcategory, lat, lon, distance_km, phone, website, opening_hours, description, image_url, location_name and distance_meters — no osm_id, address, city, country, is_seasonal, cuisine, image_attribution, nearest_location_name or walking_time_minutes. Key on poi_id, which every mode returns.
| Field | Type | Description |
|---|---|---|
id | string | POI ID (UUID). Location and region modes |
poi_id | string | POI ID (UUID) — present in every mode |
osm_id | number|null | OpenStreetMap ID |
name | string | POI name (local language), or the POI's type where OSM gave it no name — "Parking", "Pharmacy", "Supermarket". Never null. Roughly 9% of POIs are unnamed car parks, pharmacies and grocery shops that are still worth listing near a marina; they appear in location results and are excluded from q name search. Use category + subcategory if you want to render your own label. |
name_en | string|null | English name |
category | string | POI category |
subcategory | string|null | More specific category |
lat | number | Latitude |
lon | number | Longitude |
address | string|null | Street address |
city | string|null | City |
country | string|null | Country |
phone | string|null | Phone number |
website | string|null | Website URL |
opening_hours | string|null | Opening hours (OSM format) |
is_seasonal | boolean|null | Whether the business is seasonal |
description | string|null | Description (markdown). Location and coordinate modes only. Region mode omits it: descriptions are long text stored out-of-line, they were 30% of that response's bytes across a 100-row page, and no caller rendered them in a list. Fetch a single POI (/pois/{id}) for its description. |
cuisine | string|null | Cuisine type (restaurants only) |
image_url | string|null | Image URL. Our own copies are served from https://media.charter.boats/; if you receive one on another storage host, request the same path from media.charter.boats. Third-party image URLs are returned as-is |
image_attribution | string|null | Image attribution text |
is_active | boolean | Active flag (present in region mode only) |
nearest_location_name | string|null | Name of the nearest charter location (location and region modes) |
location_name | string|null | Name of the POI's nearest charter location (every mode) |
distance_meters | number|null | Straight-line distance in metres. Location mode: from the requested location. Region mode: from the POI's nearest location, computed from both current coordinates. Coordinate mode: from the POI to its nearest location (location_name) — not from the queried point; use distance_km for that |
distance_km | number | Distance from the queried point, in km (coordinate mode only) |
walking_time_minutes | number|null | Estimated walking time (location mode only) |
Error Responses
400 Bad Request
Also returned for:
- an invalid
categoryvalue (Invalid category: <value>) - an
offsetabove 5000
Earlier builds accepted a bare q with no location and answered it by searching every POI.
That was never a documented mode — all three modes above have always listed their anchor as
required — and it is now rejected. To search POIs by name across places, use
/ai/pois, which is built for it.