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.
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.
- Register the hostname only, e.g.
example.com. Scheme and port are ignored. - Matching is exact.
www.example.comandapp.example.comare separate domains fromexample.com. - Changes take effect within a few minutes of saving.
Request
A plain GET to /. No body, no query string, no API key.
| Header | Set by | Used for |
|---|---|---|
Origin | Browser | Authentication. Must match a registered domain |
User-Agent | Browser | The 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:
| Header | Value |
|---|---|
Access-Control-Allow-Origin | Your page's origin, echoed back. Never * |
Cache-Control | private, max-age=900 |
Vary | Origin |
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.
| Field | Type | Meaning |
|---|---|---|
location.country | string | Country name, e.g. "Germany" |
location.country_code | string | ISO 3166-1 alpha-2 code, e.g. "DE" |
location.continent | string | Continent name |
location.eu | boolean | Whether the country is in the EU |
location.state | string | State, region or province |
location.city | string | City name |
location.languages | array | Languages spoken in the country, primary first. Each has code and rtl (right-to-left script) |
location.privacy_regime | object | The privacy law that applies (name, e.g. "GDPR") and consent: "OPT IN" or "OPT OUT" |
device.type | string | e.g. "desktop", "mobile", "tablet" |
device.os | object | name and version |
device.browser | object | name and version |
asn.id | integer | Autonomous system number of the visitor's network |
asn.name | string | Network operator |
asn.threat_level | string | "normal", or "elevated" for known hosting, datacenter or VPN networks |
commerce.currency | object | Local currency: code, symbol, and FX rates fx_usd / fx_eur (refreshed daily) |
commerce.buying_power | number | Purchasing power vs. a US baseline of 1.0. 0.79 means 21% less buys the same |
commerce.sales_tax.label | string | Tax name (VAT, GST, ...). null if no tax applies |
commerce.sales_tax.rate | number | Standard rate, in percent |
commerce.sales_tax.rate_with_local_addons | object | min/max rate once local or provincial add-ons are included, already summed |
commerce.sales_tax.threshold | number | Seller registration threshold, in local currency |
lookup_latency_ms | number | Server-side time the lookup took |
Limits
Paid limits scale with the number of units on your subscription.
| Limit | Free | Paid, per unit |
|---|---|---|
| CORS lookups per month | 1,000 | 1,000,000 |
| Registered domains | 1 | 3 |
| Requests per minute, per visitor IP | 10 | 60 (fixed) |
| Requests per minute, whole account | 100 | 1,000 |
| Max lookups per request | 1 | 1 |
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.
Errors
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.| Status | Body | Cause |
|---|---|---|
403 | missing or disallowed Origin | The 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) |
403 | over quota: see https://getvisitor.info/docs/billing.html | Monthly CORS lookup quota used up. Add units or wait for the next period. See Units & billing |
404 | Unknown ACCOUNT_ID in the hostname | |
429 | Rate 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-Reason | Meaning |
|---|---|
cors-per-ip-request-rate-exceeded | Too many requests per minute from one visitor IP |
global-request-rate-exceeded | Too many requests per minute for your whole account |