--- name: sunrise-sunset-scraper description: Compute solar times for any coordinate and date via the Apify Actor arman-bd/sunrise-sunset-scraper. Returns sunrise, sunset, solar noon, day length in seconds and all three twilight bands (civil, nautical, astronomical) as ISO 8601 timestamps in a timezone you choose, one record per location and date. Use for photography and film scheduling, lighting and energy automation, agricultural daylight planning, astronomy planning and adding a daylight layer to coordinates you already hold. Not for weather, moon phases, place-name geocoding or tidal data. --- # Sunrise & Sunset Scraper: Solar Times by Coordinate Apify Actor `arman-bd/sunrise-sunset-scraper`. Give it coordinates and dates, get one dataset record per location-date pair. It runs without credentials, and the output is deterministic: the same coordinate and date always give the same answer. ## When to use it - Golden hour and blue hour planning across a whole location scout list. - Driving lighting, irrigation or building schedules off real sunset times rather than a fixed clock. - Usable-daylight tables per site, per day, across a season. - Astronomy planning, where astronomical twilight is the moment the sky is genuinely dark. - Enriching a set of coordinates you already hold with a cheap daylight layer. ## When not to use it - Weather, cloud cover, temperature or anything atmospheric. Only solar geometry. - Moon phase, moonrise or tides. Not returned. - Turning a place name or address into coordinates. You must supply latitude and longitude. - Sub-minute precision or refraction modelling for a specific altitude. These are standard sea-level solar times. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/sunrise-sunset-scraper').call({ locations: ['Malaga: 36.72,-4.42', 'Reykjavik: 64.15,-21.94'], dates: ['2026-08-01..2026-08-07'], timezone: 'Europe/Madrid', maxResults: 0, }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~sunrise-sunset-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"locations":["Malaga: 36.72,-4.42"],"dates":["2026-08-01..2026-08-07"],"timezone":"Europe/Madrid","maxResults":0}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/sunrise-sunset-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `locations` | string[] | yes | | Coordinates as `lat,lon`. Prefix with `Name:` or `Name|` to carry your own label into the output. A Google Maps URL is parsed, and an object of the form `{ name, lat, lng }` is accepted too. Latitude must be within -90 to 90 and longitude within -180 to 180; anything else is dropped before a request is made and listed in `RUN_SUMMARY.locationsRejected`. | | `dates` | string[] | no | today | `YYYY-MM-DD`, the keywords `today`, `tomorrow`, `yesterday`, or an inclusive range written with two dots: `2026-08-01..2026-08-07`. Expanded and de-duplicated. Unparseable entries are silently skipped, and if none survive the run throws. | | `timezone` | string | no | `"UTC"` | IANA timezone name such as `Europe/Madrid`, applied to every returned timestamp. | | `maxResults` | integer | no | `0` | Cap on total location-date records saved. `0` means no cap. | **Locations and dates multiply.** Ten locations across a full year is 3650 records and 3650 requests, so the cap exists for exactly that reason. Be aware of how the cap truncates: locations are processed in order and every date for one location is computed before the next location starts, so a cap that bites leaves the tail of your location list with no records at all rather than trimming each location evenly. Test with `maxResults` set, then remove it for the real run. The other thing to get right is `timezone`: it changes the printed offset only, never the calculation, and an unrecognised name falls back to UTC at the source without an error, so confirm the offset in the first returned timestamp rather than trusting the name you sent. ## Output One record per location-date pair. | Field | Type | Notes | |---|---|---| | `location` | string | Your own label if you supplied one, otherwise the canonical `lat,lon` string. Join back to your input on this. | | `latitude` | number | Parsed and range-validated. | | `longitude` | number | Parsed and range-validated. | | `date` | string | The day computed, `YYYY-MM-DD`, always explicit even when you asked for `today`. | | `sunrise` | string \| null | ISO 8601 with the offset of `timezone`. `null` on a polar day or polar night. | | `sunset` | string \| null | Same. | | `solarNoon` | string \| null | Moment the sun is highest. Present even on polar days. | | `dayLength` | number \| null | Seconds between sunrise and sunset. `0` during polar night, and the full 86400 during polar day. | | `civilTwilightBegin` | string \| null | Dawn, sun 6 degrees below the horizon. | | `civilTwilightEnd` | string \| null | Dusk, sun 6 degrees below the horizon. Blue hour sits between `sunset` and this. | | `nauticalTwilightBegin` | string \| null | Sun 12 degrees below the horizon. | | `nauticalTwilightEnd` | string \| null | Sun 12 degrees below the horizon. | | `astronomicalTwilightBegin` | string \| null | Sun 18 degrees below the horizon. | | `astronomicalTwilightEnd` | string \| null | Sun 18 degrees below the horizon. Full darkness begins here. | | `scrapedAt` | string | Run timestamp, ISO 8601 UTC. Note this is always UTC, unlike the solar fields. | A real record: ```json { "location": "Malaga", "latitude": 36.72, "longitude": -4.42, "date": "2026-08-06", "sunrise": "2026-08-06T07:26:18+02:00", "sunset": "2026-08-06T21:20:52+02:00", "solarNoon": "2026-08-06T14:23:35+02:00", "dayLength": 50074, "civilTwilightBegin": "2026-08-06T06:59:33+02:00", "civilTwilightEnd": "2026-08-06T21:47:37+02:00", "nauticalTwilightBegin": "2026-08-06T06:25:25+02:00", "nauticalTwilightEnd": "2026-08-06T22:21:46+02:00", "astronomicalTwilightBegin": "2026-08-06T05:48:56+02:00", "astronomicalTwilightEnd": "2026-08-06T22:58:14+02:00", "scrapedAt": "2026-08-06T12:00:00.000Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** It is where a partial run admits that it was partial. ```json { "locationsRequested": 2, "locationsRejected": ["Atlantis: 999,999"], "datesRequested": 7, "pairsFailed": 0, "failures": [], "recordsSaved": 14, "filters": { "timezone": "Europe/Madrid", "dates": ["2026-08-01", "2026-08-02", "2026-08-03", "2026-08-04", "2026-08-05", "2026-08-06", "2026-08-07"], "maxResults": 0 }, "finishedAt": "2026-08-06T12:00:09.113Z" } ``` The arithmetic to check is `recordsSaved` against `locationsRequested` multiplied by `datesRequested`. A shortfall has exactly three causes and all three are visible here: entries in `locationsRejected`, which never reached the source; entries in `failures`, each carrying `location`, `date` and `error`; and the cap in `filters.maxResults`. `filters.dates` is the fully expanded day list, so it is also the fastest way to confirm a range or a keyword parsed the way you meant. A run where every pair failed throws instead of finishing. ## Behaviour to plan around - **Polar day and polar night come back as `null`, not as 1970.** The source reports a Unix-epoch sentinel where a sunrise cannot occur, which would silently poison any sort or date arithmetic, so it is converted. Read `dayLength` to tell the two apart: `0` with a null `sunrise` is polar night, and a full day with a null `sunset` is polar day. - **Coordinates are validated locally, before any request.** An out-of-range latitude would otherwise be answered with meaningless times and a success status. Rejected entries are in `locationsRejected` and produce no row and no failure. - **A range needs two dots.** `2026-08-01..2026-08-07` expands to seven days; a single dot does not parse and the entry is dropped silently. Check `filters.dates` if you get fewer records than expected. - **`timezone` is presentation only.** Solar times are computed from the coordinate. The same instant printed in `UTC` and in `Europe/Madrid` is the same event. - **An unrecognised timezone falls back to UTC without an error.** A typo produces perfectly valid records with the wrong offset, so verify the offset in the output. - **`maxResults` truncates by location, not evenly.** The run stops entirely once the cap is reached, mid-location, so the last locations in your list may have zero rows. - **`dayLength` is seconds.** Divide by 3600 for hours. It sorts and subtracts correctly in every tool, which a formatted duration string would not. - **One request per location-date pair.** There is no batching, so a large cross product is a long run. Size the run timeout accordingly. - **Transient failures get four attempts with exponential backoff.** A rejected request is final and is not retried. One failed pair never aborts the run. ## Recipes **Golden hour for a week, in local time.** ```json { "locations": ["Malaga: 36.72,-4.42"], "dates": ["2026-08-01..2026-08-07"], "timezone": "Europe/Madrid", "maxResults": 0 } ``` Golden hour is roughly the hour after `sunrise` and before `sunset`; blue hour runs from `sunset` to `civilTwilightEnd`. **Solstice comparison across latitudes.** UTC keeps the columns comparable. ```json { "locations": ["Quito: -0.18,-78.47", "London: 51.51,-0.13", "Tromso: 69.65,18.96"], "dates": ["2026-06-21", "2026-12-21"], "timezone": "UTC", "maxResults": 100 } ``` Expect nulls at Tromso in December: that is polar night, not missing data. **Nightly automation feed.** Schedule this daily and read tomorrow's sunset. ```json { "locations": ["36.72,-4.42", "51.51,-0.13"], "dates": ["tomorrow"], "timezone": "UTC", "maxResults": 0 } ``` **Dark-sky windows for astronomy.** The usable window is `astronomicalTwilightEnd` to the next day's `astronomicalTwilightBegin`. ```json { "locations": ["Site A: 28.30,-16.51"], "dates": ["2026-09-01..2026-09-30"], "timezone": "Atlantic/Canary", "maxResults": 0 } ```