How to Use the Astronomy API Endpoint for Twilight Detection — Not Just Sunrise and Sunset

Most people hit the astronomy endpoint once, grab sunrise and sunset, and move on. That works fine for a rough day/night toggle. But there are three distinct twilight phases between daylight and full darkness — civil, nautical, and astronomical — and none of them appear in the response payload directly. You have to derive them. Here’s how, and why it matters for use cases where “is it dark?” is the wrong question.

What the Astronomy Endpoint Actually Returns

A call to /v1/astronomy.json?key=YOUR_KEY&q=51.5074,-0.1278&dt=2024-08-15 gives you: sunrise, sunset, moonrise, moonset, moon_phase, and moon_illumination. Solar noon isn’t in the payload, but you can approximate it from the midpoint of sunrise and sunset.

What’s not there: civil twilight end, nautical twilight end, astronomical twilight end — or their morning equivalents. Those require knowing the solar depression angle at a given moment, which means trigonometry client-side, or a library that handles it for you.

Why the Three Twilight Phases Matter in Practice

Civil twilight ends when the sun is 6° below the horizon. Ambient light is still enough to see clearly outdoors without artificial lighting. This is the threshold most outdoor lighting systems key off — and it’s the one that governs construction site rules, photography golden-hour calculations, and whether a civil UAV operator can fly under VLOS rules without aircraft lighting. If you’re building anything that touches outdoor operations or drone compliance, civil twilight is the number you need, not sunset.

Nautical twilight (sun 12° below horizon) is when the horizon becomes indistinct at sea. Historically it was the sweet spot for celestial navigation: bright stars visible, horizon still defined. For photography apps, it’s roughly where light meters tip into genuinely low-light territory — useful if you’re recommending camera settings rather than just flagging “golden hour.”

Astronomical twilight (sun 18° below horizon) is when sky background glow stops affecting telescope observations. Amateur astronomers don’t care about sunset — this is the threshold they’re watching. If you’re building hobby astronomy tooling, sunset time is nearly irrelevant to your users.

Deriving Twilight Times from the Astronomy Response

The approach we use: take the sunrise and sunset from the API response to anchor the date and location, then run solar position calculations locally. You don’t need WeatherAPI to give you twilight times explicitly. What you do need it for is the data that’s genuinely annoying to compute from scratch — moon phase, moon illumination, moonrise/moonset all depend on orbital mechanics and atmospheric refraction in ways that are easy to get subtly wrong.

In JavaScript, SunCalc (mourner’s library, roughly 3kb, no dependencies) handles all three twilight phases given a date and lat/lon. In Python, astral or ephem both do it. The workflow looks like this:

// JS example — combine WeatherAPI astronomy response with SunCalc
import SunCalc from 'suncalc';

const lat = 51.5074;
const lon = -0.1278;
const date = new Date('2024-08-15');

const times = SunCalc.getTimes(date, lat, lon);

console.log('Civil twilight end (eve):', times.dusk);            // sun at -6°
console.log('Nautical twilight end (eve):', times.nauticalDusk); // sun at -12°
console.log('Astronomical twilight end (eve):', times.night);    // sun at -18°
console.log('Astronomical twilight start (morn):', times.nightEnd);
console.log('Nautical twilight start (morn):', times.nauticalDawn);
console.log('Civil twilight start (morn):', times.dawn);

Pull moon_phase and moon_illumination from the WeatherAPI response. Combined, you get a complete picture: all twilight thresholds from SunCalc, moon state from WeatherAPI, one API call, one small local library.

High-Latitude Edge Cases

This matters if your users are anywhere above roughly 55°N or below 55°S — most of Scotland, Scandinavia, Iceland, large parts of Canada.

In summer at those latitudes, the sun never drops to 18° below the horizon. Astronomical twilight never ends. SunCalc returns NaN or Invalid Date for times.night and times.nightEnd in those conditions — that’s not a bug, it’s correct. You have to handle it explicitly:

const astronomicalDusk = times.night;
if (!astronomicalDusk || isNaN(astronomicalDusk.getTime())) {
  // White night / midnight sun condition — no true astronomical darkness
  console.log('No astronomical night at this location/date');
}

The same applies to civil and nautical twilight during extreme summer periods — in Tromsø in June, even civil twilight doesn’t fully end. WeatherAPI handles the sunrise/sunset fields gracefully for what they return, but if you’re building twilight logic on top, that edge case is yours to own.

A Concrete Use Case: Astrophotography Planning

An astrophotography planner needs to answer: “When tonight does it get dark enough to image, and for how long?” That breaks into four inputs:

  • Astronomical twilight end (evening) → imaging window opens
  • Moon rise time and illumination → will the moon wash out the sky?
  • Astronomical twilight start (morning) → imaging window closes
  • Cloud cover forecast → is any of this actually visible?

Three of those four come from WeatherAPI in one or two calls: the astronomy endpoint for moon data, the forecast endpoint for cloud cover. Twilight timing comes from SunCalc. Put them together and you get something actually useful — not “sunset is at 20:47” but “you have 4.3 hours of astronomical darkness starting at 23:12, with a 34%-illuminated moon rising at 01:05, and cloud cover forecast at 20% for the core window.”

One caveat worth surfacing to users: cloud in the hourly forecast is a percentage of sky covered, not a measure of atmospheric transparency. High thin cirrus might show 30% cloud cover but completely ruin an imaging session. For most hobbyist apps that approximation is fine — just don’t label it “sky quality” without flagging the distinction.

Checking Moon Illumination Against Twilight Windows

WeatherAPI returns moon_illumination as an integer percentage (e.g. 34). Cross-reference that with your derived astronomical darkness window and the moonrise/moonset times from the same response to work out how much of the darkness window is actually moon-free.

# Python sketch
from astral import LocationInfo
from astral.sun import sun
from datetime import date

loc = LocationInfo(latitude=51.5074, longitude=-0.1278, timezone='Europe/London')
s = sun(loc.observer, date=date(2024, 8, 15), tzinfo=loc.timezone)

print(f"Dawn (astronomical): {s['dawn']}")
print(f"Dusk (astronomical): {s['dusk']}")
# Then combine with moonrise/moonset from WeatherAPI astronomy response

Worth noting: astral‘s sun() returns civil twilight by default for dawn/dusk. Pass depression=18 explicitly to get astronomical twilight — this is easy to miss in the docs and will quietly give you the wrong threshold.

Free Tier Considerations

The astronomy endpoint counts against your monthly call quota the same as any other endpoint. For a planning tool where users look ahead several days, batch it: one call per day per location, cache the result for 24 hours (astronomy data for a given date doesn’t change intraday), compute twilight client-side from the cached lat/lon and date. That approach stays well within free tier limits even with a reasonably active user base.

For multi-day lookahead, the forecast endpoint (3 days free, up to 14 on paid plans) includes astro objects in the daily breakdown — same fields as the dedicated astronomy endpoint, just embedded in the forecast response. One call for 14 days of astronomy data beats 14 separate calls by a lot.

The part most developers overlook: twilight computation itself costs nothing — it’s math against a date and coordinate. Keep your API calls for the data that actually requires a live source (moon state, cloud cover) and do the solar geometry locally. That’s the pattern that scales.

Scroll to Top