Evrima

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 error
import 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 happenedWhat 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:

fieldsadds
(omitted)the display set above
fields=geometrylocation — lat/lon
fields=addresscomponents — locality, country code, postcode
fields=geometry,addressboth
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:

restrictionrefuses
allowed originsrequests from any site but yours (https://shop.example.com, https://*.example.com)
allowed IPsserver-side calls from anywhere but your machines (single addresses or CIDR)
allowed endpointsa 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:

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

statuswhenwhat 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 error
import 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()
paramrequireddefault meaning
qyesthe text typed so far.
sessionnononegroups this call into a billable session, see above.
countrynoautoa two-letter code to scope the search to one country.
langnoautotwo-letter code; see Language above.
lat, lonnononesend 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:

paramrequireddefault meaning
idthe entity's id. Stable; safe to use as a React key.
typeaddress, street, locality or place.
kinda category (port, airport, ...) for place rows, null otherwise.
text, main_text, secondary_textready-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.
componentsthe 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}}
]

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"}
paramrequireddefault meaning
qyesthe place name to resolve, e.g. Rotterdam.
kindnoanyrepeat 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.
radiusno25kman Elasticsearch distance string, e.g. 10km.
limitno20capped at 100; non-integer values are a 400.
countrynononesee the country filter above.
rank_bynoprominenceprominence (importance, then distance) or distance (nearest first).
langnoautosee Language above.
sessionnononealso 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:

paramrequireddefault meaning
queryyour q, echoed back.
nearthe row q resolved to -- same shape as an /autocomplete row (it comes from that endpoint), or null if nothing could be resolved.
resultsan array in /nearby's shape (below) -- note these rows carry distance_m but not matched.
reasonpresent 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()
paramrequireddefault meaning
lat, lonyesthe search centre. Missing or non-numeric is a 400 -- unlike /autocomplete's lat/lon, this is a hard filter, not a ranking bias.
kindnoanysame 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.
radiusno5kman Elasticsearch distance string.
limitno20capped at 100.
countrynononesee the country filter above.
rank_bynodistancedistance (nearest first) or prominence (importance, then distance).
langnoenno query text here to detect a script from, so this defaults to English rather than auto-detecting -- see Language above.
sessionnononealso 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):

Stays in this tab's memory only -- never written into the page, never logged, gone on reload.

No session yet -- run an example, or click "New session".