← All developer projects

Build a Moon-phase calendar with Node.js

Make a monthly calendar of the four primary Moon phases, then export the events as an iCalendar file. One request returns both the event data and the download.

Available on Free: 500 standard requests per UTC calendar month. No credit card required. This example makes 1 calculation request per run.

November 2026 · UTC

A fixed example month. Event times below come from the live calculation engine; the preview does not refresh automatically.

Primary Moon phases for November 2026
PhaseDateTime
Last quarter20:28:27 UTC
New Moon07:02:07 UTC
First quarter11:47:50 UTC
Full Moon14:53:34 UTC

Download November’s calendar

1. Set up your project

You need Node.js 22+ and a verified Cosmic Ephemeris account. Create a free account and verify your email if you have not already.

  1. Open API Keys and enter a Key label, such as Developer tutorial.
  2. Under Access scopes, select astro:moon-phases, then click Create API key. Selecting a specific scope replaces the default “All calculation endpoints”.
  3. Click Copy key and save the complete key privately. It is shown only once; the shortened prefix in the key list will not work.
mkdir moon-calendar-demo
cd moon-calendar-demo
npm init -y
npm install cosmicephemeris@0.5.0

Already have a key? Reuse it if it includes this tutorial’s scope. One key can serve several projects: select all their required scopes when creating it. You do not need a separate key for each tutorial.

Save your key in a private .env file

Inside the project folder you just created, use your text editor to create a file named .env (not .env.txt). Put this single line in it:

COSMICEPHEMERIS_KEY=PASTE_YOUR_FULL_API_KEY_HERE

Replace PASTE_YOUR_FULL_API_KEY_HERE with the full key you copied. This is a placeholder, not a working key. Keep .env in the same folder where you will run moon-calendar.mjs; the command later uses --env-file=.env to load it.

Create or open .gitignore in that folder and add this line so Git will ignore the key file:

.env

Keep the key on your server or local development computer. Do not put it in browser JavaScript, URLs, screenshots or a public repository. Signing in to this website does not authenticate your Node.js script; it still needs the key. See the authentication guide.

2. Save the complete script

Save this as moon-calendar.mjs, or download the script. It calls POST https://api.cosmicephemeris.com/api/astro/moon-phases.

// Node.js 22+; server-side only. Install: npm install cosmicephemeris@0.5.0
// Set COSMICEPHEMERIS_KEY privately. No automatic retries.
import { mkdir, writeFile } from "node:fs/promises";
import { CosmicEphemeris, CosmicEphemerisError } from "cosmicephemeris";

const output = "moon-calendar-output";
try {
  const api = new CosmicEphemeris();
  // Reserve a new private folder before making a metered call.
  await mkdir(output, { mode: 0o700 });
  const calendar = await api.moonPhases({
    mode: "calendar",
    start: { date: "2026-11-01", time: "00:00", timezone: "UTC" },
    end: { date: "2026-12-01", time: "00:00", timezone: "UTC" },
    display_timezone: "UTC",
    include_ical: true,
  });
  await writeFile(`${output}/moon-phases.json`, JSON.stringify(calendar, null, 2), {
    encoding: "utf8", flag: "wx", mode: 0o600,
  });
  if (calendar.result.ical !== null) {
    // Preserve the returned CRLF line endings. Do not save JSON as .ics.
    await writeFile(`${output}/moon-phases.ics`, calendar.result.ical, {
      encoding: "utf8", flag: "wx", mode: 0o600,
    });
    console.log("Created moon-calendar-output/moon-phases.ics and moon-phases.json.");
  } else {
    console.log("No phase events in this interval; saved JSON only.");
  }
} catch (error) {
  if (error instanceof CosmicEphemerisError) {
    console.error("API request failed:", error.code, error.status ?? "");
  } else if (error?.code === "EEXIST") {
    console.error("Output folder already exists. Rename it before another run.");
  } else {
    console.error("Could not finish. Check the input and folder permissions.");
  }
  process.exitCode = 1;
}

3. Run it once

node --env-file=.env moon-calendar.mjs

If your server already sets the environment variable, omit --env-file=.env. Results are saved in the private moon-calendar-output folder. Rename an existing folder before another run; the script checks that before making a request. A failed run may leave an empty or partially completed folder.

Each admitted calculation uses one standard unit, including failed calculations. A successful run uses one unit. A timeout may still consume quota. The script makes no automatic retries. Viewing these sample results and downloading files makes no calculation request.

Use the events and calendar file

calendar.result.events contains the ordered phase events. Use their UTC timestamps for storage and format them in the visitor’s timezone for display. The example’s range starts at November 1 and ends just before December 1.

calendar.result.ical is the decoded calendar text. Save it unchanged with its CRLF line endings and import the .ics file using your calendar app’s file-import function. This is a download, not a subscription; it will not refresh automatically. Import behavior depends on the calendar application.

An interval with no events returns ical: null; the script then saves JSON only. Event IDs are stable for the same phase and instant, but the export’s assembly timestamp can change between calls.

Choose another month

Change start to the first day and end to the first day of the next month. Keep both at midnight UTC for a UTC month. Change display_timezone to add local labels; it does not move the events. A calendar interval can be at most 366 elapsed days and contain at most 52 events.

Inputs and troubleshooting

If Node says it cannot find .env: run the command from the project folder and check that the filename is exactly .env. If the key is missing or invalid, replace the placeholder with your complete key. If you receive HTTP 403, check the key’s required scope above.

Modern civil dates cover 1800-01-01 through 2200-12-31, subject to route and derived-time limits. Use explicit timezones. Historical inputs require the separate historical date contract; do not substitute BCE years into these date strings. Historical iCalendar export is not supported.

A missing or malformed key fails before the request. For HTTP 401/403, check your key, account and scope. For 400, check the documented input. For 429, check allowance, key budget and rate or concurrency limits. For a timeout or server error, check service status before deciding whether to rerun.

All developer projects · SDK quickstart · Postman collection