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