Geocoding
Resolve addresses to coordinates (forward), coordinates to addresses (reverse), suggest street names (autocomplete), and check your remaining balance (quota).
This API resolves addresses through two complementary engines, and you can pick which one to use:
- Nominatim (free text / reverse) —
forwardwithqfree text andreverse(coordinate → address) run against OpenStreetMap data. Best for worldwide coverage and free-form input. - Internal, with our own maps (structured modes) —
internalresolves only against LogicSat's own street base, with explicit structured modes: street + corner, street + number, and route + km. Best when you already have the address split into fields and want the most accurate match on covered countries (Uruguay, Argentina, and more).
forward with structured fields (street / number / corner) also tries the internal engine first as part of its provider chain. Use the dedicated internal endpoint when you want to force a specific structured mode against our maps and skip the external providers entirely.
The geocoding service has an additional cost per use. Every successful forward or reverse lookup consumes one credit from your contracted geocoding allowance with LogicSat. It is not included in the free use of the rest of the API.
- Costs a credit:
forwardandreverselookups that return at least one result (1 credit per result that resolves). - Free:
autocomplete, thequotabalance check, lookups that return no results, and repeated lookups served from the short cache. - Check your balance any time with
GET /apidev/v1/geocoding/quota— it never costs a credit. - The allowance is contracted with LogicSat. When it runs out, paid lookups return HTTP 402 until the balance is renewed or extended.
All endpoints require a valid JWT token, API key, and tenant header. See Authentication. Every geocoding endpoint requires the APICLI_GEOCODE permission.
How billing works
Each paid response includes a quota block under meta so you always know where you stand after the call:
| Field | Type | Description |
|---|---|---|
contracted | number | Total credits in the active period. |
used | number | Credits already consumed in the active period. |
remaining | number | Credits left (contracted − used). |
period_start | string | null | Start of the active billing window. null means no fixed start. |
period_end | string | null | End of the active billing window. null means no expiry. |
charged | number | Credits this single call consumed (0 for cache hits and empty results). |
quotaBefore running a large batch, call GET /apidev/v1/geocoding/quota to see your remaining balance. If a batch runs out mid-way, it resolves as many items as the balance allows and returns a per-item error for the rest — see Forward Geocoding (batch).
Forward Geocoding
Turn an address into a coordinate. Send a structured address (country / department / street / number / corner, or route + km) or free text. Returns ranked candidates with precision, confidence, and normalized address components.
/apidev/v1/geocoding/forwardRequest Body
Send a single query object, or a batch with { "items": [ ... ] } (1–25 queries). See Batch mode.
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | No | auto (default), street_number, intersection, route_km, or freetext. auto infers the strategy from the fields you send. |
country | string | Conditional | Country (ID or name). Narrows the search. Required except for freetext and route_km. |
department | string | Conditional | Department / province (ID or name). Disambiguates same-named streets. Required except for freetext. |
city | string | No | City / locality (ID or name). Biases the search. |
street | string | Conditional | Street name. Accepts a full line like "Av. Brasil 2950 esq. Ellauri" — the engine splits it. |
number | string | No | Door / house number. Triggers street_number mode. |
corner | string | No | Cross street. Triggers intersection mode. |
route | string | Conditional | Route number. Combined with km, triggers route_km mode. |
km | number | Conditional | Kilometer / milestone marker. |
q | string | Conditional | Free text. Triggers freetext mode. |
providers | string[] | No | Subset of internal, nominatim, google, mapbox, bing. Intersected with the providers enabled for your company; order is preserved. |
limit | number | No | Max candidates to return. Min 1, Max 25, default 10. |
lang | string | No | Result language (es, en, …). |
bias_bbox | number[4] | No | [minLng, minLat, maxLng, maxLat] bounding box to bias the search toward an area. |
Response Fields
data.results is an array of candidates. Each candidate has:
| Field | Type | Description |
|---|---|---|
lat | string | Latitude (decimal as string). |
lng | string | Longitude (decimal as string). |
precision | string | Result quality: explicit, rooftop, range, intersection, route-km, street, approx, or centroid. |
confidence | number | null | Provider confidence from 0 to 1. |
source | string | Provider that resolved it: internal, nominatim, google, mapbox, or bing. |
address | object | Normalized components: street, number, corner, neighborhood, city, department, country, postcode. |
formatted | string | Human-readable single-line address. |
bbox | number[4] | Bounding box [minLng, minLat, maxLng, maxLat]. Present when available. |
normalized | object | Present only when the street was corrected: { street, resolved_from } (the street used vs. the text you sent). |
The meta block carries providers_tried (string[]), resolved_by (string | null), cached (boolean), and the quota block described in How billing works.
Code Example
- cURL
- JavaScript
- Python
curl -s -X POST "https://$TENANT/apidev/v1/geocoding/forward" \
-H "Authorization: Bearer $TOKEN" \
-H "X-API-Key: $APIKEY" \
-H "tenant: $TENANT" \
-H "Content-Type: application/json" \
-d '{
"country": "Uruguay",
"department": "Montevideo",
"street": "Av. Brasil",
"number": "2950"
}'
const response = await fetch(
`https://${TENANT}/apidev/v1/geocoding/forward`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"X-API-Key": API_KEY,
"tenant": TENANT,
"Content-Type": "application/json",
},
body: JSON.stringify({
country: "Uruguay",
department: "Montevideo",
street: "Av. Brasil",
number: "2950",
}),
}
);
const { data, meta } = await response.json();
const best = data.results[0];
console.log(`${best.lat}, ${best.lng} — ${meta.quota.remaining} credits left`);
response = requests.post(
f"https://{TENANT}/apidev/v1/geocoding/forward",
headers={**headers, "Content-Type": "application/json"},
json={
"country": "Uruguay",
"department": "Montevideo",
"street": "Av. Brasil",
"number": "2950",
},
)
result = response.json()
best = result["data"]["results"][0]
print(f"{best['lat']}, {best['lng']} — charged {result['meta']['quota']['charged']}")
Example Response
{
"success": true,
"data": {
"results": [
{
"lat": "-34.90568300",
"lng": "-56.18820200",
"precision": "range",
"confidence": 0.82,
"source": "internal",
"address": {
"street": "Avenida Brasil",
"number": "2950",
"corner": "Ellauri",
"neighborhood": "Pocitos",
"city": "Montevideo",
"department": "Montevideo",
"country": "Uruguay",
"postcode": "11300"
},
"formatted": "Avenida Brasil 2950 esq. Ellauri, Pocitos, Montevideo",
"bbox": [-56.190, -34.907, -56.186, -34.904],
"normalized": {
"street": "Avenida Brasil",
"resolved_from": "Av. Brasil"
}
}
]
},
"meta": {
"providers_tried": ["internal"],
"resolved_by": "internal",
"cached": false,
"quota": {
"contracted": 5000,
"used": 1242,
"remaining": 3758,
"period_start": "2026-01-01T00:00:00",
"period_end": "2026-12-31T23:59:59",
"active": true,
"charged": 1
}
}
}
A valid search that finds nothing returns 200 with "results": [] and "charged": 0. You are only billed when a lookup actually resolves to a location.
Batch mode
Send up to 25 queries at once with { "items": [ ... ] }. The response shape changes: data.items is an array of { index, success, results?, error? }, and meta.quota.charged reports the total credits consumed across the batch.
If the balance runs out partway through, the items resolved up to that point are billed and returned; each remaining item comes back with success: false and a GEOCODING_QUOTA_EXCEEDED error. The request still returns 200.
{
"success": true,
"data": {
"items": [
{
"index": 0,
"success": true,
"results": [
{
"lat": "-34.90568300",
"lng": "-56.18820200",
"precision": "range",
"confidence": 0.82,
"source": "internal",
"address": { "street": "Avenida Brasil", "number": "2950", "city": "Montevideo", "department": "Montevideo", "country": "Uruguay" },
"formatted": "Avenida Brasil 2950, Montevideo"
}
]
},
{
"index": 1,
"success": false,
"error": {
"code": "GEOCODING_QUOTA_EXCEEDED",
"message": "Geocoding quota exceeded"
}
}
]
},
"meta": {
"cached": false,
"quota": {
"contracted": 5000,
"used": 5000,
"remaining": 0,
"period_start": "2026-01-01T00:00:00",
"period_end": "2026-12-31T23:59:59",
"active": true,
"charged": 1
}
}
}
Reverse Geocoding
Turn a coordinate into an address. Same billing as forward — 1 credit per result, free on cache hit or empty result.
/apidev/v1/geocoding/reverseRequest Body
Send a single query object, or a batch with { "items": [ ... ] } (1–25 queries, same partial-billing rules as forward).
| Field | Type | Required | Description |
|---|---|---|---|
lat | string | Yes | Latitude (decimal as string). |
lng | string | Yes | Longitude (decimal as string). |
lang | string | No | Result language (es, en, …). |
zoom | number | No | Level of detail. Min 3, Max 18. |
Response Fields
Same candidate shape as Forward Geocoding. meta.providers_tried is ["nominatim"], and meta.quota carries the balance after the charge.
Code Example
- cURL
- JavaScript
- Python
curl -s -X POST "https://$TENANT/apidev/v1/geocoding/reverse" \
-H "Authorization: Bearer $TOKEN" \
-H "X-API-Key: $APIKEY" \
-H "tenant: $TENANT" \
-H "Content-Type: application/json" \
-d '{
"lat": "-34.90568300",
"lng": "-56.18820200"
}'
const response = await fetch(
`https://${TENANT}/apidev/v1/geocoding/reverse`,
{
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ lat: "-34.90568300", lng: "-56.18820200" }),
}
);
const { data, meta } = await response.json();
console.log(`${data.results[0]?.formatted} — charged ${meta.quota.charged}`);
response = requests.post(
f"https://{TENANT}/apidev/v1/geocoding/reverse",
headers={**headers, "Content-Type": "application/json"},
json={"lat": "-34.90568300", "lng": "-56.18820200"},
)
result = response.json()
print(result["data"]["results"][0]["formatted"])
Example Response
{
"success": true,
"data": {
"results": [
{
"lat": "-34.90568300",
"lng": "-56.18820200",
"precision": "rooftop",
"confidence": 0.45,
"source": "nominatim",
"address": {
"street": "Avenida Brasil",
"number": "2950",
"neighborhood": "Pocitos",
"city": "Montevideo",
"department": "Montevideo",
"country": "Uruguay",
"postcode": "11300"
},
"formatted": "Avenida Brasil 2950, Pocitos, Montevideo, Uruguay",
"bbox": [-56.189, -34.906, -56.187, -34.905]
}
]
},
"meta": {
"providers_tried": ["nominatim"],
"resolved_by": "nominatim",
"cached": false,
"quota": {
"contracted": 5000,
"used": 1243,
"remaining": 3757,
"period_start": "2026-01-01T00:00:00",
"period_end": "2026-12-31T23:59:59",
"active": true,
"charged": 1
}
}
}
Internal Geocoding (our maps)
Resolve an address only against LogicSat's own street base — no Nominatim, no external providers. You choose an explicit structured mode, so there is no guessing: each mode maps directly to a search against our maps.
Use this when you have the address already split into fields and want the most precise match on covered countries, or when you specifically want to avoid the external providers.
mode | Resolves | Required fields | Coverage |
|---|---|---|---|
street_corner | Street and its cross street (intersection) | country, street, corner | Uruguay, Argentina, Ecuador, Chile, Paraguay, Colombia |
street_number | Street and door number | country, street, number | Uruguay and Argentina (the countries with door-number ranges) |
route | Route and kilometer marker | route, km | Uruguay |
/apidev/v1/geocoding/internalInternal geocoding is billed exactly like forward and reverse: 1 credit per result that resolves, free on cache hits and empty results. Only autocomplete and quota are always free.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | Yes | street_corner, street_number, or route. Selects the structured search. |
country | string | Conditional | Country (ID or name). Required for street_corner and street_number. Ignored for route (Uruguay only). |
department | string | No | Department / province (ID or name). Disambiguates same-named streets. |
street | string | Conditional | Street name. Required for street_corner and street_number. |
corner | string | Conditional | Cross street. Required for street_corner. |
number | string | Conditional | Door / house number. Required for street_number. |
route | string | Conditional | Route number. Required for route. |
km | string | Conditional | Kilometer / milestone marker. Required for route. |
Response Fields
Same candidate shape as Forward Geocoding. Every result has source: "internal" and meta.providers_tried is ["internal"]. precision is intersection for street_corner, range for street_number (the engine returns the centroid of the block range, not the interpolated door), and route-km for route. meta.quota carries the balance after the charge.
Code Example
- cURL
- JavaScript
- Python
curl -s -X POST "https://$TENANT/apidev/v1/geocoding/internal" \
-H "Authorization: Bearer $TOKEN" \
-H "X-API-Key: $APIKEY" \
-H "tenant: $TENANT" \
-H "Content-Type: application/json" \
-d '{
"mode": "street_corner",
"country": "Uruguay",
"department": "Montevideo",
"street": "Av. Brasil",
"corner": "Ellauri"
}'
const response = await fetch(
`https://${TENANT}/apidev/v1/geocoding/internal`,
{
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
mode: "street_number",
country: "Uruguay",
department: "Montevideo",
street: "Av. Brasil",
number: "2950",
}),
}
);
const { data, meta } = await response.json();
const best = data.results[0];
console.log(`${best.lat}, ${best.lng} — ${meta.quota.remaining} credits left`);
response = requests.post(
f"https://{TENANT}/apidev/v1/geocoding/internal",
headers={**headers, "Content-Type": "application/json"},
json={
"mode": "route",
"route": "1",
"km": "110",
},
)
result = response.json()
best = result["data"]["results"][0]
print(f"{best['lat']}, {best['lng']} — charged {result['meta']['quota']['charged']}")
Example Response
{
"success": true,
"data": {
"results": [
{
"lat": "-34.90568300",
"lng": "-56.18820200",
"precision": "intersection",
"confidence": null,
"source": "internal",
"address": {
"street": "Avenida Brasil",
"corner": "Ellauri",
"neighborhood": "Pocitos",
"city": "Montevideo",
"department": "Montevideo",
"country": "Uruguay"
},
"formatted": "Avenida Brasil esq. Ellauri"
}
]
},
"meta": {
"providers_tried": ["internal"],
"resolved_by": "internal",
"cached": false,
"quota": {
"contracted": 5000,
"used": 1244,
"remaining": 3756,
"period_start": "2026-01-01T00:00:00",
"period_end": "2026-12-31T23:59:59",
"active": true,
"charged": 1
}
}
}
Autocomplete
Typeahead suggestions for a street name. Free — it returns no coordinates and never consumes a credit.
/apidev/v1/geocoding/autocompleteRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
country | string | Yes | Country (ID or name). |
department | string | No | Department / province (ID or name). Narrows the suggestions. |
street | string | Yes | Street fragment to match. Minimum 2 characters. |
limit | number | No | Max suggestions. Min 1, Max 20, default 10. |
Response Fields
data.suggestions is an array of { street, neighborhood?, city?, department? }. meta.cached indicates whether the result came from the short cache.
Code Example
- cURL
- JavaScript
- Python
curl -s -X POST "https://$TENANT/apidev/v1/geocoding/autocomplete" \
-H "Authorization: Bearer $TOKEN" \
-H "X-API-Key: $APIKEY" \
-H "tenant: $TENANT" \
-H "Content-Type: application/json" \
-d '{
"country": "Uruguay",
"department": "Montevideo",
"street": "bras"
}'
const response = await fetch(
`https://${TENANT}/apidev/v1/geocoding/autocomplete`,
{
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ country: "Uruguay", department: "Montevideo", street: "bras" }),
}
);
const { data } = await response.json();
data.suggestions.forEach((s) => console.log(s.street));
response = requests.post(
f"https://{TENANT}/apidev/v1/geocoding/autocomplete",
headers={**headers, "Content-Type": "application/json"},
json={"country": "Uruguay", "department": "Montevideo", "street": "bras"},
)
for s in response.json()["data"]["suggestions"]:
print(s["street"])
Example Response
{
"success": true,
"data": {
"suggestions": [
{
"street": "Avenida Brasil",
"neighborhood": "Pocitos",
"department": "Montevideo"
},
{
"street": "Brasilia",
"neighborhood": "Carrasco",
"department": "Montevideo"
}
]
},
"meta": {
"cached": false
}
}
Quota
Check your company's geocoding balance — contracted, used, remaining, and the active period. Free, and it never touches your allowance.
/apidev/v1/geocoding/quotaResponse Fields
| Field | Type | Description |
|---|---|---|
contracted | number | Total credits in the active period. |
used | number | Credits consumed in the active period. |
remaining | number | Credits left. |
period_start | string | null | Start of the active billing window. |
period_end | string | null | End of the active billing window. |
active | boolean | true when there is a valid contracted window. false (with remaining: 0) when geocoding is not contracted or the window has lapsed. |
Code Example
- cURL
- JavaScript
- Python
curl -s "https://$TENANT/apidev/v1/geocoding/quota" \
-H "Authorization: Bearer $TOKEN" \
-H "X-API-Key: $APIKEY" \
-H "tenant: $TENANT"
const response = await fetch(
`https://${TENANT}/apidev/v1/geocoding/quota`,
{ headers }
);
const { data } = await response.json();
console.log(`${data.remaining} of ${data.contracted} credits left`);
response = requests.get(
f"https://{TENANT}/apidev/v1/geocoding/quota",
headers=headers,
)
data = response.json()["data"]
print(f"{data['remaining']} of {data['contracted']} credits left")
Example Response
{
"success": true,
"data": {
"contracted": 5000,
"used": 1243,
"remaining": 3757,
"period_start": "2026-01-01T00:00:00",
"period_end": "2026-12-31T23:59:59",
"active": true
},
"meta": {}
}
Period dates are returned without timezone (e.g., "2026-01-01T00:00:00"). The value represents the company's configured timezone. Do not append Z or apply UTC conversion — display as-is.
Errors
All endpoints on this page may return these errors. See Error Handling for the full reference.
| Code | HTTP | Applies to | Description |
|---|---|---|---|
VALIDATION_ERROR | 400 | All | Invalid body fields (unknown field, wrong type, missing required field, batch over 25 items). Details in details[]. |
UNAUTHORIZED | 401 | All | Missing, invalid, or expired tenant / Authorization / X-API-Key. |
FORBIDDEN | 403 | All | The API key lacks the APICLI_GEOCODE permission. |
GEOCODING_NOT_CONTRACTED | 402 | Forward, Reverse, Internal | Geocoding is not contracted for your company, or the billing window has lapsed. Contract the geocoding service with LogicSat or renew the window. |
GEOCODING_QUOTA_EXCEEDED | 402 | Forward, Reverse, Internal | The geocoding balance for the period is exhausted. Wait for renewal or extend your contracted allowance with LogicSat. |
RATE_LIMITED | 429 | All | Exceeded 30 req/min. This is a time-based throttle, separate from the per-use quota. |
INTERNAL_ERROR | 500 | All | Unexpected server error. Retry; if it persists, contact support. |
The two 402 codes carry the current balance directly under error.details so your integration can tell exactly why the call was refused. The allowance is contracted with LogicSat — top it up or renew the window to keep geocoding.
{
"success": false,
"error": {
"code": "GEOCODING_QUOTA_EXCEEDED",
"message": "Geocoding quota exceeded",
"details": {
"contracted": 5000,
"used": 5000,
"remaining": 0,
"period_start": "2026-01-01T00:00:00",
"period_end": "2026-12-31T23:59:59",
"active": true
}
}
}
Related
- Authentication
- Rate Limits — Time-based throttling (separate from the per-use quota)
- Error Handling — Full error reference