Build a nightly sky report bot

One GET to /v2/today returns the whole sky for a place: the Moon, the planets, tonight's darkness and what is coming up. About forty lines of Node turn that into a report your Discord or Slack channel reads every evening.

Everything here runs on the free tier: no key, no signup, no header. The API overview has the wider picture; this page is just the build.

What you need

Step 1: one call, the whole sky

Ask for today at your coordinates. tz= renders event times in your local zone; leave it off for UTC.

curl "https://www.cyclecalcs.com/v2/today?lat=51.5074&lon=-0.1278&tz=Europe/London"

The response's data object carries everything the report needs: moon.phase (name and illumination), planets_tonight (the planets above your horizon after civil dusk ends or before civil dawn begins, brightest first, each with a plain-language visibility field and the altitude it was decided on), night (when true darkness starts and ends, and a one-sentence verdict on moonlight), and next_events (upcoming eclipses, full moons and more, each with days_until). There is also planets_up, which is what is above the horizon at the moment of the call; run the call in daylight and that is a different list, so the report reads planets_tonight.

Step 2: the script

Save this as sky-report.mjs and set your own coordinates. It was run as written before being published here.

// sky-report.mjs: tonight's sky as one short report, from one GET.
// Set your own coordinates and timezone here.
const LAT = 51.5074, LON = -0.1278, TZ = 'Europe/London';

// SKY_AT pins the instant for a test run (any ISO time); unset means now.
const at = process.env.SKY_AT ? '&at=' + encodeURIComponent(process.env.SKY_AT) : '';
const url = 'https://www.cyclecalcs.com/v2/today'
  + '?lat=' + LAT + '&lon=' + LON + '&tz=' + encodeURIComponent(TZ) + at;
const res = await fetch(url, { headers: { accept: 'application/json' } });
if (!res.ok) throw new Error('API answered ' + res.status);
const { data } = await res.json();

const lines = ['Sky report for ' + data.local_date];
lines.push('Moon: ' + data.moon.phase.name + ', '
  + data.moon.phase.illumination_percent + '% lit.');

// planets_tonight is checked against YOUR horizon at dusk and dawn, so it
// never names a planet that sets before the sky is dark. planets_up is
// what is above the horizon at the moment the script runs, daylight included.
const tonight = data.planets_tonight.filter(p => p.naked_eye);
lines.push(tonight.length
  ? 'Planets tonight: ' + tonight.map(p => p.name + ' in ' + p.constellation.name
    + ' (' + p.visibility.toLowerCase() + ')').join(', ') + '.'
  : 'No naked-eye planet is observable tonight.');

lines.push('Darkness: ' + data.night.verdict);

const next = data.next_events[0];
if (next) lines.push('Coming up: ' + next.label + ', '
  + Math.round(next.days_until) + ' days away.');

const report = lines.join('\n');
console.log(report);

// Optional: post it to a Discord channel via a webhook.
if (process.env.DISCORD_WEBHOOK_URL) {
  await fetch(process.env.DISCORD_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ content: report }),
  });
}

Run with the London coordinates above at 17:00 UTC on 2026-10-08, which is 18:00 in London with the Sun still 3 degrees up (the same hour the workflow in step 4 fires), it printed:

$ SKY_AT=2026-10-08T17:00:00Z node sky-report.mjs
Sky report for 2026-10-08
Moon: Waning Crescent, 4.3% lit.
Planets tonight: Jupiter in Leo (morning sky, before dawn), Saturn in Cetus (up most of the night), Mars in Cancer (morning sky, before dawn), Uranus in Taurus (morning sky, before dawn).
Darkness: Good. No moonlight through the dark window.
Coming up: New Moon, 2 days away.

Note what the planets line does not say. At 18:00 that evening Venus and Mercury are both above the horizon in daylight, so both are in planets_up; but from London Venus sets at 18:10, thirteen minutes before the Sun, and Mercury at 18:50, before civil dusk ends at 18:55. Neither is in a dark sky at any point, so neither is on planets_tonight, and their planets_up entries say sets_in_twilight rather than the elongation-only "evening sky". Saturn, which had not risen at 18:00, is on the list because it is 4.8 degrees up when civil dusk ends and stays up all night. Every entry carries altitude_at_civil_dusk_deg and altitude_at_civil_dawn_deg, and planets_tonight_basis gives the two instants they were sampled at, so you can check any verdict against /v2/positions yourself. The same evening from Sydney, Venus is 18 degrees up at dusk and is listed. The check reads your horizon.

Step 3: point it at a channel

The script already posts to Discord when DISCORD_WEBHOOK_URL is set. Slack's incoming webhooks take {"text": report} instead of {"content": report}; everything else is identical. For anything fancier (Telegram, Matrix, an e-ink display on your desk), the report is just a string.

Step 4: schedule it

A GitHub Actions workflow runs it nightly for free. Commit the script to a repository, add the webhook URL as a repository secret, and add .github/workflows/sky-report.yml:

name: sky-report
on:
  schedule:
    - cron: "0 17 * * *"   # every day at 17:00 UTC; adjust to your dusk
  workflow_dispatch: {}     # lets you run it by hand from the Actions tab
jobs:
  post:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: node sky-report.mjs
        env:
          DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}

One run a day is 0.02% of the free tier's 5,000 requests, so quota will never be your problem. If you ever build something chattier, the RateLimit headers on every response carry your remaining budget, and going over returns a 429 with a Retry-After you can honor.

Step 5: level up

Two natural upgrades, each still one request:

The report is yours: the numbers in an API response are computed astronomical facts, and the terms let you publish and build on them, commercially or not.

Stuck, or found an error in this guide? Contact us at info@cyclecalcs.com.

More build guides