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.
| Restriction | Your app must send | Example entry |
| Websites | Referer or Origin | *.example.com/* — subdomains only, add example.com/* for the bare domain |
| Android apps | X-Android-Package and X-Android-Cert | com.example.app;AB:CD:EF… |
| iOS apps | X-Ios-Bundle-Identifier | com.example.app |
| IP addresses | nothing — read from the connection | 198.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 →
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
| HTTP | error | When |
| 401 | invalid_api_key | Missing, malformed, revoked or unknown key or token. |
| 402 | payment_required | No credits left. Top up in your dashboard; GET /v1/usage is free and shows the balance. |
| 403 | key_restriction | Key used from a website, app or address it is not restricted to. |
| 403 | endpoint_not_allowed | Key is not scoped to this endpoint. See restricting a key. |
| 403 | key_suspended | Key suspended after passing its alert threshold. Resume it in the dashboard. |
| 422 | validation_failed | A field is wrong — lat −90..90, lng −180..180, 1–100 points, azimuth [0,360), and so on. See errors for which. |
| 400 | both_density_params | /v1/profile got both stepM and samples — pass one. |
| 400 | distance_out_of_range | /v1/profile line outside 1 m..50 km. |
| 400 | incomplete_azimuth_range | /v1/horizon got only one of fromAzimuthDeg/toAzimuthDeg. |
| 400 | incomplete_light_params | Only one of lightAzimuthDeg/lightAltitudeDeg on /v1/sun-exposure or /v1/grid. |
| 400 | unknown_dataset · invalid_bbox · grid_too_large | /v1/grid: dataset not recognised, min ≥ max, or the window exceeds 4096 cells. |
| 429 | rate_limit_exceeded | An account window (hour / day / week) is full. Back off; honour Retry-After. |
| 429 | key_daily_limit | This key hit the daily cap you set on it. Other keys keep working. |
| 429 | install_daily_limit | A single mobile install hit its per-device cap. Other installs are unaffected. |
| 503 | service_unavailable | Terrain/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.