Public API

Three GET endpoints, no key, no sign-up, no rate-limit wall. They return the same numbers the site renders: radiant altitude, moon interference, hourly cloud cover and sky brightness, folded into one verdict. Built for integration authors — Home Assistant, club dashboards, camping sites, Discord bots.

Data that is not connected yet

All three endpoints are live. Two inputs are not, and the responses say so rather than filling the gap with a plausible number:

All 12 showers, their radiants, drift, velocity and activity windows are real data and are live now.

Conventions

Base URL
https://meteorshowercal.com/api/v1
Versioning
Fields are only ever added inside v1. A breaking change ships as /api/v2; v1 keeps working.
Format
application/json; charset=utf-8, pretty-printed. Angles are degrees, fractions are 0–1, percentages are 0–100, instants are ISO-8601. Local times carry the observer's UTC offset (2026-08-13T03:00:00+08:00) — read the wall clock straight off the string; do not re-zone it.
null means unknown
cloudPct: null means no forecast was available for that point. It does not mean clear sky. location.bortle: null means the sky brightness there was never measured — it does not mean a pristine sky. Render both as unknown; the site does. Nothing in this API is ever a placeholder number.
CORS
Access-Control-Allow-Origin: * on every response. Call it straight from a browser.
Location resolution
A coordinate inside about 25 km of one of the 300 city rows adopts that city's name, timezone and elevation. Otherwise the label is the formatted coordinate, elevation is 0, and the timezone comes from Cloudflare's own geolocation — but only when the caller is standing within 50 km of the point they asked about — then from a city within 150 km, then from a fixed solar offset (Etc/GMT±N). location.tz always tells you which of those you got. A fixed solar offset has no DST and can sit up to an hour off civil time; if your integration needs civil time for a remote point, resolve the zone yourself and read the UTC fields.
Coordinate grid
Coordinates are snapped to a 0.1° grid (about 11 km) before computation, and the response echoes the snapped point. None of the four verdict factors changes measurably inside one cell, and it keeps upstream weather requests polite.
Caching
Verdicts: Cache-Control: public, max-age=900 (15 minutes). Shower elements: max-age=86400 (24 hours). Errors are no-store.
Methods
GET and OPTIONS. Anything else is 405.

Rate limits and etiquette

There is no enforced quota today, and keeping it that way is up to the people using it. The upstream weather providers (MET Norway, NOAA/NWS) have real limits and this site is bound by them, so:

Licence and attribution

Everything these endpoints return is licensed CC BY 4.0. You may use it commercially, modify it and redistribute it. In exchange, attribution is required and it must include a link back:

<a href="https://meteorshowercal.com/">Meteor data by MeteorShowerCal</a>
(<a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>)

If you change the numbers (rounding, unit conversion, your own visibility model on top), say so — CC BY requires indicating modifications. Underlying sources have their own credits, listed in every response's meta.sources and in full on the methodology page.

GET /api/v1/showers

Every shower in the calendar, with the orbital elements used by the rest of the API. No parameters, no location. Codes: QUA LYR ETA SDA PER DRA ORI STA NTA LEO GEM URS.

Responses for /api/v1/showers
StatusMeaning
200Shower list.
405Method other than GET/OPTIONS.

drift.raPerDeg / drift.decPerDeg — and the driftRa/driftDec behind them — are degrees per degree of solar longitude, not per day, and the radiant is already reduced to solLongPeak. Apply ra = ra0 + driftRa × (λ − λ_peak) directly; do not divide by 0.9856 again.

Real response, truncated to the first two showers:

{
  "meta": {
    "version": "v1",
    "generatedAt": "2026-08-12T18:00:00.000Z",
    "sources": [
      "IAU Meteor Data Center — established shower list V.2 (radiant, drift, Vg, solar longitude)",
      "Stellarium MeteorShowers.json — ZHR, population index, activity window (one-off seed import)",
      "GeoNames cities15000 (CC BY 4.0) — city names, coordinates, elevation, timezone",
      "astronomy-engine (MIT) — ephemeris"
    ],
    "license": "CC-BY-4.0",
    "licenseUrl": "https://creativecommons.org/licenses/by/4.0/",
    "attribution": "Data from meteorshowercal.com (CC BY 4.0). Please link back."
  },
  "showers": [
    {
      "code": "QUA",
      "name": "Quadrantids",
      "slug": "quadrantids",
      "zhr": 110,
      "popIndex": 2.1,
      "url": "https://meteorshowercal.com/showers/quadrantids/",
      "solLongPeak": 283.15,
      "active": {
        "start": "12-28",
        "end": "01-12"
      }
    },
    {
      "code": "LYR",
      "name": "April Lyrids",
      "slug": "april-lyrids",
      "zhr": 18,
      "popIndex": 2.1,
      "url": "https://meteorshowercal.com/showers/april-lyrids/",
      "solLongPeak": 32.32,
      "active": {
        "start": "04-14",
        "end": "04-30"
      }
    }
  ],
  "license": {
    "license": "CC-BY-4.0",
    "licenseUrl": "https://creativecommons.org/licenses/by/4.0/",
    "attribution": "Data from meteorshowercal.com (CC BY 4.0). Please link back."
  }
}

