Docs Pricing FAQ Dashboard
Docs

CORS / Web calls

Browser-side lookups from a registered domain, no API key: request and response shape, headers, limits and errors.

Call the API straight from your frontend with no API key and no backend. The browser is the visitor, so the API reads the IP and user agent from the request itself.

GEThttps://<ACCOUNT_ID>.web.getvisitor.info/

Your ACCOUNT_ID and full endpoint URL are shown in the CORS domains section of your dashboard.

Before you start: register your domain

CORS calls are authenticated by the page's origin. Add every domain that will call the API under CORS domains in your dashboard. Requests from any other origin are rejected.

Request

A plain GET to /. No body, no query string, no API key.

HeaderSet byUsed for
OriginBrowserAuthentication. Must match a registered domain
User-AgentBrowserThe device fields

The visitor's IP comes from the connection itself. Don't add custom headers like Authorization or Content-Type. They make the browser send a preflight OPTIONS request, which this endpoint doesn't answer, and the call fails.

const ACCOUNT_ID = "YOUR_ACCOUNT_ID";

// No key, no body. The browser IS the visitor, so the API reads ip/ua
// straight off the request. Your domain must be registered first (see
// your dashboard) or this is rejected.
const response = await fetch(`https://${ACCOUNT_ID}.web.getvisitor.info/`);
const data = await response.json();
console.log(data);

With error handling:

async function getVisitor() {
  try {
    const response = await fetch(`https://${ACCOUNT_ID}.web.getvisitor.info/`);
    if (!response.ok) return null;
    return await response.json();
  } catch {
    // Rejected (unregistered domain, quota, rate limit) or network failure.
    return null;
  }
}

Response

200 OK with a single JSON object, the same shape as an API call response.

{
  "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 headers:

HeaderValue
Access-Control-Allow-OriginYour page's origin, echoed back. Never *
Cache-Controlprivate, max-age=900
VaryOrigin

The browser caches the response for 15 minutes. Calling it again on the next page load or component mount is served from the browser cache, with no request and no lookup counted.

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

Limits

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

LimitFreePaid, per unit
CORS lookups per month1,0001,000,000
Registered domains13
Requests per minute, per visitor IP1060 (fixed)
Requests per minute, whole account1001,000
Max lookups per request11

CORS lookups have their own monthly quota, separate from API calls. The account-wide request rate is shared between the two. There are no connection limits on CORS calls.

Going over a rate limit returns 429. Traffic that stays well above the limit (about 20% over) is dropped without a response.

Seeing rate limit errors regularly, even while under your monthly quota? It's probably time to add units: each unit raises your quota and account-wide request rate. The per-visitor IP limit is fixed; if that's the one you hit, call the API less often per visitor. See Units & billing.

Errors

Error responses don't carry an Access-Control-Allow-Origin header, so the browser blocks them. fetch() rejects with a TypeError and your code can't read the status. Check the Network tab in your browser's dev tools to see the real status and body.
StatusBodyCause
403missing or disallowed OriginThe page's domain isn't registered on this account, or the request has no Origin header (e.g. called from a server or opened directly in a tab)
403over quota: see https://getvisitor.info/docs/billing.htmlMonthly CORS lookup quota used up. Add units or wait for the next period. See Units & billing
404Unknown ACCOUNT_ID in the hostname
429Rate limit hit. See the X-Rate-Limit-Reason header below

429 reasons

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

X-Rate-Limit-ReasonMeaning
cors-per-ip-request-rate-exceededToo many requests per minute from one visitor IP
global-request-rate-exceededToo many requests per minute for your whole account