Geo API

Live

Self-hosted IP geolocation on Bolrach-owned data (ip-location-db CC0 country ranges + DB-IP Lite CC-BY city data). No third-party lookup service is involved: your traffic never leaves the Bolrach platform.

Base URL https://api.bolrach.ioScope geo:readSpec openapi/geo.json

Authentication

Pass your key on every request, either as Authorization: Bearer bt_... or as an x-api-key header. Keys are scoped; the Geo API needs geo:read. A missing key returns 401, a key without the scope returns 403.

curl "https://api.bolrach.io/v1/geo" \
  -H "Authorization: Bearer bt_your_key_here"

Calling /v1/geo with no ip parameter locates the caller, which doubles as the fastest way to test a fresh key.

Endpoints

GET /v1/geo

Locate one address, or the caller when ip is omitted.

ParameterInTypeDescription
ipquerystring, optionalIPv4 or IPv6 address. Omit to locate your own public address.
curl "https://api.bolrach.io/v1/geo?ip=8.8.8.8" \
  -H "Authorization: Bearer bt_your_key_here"

Response

{
  "ip": "8.8.8.8",
  "country": "US",
  "region": "California",
  "city": "Mountain View",
  "lat": 37.422,
  "lon": -122.085,
  "attribution": "IP geolocation by DB-IP (https://db-ip.com) and ip-location-db"
}
GET /v1/geo/{ip}

The same lookup with the address in the path. IPv6 works unescaped.

curl "https://api.bolrach.io/v1/geo/2001:4860:4860::8888" \
  -H "Authorization: Bearer bt_your_key_here"
POST /v1/geo/batch

Resolve 1-100 addresses in one call. Results come back in request order, and the whole batch counts as one request against your limit.

FieldTypeDescription
ipsstring[], required1-100 IPv4/IPv6 addresses.
curl -X POST "https://api.bolrach.io/v1/geo/batch" \
  -H "Authorization: Bearer bt_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"ips": ["8.8.8.8", "1.1.1.1"]}'

Response

{
  "count": 2,
  "results": [
    { "ip": "8.8.8.8", "country": "US", "region": "California", "city": "Mountain View", "lat": 37.422, "lon": -122.085, "attribution": "..." },
    { "ip": "1.1.1.1", "country": "AU", "region": "New South Wales", "city": "Sydney", "lat": -33.8688, "lon": 151.209, "attribution": "..." }
  ]
}

Result fields

country resolves for both IPv4 and IPv6. region, city, lat and lon come from the city-level dataset, which currently covers IPv4; for IPv6 they return null with the country still filled in. Private and unrouted ranges resolve to all-null fields.

Rate limits and quotas

Limits apply per key. Every authenticated response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets); a 429 adds Retry-After.

PlanRequests / minuteRequests / day
Free605,000
Growth30050,000
Scale1,200500,000

Errors

Every error, on every service, uses one envelope:

{
  "error": {
    "code": "rate_limited",
    "message": "You hit the rate limit for your plan (free). Retry in 13s.",
    "status": 429
  }
}
StatusCodeMeaning
400invalid_ip, bad_request, batch_too_largeThe input is not usable: malformed address, wrong body shape, or more than 100 batch items.
401missing_key, invalid_keyNo key was sent, or the key is unknown or revoked.
403missing_scopeThe key is valid but lacks geo:read.
429rate_limitedPer-minute rate, daily quota, or platform backstop exceeded. Honor Retry-After.
502upstream_errorThe geo backend did not answer. Safe to retry with backoff.

Data and attribution

Lookups run entirely on Bolrach-hosted datasets: country ranges from ip-location-db (CC0) and city-level data from DB-IP Lite (CC-BY 4.0). Your queries never touch a third-party lookup service.

The CC-BY license behind the city data requires attribution. Every response includes an attribution string; keep it visible wherever you display city-level results.

Changelog

DateChange
2026-07-14v1 launch: GET /v1/geo, GET /v1/geo/{ip}, POST /v1/geo/batch with keys, scopes, rate limits and usage metering.