You can call GetVisitor.info directly from the browser or route it through your own backend. Both work. The right choice depends on your stack and how often you're calling it per session.
Client-Side (CORS)
Drop a fetch() call into your frontend and you're done, no backend route needed. This fits static sites, Jamstack setups, and SPAs hosted on static storage.
The tradeoff is a cold start on the first call. A browser hitting a new cross-origin domain has to do DNS resolution, a TCP handshake, and a TLS handshake before it can send the actual request:
Total Latency ≈ DNS Lookup + (3 × RTT)
Browser (Client) GVI Edge Server
| |
|--- 1. DNS Resolution Request ----------------------->| (0.5 - 1 RTT)
| |
|--- 2. TCP SYN ------------------------------------->|
|<-- TCP SYN-ACK -------------------------------------| (1st RTT: TCP Handshake)
| |
|--- 3. TLS Client Hello ---------------------------->|
|<-- TLS Server Hello & Cert -------------------------| (2nd RTT: TLS 1.3 Handshake)
| |
|--- 4. HTTP GET / ---------------------------------->|
|<-- 200 OK (JSON Payload) ---------------------------| (3rd RTT: Data Transfer)
v v
On a typical connection with an 80ms RTT, that's 250 to 400ms before your script has the data. If your page layout depends on that response for pricing or banners, expect visible layout shift.
Backend API / Proxy Route
Route the request through your own server or edge function instead:
Browser Your Backend Server GVI Edge Server
| | |
|--- Standard HTTP Request ->| |
| |=== Persistent Warm Conn ======>| (1 RTT)
| |<-- Instant GVI Payload --------|
|<-- Rendered HTML / Data ---| |
Your backend keeps a warm, pooled connection to GetVisitor.info, so the DNS lookup and both handshakes are already done. The browser's connection to your own origin is also already warm from page load. Net result: roughly 2 RTTs total instead of the 3+ a cold cross-origin call needs.
This only holds for the first call in a session. If your frontend calls GVI multiple times in one page load, the browser socket from call #1 stays open and subsequent CORS calls are actually the fastest option. Since most integrations fetch location and tax data once per session, the backend route wins in practice almost every time.
Backend routing also gets you a few things CORS can't:
- A larger monthly quota. API lookups have their own quota, several times the CORS one, and no per-visitor-IP rate limit.
- Batch lookups. Up to 16 visitors in a single request.
- No domain registration. Your API key is the only credential, and it never leaves your server.
The tradeoff is you need a route handler or middleware, and you need to send the visitor's real IP and user agent in the request body. Behind a proxy or CDN, read the IP from X-Forwarded-For on your side first.
Comparison
| Client-Side (CORS) | Backend / Proxy | |
|---|---|---|
| Setup | Register a domain, add a fetch() call | Add a server route |
| First-request latency | ~3 RTT + DNS | ~2 RTT, warm connections |
| Repeat-request latency | Lowest, socket already open | Slightly higher |
| Monthly quota | Separate, smaller CORS quota | Larger API quota |
| Credentials | None in the browser; the registered domain is checked | API key, kept on your server |
| Best for | Static sites, SPAs, prototypes | High-traffic SaaS, SSR, checkout flows |
Code
Client-side:
const ACCOUNT_ID = "YOUR_ACCOUNT_ID";
// No key, no headers, no body. Your domain must be registered first.
async function fetchVisitorInfo() {
const response = await fetch(`https://${ACCOUNT_ID}.web.getvisitor.info/`);
return response.json();
}
Backend proxy (Express):
const ACCOUNT_ID = process.env.GVI_ACCOUNT_ID;
const API_KEY = process.env.GVI_API_KEY;
// fetch() reuses the connection to the same origin across calls.
app.get("/api/visitor-info", async (req, res) => {
const ip = req.headers["x-forwarded-for"]?.split(",")[0].trim() || req.socket.remoteAddress;
const gviResponse = await fetch(`https://${ACCOUNT_ID}.api.getvisitor.info/`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ ip, ua: req.headers["user-agent"] }),
});
res.json(await gviResponse.json());
});
Full request, response, limits and errors: CORS docs and API docs.
Use client-side if you have no backend or you're prototyping. Use a backend proxy for payments, SSR pricing pages, or anything where first-load speed and quota matter.
Either way, cache the response for the session instead of calling on every page load. Location, currency, and tax rarely change mid-session, so one call plus a stored result beats both approaches run repeatedly.