Quickstart API reference Building a harness ⌘ view as markdown

API reference

Base URLhttps://api.topoloop.com
AuthX-API-Key: tl_live_… header on every request — get a key
FormatJSON in, JSON out (Content-Type: application/json)
EndpointsPOST /places/search · POST /places/nearest · POST /places/details · POST /web/search · POST /web/page

Authentication

Send your key as the X-API-Key header. Missing or invalid keys return:

// HTTP 401
{
  "error": "unauthorized",
  "message": "Missing or invalid API key. Send it as the X-API-Key header. Get one at https://dash.topoloop.com"
}

Keys are managed at dash.topoloop.com (create up to 5, revoke any time — revocation takes effect within ~15 seconds).

Rate limits & quotas

TierRateMonthly quota
Free2 requests/s, burst 1010,000 requests

Exceeding either returns HTTP 429. The burst allowance means a small agent fan-out (several parallel tool calls) won't instantly 429; sustained traffic should stay under the per-second rate. Usage is visible per-key in the dashboard.

Semantic + lexical place search near a coordinate. This is the agent-facing workhorse: approximate names, translations, misspellings, and category-flavored queries ("ramen near the station") all resolve. Results are ranked by a learned reranker over semantic similarity, lexical match, distance, and name overlap — not by distance alone: an exact name match 2 km away outranks a vaguely-related place 100 m away.

Request

{
  "query": "kinkakuji golden pavilion",
  "lat": 35.03,
  "lng": 135.72,
  "limit": 5
}
FieldTypeRequiredDescription
querystringyesNatural-language place query (min 1 char).
latfloatyesLatitude of the search center (−90..90). Use 3–4 decimals of precision — 1 decimal (~11 km) is too coarse and pulls the search to the wrong neighborhood.
lngfloatyesLongitude of the search center (−180..180).
radiusfloatnoSearch radius in km (0–500). Omit for a dynamic radius picked from local landmark density: ~5 km in dense cities up to ~100 km in remote areas. Values below 1 clamp to 1.
limitintnoMax results (1–100, default 10). For agent context, 3–5 is usually plenty.
include_adminboolnoInclude administrative-boundary entities (default false). A geocoding agent almost never wants the boundary polygon's centroid; turn on only for gazetteer/region-resolution callers.

Response

{
  "results": [
    {
      "osm_id": "3402555795",
      "osm_type": "node",
      "name": "金閣寺",
      "name_en": "Kinkaku-ji",
      "category": "buddhist place of worship",
      "lat": 35.0394,
      "lng": 135.7292,
      "distance_meters": 1230.4,
      "wikipedia": null,
      "wikidata": null
    }
  ],
  "count": 5,
  "radius_km": 5.0
}
FieldTypeDescription
resultsarrayMatching places, best first.
countintNumber of results returned.
radius_kmfloatThe radius actually used — when radius was omitted, this is the dynamically chosen one.

Place object:

FieldTypeDescription
osm_idstringOpenStreetMap object ID.
osm_typestringnode, way, or relation. Join osm_type[0].upper() + osm_id (e.g. "N3402555795") to get the ID that /places/details accepts.
namestringPrimary name (typically the local language).
name_enstring | nullEnglish name when it exists and differs from name.
categorystring | nullHuman-readable category ("cafe", "mountain peak", "railway station", …).
lat, lngfloatCoordinates.
distance_metersfloatDistance from the search center.
wikipedia, wikidatanullAlways null in search responses — fetch via /places/details.

POST /places/nearest

The N most relevant places around a coordinate — no query. Ranking blends distance with fame (a normalized encyclopedia + travel-writing signal), so a famous landmark outranks equally-near trivia. Use it for "what's around here" grounding: give your agent spatial awareness of a location it's working in.

Request

{
  "lat": 35.71, "lng": 139.796,
  "limit": 10,
  "max_radius_km": 5
}
FieldTypeRequiredDescription
lat, lngfloatyesThe query point.
limitintnoPlaces to return (1–100, default 10).
max_radius_kmfloatnoHard distance cap in km (0–500, default 50).
prominence_weightfloatnoFame blend strength (0–10, default 2.0). Candidates are ranked by distance / (1 + prominence_weight × fame). 0 = pure nearest-by-distance.
prominent_slotsintnoGuaranteed spots for the most famous places within the radius (0–20, default 3), regardless of distance. This is what surfaces the landmark temple from a side street when the blend alone is crowded out by very-close trivia. 0 disables.
include_adminboolnoInclude administrative boundaries (default false).

Response

