API reference
Address autocomplete, place search and nearby search over 436 million addresses in 82 countries. Three endpoints, one credential, billed by the address, not the keystroke.
Quickstart
One key, one endpoint, one session token reused while the user types. This is the same
/autocomplete example documented in full below -- it is repeated here because
it is the single most-used part of this page and the one thing worth reading before
anything else: session is the billing unit.
# `session` groups every keystroke of ONE address lookup into a SINGLE
# billable session -- reuse the same token on each call, drop it once the
# user picks a result. See "Sessions and what you are billed for" below.
curl -H "Authorization: Bearer YOUR_API_KEY" \
"http://49.12.123.164/places/autocomplete?q=Damrak+1+Amsterdam&session=9f2e1c-a-user-picked-uuid"// `session` is the billing unit: reuse the SAME token on every keystroke of one
// address lookup and the whole lookup is billed ONCE, not once per request.
const res = await fetch(
"http://49.12.123.164/places/autocomplete?q=Damrak+1+Amsterdam&session=" + sessionToken,
{ headers: { Authorization: "Bearer " + apiKey } }
);
const rows = await res.json(); // [] on no matches -- that is not an errorimport requests
# `session` groups every keystroke of ONE address lookup into a SINGLE billable session.
# Mint one token (any UUID) per lookup, reuse it on every call, then start a new one.
r = requests.get(
"http://49.12.123.164/places/autocomplete",
params={"q": "Damrak 1 Amsterdam", "session": session_token},
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
rows = r.json()Full reference for this and two more endpoints: jump to /autocomplete, /search or /nearby in the left nav any time.
Authentication
Send your key as a bearer token: Authorization: Bearer YOUR_API_KEY. If you
cannot set headers, ?key=YOUR_API_KEY in the query string works the same way --
prefer the header where you can, since query strings end up in access logs.
Every endpoint on this page requires a valid key. A missing or unrecognised one gets a
plain 401 (not JSON -- see Errors below). /health
is the one route that stays open, for load balancers that cannot hold a credential.
Sessions and what you are billed for
The unit that is billed is a session, not a request.
Someone typing "Rotterdam" fires an /autocomplete call after almost every
keystroke -- that is normal and expected, and it is not eight charges. Generate one token
per address lookup (a UUID you make up client-side is fine) and send it as
&session= on every call that belongs to that lookup: every keystroke's
autocomplete request, and the request that resolves whichever suggestion got picked. All of
it counts as one session.
The token is opaque to us on purpose -- send a UUID, not something that identifies a person. It is hashed the moment it arrives and only the hash is kept; the raw value is never logged or returned.
A session is not open-ended: one session covers up to 30 requests, which is roughly four times what a real address lookup takes. Anything past that is billed per request, so reusing a single token for unrelated lookups costs the same as not sending one at all. Use a fresh token per address and you will never meet this limit.
Your account page shows sessions (the billable count) alongside
requests and requests_per_session -- the last one is "how many
keystrokes does one address cost you", which is the number worth watching if you are
migrating pricing expectations from another provider.
session is not exclusive to /autocomplete: it is read once,
before routing, so any request to any endpoint that carries the same token is folded into
the same session. If a lookup happens to span /autocomplete and a
/search or /nearby call, pass the same token to all of them and it
is still one session.
The session is a discount you earn by finishing the lookup
One rule, three cases. It is the same rule Google Places applies to an abandoned session, so if you are porting an integration the behaviour will already be familiar:
| What happened | What is billed |
|---|---|
session sent, and a suggestion was resolved
(/chose or /place/) |
one session, however many keystrokes it took |
session sent, nothing ever resolved |
every request, individually |
no session at all |
every request, individually |
So sending the token is worth doing, and closing the loop with
/chose when the user picks is what actually converts the discount. Omitting
the token still works -- nothing breaks, you simply pay per request.
Choosing what comes back: fields
By default a suggestion carries what a dropdown needs: id,
main_text, secondary_text, text, type,
kind and the matched spans. Coordinates and the structured
address are opt-in:
| fields | adds |
|---|---|
| (omitted) | the display set above |
fields=geometry | location — lat/lon |
fields=address | components — locality, country code, postcode |
fields=geometry,address | both |
fields=* | the whole record |
A name we have not shipped yet is ignored rather than rejected, so adding a
tier to your client is always safe. id is always returned — it is the one
field you may store indefinitely (see below).
What you may store
Store the id, refresh the record. Our id is stable and
yours to keep for as long as you like — put it in your orders table and re-resolve it
whenever you need the address. The rest of a response may be cached for up to 30 days.
Building a persistent copy of the database is not permitted.
This is better for you as well as for us: an address that is re-resolved stays correct when a street is renamed or a postcode is reallocated.
Locking a key down
A browser key is published by design — it ships in your page source. Every key can therefore be restricted, and an unrestricted key behaves exactly as it always has:
| restriction | refuses |
|---|---|
| allowed origins | requests from any site but yours
(https://shop.example.com, https://*.example.com) |
| allowed IPs | server-side calls from anywhere but your machines (single addresses or CIDR) |
| allowed endpoints | a widget key reaching /batch |
A refusal is 403, not 401, and the message names which list
refused — the common case is that you have just locked a key down and need to know
which one to widen.
Be clear on what an origin restriction is worth: browsers send
Origin, and anything that is not a browser can forge it. It stops your key
working on somebody else's website. It does not stop a determined attacker with a shell.
Language: lang and Accept-Language
Two independent things share the word "language" here, and it is worth keeping them apart:
- What you typed decides what matches. Untouched by anything below.
- What name is shown back to you -- a Munich/München/מינכן/慕尼黑 pick --
is decided, in order: an explicit
?lang=xx(a two-letter code) if you send one; otherwise yourAccept-Languageheader, parsed per RFC 9110 with its;q=weights, first match wins; and for/autocompleteand/searchonly, if neither is present, the script of what you typed decides it (a Hebrew query gets a Hebrew label) before falling back to English.
/nearby has no query text to detect a script from, so with no
lang given it defaults to plain en -- Accept-Language
still applies if you send it, only the query-script fallback is unavailable there.
The country filter
An optional two-letter code (NL, BG, ...) that scopes the
search to one country's index instead of all 82 -- faster, and it removes cross-country
ambiguity for short names. All three endpoints accept it. On /autocomplete,
if you do not pass it, a country named inside the query itself ("Sofia Bulgaria") can still
scope the search the same way, on top of everything you would get for free otherwise.
Rate limits
Each key is a token bucket refilled continuously at your configured requests/second
(20 req/s is the default for a key with no custom rate configured), with
a burst allowance of 3 seconds' worth of tokens -- typing five characters
in a second is normal and will not be throttled. Once the bucket is empty you get
429 with a Retry-After: 1 header and no body;
wait a second and retry.
Errors
Error responses are plain HTML error pages (the Python stdlib's default), not JSON --
do not .json() them blindly. Check the status code first.
| status | when | what to do |
|---|---|---|
400 |
a required parameter is missing or malformed (q on /search,
lat/lon on /nearby, a non-integer limit), or a
kind you asked for is not one we recognise. |
fix the parameter named in the response body and retry. For an unknown kind
the body lists every valid value, so it is worth validating client-side before the call
rather than round-tripping to find out. |
401 |
missing or unrecognised API key. | check that Authorization: Bearer ... (or ?key=) carries a
real key from your account page. Retrying with the same key will not succeed --
that is not a transient failure. |
404 |
the path itself does not exist. | check the endpoint path and method. A query that legitimately finds nothing is not
this -- that is 200 with an empty result, see below. |
429 |
this key's token bucket is empty -- see Rate limits above. | wait for the interval named in the Retry-After header (currently always
1 second) and retry once. Retrying immediately in a tight loop just keeps the
bucket empty. |
A search that simply finds nothing is not an error -- it is
200 with an empty list (or, for /search, a results: [] with a
reason).
GET /autocomplete
Address, street and place suggestions as someone types. Examples below show the current deployment, http://49.12.123.164/places. The try-it panel does not hard-code that -- it calls relative to wherever this page is served, so it keeps working if the host changes.
# `session` groups every keystroke of ONE address lookup into a SINGLE
# billable session -- reuse the same token on each call, drop it once the
# user picks a result. See "Sessions and what you are billed for" below.
curl -H "Authorization: Bearer YOUR_API_KEY" \
"http://49.12.123.164/places/autocomplete?q=Damrak+1+Amsterdam&session=9f2e1c-a-user-picked-uuid"// `session` is the billing unit: reuse the SAME token on every keystroke of one
// address lookup and the whole lookup is billed ONCE, not once per request.
const res = await fetch(
"http://49.12.123.164/places/autocomplete?q=Damrak+1+Amsterdam&session=" + sessionToken,
{ headers: { Authorization: "Bearer " + apiKey } }
);
const rows = await res.json(); // [] on no matches -- that is not an errorimport requests
# `session` groups every keystroke of ONE address lookup into a SINGLE billable session.
# Mint one token (any UUID) per lookup, reuse it on every call, then start a new one.
r = requests.get(
"http://49.12.123.164/places/autocomplete",
params={"q": "Damrak 1 Amsterdam", "session": session_token},
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
rows = r.json()| param | required | default | meaning |
|---|---|---|---|
q | yes | — | the text typed so far. |
session | no | none | groups this call into a billable session, see above. |
country | no | auto | a two-letter code to scope the search to one country. |
lang | no | auto | two-letter code; see Language above. |
lat, lon | no | none | send both to bias ranking toward this point (a Gauss decay, not a filter) -- a distant match still ranks, just lower. This is different from /nearby's lat/lon, which is a hard search radius. |
There is no limit parameter on this endpoint -- it always
returns up to 10 rows. (Verified live: adding &limit=2 to a query that
otherwise returns 10 rows still returns 10.) If you need a different count, that is what
/search and /nearby are for.
Response
A JSON array, best match first. Each row:
| param | required | default | meaning |
|---|---|---|---|
id | the entity's id. Stable; safe to use as a React key. | ||
type | address, street, locality or place. | ||
kind | a category (port, airport, ...) for place rows, null otherwise. | ||
text, main_text, secondary_text | ready-to-render label, split the way Google's autocomplete widget splits it (bold part / grey part). | ||
matched | [[start, length], ...] spans into main_text -- the part that justifies the match, for highlighting it. | ||
components | the same address broken into fields (street, house_number, locality, admin_area, postcode, country_code) -- each key is present only when that entity actually has it, so its absence is itself information. | ||
location | {{"lat":..,"lon":..}}, when known. |
Real response, captured live and trimmed to two rows:
[
{"id": "a604088905", "type": "address", "kind": null,
"text": "Damrak 1-5, 1012LG Amsterdam, Netherlands",
"main_text": "Damrak 1-5", "secondary_text": "1012LG Amsterdam, Netherlands",
"matched": [[0, 6]],
"components": {"street": "Damrak", "house_number": "1-5", "locality": "Amsterdam",
"postcode": "1012LG", "country_code": "NL"},
"location": {"lat": 52.3772801, "lon": 4.8976771}},
{"id": "a130895906", "type": "address", "kind": null,
"text": "Damrak 1, 1012LG Amsterdam, Netherlands",
"main_text": "Damrak 1", "secondary_text": "1012LG Amsterdam, Netherlands",
"matched": [[0, 6]],
"components": {"street": "Damrak", "house_number": "1", "locality": "Amsterdam",
"postcode": "1012LG", "country_code": "NL"},
"location": {"lat": 52.3771445, "lon": 4.8980385}}
]GET /search
A named place plus an optional category, when you have a place name but not
coordinates -- "ports near Rotterdam" without knowing Rotterdam is 51.9244, 4.4777. Under
the hood this resolves q with the same logic as /autocomplete,
then runs /nearby around whatever it resolved to.
curl -H "Authorization: Bearer YOUR_API_KEY" \
"http://49.12.123.164/places/search?q=Rotterdam&kind=port&kind=pier&limit=5"const res = await fetch(
"http://49.12.123.164/places/search?q=Rotterdam&kind=port&kind=pier&limit=5",
{ headers: { Authorization: "Bearer " + apiKey } }
);
const { query, near, results } = await res.json();import requests
r = requests.get(
"http://49.12.123.164/places/search",
params=[("q", "Rotterdam"), ("kind", "port"), ("kind", "pier"), ("limit", 5)],
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
data = r.json() # {"query", "near", "results"}| param | required | default | meaning |
|---|---|---|---|
q | yes | — | the place name to resolve, e.g. Rotterdam. |
kind | no | any | repeat the parameter per kind you want, e.g. &kind=port&kind=pier -- a single comma-joined value will not match anything and returns 400. Valid values: port, pier, marina, airport, airport_terminal, rail_terminal, road_terminal, multimodal_terminal, border_crossing, warehouses, shipping_center, freight_and_cargo_service, freight_forwarding_agency, transportation, wholesaler, business, business_manufacturing_and_supply, commercial_industrial, company, corporate_office, professional_services, industrial_company, industrial_equipment, location. |
radius | no | 25km | an Elasticsearch distance string, e.g. 10km. |
limit | no | 20 | capped at 100; non-integer values are a 400. |
country | no | none | see the country filter above. |
rank_by | no | prominence | prominence (importance, then distance) or distance (nearest first). |
lang | no | auto | see Language above. |
session | no | none | also honored here, not just on /autocomplete -- see Sessions & billing below. A call carrying the same token as an earlier /autocomplete call in the same lookup still counts toward that one session. |
Response
An object, not an array:
| param | required | default | meaning |
|---|---|---|---|
query | your q, echoed back. | ||
near | the row q resolved to -- same shape as an /autocomplete row (it comes from that endpoint), or null if nothing could be resolved. | ||
results | an array in /nearby's shape (below) -- note these rows carry distance_m but not matched. | ||
reason | present only when near is null, explaining why. |
Real response, captured live and trimmed to two results:
{
"query": "Rotterdam",
"near": {"id": "l75957", "type": "locality", "kind": null,
"text": "Rotterdam, Netherlands", "main_text": "Rotterdam",
"secondary_text": "Netherlands", "matched": [[0, 9]],
"components": {"locality": "Rotterdam", "country_code": "NL"},
"location": {"lat": 51.9244424, "lon": 4.47775}},
"results": [
{"id": "p224789", "type": "place", "kind": "port",
"text": "Rotterdam \u2014 Rotterdam, Netherlands", "main_text": "Rotterdam",
"secondary_text": "Rotterdam, Netherlands",
"components": {"locality": "Rotterdam", "country_code": "NL"},
"distance_m": 2744.7, "location": {"lat": 51.9, "lon": 4.483333}},
{"id": "p224823", "type": "place", "kind": "port",
"text": "Schiedam \u2014 Pernis Rotterdam, Netherlands", "main_text": "Schiedam",
"secondary_text": "Pernis Rotterdam, Netherlands",
"components": {"locality": "Pernis Rotterdam", "country_code": "NL"},
"distance_m": 5985.7, "location": {"lat": 51.9, "lon": 4.4}}
]
}GET /nearby
What is within a radius of a point you already have coordinates for -- a driver's GPS
ping, a warehouse's stored lat/lon. Restricted to type=place rows: addresses
and streets carry no kind, so a plain radius search over them would bury the
two ports you wanted under ten thousand houses.
curl -H "Authorization: Bearer YOUR_API_KEY" \
"http://49.12.123.164/places/nearby?lat=51.9244&lon=4.4777&radius=5km&kind=port"const res = await fetch(
"http://49.12.123.164/places/nearby?lat=51.9244&lon=4.4777&radius=5km&kind=port",
{ headers: { Authorization: "Bearer " + apiKey } }
);
const rows = await res.json();import requests
r = requests.get(
"http://49.12.123.164/places/nearby",
params={"lat": 51.9244, "lon": 4.4777, "radius": "5km", "kind": "port"},
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
rows = r.json()| param | required | default | meaning |
|---|---|---|---|
lat, lon | yes | — | the search centre. Missing or non-numeric is a 400 -- unlike /autocomplete's lat/lon, this is a hard filter, not a ranking bias. |
kind | no | any | same repeated-parameter rule as /search's kind. Valid values: port, pier, marina, airport, airport_terminal, rail_terminal, road_terminal, multimodal_terminal, border_crossing, warehouses, shipping_center, freight_and_cargo_service, freight_forwarding_agency, transportation, wholesaler, business, business_manufacturing_and_supply, commercial_industrial, company, corporate_office, professional_services, industrial_company, industrial_equipment, location. |
radius | no | 5km | an Elasticsearch distance string. |
limit | no | 20 | capped at 100. |
country | no | none | see the country filter above. |
rank_by | no | distance | distance (nearest first) or prominence (importance, then distance). |
lang | no | en | no query text here to detect a script from, so this defaults to English rather than auto-detecting -- see Language above. |
session | no | none | also honored here, not just on /autocomplete -- see Sessions & billing below. A call carrying the same token as an earlier /autocomplete call in the same lookup still counts toward that one session. |
Response
A JSON array, same row shape as /autocomplete's minus matched,
plus distance_m (metres, from your lat/lon):
Real response, captured live:
[
{"id": "p224789", "type": "place", "kind": "port",
"text": "Rotterdam \u2014 Rotterdam, Netherlands", "main_text": "Rotterdam",
"secondary_text": "Rotterdam, Netherlands",
"components": {"locality": "Rotterdam", "country_code": "NL"},
"distance_m": 2740.5, "location": {"lat": 51.9, "lon": 4.483333}}
]Try it
Runs for real, from your browser, against wherever this page is hosted. Your key is never sent anywhere but the API itself, and it is not saved anywhere -- reload the page and it is gone.
Prefilled examples -- one click loads a worked query and runs it (Latin and native-script addresses, a typo, a radius search, a named place):