Docs Pricing FAQ Dashboard
Docs

API calls

Server-side lookups with your API key: request and response shape, headers, limits and errors.

Call the API from your own backend. Your server sends the visitor's IP and user agent and gets back location, device, network and commerce data.

POSThttps://<ACCOUNT_ID>.api.getvisitor.info/

Your ACCOUNT_ID and full endpoint URL are shown in the API keys section of your dashboard. The account ID is not a secret. The API key is.

Never call this endpoint from a web page or mobile app. Anything shipped to a browser or app can be read by anyone, and your API key would be exposed with it. Whoever has the key can spend your quota. Keep the key on your server, and use CORS / Web calls for client-side lookups. If a key is ever exposed, revoke it in your dashboard and issue a new one.

Headers

HeaderRequiredValue
AuthorizationYesBearer gv_YOUR_KEY. The key must belong to the same account as the ACCOUNT_ID in the hostname
Content-TypeRecommendedapplication/json. The body is always parsed as JSON

Request body

A JSON object with the visitor's IP and user agent. Both are optional. Leave one out and the fields that depend on it come back null.

{
  "ip": "84.198.10.20",
  "ua": "Mozilla/5.0 (Linux; Android 14; SM-S928B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36"
}
FieldTypeMeaning
ipstringVisitor IP, IPv4 or IPv6
uastringVisitor's User-Agent header

Batch requests

Send a JSON array of up to 16 objects to look up several visitors in one call. The response is an array in the same order. Each item counts as one lookup toward your monthly quota. A batch with more than 16 items is rejected with 413, and nothing is counted.

[
  { "ip": "84.198.10.20", "ua": "Mozilla/5.0 ..." },
  { "ip": "2a02:c7c:1234::1", "ua": "Mozilla/5.0 ..." }
]

An array item that isn't an object gets an error in its slot, and the other items still succeed:

[
  { "location": { ... }, ... },
  { "error": "invalid item: expected an object" }
]

Response

200 OK with Content-Type: application/json. A single object for a single request, an array for a batch.

{
  "location": {
    "country": "Germany",
    "country_code": "DE",
    "continent": "Europe",
    "eu": true,
    "state": "Hesse",
    "city": "Frankfurt am Main",
    "languages": [{ "code": "de", "rtl": false }],
    "privacy_regime": { "name": "GDPR", "consent": "OPT IN" }
  },
  "device": {
    "type": "mobile",
    "os": { "name": "Android", "version": "14" },
    "browser": { "name": "Chrome", "version": "126" }
  },
  "asn": { "id": 3209, "name": "Vodafone GmbH", "threat_level": "normal" },
  "commerce": {
    "currency": { "code": "EUR", "symbol": "€", "fx_usd": 1.16, "fx_eur": 1.0 },
    "buying_power": 0.79,
    "sales_tax": {
      "label": "VAT",
      "rate": 19.0,
      "rate_with_local_addons": { "min": 19.0, "max": 19.0 },
      "threshold": 0.0
    }
  },
  "lookup_latency_ms": 0.04
}

Response fields

Any field can be null when the data isn't known for that visitor. Without an IP, every location, asn and commerce field is null. Without a user agent, device is null.

FieldTypeMeaning
location.countrystringCountry name, e.g. "Germany"
location.country_codestringISO 3166-1 alpha-2 code, e.g. "DE"
location.continentstringContinent name
location.eubooleanWhether the country is in the EU
location.statestringState, region or province
location.citystringCity name
location.languagesarrayLanguages spoken in the country, primary first. Each has code and rtl (right-to-left script)
location.privacy_regimeobjectThe privacy law that applies (name, e.g. "GDPR") and consent: "OPT IN" or "OPT OUT"
device.typestringe.g. "desktop", "mobile", "tablet"
device.osobjectname and version
device.browserobjectname and version
asn.idintegerAutonomous system number of the visitor's network
asn.namestringNetwork operator
asn.threat_levelstring"normal", or "elevated" for known hosting, datacenter or VPN networks
commerce.currencyobjectLocal currency: code, symbol, and FX rates fx_usd / fx_eur (refreshed daily)
commerce.buying_powernumberPurchasing power vs. a US baseline of 1.0. 0.79 means 21% less buys the same
commerce.sales_tax.labelstringTax name (VAT, GST, ...). null if no tax applies
commerce.sales_tax.ratenumberStandard rate, in percent
commerce.sales_tax.rate_with_local_addonsobjectmin/max rate once local or provincial add-ons are included, already summed
commerce.sales_tax.thresholdnumberSeller registration threshold, in local currency
lookup_latency_msnumberServer-side time the lookup took

Examples

Every example below keeps one client or connection alive and reuses it across calls. Do the same in your code. Opening a new connection per request roughly triples latency and quickly runs into the connection rate limit below.

ACCOUNT_ID=YOUR_ACCOUNT_ID
API_KEY=gv_YOUR_KEY

curl https://$ACCOUNT_ID.api.getvisitor.info/ \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ip": "84.198.10.20",
    "ua": "Mozilla/5.0 (Linux; Android 14; SM-S928B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36"
  }'

Limits

Paid limits scale with the number of units on your subscription.

LimitFreePaid, per unit
Lookups per month6,0005,000,000
Requests per minute1001,000
New connections per minute10100
Concurrent connections28
Max lookups per request16 (fixed)16 (fixed)
Request body size10 KB10 KB

The request rate limit is shared with CORS / Web calls. CORS lookups have their own monthly quota.

Going over a rate limit returns 429. Traffic that stays well above the limit (about 20% over) is dropped without a response. Back off when you see a 429 and honor Retry-After.

Seeing rate limit errors regularly, even while under your monthly quota? It's probably time to add units: each unit raises every limit above, not just the quota. See Units & billing.

Why there's a limit on new connections

Each new connection costs a TCP and a TLS handshake. That roughly triples your lookup latency, and costs us far more CPU than the lookup itself. With keep-alive, one connection carries many requests, so the new connections limit (100/min per unit) is set well below the request limit (1,000/min). The concurrent connections limit (8 per unit) counts every open connection, idle ones included.

Going over returns 429 (bearer-connection-rate-exceeded or bearer-concurrent-connections-exceeded). Well past the limit, new connections are dropped during the handshake.

Errors

StatusBodyCause
400{"error": "invalid JSON body"}The body isn't valid JSON
401invalid or missing API keyNo Authorization header, an unknown or revoked key, or a key from a different account than the ACCOUNT_ID in the hostname
403over quota: see https://getvisitor.info/docs/billing.htmlMonthly lookup quota used up. Add units or wait for the next period. See Units & billing
404Unknown ACCOUNT_ID in the hostname
413request too largeRequest body over 10 KB
413{"error": "too many lookups: max 16 per request"}Batch array with more than 16 items
404{"error": "not found"}Path other than /
429Rate limit hit. See the X-Rate-Limit-Reason header below

Error bodies are plain text, except 400, 404 not found and 413 too many lookups, which are JSON.

429 reasons

Every 429 carries Retry-After: 60 and an X-Rate-Limit-Reason header:

X-Rate-Limit-ReasonMeaning
global-request-rate-exceededToo many requests per minute for your account
bearer-connection-rate-exceededToo many new connections per minute. Reuse a keep-alive connection instead of opening one per request
bearer-concurrent-connections-exceededToo many connections open at once. The server closes this connection

A connection that keeps sending requests after repeated rejections (limit, quota, unknown account) is closed by the server.