Same shape as /places/search, ordered by blended effective distance. Each result additionally carries fame (0..1 normalized score — the ranking signal) and prom_pct (prominence percentile) when the place has any signal. radius_km is the distance to the furthest returned place.

POST /places/details

OSM tag details for up to 50 places by ID. Use it to enrich search results — /places/search intentionally omits these fields to keep agent context lean.

Request

{ "osm_ids": ["N6960525765", "W12903132"] }
FieldTypeRequiredDescription
osm_idsstring[]yes1–50 IDs in N/W/R + digits format — the joined form of osm_type + osm_id from search results.

Response

An object keyed by the requested IDs. IDs that don't exist are simply absent. Only fields present in the data are included per place:

{
  "W12903132": {
    "name": "Space Needle",
    "name_en": "Space Needle",
    "website": "https://www.spaceneedle.com/",
    "phone": "+1-206-905-2100",
    "opening_hours": "Mo-Th 09:00-22:00; Fr-Su 08:00-00:00",
    "wikipedia": "en:Space Needle",
    "wikidata": "Q5317",
    "wheelchair": "yes",
    "fee": "yes",
    "_sources": {"website": "osm", "phone": "osm"}
  }
}

Possible fields: name, name_en, website, phone, email, opening_hours, wikipedia, wikidata, cuisine, wheelchair, fee, stars, description, image, address, socials, plus extra_websites / extra_phones / extra_emails arrays when multiple values exist.

opening_hours uses OSM opening-hours syntax (Mo-Th 09:00-22:00; Fr-Su 08:00-00:00) — compact, machine-parseable, and LLMs read it natively.

Contact fields fall back to the Overture places dataset when OSM lacks them; _sources records where each field came from ("osm" or "overture").

Errors: HTTP 400 with code INVALID_OSM_ID if any ID isn't N/W/R + digits.

Search a curated corpus of travel blogs and trip reports — the written evidence behind places. Same key, same rate limit and quota as the places endpoints; each call counts as one request. Use it when the agent needs why / how (which hut to book, whether the trail is open in May, what a day in Chamonix actually looks like), and /places/* when it needs where.

Request

{ "query": "best day hikes in the Dolomites", "limit": 3 }
FieldTypeRequiredDescription
querystringyesNatural-language query. Hybrid semantic + BM25 retrieval, so both concept phrasing and exact place names work.
limitintnoMax results (1–20, default 5).

Response

{
  "query": "best day hikes in the Dolomites",
  "limit": 3,
  "count": 3,
  "results": [
    {
      "title": "29 Best Day Hikes In The Dolomites Rated From Easy To Hard",
      "url": "https://inafarawayland.com/top-day-hikes-in-the-italian-dolomites",
      "blog": "inafarawayland.com",
      "score": 0.812,
      "context": "One-line summary of what this passage covers",
      "content": "The matched passage, truncated to ~500 characters…",
      "semantic_score": 0.741,
      "bm25_score": 18.52
    }
  ]
}

content is the retrieved chunk, not the whole article — cite the url and feed the passage to the model. blog is the source domain for attribution.

POST /web/page

Full text of one article from the corpus by URL (the url of a /web/search result). Only URLs already in the corpus resolve — this is not a live fetcher.

Request

{ "url": "https://inafarawayland.com/top-day-hikes-in-the-italian-dolomites" }

Response

{ "url": "…", "title": "…", "blog": "inafarawayland.com", "content": "…", "content_length": 12345 }

Errors: HTTP 404 if the URL is not in the corpus.

Errors

StatusMeaning
400Invalid parameters (JSON body includes error, message, code).
401Missing or invalid API key.
422Validation error (field out of range — body lists the offending fields).
404Unknown route under /web/, or /web/page URL not in the corpus.
429Rate limit (2 r/s burst 10) or monthly quota exceeded.
5xxServer error — safe to retry with backoff.

In an agent loop, prefer catching errors in your dispatch layer and returning an empty result list (optionally with a reason) to the model, rather than surfacing raw HTTP errors — models handle "no results found" far more gracefully than stack traces. See Building a places harness.

Data licensing

Web results (/web/*) return short excerpts of third-party blog posts with their source URL; link back and attribute the source blog. The excerpts are for retrieval and citation, not republication.

Place data © OpenStreetMap contributors, available under the Open Database License (ODbL). You may cache, store, and train on API responses. Attribute "© OpenStreetMap contributors" where you use the data. Contact fields marked "overture" in _sources come from the Overture Maps Foundation dataset (CDLA-Permissive-2.0).