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
Locate one address, or the caller when ip is omitted.
| Parameter | In | Type | Description |
|---|---|---|---|
ip | query | string, optional | IPv4 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"
}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"Resolve 1-100 addresses in one call. Results come back in request order, and the whole batch counts as one request against your limit.
| Field | Type | Description |
|---|---|---|
ips | string[], required | 1-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.
| Plan | Requests / minute | Requests / day |
|---|---|---|
| Free | 60 | 5,000 |
| Growth | 300 | 50,000 |
| Scale | 1,200 | 500,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
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_ip, bad_request, batch_too_large | The input is not usable: malformed address, wrong body shape, or more than 100 batch items. |
| 401 | missing_key, invalid_key | No key was sent, or the key is unknown or revoked. |
| 403 | missing_scope | The key is valid but lacks geo:read. |
| 429 | rate_limited | Per-minute rate, daily quota, or platform backstop exceeded. Honor Retry-After. |
| 502 | upstream_error | The 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.
attribution string; keep it visible wherever you display city-level results.Changelog
| Date | Change |
|---|---|
| 2026-07-14 | v1 launch: GET /v1/geo, GET /v1/geo/{ip}, POST /v1/geo/batch with keys, scopes, rate limits and usage metering. |