Geolocation

Detect the visitor country with IPinfo or the Cloudflare CF-IPCountry header, then use it for country-based pricing and location-aware Stripe checkout sessions.

This template detects the visitor's location for features like dynamic pricing, localized content, and country-specific experiences. It has two providers: IPinfo, which looks up the visitor's IP address, and Cloudflare, which reads the country from the CF-IPCountry header when your site runs behind the Cloudflare proxy.

Overview

The geolocation integration provides:

  • Location detection - Country with either provider; city, region and timezone with IPinfo.
  • One API endpoint - /api/ip returns the current visitor's location.
  • Server-side processing - Secure API key management.
  • Privacy-focused - No personal data storage required.

Configuration

Pick the provider with NUXT_GEO_PROVIDER. The server reads it at startup, so switching providers takes a restart, not a rebuild.

ValueSourceReturns
ipinfo (default)IPinfo API lookup of the visitor's IPThe full IPinfo payload
cloudflareThe CF-IPCountry request header, no API callip and country only

The value is case-insensitive. An empty value means ipinfo; any other value logs a warning at startup and uses ipinfo.

IPinfo

Add your IPinfo API token to .env:

.env
NUXT_GEO_PROVIDER="ipinfo"
IPINFO_TOKEN="your_ipinfo_token"
Sign up at IPinfo to get your token. The free tier (IPinfo Lite) includes unlimited API requests with 7 essential IP attributes (country, continent, ASN). Paid plans start at $49/month for city-level accuracy and privacy detection.

The visitor's IP comes from X-Forwarded-For. Your reverse proxy must overwrite that header, or a visitor can send any IP they like.

Cloudflare

.env
NUXT_GEO_PROVIDER="cloudflare"

In the Cloudflare dashboard, turn on IP Geolocation under Network, or enable the Add visitor location headers managed transform. Either one adds CF-IPCountry to every request Cloudflare proxies to your origin. No token is needed.

Cloudflare sends XX when it has no country for the visitor and T1 for Tor. The endpoint treats both, and a missing or malformed header, as an unknown country.

Anyone who can reach your origin directly can send their own CF-IPCountry header. Configure your host or firewall to accept traffic from Cloudflare's IP ranges only.

API endpoint

/api/ip returns the location of the current visitor.

  • ipinfo: { ip, hostname, city, region, country, loc, org, postal, timezone }, depending on your IPinfo plan
  • cloudflare: { ip, country }

When the provider has no answer, or the IPinfo lookup fails, the endpoint answers 204 No Content with an empty body. IPinfo answers private and loopback addresses (local development, or a proxy that does not forward the visitor IP) with 200 and { ip, bogon: true }, so check for country rather than the status code.

Server code calls the same function the endpoint uses, getVisitorGeo(event) from server/services/geo-server-service.ts. It respects NUXT_GEO_PROVIDER, never throws, and returns null for an unknown location.

Usage examples

Get current user's location

City and timezone come from IPinfo only. With the cloudflare provider, ipInfo holds just ip and country.

<script setup>
const { data: ipInfo } = await useFetch('/api/ip')
</script>

<template>
  <div>
    <p>Country: {{ ipInfo?.country }}</p>
    <p>City: {{ ipInfo?.city }}</p>
    <p>Timezone: {{ ipInfo?.timezone }}</p>
  </div>
</template>

Dynamic pricing by country

Detect user's country and show appropriate currency:

<script setup>
const { data: ipInfo } = await useFetch('/api/ip')

const currency = computed(() => {
  const country = ipInfo.value?.country
  if (country === 'GB') return 'GBP'
  if (['DE', 'FR', 'ES', 'IT'].includes(country)) return 'EUR'
  return 'USD'
})

const price = computed(() => {
  const prices = { USD: 29, EUR: 25, GBP: 22 }
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: currency.value,
  }).format(prices[currency.value])
})
</script>

<template>
  <div>{{ price }}/month</div>
</template>

Stripe checkout with location-based pricing

Look up the visitor's country in your checkout endpoint:

server/api/stripe/create-checkout-session.post.ts
import { getVisitorGeo } from '~~/server/services/geo-server-service'

export default defineEventHandler(async event => {
  const { plan, interval } = await readBody(event)

  // Get the visitor's location (null when unknown)
  const geo = await getVisitorGeo(event)

  // Map country to Stripe price ID
  // You'll need to create price IDs for each currency in Stripe
  const priceId = getPriceIdForCountry(plan, interval, geo?.country)
  
  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [{ price: priceId, quantity: 1 }],
    // ...
  })

  return { url: session.url }
})

Additional use cases

Localized content

Show different content or messages based on user's country:

<script setup>
const { data: ipInfo } = await useFetch('/api/ip')

const welcomeMessage = computed(() => {
  const messages = {
    US: 'Welcome! 🇺🇸',
    DE: 'Willkommen! 🇩🇪',
    FR: 'Bienvenue! 🇫🇷',
  }
  return messages[ipInfo.value?.country] || 'Welcome!'
})
</script>

Regional restrictions

Restrict access based on location in server middleware:

server/middleware/geo-check.ts
import { getVisitorGeo } from '~~/server/services/geo-server-service'

export default defineEventHandler(async event => {
  // Middleware runs on every request, and in ipinfo mode each lookup is an API call,
  // so gate only the routes you need to restrict
  if (!event.path.startsWith('/checkout')) return

  const geo = await getVisitorGeo(event)

  const blockedCountries = ['AA', 'BB']
  if (geo?.country && blockedCountries.includes(geo.country)) {
    throw createError({
      statusCode: 403,
      message: 'Service not available in your region',
    })
  }
})

The detected country is a hint that a visitor can spoof. Base pricing or access decisions on it only when your origin accepts traffic from Cloudflare alone (cloudflare) or your proxy overwrites X-Forwarded-For (ipinfo).

User preference override

Allow users to manually select their currency/location. PricingPlans and LandingPricingTable include a currency selector backed by the prices store; pass show-currency-selector to turn it on. This cookie-based example shows the pattern for your own components:

<script setup>
const { data: ipInfo } = await useFetch('/api/ip')
const userCurrency = useCookie('user-currency')

const currency = computed({
  get: () => userCurrency.value || getCurrencyFromCountry(ipInfo.value?.country),
  set: (value) => { userCurrency.value = value }
})
</script>

<template>
  <Select v-model="currency">
    <SelectItem value="USD">USD</SelectItem>
    <SelectItem value="EUR">EUR</SelectItem>
    <SelectItem value="GBP">GBP</SelectItem>
  </Select>
</template>

Privacy and performance

GDPR compliance: IP-based geolocation is generally acceptable under GDPR as IPs are used for technical purposes. Include geolocation usage in your privacy policy.

Caching: The free tier provides unlimited API requests, but consider caching IP lookups to improve performance and reduce latency.

Error handling: When the country is unknown in either mode, the pricing tables show paymentsConfig.defaultCurrency. With show-currency-selector on, the visitor can still pick any currency you price in.

Reference

Always provide fallback values (default country/currency) for a null location. It happens on network errors, a missing IPinfo token, or a request without CF-IPCountry.