Adding Sunrise and Sunset Times to Your Application

javascript
// Get sun times for San Francisco today
const response = await fetch("https://worlddataapi.com/v1/sun/37.77,-122.42", {
	headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await response.json();

console.log(`Sunrise: ${data.sunrise}`);
console.log(`Sunset: ${data.sunset}`);
python
import requests

response = requests.get(
    "https://worlddataapi.com/v1/sun/37.77,-122.42",
    headers={"X-API-Key": "YOUR_API_KEY"}
)
data = response.json()

print(f"Sunrise: {data['sunrise']}")
print(f"Sunset: {data['sunset']}")
bash
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://worlddataapi.com/v1/sun/37.77,-122.42"

This guide covers implementing sunrise and sunset times in your application, including the edge cases that break naive implementations.

The Challenge#

Calculating sunrise and sunset times appears straightforward until you encounter real-world complexity. Naive implementations break when users query locations in polar regions where the sun may not rise or set for weeks. Timezone handling adds another layer: a location's sunrise time must be returned in the correct local timezone, accounting for DST transitions. Many applications also need twilight times (civil, nautical, astronomical) for features like outdoor activity scheduling or photography planning.

Building a robust solution requires handling all these cases correctly, or relying on an API that does.

Prerequisites#

Before you begin, you need:

  • A World Data API key (the free tier works for testing, but sun data requires a paid plan)

  • Basic familiarity with REST APIs

  • For JavaScript examples: Node.js 18+ or a modern browser with fetch support

  • For Python examples: Python 3.7+ with the requests library installed

API Response Structure#

The sun endpoint returns comprehensive solar data:

json
{
	"location": { "latitude": 37.77, "longitude": -122.42 },
	"date": "2026-01-15",
	"timezone": "America/Los_Angeles",
	"sunrise": "2026-01-15T07:25:00-08:00",
	"sunset": "2026-01-15T17:19:00-08:00",
	"solar_noon": "2026-01-15T12:22:00-08:00",
	"day_length": "09:54",
	"day_length_minutes": 594,
	"polar_condition": null,
	"twilight": {
		"civil": {
			"dawn": "2026-01-15T06:56:00-08:00",
			"dusk": "2026-01-15T17:48:00-08:00"
		},
		"nautical": {
			"dawn": "2026-01-15T06:24:00-08:00",
			"dusk": "2026-01-15T18:20:00-08:00"
		},
		"astronomical": {
			"dawn": "2026-01-15T05:52:00-08:00",
			"dusk": "2026-01-15T18:52:00-08:00"
		}
	},
	"golden_hour": {
		"morning": {
			"start": "2026-01-15T07:25:00-08:00",
			"end": "2026-01-15T08:00:00-08:00"
		},
		"evening": {
			"start": "2026-01-15T16:44:00-08:00",
			"end": "2026-01-15T17:19:00-08:00"
		}
	},
	"blue_hour": {
		"morning": {
			"start": "2026-01-15T06:56:00-08:00",
			"end": "2026-01-15T07:25:00-08:00"
		},
		"evening": {
			"start": "2026-01-15T17:19:00-08:00",
			"end": "2026-01-15T17:48:00-08:00"
		}
	}
}

Basic Implementation#

JavaScript#

javascript
class SunTimes {
	constructor(apiKey) {
		this.apiKey = apiKey;
		this.baseUrl = "https://worlddataapi.com/v1/sun";
	}

	async get(latitude, longitude, date = null) {
		const url = date
			? `${this.baseUrl}/${latitude},${longitude}?date=${date}`
			: `${this.baseUrl}/${latitude},${longitude}`;

		const response = await fetch(url, {
			headers: { "X-API-Key": this.apiKey },
		});

		if (!response.ok) {
			throw new Error(`API error: ${response.status}`);
		}

		return response.json();
	}

	async getByCity(cityId, date = null) {
		// GeoNames city ID
		const url = date
			? `${this.baseUrl}/${cityId}?date=${date}`
			: `${this.baseUrl}/${cityId}`;

		const response = await fetch(url, {
			headers: { "X-API-Key": this.apiKey },
		});

		return response.json();
	}

	async getByAirport(iataCode, date = null) {
		const url = date
			? `${this.baseUrl}/${iataCode}?date=${date}`
			: `${this.baseUrl}/${iataCode}`;

		const response = await fetch(url, {
			headers: { "X-API-Key": this.apiKey },
		});

		return response.json();
	}
}

// Usage
const sun = new SunTimes(process.env.API_KEY);

// By coordinates
const sfData = await sun.get(37.77, -122.42);

// By airport code
const jfkData = await sun.getByAirport("JFK", "2026-06-21");

// By GeoNames city ID (Tokyo = 1850147)
const tokyoData = await sun.getByCity(1850147);

Python#

python
import requests
from datetime import date
from typing import Optional

class SunTimes:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://worlddataapi.com/v1/sun"
        self.session = requests.Session()
        self.session.headers["X-API-Key"] = api_key

    def get(self, latitude: float, longitude: float,
            date_str: Optional[str] = None) -> dict:
        url = f"{self.base_url}/{latitude},{longitude}"
        params = {"date": date_str} if date_str else {}

        response = self.session.get(url, params=params)
        response.raise_for_status()
        return response.json()

    def get_by_airport(self, iata_code: str,
                       date_str: Optional[str] = None) -> dict:
        url = f"{self.base_url}/{iata_code}"
        params = {"date": date_str} if date_str else {}

        response = self.session.get(url, params=params)
        response.raise_for_status()
        return response.json()

# Usage
sun = SunTimes(os.environ["API_KEY"])

# Today's sun times for London Heathrow
data = sun.get_by_airport("LHR")
print(f"Sunrise: {data['sunrise']}")
print(f"Sunset: {data['sunset']}")

Understanding Twilight#

Twilight occurs when the sun is below the horizon but still illuminates the sky. Three types exist:

Twilight TypeSun AngleVisibility
Civil0° to -6°Enough light for outdoor activities without artificial lighting
Nautical-6° to -12°Horizon still visible at sea
Astronomical-12° to -18°Sky dark enough for astronomical observations
javascript
function describeTwilight(sunData) {
	const { twilight, sunrise, sunset } = sunData;

	return {
		earlyRisers: {
			start: twilight.civil.dawn,
			description:
				"Civil dawn — enough light to see without a flashlight",
		},
		photographers: {
			morningBlueHour: {
				start: twilight.civil.dawn,
				end: sunrise,
			},
			eveningBlueHour: {
				start: sunset,
				end: twilight.civil.dusk,
			},
		},
		astronomers: {
			morningEnd: twilight.astronomical.dawn,
			eveningStart: twilight.astronomical.dusk,
			description: "True darkness for stargazing",
		},
	};
}

Handling Polar Regions#

Above the Arctic Circle (or below the Antarctic Circle), the sun may not rise or set for days or weeks:

javascript
async function getSunTimesSafe(latitude, longitude, date) {
	const data = await sun.get(latitude, longitude, date);

	if (data.polar_condition === "midnight_sun") {
		return {
			type: "midnight_sun",
			message: "The sun does not set on this date",
			sunrise: null,
			sunset: null,
			solar_noon: data.solar_noon,
			day_length_minutes: 1440, // 24 hours
		};
	}

	if (data.polar_condition === "polar_night") {
		return {
			type: "polar_night",
			message: "The sun does not rise on this date",
			sunrise: null,
			sunset: null,
			twilight: data.twilight, // May still have twilight
		};
	}

	return {
		type: "normal",
		sunrise: data.sunrise,
		sunset: data.sunset,
		day_length_minutes: data.day_length_minutes,
	};
}

// Tromsø, Norway on June 21
const tromso = await getSunTimesSafe(69.6496, 18.956, "2026-06-21");
// { type: 'midnight_sun', message: 'The sun does not set on this date', ... }

// Same location on December 21
const tromsoWinter = await getSunTimesSafe(69.6496, 18.956, "2026-12-21");
// { type: 'polar_night', message: 'The sun does not rise on this date', ... }

Displaying Sun Times#

Time Formatting#

Always use the timezone from the response:

javascript
function formatSunTime(isoString, locale = "en-US") {
	const date = new Date(isoString);

	return date.toLocaleTimeString(locale, {
		hour: "numeric",
		minute: "2-digit",
		hour12: true,
	});
}

// "7:25 AM"
console.log(formatSunTime(data.sunrise));

Day Length Display#

javascript
function formatDayLength(minutes) {
	const hours = Math.floor(minutes / 60);
	const mins = minutes % 60;

	if (hours === 0) {
		return `${mins} minutes`;
	}

	if (mins === 0) {
		return `${hours} hours`;
	}

	return `${hours}h ${mins}m`;
}

// "9h 54m"
console.log(formatDayLength(data.day_length_minutes));

Visual Timeline#

jsx
function SunTimeline({ sunData }) {
	const events = [
		{
			time: sunData.twilight.astronomical.dawn,
			label: "Astronomical dawn",
			type: "twilight",
		},
		{
			time: sunData.twilight.nautical.dawn,
			label: "Nautical dawn",
			type: "twilight",
		},
		{
			time: sunData.twilight.civil.dawn,
			label: "Civil dawn",
			type: "twilight",
		},
		{ time: sunData.sunrise, label: "Sunrise", type: "sun" },
		{ time: sunData.solar_noon, label: "Solar noon", type: "sun" },
		{ time: sunData.sunset, label: "Sunset", type: "sun" },
		{
			time: sunData.twilight.civil.dusk,
			label: "Civil dusk",
			type: "twilight",
		},
		{
			time: sunData.twilight.nautical.dusk,
			label: "Nautical dusk",
			type: "twilight",
		},
		{
			time: sunData.twilight.astronomical.dusk,
			label: "Astronomical dusk",
			type: "twilight",
		},
	].filter((e) => e.time !== null);

	return (
		<div className="sun-timeline">
			{events.map((event, i) => (
				<div key={i} className={`event ${event.type}`}>
					<time>{formatSunTime(event.time)}</time>
					<span>{event.label}</span>
				</div>
			))}
		</div>
	);
}

Use Cases#

Smart Lighting Control#

Trigger lights based on sunset:

javascript
async function shouldLightsBeOn(latitude, longitude) {
	const data = await sun.get(latitude, longitude);
	const now = new Date();

	const civilDusk = new Date(data.twilight.civil.dusk);
	const civilDawn = new Date(data.twilight.civil.dawn);

	// Lights on after civil dusk, off after civil dawn
	return now > civilDusk || now < civilDawn;
}

// Or with offset
async function getLightsSchedule(latitude, longitude, offsetMinutes = 30) {
	const data = await sun.get(latitude, longitude);

	const onTime = new Date(data.sunset);
	onTime.setMinutes(onTime.getMinutes() - offsetMinutes);

	const offTime = new Date(data.sunrise);
	offTime.setMinutes(offTime.getMinutes() + offsetMinutes);

	return { on: onTime, off: offTime };
}

Prayer Time Estimation#

Many prayer times are defined relative to sun position:

javascript
function estimatePrayerTimes(sunData) {
	// Note: These are approximations. For accurate prayer times,
	// use a dedicated Islamic prayer time calculator.

	return {
		fajr: sunData.twilight.astronomical.dawn, // Before dawn
		sunrise: sunData.sunrise,
		dhuhr: sunData.solar_noon,
		asr: estimateAsr(sunData), // Afternoon — calculation varies by school
		maghrib: sunData.sunset,
		isha: sunData.twilight.astronomical.dusk, // After dusk
	};
}

Activity Recommendations#

javascript
function getOutdoorActivityTimes(sunData) {
	const recommendations = [];

	// Running/cycling: after sunrise, before it gets hot
	recommendations.push({
		activity: "Morning exercise",
		start: sunData.sunrise,
		end: addHours(sunData.sunrise, 2),
		reason: "Cool temperatures, good visibility",
	});

	// Photography
	recommendations.push({
		activity: "Photography (golden hour)",
		start: sunData.golden_hour.evening.start,
		end: sunData.golden_hour.evening.end,
		reason: "Best natural lighting",
	});

	// Stargazing
	if (sunData.twilight.astronomical.dusk) {
		recommendations.push({
			activity: "Stargazing",
			start: sunData.twilight.astronomical.dusk,
			end: sunData.twilight.astronomical.dawn,
			reason: "True darkness",
		});
	}

	return recommendations;
}

Solar Panel Optimization#

javascript
async function getSolarProductionWindow(latitude, longitude, date) {
	const data = await sun.get(latitude, longitude, date);

	// Peak production is typically 2-3 hours around solar noon
	const noon = new Date(data.solar_noon);

	const peakStart = new Date(noon);
	peakStart.setHours(noon.getHours() - 2);

	const peakEnd = new Date(noon);
	peakEnd.setHours(noon.getHours() + 2);

	return {
		production_start: data.sunrise,
		peak_start: peakStart.toISOString(),
		solar_noon: data.solar_noon,
		peak_end: peakEnd.toISOString(),
		production_end: data.sunset,
		total_daylight_hours: data.day_length_minutes / 60,
	};
}

Multi-Day Queries#

For trip planning or yearly analysis:

javascript
async function getSunDataRange(latitude, longitude, startDate, days) {
	const results = [];

	for (let i = 0; i < days; i++) {
		const date = new Date(startDate);
		date.setDate(date.getDate() + i);
		const dateStr = date.toISOString().split("T")[0];

		const data = await sun.get(latitude, longitude, dateStr);
		results.push(data);
	}

	return results;
}

// Get day length progression (useful for seasonal affective disorder tracking)
async function getDayLengthProgression(latitude, longitude, startDate, days) {
	const sunData = await getSunDataRange(latitude, longitude, startDate, days);

	return sunData
		.map((day) => ({
			date: day.date,
			dayLength: day.day_length_minutes,
			change: null, // Calculate change from previous day
		}))
		.map((day, i, arr) => ({
			...day,
			change: i > 0 ? day.dayLength - arr[i - 1].dayLength : 0,
		}));
}

Caching#

Sun data is deterministic — same coordinates and date always produce the same result:

javascript
const cache = new Map();

async function getCachedSunData(latitude, longitude, date) {
	const key = `${latitude.toFixed(4)},${longitude.toFixed(4)}:${date}`;

	if (cache.has(key)) {
		return cache.get(key);
	}

	const data = await sun.get(latitude, longitude, date);
	cache.set(key, data);

	return data;
}

For frequently queried locations, consider pre-fetching a year's worth of data.

Error Handling#

javascript
async function getSunDataSafe(latitude, longitude, date) {
	try {
		// Validate coordinates
		if (latitude < -90 || latitude > 90) {
			throw new Error("Latitude must be between -90 and 90");
		}
		if (longitude < -180 || longitude > 180) {
			throw new Error("Longitude must be between -180 and 180");
		}

		const response = await fetch(
			`https://worlddataapi.com/v1/sun/${latitude},${longitude}?date=${date}`,
			{
				headers: { "X-API-Key": API_KEY },
				signal: AbortSignal.timeout(5000),
			},
		);

		if (response.status === 400) {
			throw new Error("Invalid date format — use YYYY-MM-DD");
		}

		if (!response.ok) {
			throw new Error(`API error: ${response.status}`);
		}

		return await response.json();
	} catch (error) {
		if (error.name === "AbortError") {
			throw new Error("Request timed out");
		}
		throw error;
	}
}

Important Limitations#

Atmospheric Accuracy#

All sunrise, sunset, and twilight times are calculated using standard astronomical algorithms. These calculations assume a flat horizon and do not account for:

  • Local terrain: Mountains, hills, or buildings can delay sunrise or advance sunset at your specific location

  • Weather conditions: Atmospheric refraction varies with temperature, pressure, and humidity

  • Elevation: Higher elevations see sunrise earlier and sunset later than sea level

Actual observed times may differ by several minutes from calculated values. For applications requiring precise timing (such as photography), consider arriving 10-15 minutes before calculated golden hour times.

Daylight Saving Time Edge Cases#

The API does not account for DST transitions that occur on the query date itself. If you query sunrise/sunset times for a date when clocks change, the returned times reflect the timezone offset that applies for most of that day. For most applications this is sufficient, but if you need sub-minute precision during DST transition dates, verify the times against local official sources.

Common Pitfalls#

Ignoring polar conditions: Always check the polar_condition field before using sunrise/sunset values. In polar regions, these fields may be null during midnight sun or polar night periods. Attempting to parse null values as dates will crash your application.

Hardcoding timezone offsets: Never assume a location's timezone offset. Use the timezone field from the response and let a proper datetime library handle DST. San Francisco is UTC-8 in winter but UTC-7 in summer.

Caching without date: Sun times change daily. A cache key must include both coordinates and date. Caching "San Francisco sunrise" without the date returns stale data the next day.

Ignoring twilight for outdoor apps: If your app schedules outdoor activities, civil twilight (not sunrise) is when there is enough light to see without artificial lighting. Using sunrise as "first light" frustrates users who arrive to find it is already bright.

Not handling API errors gracefully: Network failures happen. Provide fallback behavior or cached estimates rather than crashing when the API is unreachable.

Summary#

Implementing sunrise and sunset times correctly requires handling polar regions, timezone awareness, and understanding the difference between calculated and observed times. The World Data API handles the complex astronomical calculations and returns properly localized times with comprehensive twilight data.

Key takeaways:

  • Always check polar_condition before using sunrise/sunset values

  • Use the returned timezone field rather than assuming offsets

  • Cache responses by coordinates and date for efficiency

  • Consider twilight times for outdoor activity features

  • Account for atmospheric limitations in precision-sensitive applications

Ready to add sun times to your application? Get your API key and start with the free tier, then upgrade to a paid plan for production access to astronomy endpoints.

Next Steps#