TimezoneIP GeolocationJavaScriptNode.jsTutorial

Get Timezone from IP Address in JavaScript and Node.js

ApogeoAPI5 min read

Why get timezone from IP?

Timezone detection from IP enables:

  • Scheduling — show "Your time: 3:00 PM" instead of "UTC 12:00"
  • Trial expiry — expire at midnight in the user's timezone, not UTC
  • Support hours — "We're online right now" vs "Office opens in 4h"
  • Email send times — schedule marketing emails at 9 AM local time

API call

curl "https://api.apogeoapi.com/v1/geo/8.8.8.8?apikey=YOUR_KEY"
# Returns IANA timezone string:
# "timezone": "America/New_York"

JavaScript — browser (Intl API fallback)

The browser's Intl.DateTimeFormat().resolvedOptions().timeZone is the fastest approach — zero API calls. Use IP lookup as a server-side fallback:

// Browser: use browser's own timezone (most accurate, no API needed)
function getBrowserTimezone() {
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
  // "America/New_York", "Europe/Berlin", "Asia/Tokyo", etc.
}

// Format a date in the user's timezone
function formatInUserTimezone(date, timezone) {
  return new Intl.DateTimeFormat('en-US', {
    timeZone: timezone,
    dateStyle: 'medium',
    timeStyle: 'short',
  }).format(date);
}

const tz = getBrowserTimezone();
const now = new Date();
console.log(`Your time: ${formatInUserTimezone(now, tz)}`);
// "May 14, 2026, 2:30 PM"

Node.js — IP-based timezone for server-side rendering

// timezone.ts — with /24 subnet cache
const cache = new Map<string, { timezone: string; expires: number }>();

export async function getTimezoneFromIP(ip: string): Promise<string> {
  const subnet = ip.split('.').slice(0, 3).join('.') + '.0';
  const cached = cache.get(subnet);
  if (cached && cached.expires > Date.now()) return cached.timezone;

  try {
    const res = await fetch(
      `https://api.apogeoapi.com/v1/geo/${ip}?apikey=${process.env.APOGEO_KEY}`,
      { signal: AbortSignal.timeout(2000) }
    );
    if (!res.ok) return 'UTC';

    const { timezone } = await res.json();
    cache.set(subnet, { timezone: timezone ?? 'UTC', expires: Date.now() + 3_600_000 });
    return timezone ?? 'UTC';
  } catch {
    return 'UTC';
  }
}

// Format date in user's local timezone
export function formatLocalTime(date: Date, timezone: string): string {
  return new Intl.DateTimeFormat('en-US', {
    timeZone: timezone,
    month: 'short',
    day: 'numeric',
    hour: '2-digit',
    minute: '2-digit',
    timeZoneName: 'short',
  }).format(date);
}

// Example: Next.js API route
// GET /api/local-time
export default async function handler(req, res) {
  const ip = req.headers['x-forwarded-for']?.split(',')[0] ?? '127.0.0.1';
  const timezone = await getTimezoneFromIP(ip === '127.0.0.1' ? '8.8.8.8' : ip);

  const now = new Date();
  return res.json({
    timezone,
    localTime: formatLocalTime(now, timezone),
    utcOffset: getUTCOffset(timezone, now),
  });
}

function getUTCOffset(timezone: string, date: Date): string {
  const parts = new Intl.DateTimeFormat('en', {
    timeZone: timezone,
    timeZoneName: 'shortOffset',
  }).formatToParts(date);
  return parts.find(p => p.type === 'timeZoneName')?.value ?? 'UTC';
  // Returns "GMT-4", "GMT+2", etc.
}

Show "open / closed" based on timezone

// Is our support team online right now from the user's perspective?
function isWithinBusinessHours(timezone: string): boolean {
  const now = new Date();
  // Get hour in our support team's timezone (Eastern Time)
  const supportHour = parseInt(
    new Intl.DateTimeFormat('en-US', {
      timeZone: 'America/New_York',
      hour: 'numeric',
      hour12: false,
    }).format(now)
  );
  return supportHour >= 9 && supportHour < 18; // 9 AM – 6 PM ET
}

// Show time-until-open from visitor's perspective
function timeUntilOpen(visitorTimezone: string): string {
  const now = new Date();
  const supportHour = parseInt(
    new Intl.DateTimeFormat('en-US', {
      timeZone: 'America/New_York',
      hour: 'numeric',
      hour12: false,
    }).format(now)
  );

  if (isWithinBusinessHours(visitorTimezone)) {
    return 'Support is online now';
  }

  // Calculate hours until 9 AM ET
  const hoursUntilOpen = supportHour >= 18
    ? 24 - supportHour + 9   // after close
    : 9 - supportHour;        // before open

  // Convert to visitor's local time
  const openTime = new Date(now.getTime() + hoursUntilOpen * 3_600_000);
  const localOpenTime = new Intl.DateTimeFormat('en-US', {
    timeZone: visitorTimezone,
    hour: 'numeric',
    minute: '2-digit',
    hour12: true,
  }).format(openTime);

  return `Opens at ${localOpenTime} your time`;
}

// Usage
const timezone = await getTimezoneFromIP(userIP);
console.log(timeUntilOpen(timezone));
// "Opens at 3:00 PM your time" (if user is in Berlin and support opens at 9 AM ET)

Trial expiry at midnight local time

// Calculate trial end at midnight in user's timezone
function trialEndAtMidnight(timezone: string, trialDays = 14): Date {
  const now = new Date();

  // Get current date in user's timezone
  const localDate = new Intl.DateTimeFormat('en-CA', {
    timeZone: timezone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).format(now); // "2026-05-14"

  // Calculate end date (14 days later)
  const [year, month, day] = localDate.split('-').map(Number);
  const endDate = new Date(Date.UTC(year, month - 1, day + trialDays));

  // Convert midnight in user's timezone to UTC
  const midnightLocal = new Intl.DateTimeFormat('en-US', {
    timeZone: timezone,
    year: 'numeric', month: 'numeric', day: 'numeric',
    hour: 'numeric', minute: 'numeric', second: 'numeric',
    hour12: false,
  }).format(endDate);

  return endDate; // Store this as the trial end timestamp
}

const userTimezone = await getTimezoneFromIP(userIP);
const trialEnd = trialEndAtMidnight(userTimezone);
// Store trialEnd in your database — expires at midnight user's time

Country-to-timezone mapping (without API)

For simple use cases, map country code to a representative timezone. Less accurate (countries can span multiple timezones) but works for most UX purposes:

const COUNTRY_TIMEZONE = {
  US: 'America/New_York',   // East Coast as default
  GB: 'Europe/London',
  DE: 'Europe/Berlin',
  FR: 'Europe/Paris',
  JP: 'Asia/Tokyo',
  AU: 'Australia/Sydney',
  BR: 'America/Sao_Paulo',
  IN: 'Asia/Kolkata',
  CA: 'America/Toronto',
  MX: 'America/Mexico_City',
  // Add more as needed
};

// Get timezone from country code (faster, but less accurate for large countries)
function getTimezoneFromCountry(countryCode) {
  return COUNTRY_TIMEZONE[countryCode] ?? 'UTC';
}

For accurate timezone by IP (not just country), always use the geo API — Russia, US, Canada, and Australia all span many timezones.

API reference: api.apogeoapi.com/api/docs. Get a free key: app.apogeoapi.com/register.

Try ApogeoAPI free

1,000 requests/month forever. 14-day full-access trial. No credit card.

Get your free API key