GeoInsight API Docs Log in Get an API key

GeoInsight API

Terrain data (Copernicus GLO-30) over a simple HTTP API — point elevation, terrain profiles and sky-object visibility. Every endpoint lives under /v1, so a full URL is https://geoinsight.dev/v1/<endpoint> — for example https://geoinsight.dev/v1/timezone. The base URL never includes /v1 itself.

Getting started

  1. Create an account and verify your email.
  2. Create an API key in your dashboard — the secret is shown once.
  3. Call the API with the X-API-Key header. New accounts start with free credits.

Authentication

Send your key in the X-API-Key header on every request:

X-API-Key: gi_live_...

A missing or invalid key returns 401. Manage keys in your dashboard.

Restricting a key

Every key can be locked to where it is allowed to be used, and capped at how much it may spend per day. Both are set per key in your dashboard and take effect within seconds. A key used outside its restriction returns 403 with "error": "key_restriction" — the key itself is still valid, so do not treat this as an authentication failure.

RestrictionYour app must sendExample entry
WebsitesReferer or Origin*.example.com/* — subdomains only, add example.com/* for the bare domain
Android appsX-Android-Package and X-Android-Certcom.example.app;AB:CD:EF…
iOS appsX-Ios-Bundle-Identifiercom.example.app
IP addressesnothing — read from the connection198.51.100.0/24

Endpoints a key may call

Independently of where a key may be used, you can pick what it may call. Untick an endpoint and it returns 403 with "error": "endpoint_not_allowed". This is the cheapest cap there is, because costs differ by two orders of magnitude: a key narrowed to /v1/elevation (1 credit) cannot be turned on /v1/grid (103 credits a call). A key with nothing unticked can call everything, including endpoints we add later.

GET /v1/usage and POST /v1/token stay reachable from any scope — both are free, and a key that cannot check its own balance or refresh its session is worse for you without protecting anything.

Daily limit per key

A key with a daily limit carries X-RateLimit-Limit-Key-Day and X-RateLimit-Remaining-Key-Day on every response. Exceeding it returns 429 with "error": "key_daily_limit" and a Retry-After pointing at the next UTC midnight. Only successful calls count, and the limit applies to that key alone — your other keys keep working, which is what makes it useful for containing a leak.

Getting told before it hurts

Separately from the hard limit, a key can carry an alert threshold: the first time it passes that many credits in a UTC day we email the account owner and let the request through. One message per key per day, no matter how much traffic follows. Set it below the daily limit — a runaway key is worth hearing about before it is cut off, not after.

You can also ask us to suspend the key when it passes that threshold, which turns the warning into a response: the key stops answering until you resume it from the dashboard, and calls return 403 with "error": "key_suspended". Suspending is reversible and affects that key only; revoking is permanent. Leave it off if downtime costs you more than the credits would.

Short-lived tokens

Exchange a key for a token that expires in 15 minutes:

curl -X POST -H "X-API-Key: gi_live_..." \
  -H 'Content-Type: application/json' -d '{"deviceId":"optional"}' \
  https://geoinsight.dev/v1/token

{"token": "gi_tok_...", "expires_in": 900, "header": "X-API-Key"}

Send the token in the same X-API-Key header. It inherits everything from the key behind it — restrictions, endpoint scope, daily limit — read fresh on every request, so suspending or narrowing the key takes effect at once rather than when tokens expire. Revoking the key kills its tokens immediately. Exchanging costs no credits, and a token cannot mint another token.

Be clear about what this buys you: we hand out a token in exchange for a key, so whoever holds the key can mint tokens too. The gain is that a token captured from a device is worthless tomorrow. The full gain comes when the key never reaches the device at all — see below.

Rotating and running several keys

You can hold as many keys as you like, all billing the same account. That is how you rotate without downtime: issue a new key, move traffic to it, and revoke the old one once nothing uses it — no account reset, no gap. Revoking is instant and affects only that key. If you suspect a key leaked, revoke it and issue a replacement; the rest keep working. Manage all of this in your dashboard.

If your key ships to a client you do not control

This covers mobile apps and browser / single-page apps alike: any client where the key leaves your servers can be decompiled or sniffed, and the key extracted. Because credits are prepaid, a leaked key spends real money, so this matters more than a plain rate limit.

Be honest about what the restrictions above buy you here. App package, bundle-id and referrer / Origin headers are all sent by the client, so someone who extracts your key can send them too — they stop casual copy-and-paste reuse but are not a cryptographic guarantee against a determined attacker. IP restriction is the exception: the address comes from the connection reaching our proxy, not from a header, so a copied key cannot talk its way past it.

The robust pattern is therefore the same for web and mobile: do not ship the key at all. Keep it on your own server, restrict it by IP, and have the client call your backend, which either proxies the request or mints a short-lived token for the client to use directly. Anything scraped from the client then expires on its own. If you have no backend at all, scope the key to only the endpoints you call, set a daily limit so a leak is bounded, and — on mobile — register per-install with attestation so the app carries no shared key. Full mobile guide →

Rate limits & credits

Each request costs credits, which you buy up front — there is no subscription. Run out and the API returns 402 until you top up. On top of that, throughput is smoothed across three windows — hour, day and week — sized by your highest purchased tier. Every response carries three headers per window — X-RateLimit-Limit-Hour, X-RateLimit-Remaining-Hour and X-RateLimit-Reset-Hour (and the same for Day / Week). Reset is a Unix epoch (seconds) — read Remaining and back off before it hits zero, using Reset to know when the window frees up. Exceeding a window returns 429 with a Retry-After header (seconds). See what each call costs below and check your balance any time with GET /v1/usage.

PackageCreditsHourDayWeek
free 2,000 200 1,000 3,000
starter 100,000 350 3,500 10,000
pro 1,000,000 2,000 20,000 60,000
business 5,000,000 10,000 100,000 300,000
scale 10,000,000 20,000 200,000 600,000
enterprise 25,000,000 50,000 500,000 1,500,000

Cost per call

Only successful (2xx) responses are billed. Rejected requests (validation 400/422) and temporary outages (503) cost nothing and do not count against your limits.

EndpointCost per successful call
GET /v1/elevation1 unit
POST /v1/elevation1 unit per point (N points = N units, max 100)
GET /v1/light-pollution1 unit per point
POST /v1/light-pollution1 unit per point (N points = N units, max 100)
GET /v1/profile2 units for the endpoints (first + last), then 1 per 10 interior samples, rounded up — i.e. 2 + ceil((N−2)/10) for N returned samples (2–512)
GET /v1/visibility1 unit
GET /v1/horizon1 unit per 10 returned azimuths, rounded up (full 360° @ 1° = 36)
GET /v1/grid1 unit per 10 cells for elevation/terrain, 1 per 100 cells for light-pollution, rounded up — capped at the number of native raster pixels inside your bbox (30 m for elevation/terrain, 463 m for light pollution), so asking for a grid finer than the data costs no more than the data itself
GET /v1/sun-exposure1 unit per point
POST /v1/sun-exposure1 unit per point (N points = N units, max 100)
GET /v1/timezone1 unit
GET /v1/usage0 — never counts against limits
GET /health0 — no API key required

Worked examples:

  • Single point elevation → 1 unit.
  • Batch of 40 points → 40 units.
  • Profile with stepM=50 over a 3 km line → ~61 points → 2 + ⌈59/10⌉ = 8 units.
  • Profile with samples=100 → 2 + ⌈98/10⌉ = 12 units.
  • Profile with just the two endpoints (samples=2) → 2 units.
  • 1,000 visibility checks → 1,000 units.
  • Light pollution overlay for a 0.6°×0.8° region at 50×50 → 2,500 cells ÷ 100 = 25 units.
  • The same overlay zoomed to a 5×5 km city at 64×64 → 4,096 cells requested, but VIIRS only has 13×11 = 143 pixels there → ⌈143/100⌉ = 2 units.
  • Elevation grid 64×64 over 550×500 m → 19×17 = 323 native pixels → ⌈323/10⌉ = 33 units, not 410.

Endpoints

Each endpoint has its own page with request parameters, code examples (curl, Python, JavaScript) and a table describing every response field.

Service status

GET /health

Public service status. No API key required.

Request

No parameters and no authentication.

Example

curl "https://geoinsight.dev/health"

Response

{
      "status": "ok",
      "checks": { "database": "ok", "redis": "ok" }
    }
Field Type Unit Description
status string Overall service status. ok only when every dependency below is ok.
checks object Per-dependency status, keyed by component name.
checks.database string Postgres connectivity — the store of record for credit balances.
checks.redis string Redis connectivity, used for rate-limit windows. Note that a Redis outage does not block API calls: windows fail open, while credits keep being enforced.

Caching

We do not send Cache-Control or ETag headers — caching is your call. What is safe to cache differs by endpoint, because some answers never change and some are tied to a moment in time.

EndpointCache
/v1/elevation, /v1/profile, /v1/visibility, /v1/horizon, /v1/gridFreely, per rounded coordinate. These read a fixed terrain model (Copernicus GLO-30) — the ground does not move, so the same inputs always return the same answer. Cache indefinitely; the only reason to refresh is if we announce a dataset update.
/v1/light-pollutionFreely, per coordinate. Backed by an annual VIIRS raster; a yearly cache refresh is more than enough.
/v1/sun-exposureOnly against the same time. Terrain aspect is static, but the sun position — and therefore illumination — depends on the timestamp you pass.
/v1/timezoneSplit. The timezone id is static per coordinate — cache it forever. The offset / DST / local-time fields depend on at — cache those only against the exact at. See the timezone page.
/v1/usageNever. It reflects your live balance and rate-limit windows.

Rounding coordinates before you cache (e.g. to 5 decimal places, ~1 m) collapses near-duplicate lookups into one entry without changing the answer meaningfully.

Errors

Every error carries the same shape: a machine-readable error code you can branch on, and a human-readable message you can show or log. Switch on error, never on message — the text may change, the code will not.

{ "error": "endpoint_not_allowed", "message": "This API key is not allowed to call this endpoint" }

Validation failures (422) add one field on top — errors, a map of field name to the messages for it:

{
  "error": "validation_failed",
  "message": "The lat field is required.",
  "errors": { "lat": ["The lat field is required."] }
}

Every error code

HTTPerrorWhen
401invalid_api_keyMissing, malformed, revoked or unknown key or token.
402payment_requiredNo credits left. Top up in your dashboard; GET /v1/usage is free and shows the balance.
403key_restrictionKey used from a website, app or address it is not restricted to.
403endpoint_not_allowedKey is not scoped to this endpoint. See restricting a key.
403key_suspendedKey suspended after passing its alert threshold. Resume it in the dashboard.
422validation_failedA field is wrong — lat −90..90, lng −180..180, 1–100 points, azimuth [0,360), and so on. See errors for which.
400both_density_params/v1/profile got both stepM and samples — pass one.
400distance_out_of_range/v1/profile line outside 1 m..50 km.
400incomplete_azimuth_range/v1/horizon got only one of fromAzimuthDeg/toAzimuthDeg.
400incomplete_light_paramsOnly one of lightAzimuthDeg/lightAltitudeDeg on /v1/sun-exposure or /v1/grid.
400unknown_dataset · invalid_bbox · grid_too_large/v1/grid: dataset not recognised, min ≥ max, or the window exceeds 4096 cells.
429rate_limit_exceededAn account window (hour / day / week) is full. Back off; honour Retry-After.
429key_daily_limitThis key hit the daily cap you set on it. Other keys keep working.
429install_daily_limitA single mobile install hit its per-device cap. Other installs are unaffected.
503service_unavailableTerrain/timezone data source briefly unreachable. Costs nothing; retry with backoff.

Registration and token endpoints add a few more (registration_refused, too_many_registrations, token_exchange_requires_key) — documented where they occur, in the mobile guide. Rate-limit headers accompany every response: see rate limits.