API Reference
The GeoContext API gives your AI agent a sense of place — ranked semantic POI results, neighbourhood profiles, and timezone in a single call.
job_id (HTTP 202). Poll GET /jobs/{job_id} for the result.
transit,
streetview, and weather are always null. The
neighborhood and map enrichments are available.
Authentication
Every public endpoint (except GET /health) requires a Bearer token:
Authorization: Bearer <your_api_token>
| Status | Detail |
|---|---|
| 401 | Authorization: Bearer <token> required — header missing or not Bearer … |
| 403 | invalid token — well-formed header, unknown token |
Async Job Pattern
All submit endpoints return a job_id immediately. The request is processed asynchronously by the API backend. Client polls until the result is ready.
Submit
POST your request. Receive job_id in a 202 response instantly.
Poll
GET /jobs/{id} every second. Status moves from queued → processing.
Result
Status becomes complete. Full result in the same response body.
Job statuses
| Status | Meaning |
|---|---|
| queued | Accepted, awaiting processing |
| processing | Being processed by the API backend |
| complete | Result available in result field |
| error | Execution failed; see error field |
Recommended polling strategy
1. Submit → receive job_id
2. Wait 500 ms
3. Poll GET /jobs/{job_id} every 1 s
4. Stop after 120 s (service may be unavailable)
POST /forward
Request body
{
"query": "wheelchair-accessible Italian restaurant",
"lat": 52.5200,
"lon": 13.4050,
"radius_m": 1000,
"max_results": 10,
"neighborhood": false,
"map": false
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| query | string | yes | — | Natural language search query (1–500 chars) |
| lat | float | recommended | — | Latitude (-90 to 90). If omitted, the location is resolved from the query text. |
| lon | float | recommended | — | Longitude (-180 to 180). Pair with lat. |
| region | string | no | null | Free-text geographic context appended before geocoding to disambiguate place names, e.g. "New York, USA" so "Central Park" resolves to Manhattan, not Buffalo. Only applies when geocoding from the query; ignored when lat/lon are given. |
| viewbox | float[4] | no | null | Bias geocoding toward a bounding box [min_lon, min_lat, max_lon, max_lat]. Soft preference unless bounded=true. Ignored when lat/lon are given. |
| bounded | bool | no | false | When true and viewbox is set, restrict geocoding strictly to the box. |
| boundary | object | no | null | GeoJSON Polygon/MultiPolygon to restrict results to — only POIs inside the polygon are returned (no spillover). Pass the geometry from a POST /boundaries result to scope a search to an admin region. Replaces radius_m when set. See the region-scoping note below. |
| radius_m | int | no | 1000 | Search radius in metres (200–50,000). Ignored when boundary is supplied. |
| max_results | int | no | 10 | Max primary results (1–100). Also accepted as result_count_limit (alias). Requesting the maximum adds only ~6 ms latency vs the default. |
| result_count_limit | int | no | 10 | Alias of max_results (1–100). Use whichever name you prefer; do not pass both. |
| categories | string[] | no | null | Restrict to OSM category keys, e.g. ["amenity","shop"] |
| filters | object | no | null | Hard OSM tag filters, e.g. {"cuisine":"italian"} |
| time | string | no | null | "now" or ISO 8601. Optional — open status is annotated by default (see below). |
| language | string | no | "en" | BCP-47 tag for name resolution (e.g. "de") |
| neighborhood | bool | no | false | Include neighbourhood POI profile |
| map | bool | no | false | Include static map image URL |
lat/lon), names that exist in several regions ("Central Park",
"Chelsea") may resolve to the wrong one. Pass region (e.g.
"New York, USA") and/or a viewbox box to bias resolution to the
intended area, e.g. { "query": "coffee near Central Park with wifi", "region": "New York, USA" }.
POST /boundaries with the
boundary parameter. radius_m/viewbox are circles/rectangles that
spill across borders; boundary clips to the exact polygon.
- Resolve the region:
POST /boundaries { "query": "Manhattan New York", "admin_level": 8, "region": "US-NY" }→ returns itsgeometry+bbox. - Search inside it:
POST /forward { "query": "restaurants with wifi and wheelchair access", "boundary": <geometry> }.
lat/lon
are optional (proximity is scored from the region centre); radius_m is ignored.
is_open and opening_hours, evaluated at "now" in the venue's
local timezone — no time parameter needed. is_open is
null when the venue has no parseable opening_hours.
Response
The result is the full search response. Note the POI array is primary_results (not results), and identity / tags / coordinates are nested.
{
"job_id": "3f8e2a1b-4c9d-4e5f-8a1b-2c3d4e5f6a7b",
"status": "complete",
"result": {
"query_interpretation": {
"original_query": "wheelchair-accessible Italian restaurant",
"resolved_location": "Mitte, Berlin, Germany",
"anchor_lat": 52.52, "anchor_lon": 13.405,
"search_radius_m": 1000, "search_mode": "provided"
},
"primary_results": [
{
"rank": 1,
"relevance_score": 0.91,
"proximity_score": 0.88,
"combined_score": 0.89,
"identity": {
"osm_id": 123456789, "osm_type": "node",
"name": "Trattoria Roma", "name_translations": { "it": "Trattoria Roma" }
},
"classification": {
"primary_category": "amenity", "subcategory": "restaurant",
"tags": { "amenity": "restaurant", "cuisine": "italian", "wheelchair": "yes", "outdoor_seating": "yes" }
},
"location": {
"lat": 52.5198, "lon": 13.4048,
"distance_from_anchor_m": 45.0, "distance_human": "45 m",
"address": "Unter den Linden 1, Berlin"
},
"agent_summary": "Trattoria Roma (restaurant) — 45 m away at Unter den Linden 1, Berlin",
"osm_url": "https://www.openstreetmap.org/node/123456789",
"is_open": true,
"opening_hours": "Mo-Su 12:00-22:00",
"attributes": { "cuisine": "italian", "outdoor_seating": "yes", "wheelchair": "yes" },
"confidence_score": 0.95, "confidence_label": "high"
}
],
"neighborhood_context": null,
"administrative_context": {
"country": "Germany", "country_code": "de", "region": "Berlin",
"city": "Berlin", "postcode": "10117", "timezone": "Europe/Berlin"
},
"agent_instructions": {
"limitations": ["Opening hours may not reflect holidays or recent changes.", "Distances are straight-line, not walking or driving distances."],
"suggested_follow_up_queries": ["Find parking near Trattoria Roma"]
},
"search_metadata": {
"latency_ms": 312.4, "vector_search_latency_ms": 22.7, "total_candidates": 38,
"cache_hit": false,
"matched_categories": ["food"],
"category_gate_relaxed": false,
"diet_filter_relaxed": false
},
"comparison_matrix": null,
"map": null
}
}
Notable result fields
| Field | Description |
|---|---|
| primary_results[] | Ranked results. Field name is primary_results, not results; name is identity.name, tags are classification.tags. |
| is_open | true/false/null. Annotated by default at venue-local "now". |
| attributes | Curated tags lifted top-level: wheelchair, outdoor_seating, cuisine, takeaway, delivery, internet_access, smoking, phone, website, brand, fee, plus a collapsed diet. |
| search_metadata.matched_categories | Entity types the query matched — a query is gated to its type (e.g. "playground" won't return subway entrances). |
| search_metadata.category_gate_relaxed | true if the category gate was widened for too few results. |
| search_metadata.diet_filter_relaxed | true if a dietary preference (halal/kosher/vegan/…) was relaxed to semantic ranking. |
diet:halal venues; "wheelchair accessible cafe"
filters on wheelchair=yes. No special parameters required.
curl example
curl -X POST https://api.bitemap.ai/forward \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "wheelchair-accessible Italian restaurant",
"lat": 52.52, "lon": 13.405
}'
# → { "job_id": "3f8e...", "status": "queued" }
curl https://api.bitemap.ai/jobs/3f8e2a1b-... \
-H "Authorization: Bearer $API_TOKEN"
# → { "job_id": "...", "status": "complete", "result": { "primary_results": [ ... ], ... } }
POST /neighbourhood
Request body
{
"lat": 52.5200,
"lon": 13.4050,
"radius_m": 500
}
Response result
{
"anchor_lat": 52.52,
"anchor_lon": 13.405,
"search_radius_m": 500,
"neighborhood_context": {
"category_summary": {
"restaurants": { "count": 23, "nearest_m": 45.0 },
"transport": { "count": 6, "nearest_m": 210.0 },
"supermarkets": { "count": 2, "nearest_m": 151.0 },
"healthcare": { "count": 4, "nearest_m": 168.0 },
"education": { "count": 3, "nearest_m": 181.0 },
"accommodation": { "count": 8, "nearest_m": 63.0 },
"attractions": { "count": 5, "nearest_m": 208.0 },
"parks_leisure": { "count": 4, "nearest_m": 87.0 },
"finance": { "count": 7, "nearest_m": 82.0 }
},
"walkability_profile": {
"nearest_public_transport_m": 210.0,
"density_score": 0.82,
"density_label": "very dense urban"
}
},
"administrative_context": {
"country": "Germany", "country_code": "de", "region": "Berlin",
"city": "Berlin", "postcode": "10117", "timezone": "Europe/Berlin"
},
"map": null
}
POST /reverse
Request body
{
"lat": 52.5200,
"lon": 13.4050,
"neighborhood": false,
"map": false
}
Response result
Address components are flat top-level fields (null when unavailable), not a nested object.
{
"lat": 52.52,
"lon": 13.405,
"display_name": "Unter den Linden 1, Mitte, Berlin, 10117, Germany",
"name": "Brandenburg Gate",
"place_type": "tourism",
"osm_type": "way",
"osm_id": 5123456,
"house_number": "1",
"road": "Unter den Linden",
"suburb": "Mitte",
"city": "Berlin",
"postcode": "10117",
"region": "Berlin",
"country": "Germany",
"country_code": "de",
"boundingbox": [52.5159, 52.5165, 13.3769, 13.3784],
"latency_ms": 41.0,
"neighborhood_context": null,
"map": null
}
POST /context
Request body
{
"lat": 52.5200,
"lon": 13.4050
}
Response result
Sub-objects reuse the /reverse, /timezone, and /neighbourhood schemas. weather and transit are always null.
{
"lat": 52.52,
"lon": 13.405,
"address": { "display_name": "Unter den Linden 1, Mitte, Berlin, 10117, Germany", "road": "Unter den Linden", "city": "Berlin", "postcode": "10117", "country_code": "de", "latency_ms": 38.0 },
"timezone": { "lat": 52.52, "lon": 13.405, "timezone_id": "Europe/Berlin", "utc_offset_s": 7200, "utc_offset_str": "+02:00", "dst_active": true },
"weather": null,
"neighbourhood": { "anchor_lat": 52.52, "anchor_lon": 13.405, "search_radius_m": 500, "neighborhood_context": { "category_summary": {}, "walkability_profile": {} } },
"transit": null,
"latency_ms": 410.0,
"warnings": []
}
POST /boundaries
Request body
{
"query": "Brooklyn",
"limit": 5,
"admin_level": 8,
"region": "US-NY"
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| query | string | yes | — | Natural-language region name, e.g. "Brooklyn" or "King County Washington" (1–300 chars) |
| limit | int | no | 5 | Max matches to return (1–50) |
| admin_level | int | no | null | Restrict to one level: 4 = state, 6 = county, 8 = city/town/place |
| region | string | no | null | Restrict to one ISO 3166-2 region, e.g. "US-NY". Combine with admin_level to scope semantic search to e.g. "counties in NY state" |
Response result
{
"query": "Brooklyn",
"count": 1,
"results": [
{
"id": "abb24048-d76a-4457-95d0-6c39e7be5c62",
"name": "Brooklyn",
"admin_level": 8,
"subtype": "locality",
"gers_id": "abb24048-d76a-4457-95d0-6c39e7be5c62",
"country": "US",
"region": "US-NY",
"area_km2": 92.53,
"centroid_lat": 40.6456,
"centroid_lon": -73.9559,
"bbox": [-74.0419, 40.5707, -73.8334, 40.7394],
"geometry": { "type": "MultiPolygon", "coordinates": [ "..." ] },
"relevance_score": 0.90
}
]
}
| Field | Description |
|---|---|
| admin_level | 4 = state, 6 = county, 8 = city/town/place |
| area_km2 | null for a real share of boundaries (self-intersecting source polygons) — treat as optional, never assume it's present |
| bbox | [min_lon, min_lat, max_lon, max_lat] of the geometry (null if absent). Feed into /forward's viewbox, or the geometry into /forward's boundary, to scope a POI search to this region. |
| geometry | Full GeoJSON Polygon / MultiPolygon |
| relevance_score | Cosine similarity to the query text |
curl example
curl -X POST https://api.bitemap.ai/boundaries \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "Brooklyn", "limit": 5}'
# → { "job_id": "3f8e...", "status": "queued" }
curl https://api.bitemap.ai/jobs/3f8e2a1b-... \
-H "Authorization: Bearer $API_TOKEN"
# → { "job_id": "...", "status": "complete", "result": { "results": [ ... ], ... } }
POST /boundaries/reverse
Request body
{ "lat": 40.7580, "lon": -73.9855 }
| Field | Type | Required | Description |
|---|---|---|---|
| lat | float | yes | Latitude (-90 to 90) |
| lon | float | yes | Longitude (-180 to 180) |
Response result
{
"lat": 40.758,
"lon": -73.9855,
"count": 4,
"containing": [
{ "name": "New York", "admin_level": 4, "region": "US-NY", "geometry": { "...": "..." }, "relevance_score": null },
{ "name": "New York County", "admin_level": 6, "region": "US-NY", "geometry": { "...": "..." }, "relevance_score": null },
{ "name": "New York", "admin_level": 8, "region": "US-NY", "geometry": { "...": "..." }, "relevance_score": null },
{ "name": "Manhattan", "admin_level": 8, "region": "US-NY", "geometry": { "...": "..." }, "relevance_score": null }
]
}
/reverse returns a
street-level address for a point. /boundaries/reverse returns the
administrative region polygons containing that point — use it when an agent needs
to know, or constrain results to, the exact boundary of a state, county, or city, not just
its name. Results are always ordered state → county → place, so the last entry in
containing is the most specific region.
curl example
curl -X POST https://api.bitemap.ai/boundaries/reverse \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"lat": 40.7580, "lon": -73.9855}'
# → { "job_id": "3f8e...", "status": "queued" }
curl https://api.bitemap.ai/jobs/3f8e2a1b-... \
-H "Authorization: Bearer $API_TOKEN"
# → { "job_id": "...", "status": "complete", "result": { "containing": [ ... ], ... } }
GET /boundaries/children
/boundaries result (state → its counties → a county's places).Query parameters
GET /boundaries/children?parent_id=abb24048-d76a-4457-95d0-6c39e7be5c62&admin_level=6&limit=100
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| parent_id | string | yes | — | The id field from a /boundaries or /boundaries/reverse result — identifies the exact parent boundary to search within |
| admin_level | int | no | null | Restrict to one level: 4 = state, 6 = county, 8 = place. Omit to list every level within the parent. |
| limit | int | no | 100 | Max results (1–500) |
parent_id and not a region code: filtering by state alone
would be wrong for county→place lookups — every place in California shares the same
state-level region code, so "children of Orange County" would incorrectly include places
from every other CA county too. parent_id resolves the parent's actual geometry
and only returns candidates whose centroid falls inside it, which is correct at any level.
Response result
{
"parent_id": "d4532322-0808-4d95-9516-af840ae84f27",
"admin_level": 6,
"count": 2,
"children": [
{ "name": "Albany County", "admin_level": 6, "region": "US-NY", "geometry": { "...": "..." }, "relevance_score": null },
{ "name": "Erie County", "admin_level": 6, "region": "US-NY", "geometry": { "...": "..." }, "relevance_score": null }
]
}
| Field | Description |
|---|---|
| children[] | Same BoundaryResult shape as /boundaries and /boundaries/reverse. relevance_score is always null here — there's no query to score against. |
curl example
curl "https://api.bitemap.ai/boundaries/children?parent_id=abb24048-d76a-4457-95d0-6c39e7be5c62&admin_level=6&limit=100" \
-H "Authorization: Bearer $API_TOKEN"
# → { "job_id": "3f8e...", "status": "queued" }
curl https://api.bitemap.ai/jobs/3f8e2a1b-... \
-H "Authorization: Bearer $API_TOKEN"
# → { "job_id": "...", "status": "complete", "result": { "children": [ ... ], ... } }
GET /timezone
Query parameters
GET /timezone?lat=52.52&lon=13.405
Response result
The timezone_id and offset fields are null over open ocean.
{
"lat": 52.52,
"lon": 13.405,
"timezone_id": "Europe/Berlin",
"utc_offset_s": 7200,
"utc_offset_str": "+02:00",
"dst_active": true
}
GET /changes
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| since | string | yes | ISO 8601 datetime, e.g. 2024-01-01T00:00:00Z |
| bbox | string | yes | minLat,minLon,maxLat,maxLon, e.g. 52.50,13.39,52.54,13.42 |
| limit | int | no | Max results (default 100) |
Example
curl "https://api.bitemap.ai/changes?since=2024-01-01T00:00:00Z&bbox=52.50,13.39,52.54,13.42" \ -H "Authorization: Bearer $API_TOKEN"
Response result
{
"since": "2024-01-01T00:00:00Z",
"bbox": [52.50, 13.39, 52.54, 13.42],
"count": 1,
"entities": [
{
"osm_id": 123,
"osm_type": "node",
"name": "Café Berlino",
"category": "amenity",
"subcategory": "cafe",
"lat": 52.5201,
"lon": 13.4011,
"last_modified": "2024-03-14T09:12:00+00:00",
"tags": { "amenity": "cafe", "name": "Café Berlino" }
}
]
}
GET /jobs/{job_id}
Responses
{ "job_id": "3f8e...", "status": "queued" }
{ "job_id": "3f8e...", "status": "processing" }
{ "job_id": "3f8e...", "status": "complete", "result": { ... } }
{ "job_id": "3f8e...", "status": "error", "error": "upstream error" }
GET /jobs/{id} returns 404.
GET /health
{ "status": "ok" }
Rate Limits
Each Bearer token is subject to a sliding-window rate limit:
| Limit | Value |
|---|---|
| Requests per token | 30 per 60 seconds |
| Window type | Sliding (rolling 60-second window) |
When the limit is exceeded the server responds with HTTP 429:
{"detail": "rate limit exceeded"}
If the global job queue exceeds 500 pending jobs, new submissions also return HTTP 429:
{"detail": "queue full, try again later"}
Retry-After or X-RateLimit-* headers are sent. On a 429, back off 1–5 seconds and retry — repeated 429s will not unblock faster.Error Codes
| HTTP | Cause |
|---|---|
| 401 | Missing or malformed Authorization header |
| 403 | Invalid Bearer token |
| 404 | GET /jobs/{id} — job not found or expired |
| 422 | Request body/query validation error (missing or out-of-range fields) |
| 429 | Rate limit exceeded or queue full — back off and retry |
status: "error" and an error message — not an HTTP 5xx.Polling Guide
The recommended client polling loop:
import httpx, time
BASE = "https://api.bitemap.ai"
HDRS = {"Authorization": "Bearer YOUR_TOKEN", "Content-Type": "application/json"}
def search(query: str, lat: float, lon: float) -> dict:
# 1. Submit
r = httpx.post(f"{BASE}/forward",
json={"query": query, "lat": lat, "lon": lon},
headers=HDRS)
job_id = r.json()["job_id"]
# 2. Poll
time.sleep(0.5)
for _ in range(120):
r = httpx.get(f"{BASE}/jobs/{job_id}", headers=HDRS)
data = r.json()
if data["status"] == "complete":
return data["result"] # → result["primary_results"], result["search_metadata"], ...
if data["status"] == "error":
raise RuntimeError(data.get("error", "unknown error"))
time.sleep(1.0)
raise TimeoutError("job did not complete within 120 s")
MCP Tool Example
Wrap POST /context as a Model Context Protocol tool to give any LLM a sense of place before inference:
# MCP tool definition
{
"name": "get_location_context",
"description": "Get rich geospatial context for a coordinate: address, timezone, POI profile.",
"input_schema": {
"type": "object",
"properties": {
"lat": { "type": "number", "description": "Latitude" },
"lon": { "type": "number", "description": "Longitude" }
},
"required": ["lat", "lon"]
}
}
# Handler: POST /context → poll /jobs/{id} → return result as tool output
/context endpoint runs address, timezone, and neighbourhood lookups in parallel — a single tool call gives the LLM full environmental awareness.
Pricing
| Tier | Price | Included requests | Overage |
|---|---|---|---|
| Free | $0 / mo | 5,000 / mo (first 3 months, then 3,000 / mo) | — |
| Starter | $5.90 / mo | 10,000 | $0.85 / 1,000 |
| Growth | $36 / mo | 100,000 | $0.37 / 1,000 |
| Pro | $120 / mo | 600,000 | $0.20 / 1,000 |
| Enterprise | Custom | Custom volume + SLA | Self-hosted option available |
Straightforward usage-based pricing with a generous free tier — no credit card required to start prototyping.
Data Sources
Place (POI) results are conflated from OpenStreetMap and Overture Maps Places. Overture Places is itself an open, multi-provider dataset; the individual contributing providers and their licenses are listed below.
| Component | Source | License |
|---|---|---|
| POI data (base) | OpenStreetMap | ODbL 1.0 |
| POI data (coverage + brand/chain) | Overture Maps Places — aggregated from the providers below | Per-provider |
| ↳ via Overture | Meta | CDLA Permissive 2.0 |
| ↳ via Overture | Microsoft | CDLA Permissive 2.0 |
| ↳ via Overture | Foursquare | Apache 2.0 |
| ↳ via Overture | PinMeTo, RenderSEO, DAC, BrightQuery, Krick | CDLA Permissive 2.0 |
| ↳ via Overture | AllThePlaces | CC0 1.0 (public domain) |
Administrative boundary geometry (/boundaries, /boundaries/reverse) | Overture Maps Divisions | Overture Maps Foundation (includes ODbL-derived components) |
| Timezone | IANA timezone database | Public domain |
All of the above licenses permit commercial use and redistribution; none are share-alike except OpenStreetMap's ODbL.
Attribution
Results returned by this API are derived/computational works ("Produced Works"), so you are not required to embed license text in each response. You are asked to credit the underlying data wherever results are displayed to end users:
© OpenStreetMap contributors · Includes data ©
Overture Maps Foundation (overturemaps.org); place data sourced from Meta,
Microsoft, Foursquare and others. Foursquare-sourced records:
Copyright 2024 Foursquare Labs, Inc. Administrative boundary geometry
(/boundaries) is © Overture Maps Foundation (Divisions theme).
- OpenStreetMap (ODbL) attribution — "© OpenStreetMap contributors" — is mandatory wherever OSM-derived data is shown.
- The Overture / Foursquare notices satisfy best practice for the permissive Overture providers.
- This is required only as a visible credit (app, site, or docs); it does not need to appear in the JSON body of every API response. Bulk export of the underlying records is not offered and would carry additional license obligations.
This is a summary of the data licenses, not legal advice.