GET /api/v1/tonight

The whole product in one object: tonight's verdict, the best observing window in the observer's local time, the hourly table and every other shower active tonight.

Parameters for /api/v1/tonight
ParameterTypeRequiredNotes
latnumberconditional Decimal degrees, −90 to 90, north positive.
lonnumberconditional Decimal degrees, −180 to 180, east positive.

Both or neither. With neither, the request is answered for the location Cloudflare derives from the caller's IP; if there is no such hint the response is 400. There is no default city — a confidently wrong location is worse than an error.

Responses for /api/v1/tonight
StatusMeaning
200Verdict for the resolved location.
400 bad_request (only one of lat/lon, or no coordinates and no IP hint) or invalid_coordinates (not a number, or out of range).
503 internal_error — the verdict could not be computed. The message says why. Never a fabricated result.
curl "https://meteorshowercal.com/api/v1/tonight?lat=22.28&lon=114.17"

Response shape — the numbers below are zeros and ellipses on purpose, so nothing here can be mistaken for a forecast:

{
  "meta": {
    "version": "v1",
    "generatedAt": "2026-08-12T18:00:00.000Z",
    "sources": [ "IAU Meteor Data Center — …", "…" ],
    "license": "CC-BY-4.0",
    "licenseUrl": "https://creativecommons.org/licenses/by/4.0/",
    "attribution": "Data from meteorshowercal.com (CC BY 4.0). Please link back."
  },
  "location": {
    "lat": 22.3, "lon": 114.2, "label": "Hong Kong", "tz": "Asia/Hong_Kong",
    "country": "HK",
    "bortle": null, "sqm": null, "skyBrightnessKnown": false
  },
  "nightOf": "2026-08-12",
  "verdict": { "tier": "go", "headline": "…", "detail": "…" },
  "shower": { "code": "PER", "name": "Perseids", "slug": "perseids",
              "zhr": 100, "popIndex": 2.2,
              "url": "https://meteorshowercal.com/showers/perseids/" },
  "bestWindow": { "start": "2026-08-13T03:00:00+08:00",
                  "end":   "2026-08-13T05:00:00+08:00" },
  "peakHr": 0,
  "radiantAltMax": 0,
  "moonIllum": 0,
  "cloudPct": null,
  "weather": { "source": "met.no", "available": true, "attribution": "…" },
  "nextGoodNight": null,
  "hourly": [ { "local": "2026-08-12T21:00:00+08:00",
                "utc": "2026-08-12T13:00:00.000Z",
                "radiantAlt": 0, "moonAlt": 0, "moonIllum": 0,
                "cloud": null, "limitingMag": 0, "hr": 0 } ],
  "alsoActive": [],
  "license": { "license": "CC-BY-4.0", "licenseUrl": "…", "attribution": "…" }
}

verdict.tier is one of go, maybe, no, invisible. invisible means the radiant never rises at that latitude tonight. Both no and invisible carry a nextGoodNight — use it, it is the most useful thing you can show someone whose sky is not cooperating.

GET /api/v1/shower/{code}

One shower in depth. code is the IAU three-letter code, case-insensitive.

Parameters for /api/v1/shower/{code}
ParameterTypeRequiredNotes
lat, lonnumberno Together they add the local block. No IP fallback here — local appears only when you asked for it explicitly.
datestringno YYYY-MM-DD, the local night to evaluate. Defaults to tonight. Cloud cover only exists roughly 48 hours ahead; beyond that cloud is null.
Responses for /api/v1/shower/{code}
StatusMeaning
200Shower elements, peak, and local when coordinates were given.
400bad_request / invalid_coordinates.
404unknown_shower — the message lists the valid codes.
503internal_error — local computation unavailable.
curl "https://meteorshowercal.com/api/v1/shower/PER?lat=40.71&lon=-74.01&date=2026-08-12"

peak currently resolves for the 2026 reference year only. Other years need the solar-longitude inversion that ships with the calculation module; until then the field is null instead of an extrapolation.

Errors

Every error is JSON with the same shape and a machine-readable code:

{
  "meta": { "version": "v1", "generatedAt": "2026-08-12T18:00:00.000Z" },
  "error": {
    "code": "invalid_coordinates",
    "message": "`lat` must be between -90 and 90 and `lon` between -180 and 180."
  },
  "license": { … }
}

Codes: bad_request, invalid_coordinates, unknown_shower, rate_limited, internal_error.

Embed widget

For websites, the widget is better than calling the API yourself: one script tag, no build step, renders in a shadow root so it cannot collide with your CSS, and it removes itself silently if anything fails.

<script src="https://meteorshowercal.com/embed.js"
        data-lat="22.28" data-lon="114.17"></script>

Leave data-lat/data-lon off and it answers for the visitor's own approximate location. The card links back to meteorshowercal.com — that link is the CC BY attribution, so please keep it. See it running.

Known limits

The full method, including the ZHR equation and every source, is on the methodology page.