API accuracy and residuals

Every JSON response from the CycleCalcs v2 API carries meta.accuracy.residuals_url, and it points here. All 31 shipped routes do it. This page is what that link owes you: a specific, checkable statement of what these numbers are worth, where the error actually comes from, and what is deliberately not computed at all.

Nothing on this page was copied from a specification. Every figure below was either read out of the shipped source or measured by invoking the shipped endpoints. Where a number varies, the measured range is given rather than a convenient single value.

On this page

The short answer

The claim the API makes on every single response, in meta.accuracy.statement, is this:

Positions are arcminute class for the Sun, Moon and planets across 1700 to 2200, degrading toward the ends of that window.

That is a claim about positions. For most callers the position is not what limits the answer. Three other things dominate, and in ordinary use they dominate by a wide margin. In descending order of size:

  1. The dip of the sea horizon is not modeled. If your observer is high up with a clear sea horizon, rise and set times are wrong by minutes, and in the opposite direction from the correction the API does apply. Measured: at 3000 m the published sunrise is 13.9 minutes later than a true sea horizon would give. Details, and the formula to apply it yourself.
  2. Refraction takes no weather. No temperature, pressure or humidity parameter is accepted anywhere in the API. A cold, high-pressure night moves a real sunrise by tens of seconds against what is published. Details and measured magnitudes.
  3. UT1 is treated as equal to UTC. Every instant the API returns, a Moon phase as much as a sunrise, can be out by up to 0.9 s from that alone. Details.

The ephemeris itself is the smallest term in that list for almost every question this API is asked. That ordering is the useful thing to know, and it is why this page spends more words on horizons and clocks than on planetary theory.

The engine

There is one ephemeris and it is vendored into this repository: Astronomy Engine 2.1.19, MIT licensed, Copyright (c) 2019-2023 Don Cross. The vendored file is never edited, so its hash stays meaningful. Its SHA-256 is 068f1445ed0c636c94818fe6d20d7d125120e605e0bab9fc4675c3d531be5ad7, matched byte for byte against upstream v2.1.19 (412,025 bytes), and a build check recomputes that hash on every run and fails the build if it moves. The same record is published on the wire in meta.engine on every response and at /v2/attribution.

The models it carries, read from the vendored source rather than from anybody's summary:

QuantityModel in the vendored source
PlanetsVSOP87
MoonThe Nautical Almanac Office's Improved Lunar Ephemeris of 1954, deriving from E. W. Brown's lunar theory, adapted from Montenbruck and Pfleger
PlutoA gravitational integration between 51 tabulated state vectors, not a series expansion
NutationIAU 2000B
Terrestrial TimeThe Espenak and Meeus piecewise delta-T polynomial, which the API replaces with IERS values from 1972 (time scales)
RefractionSaemundsson, as given by Meeus, in the engine's "normal" mode

What the engine's own file documents about its accuracy: almost nothing, and it is honest to say so. The vendored source carries the MIT notice and no error table. The two accuracy-relevant sentences it does contain are worth quoting because they are the origin of two of the limits on this page. On its time class it says the library approximates UT1 and UTC as being the same thing, and that this gives sufficient accuracy for the precision requirements of that project. On its rise and set search it says it uses a correction of 34 arcminutes, and that real-world refraction varies with air temperature, pressure and humidity, which are outside the scope of the engine.

So the arcminute-class statement above is CycleCalcs's claim, not the engine's. Everything else on this page is a measurement of the shipped system, and where a claim could not be verified it is not made.

Source: netlify/lib/vendor/astronomy-engine.mjs and netlify/lib/vendor/engine-version.mjs, both in the repository.

Measured against JPL Horizons, 1700 to 2200

Over 69,402 geocentric apparent places, the engine against JPL Horizons at the same Universal Time, the Sun, the planets and Pluto stay inside the arcminute across the whole span; the largest residual among them is 51.408 arcsec (Pluto, 1741-05-06). The Moon holds it through the 1900s, first passes one arcminute on 2081-08-06, its yearly median passes it in 2090, and it reaches 240.337 arcsec on 2199-07-09: past the measured record the two systems read a future time differently, Horizons as UTC with no new leap seconds and the API as UT1 on an extrapolated delta-T, and that difference in convention is the gap. The lunar theory is not the cause. Where delta-T is measured, the Moon's 1900s median is 1.023 arcsec.

BodynMedian (arcsec)p99 (arcsec)Worst (arcsec)When
Sun3,6531.58314.12515.8282199-12-11
Moon36,5257.483208.139240.3372199-07-09
Mercury3,6533.80226.85037.3322199-07-14
Venus3,6532.54718.30823.6341703-11-02
Mars3,6532.28011.89723.1891830-09-27
Jupiter3,6534.65413.16216.8331715-11-19
Saturn3,6538.84821.06023.8351709-12-30
Uranus3,6536.48717.57519.5702036-01-30
Neptune3,65310.38420.38621.5091848-09-02
Pluto3,6538.75349.87451.4081741-05-06

Measured on contract 2.2.0, which takes delta-T from the IERS. Of the 69,402 places, 9,441 moved from contract 2.1.0's figures, all dated 1972 to 2039: 5,192 closer to Horizons, 4,161 farther and 88 by too little to change the separation; the Moon by up to 4.3 arcsec and every other body by under 1 arcsec. The report's section 1.1 has the detail.

Report: The CycleCalcs API against JPL Horizons, 1700 to 2200 (PDF, version 2026-10.2, contract 2.2.0; the first edition, version 2026-10, measured contract 2.1.0). Data: the public study (summary.json, every residual, the raw Horizons replies), archived under DOI 10.5281/zenodo.21905241, measured on contract 2.1.0. The report's section 8 says how this edition's figures are rebuilt from it.

Time scales: UT1 and delta-T

The instant you send is interpreted as UTC, or as local civil time in the zone you send in tz. From there:

UT1 is assumed equal to UTC

A time you send is read as UT1, and the IERS values under delta-T below are TT minus UT1, so everything is computed for the instant whose UT1 is the time you sent. That instant is UT1 - UTC from the UTC instant you meant. Every response says so, in meta.time.ut1_source, and /v2/time repeats it inside the data as scales.ut1_note. The leap-second system keeps UT1 - UTC inside 0.9 s, so that is the bound.

In plain terms. Because UT1 is being read off a UTC clock, every instant the API returns, a sunrise as much as a Moon phase or an eclipse contact, can be out by up to 0.9 s before any other error is counted. A millisecond in the output is a stable identifier, not a promise.

The 0.9 s bound is a property of the modern leap-second system, and the leap-second table this API carries begins in 1972. Before 1972 the larger uncertainty is delta-T itself, below, and /v2/conventions says so under time_scales.ut1.

Delta-T, and the residual the response publishes

Terrestrial Time is UTC plus delta-T, and since contract 2.2.0 delta-T comes from the IERS from 1972: the EOP 20 C04 series where the Earth's rotation has been measured (through 2025-05-05), the IERS Rapid Service's measurements after that (through 2026-10-08), and IERS Bulletin A where it is predicted (issued 2026-10-08, through 2027-10-08), interpolated linearly. Before 1972 it is the engine's Espenak and Meeus polynomial, and after the prediction it blends linearly back to that polynomial by 2040. /v2/conventions states the model in full under time_scales.delta_t_description. TAI comes from the leap-second table, which starts in 1972; before that, /v2/time reports tai: null with tai_source: "undefined_before_1972" and a time_scale_undefined warning, rather than inventing a value.

Because TT - TAI is 32.184 s by definition, those two independent routes to the same instant can be differenced. That difference is the delta-T model's departure from the leap-second chain, and every /v2/time response publishes it as delta_t_model_residual_seconds. Measured:

Instant (UTC)delta_t (s)TAI - UTC (s)Residual (s)
1972-01-01T12:0042.230659100.047
1980-01-01T12:0050.54024719-0.644
1990-01-01T12:0056.85606725-0.328
2000-01-01T12:0063.82943432-0.355
2010-01-01T12:0066.07054434-0.113
2020-01-01T12:0069.361226370.177
2026-07-31T12:0069.17150037-0.013
2027-08-02T12:0069.375500370.192
2040-01-01T12:0084.7203803715.536

Read that table as follows. From 1972 to the end of the prediction the residual is minus UT1 - UTC, the Earth's measured rotation against the leap-second chain, so it stays inside 0.9 s. By 2040 the model is the polynomial again, 15.5 s above the chain as it stands. Before contract 2.2.0 the API used the polynomial throughout, and its branch for years after 2005 is an extrapolating parabola the Earth's rotation did not follow: it ran 2.4 s high in 2020 and 6.224 s high at 2026-07-31T12:00, the residual this table showed then.

What that means for you. Delta-T decides which ephemeris instant a given UTC is evaluated at. The 6.22 s that contract 2.2.0 took out of 2026 moved geocentric apparent place by Moon 3.26 arcsec, Sun 0.25, Mercury 0.21, Mars 0.18, Jupiter 0.06. The 15.5 s residual of 2040 is worth Moon 7.75 arcsec, Sun 0.66. Every one of those is far inside arcminute class, so for a position delta-T hardly matters.

It matters when the question is about a point on the ground rather than a direction in the sky. The Earth turns about 0.465 km per second at the equator, so an error of a few seconds in delta-T carries a few kilometers straight into any answer of the form "where on the Earth", and the same seconds into every eclipse contact: on contract 2.1.0 the contacts of the 2027 total eclipse printed about 7 s early. From 1972 to the end of the prediction the error left is UT1 - UTC. Past it, take delta_t_model_residual_seconds off the response and carry it through.

Before 1972 there is no leap-second chain to difference against, so no residual can be published, and delta-T itself is a reconstruction from historical timings whose standard error is about 5 s in 1700, 1 s in 1800 and 0.1 s in 1900 (Stephenson and Houlden 1986, as tabulated on NASA's eclipse site); a phase, season or conjunction instant in UTC inherits that uncertainty second for second. Before 1900 the engine's own positions differ from JPL Horizons by more than that; the new-moon rows under The reliable date range measure it.

Refraction and the atmosphere

The model is Saemundsson, as given by Meeus and as implemented by the engine's Refraction() in normal mode. Not Bennett, which is a different expression with different coefficients. The exact evaluation, read from the vendored source:

// h is the GEOMETRIC altitude in degrees; result in arcminutes
refraction = 1.02 / tan( (h + 10.3 / (h + 5.11)) degrees )

// h is clamped at -1.0 deg before evaluation, and below -1.0 deg
// the result is tapered linearly to zero at the nadir

The three constants are 1.02, 10.3 and 5.11, and a build check re-reads them out of the vendored file on every run so this page and the code cannot drift apart. Values produced by the shipped code:

Geometric altitude (deg)Refraction (arcmin)
-1.038.795
-0.833337.094
-0.566734.359
028.982
121.744
59.674
105.408
301.746
600.585

There is no temperature or pressure in it

The Saemundsson expression as coded is a function of altitude alone. It embeds no temperature term and no pressure term, so there is nothing for a weather parameter to enter and nothing the refraction model can be said to "assume". This is why /v2/conventions publishes refraction.assumed_temperature_c and refraction.assumed_pressure_mbar as null.

The U.S. Standard Atmosphere (1976) does appear in this API, but somewhere else and doing something narrower. The engine's Atmosphere() gives relative air density at the observer's elevation, referenced to sea level at 15.0 C (288.15 K) and 1013.25 mbar (101325 Pa). The rise and set search uses it for one purpose only: to scale its fixed 34-arcminute horizon allowance by air density. Measured from the shipped code:

Elevation (m)Relative densityPressure (mbar)Temperature (C)Scaled allowance (arcmin)
01.000001013.2515.0034.000
5000.95287954.6111.7532.398
10000.90747898.758.5030.854
20000.82163794.962.0027.935
30000.74215701.09-4.5025.233

Those temperatures and pressures are the standard atmosphere's own profile, used to get a density ratio. They are not weather, and sending your own is not possible.

No weather parameter is accepted, anywhere

The 31 shipped routes advertise 72 distinct parameters between them. None of them is a temperature, a pressure or a humidity. (depressions and sun_depression_deg are solar depression angles for twilight, not atmospheric.) This is a deliberate limit, not an oversight: a refraction model with no weather term cannot use weather you supply, and accepting a parameter that changes nothing would be worse than refusing it.

The magnitude of what you are giving up. The classical scaling for non-standard conditions, from Meeus, multiplies refraction by (P / 1010) * (283 / (273 + T)) with P in millibars and T in degrees Celsius. This API does not apply it. Worked against the 34-arcminute horizon allowance:

ConditionsFactorAllowance (arcmin)Change (arcmin)
-10 C, 1030 mbar (cold, high pressure)1.097437.31+3.31
0 C, 1020 mbar1.046935.59+1.59
15 C, 1013.25 mbar0.985833.52-0.48
30 C, 1000 mbar (hot, low pressure)0.924731.44-2.56

The cold case is 0.0552 deg of horizon. Run through the shipped search, that moves sunrise at London on 2026-06-21 28.2 s earlier than the published time. To convert any horizon error into a time error yourself, here is the sensitivity measured directly from the shipped search:

Place and date0.0555 deg0.1667 deg0.5 deg
Equator, 2026-03-2013.3 s40.0 s120.0 s
London (51.5 N), 2026-06-2128.4 s85.4 s257.3 s
London (51.5 N), 2026-12-2127.3 s81.8 s244.4 s
60 N, 2026-06-0142.2 s127.3 s385.8 s
66 N, 2026-09-2132.9 s98.8 s296.4 s

So: ordinary weather is worth tens of seconds at mid-latitudes and more toward the poles. A strong low-level temperature inversion over cold ground can lift a body by far more than any of this, and no ephemeris in the world predicts that from a date and a coordinate.

Two things soften the blow. Every horizontal position in the API returns altitude_refracted_deg and altitude_unrefracted_deg side by side with refraction_deg between them, so you never have to guess which one you are holding and you can substitute your own model at will. And refraction=none is accepted on /v2/positions if you want the geometric altitude alone.

What a rise or set time solves for

This is the section most astronomy APIs do not write, and it is the one to read if you are comparing two services and finding a difference of a minute or two.

The engine's rise and set search targets horizon_dip - (34/60) * air_density and solves it against a geometric altitude: the horizon conversion inside the search is called with the refraction argument omitted, which the code reads as zero refraction. The crossing is therefore geometric, not refracted. It does apply the body's semidiameter, for the Sun and Moon only, and topocentric parallax for everything.

BodyWhat the search solvesGeometric center altitude at the event
Sun Not the limb search. The API solves the Sun's center at the classical 50 arcminutes below the true horizon: about 16 of semidiameter plus about 34 of refraction. -0.8333 exactly, by adoption
Moon Upper limb at the density-scaled 34-arcminute allowance. The center therefore crosses at -0.5667 minus the Moon's own topocentric semidiameter, which is not a constant. -0.8459 to -0.8116
measured, London, 2026 and 2027
Every planet, and any fixed star you supply The engine allows a point source no semidiameter, so the center itself crosses the allowance. -0.566904 to -0.566430
measured; the spread is the root finder's 0.1 s tolerance, not physics

The Sun's value is a convention, not a computation. The Sun's real semidiameter runs from 15.73 to 16.27 arcminutes over a year, measured from the adopted solar radius of 696,000 km, so a fixed -0.8333 cannot be the limb at every date. It is the standard almanac definition and the API adopts it deliberately. The difference against the engine's own upper-limb search is small and is worth knowing: measured at London, the two differ by 2.10 s on 2026-01-03 and by 2.26 s on 2026-06-21, in opposite directions as the Earth-Sun distance crosses its mean.

The Moon's value genuinely has no single number. Its topocentric semidiameter at its own rise ranges from 14.69 to 16.75 arcminutes across the year, so quoting one figure for the Moon's crossing altitude would be wrong on most nights. That is why the API publishes the Moon's rise and set definition in words rather than a number, and why each event carries the altitude it actually solved for.

The rule to code against. If you need to answer "is this body up, the same way the rise and set search would answer it", compare the geometric center altitude plus the body's angular radius against -0.5667 deg. Do not compare a refracted altitude against zero. For the Moon the two differ by about 0.2 deg, which is roughly two minutes of its motion, and mixing them is exactly how an observing tool reports a bright gibbous night as moonless.

The API states all of this on the wire rather than leaving you to infer it. A rise or set event reports the unrefracted altitude, because that is the value the search solved for. A transit reports the refracted altitude, because that is what an observer would measure. That rule is published once as event_altitude_convention at /v2/conventions, not repeated in every block, so a microcontroller does not pay for it on every request.

One coincidence worth knowing, because it is the reason the two conventions are not as far apart as they look. Saemundsson evaluated at the search target of -0.5667 deg gives 34.359 arcminutes, against the flat 34 the search uses. The gap is 0.36 arcminutes, about 3 s of time at London. The fixed allowance and the refraction model agree at the horizon and diverge above it, which is why refraction is not an accepted parameter on the rise and set path: there is no refraction model in that code path to switch off.

Elevation, and the horizon dip we do not model

Sending elevation_m raises the observer on a locally flat plateau. The dip of the sea horizon is not modeled: the engine's dip function is called with zero metres above ground, so it returns zero.

The only thing your elevation does to a rise or set time, therefore, is thin the air. Thinner air shrinks the 34-arcminute allowance, which raises the target altitude, which makes the event later. Measured through the shipped endpoints at London on 2026-06-21, UTC day anchor:

EventSea level100 m1000 m3000 m
Moonrise11:39:57.7+2.2 s+21.3 s+59.5 s
Jupiter rise06:03:49.0+2.6 s+24.9 s+69.3 s
Sunrise03:42:45.401+0.000 s+0.000 s+0.001 s

Sunrise does not move, to within a millisecond, because the Sun's target is the fixed -0.8333 deg and is not density-scaled. That inconsistency between the Sun and every other body is real, and it is stated here rather than smoothed over.

This is the largest routine error on this page, and the sign is the wrong way round. A real observer at 3000 m with a sea horizon sees the Sun about 13.9 minutes early, because the horizon has dropped 1.60 deg below their horizontal. The API instead reports sunrise unchanged, and reports moonrise and planet rises about a minute late. If your observer is on a mountain, a coastline cliff or an aircraft, you must apply the dip yourself.

The value you are forgoing is published by /v2/conventions precisely so that you can apply it: dip in arcminutes = 1.753 * sqrt(elevation in metres). Measured from the shipped helper:

Elevation (m)1010010003000
Dip (arcmin)5.5417.5355.4396.02

Subtract the dip from the event altitude, re-solve, and use the sensitivity table in the refraction section if you only need the size of the shift. The dip is deliberately not applied by default because most callers at a nonzero elevation are inland, behind terrain, and would get a worse answer, not a better one.

The reliable date range

/v2 publishes Reliable from 1700 to 2200, and /v2/conventions publishes the same as date_range.min_year and date_range.max_year. Behavior at the edges, verified by invoking the endpoints:

  • Outside the range: a refusal, not a degraded answer. The year 1699 and the year 2205 both return 400 DATE_OUT_OF_RANGE with the message naming the year and the supported range.
  • Within 20 years of either boundary: /v2/positions returns 200 with an extrapolated_epoch warning. Verified at 1710 and at 2190.
  • The other date-taking endpoints do not emit that warning yet, and /v2/conventions says so in date_range.note rather than implying they do. Near a boundary, on those routes, the responsibility is yours.

The clearest single reason the window has ends is delta-T. From the shipped model:

Year1700180019002000202621002200
delta-T (s)8.913.6-2.264.069.2203.7443.1
First new moon, API minus Horizons, TT (s)-34.9-17.2-4.3+0.2+2.8-0.9-10.1
The same instant in UT (s)-28.9-12.3-3.5+0.5+2.9-134.4-383.0

Past 2150 the model is a bare parabola with no observational anchor whatever. The far end of the published range is an extrapolation, and the near-boundary warning exists to say so.

The two rows below the delta-T row time the first new moon of each column year against JPL Horizons (DE441): the instant the apparent ecliptic longitudes Horizons reports for the Moon and the Sun cross, read at one-minute steps (queries in tools/fixtures/horizons-new-moons-1700-2200.json). In Terrestrial Time, which takes delta-T out, the API is within 5 s of Horizons from 1900 to 2100, and 17 s early in 1800, 35 s early in 1700 and 10 s early in 2200. Every TT figure includes about -1.3 s from the Moon's light time, which Horizons applies and the API leaves out; the rest is the engine's own lunar and solar theory, and before 1900 it is larger than the delta-T uncertainty. In UT the two delta-T models enter too. Horizons puts UT 69.2 s behind TT in 2100 and 2200, the same as in 2026, where the shipped model has about 203 s and 442 s, so the UT gaps there are the difference between two assumptions about the Earth's future rotation, not an error either ephemeris can measure.

A separate fact, not a range boundary and deliberately reported differently: a time scale that did not exist at the requested instant comes back null with a time_scale_undefined warning. TAI before 1972 is the case you will meet.

Precision is not accuracy

Instants are printed to the millisecond. That is a stable identifier, not a claim, and this API would rather you know exactly how much of it is real.

QuantityRoot finder tolerance
Rise, set and any altitude crossing (twilights, custom depressions)0.1 s
Moon phase instants0.1 s
Solar longitude crossings (equinoxes, solstices, cross-quarters)0.01 s
Separation minima and longitude crossings (/v2/conjunctions, /v2/separation)1 s

Set those against the 0.9 s that UT1 alone is worth, and the tens of seconds that weather is worth near the horizon, and the ordering is clear: the search converges far tighter than the physics is known. The extra digits let you compare two responses and detect a change; they are not an error bar. The same goes for instant_precision_seconds on every event row: it is the search bracket, not an accuracy.

Conjunctions and close approaches

A pair event is only as good as the two positions behind it, and when two bodies move slowly against each other a small error in position becomes a large error in time. Measured against JPL Horizons (DE441) longitude crossings, apparent and geocentric: Venus with Saturn in April 2025 and with Mars in February and March 2022 agree to 7 to 21 minutes, and Jupiter with Saturn over the 30 crossings of 1700 to 2200 to within an hour, 10 minutes typically. The slowest pairs are hours off: Saturn and Uranus in 1988 by 1.2 to 1.8 hours, Uranus and Neptune in 1993 by 2.7 to 7.5 hours, one of those crossings landing on the next UT date. Read a slow outer-planet pair to the date, not the minute. The least separation of a slow, close pair is good to about 0.3 arcmin: Jupiter and Uranus reach 0.0618 deg on 2038-02-19 and 0.0263 deg on 2038-03-30 here, against 0.0566 and 0.0212 in DE441.

Oppositions, quadratures and conjunctions with the Sun

On /v2/planet-events, the conjunction with the Sun, western quadrature, opposition and eastern quadrature of Mars to Neptune are the instants the planet's apparent geocentric ecliptic longitude minus the Sun's, the two longitudes /v2/positions reports, reaches 0, 270, 180 and 90 deg. Against JPL Horizons (DE441) over 2025 to 2027 they fall within 1.4 minutes for Mars, Jupiter and Uranus, 4.5 minutes for Saturn and 5.6 minutes for Neptune. The search finds each instant to about a second, so the difference is the engine's planetary theory, and each figure covers the largest difference measured plus the 2-second search precision, because an instant moves by up to about a second with the request window. The response states the same figures in meta.conventions.solar_angle_accuracy_note.

Constellation entries

A constellation_entry on /v2/planet-events is timed on the planet's apparent geocentric place, the place /v2/positions takes its constellation from, against the IAU 1930 boundaries, and each crossing is found to under a second. Checked against Horizons' constellation column at one-minute rows (four boundary crossings of Mercury in May 2025 and Mars in April 2026), Horizons changes its name within a minute of the apparent place's crossing, so in practice it names the constellation of the apparent place, although the Horizons manual describes the astrometric one. The crossing times still differ from these by minutes (1.5 for Mercury, 5 for Mars) and by up to about 2 hours near a station (Saturn, Uranus): the engine's lookup rotates into the true equator of B1875, nutation included, while the IAU boundaries are drawn on the mean equator, so every boundary sits several arcsec off. In every window checked against Horizons the two name the same constellations in the same order; the accuracy report, section 7.1, has the measurement.

The precision parameter rounds decimals on output and changes nothing that was computed. Julian day fields are exempt from it by name, deliberately, because at precision=3 a Julian Day would round to the nearest 86 seconds and destroy the one field a caller reaches for when they want an instant to sub-second resolution.

Published disagreements

Where two defensible models of the same quantity exist, this API computes both, answers from the one it has chosen, and publishes the difference. A residual on the wire is more useful than a clean-looking number, because it tells you what you can and cannot ask of the answer.

The selenographic frame, on /v2/libration

The sub-Earth point comes from the engine's own libration function. The sub-solar point and the colongitude do not: the engine exposes no sub-solar function, so the Moon's body-fixed frame is rebuilt from its rotation axis using the IAU WGCCRE 2015 rotation elements. Two models, so the sub-Earth point is recomputed in the rebuilt frame and compared against the engine's own answer. The difference is published as method.cross_check_residual_deg.

SampleResidual (deg)
2026-11-14T21:30Z, the reference instant0.020123
3653 daily samples, 2020 to 2029: minimum0.017496
3653 daily samples, 2020 to 2029: maximum0.027570
3653 daily samples, 2020 to 2029: mean0.022399
Century marks 1700 / 1800 / 1900 / 2000 / 2100 / 22000.0229 / 0.0179 / 0.0197 / 0.0208 / 0.0240 / 0.0259

On a range request the published value is the worst residual across the rows, not an average, and a model_disagreement warning fires above 0.2 deg.

About 0.02 deg is roughly 0.6 km on the lunar surface at the sub-Earth point. It is stable across the whole supported date range, which is the signature of a frame that is built correctly rather than one that happens to agree today. The primary answer always comes from the engine's libration function: the recomputed angles are never averaged in, never preferred when they look better, and never silently substituted when the residual is large.

Publishing it costs nothing and buys two things. A caller who needs the colongitude to better than 0.6 km now knows they cannot have it here, instead of finding out later. And if that residual ever grew, the colongitude would be the first thing to distrust, so the number belongs on the wire rather than in a comment nobody reads.

Two supermoon rules, on /v2/phases

The same principle applied to a definition rather than to a frame. A full or new Moon is classified under the site's fixed distance rule and under Espenak's rule, which follows Nolle: a supermoon lies at a relative distance of 0.9 or more, (apogee - distance) / (apogee - perigee) for the apogee and perigee of its own orbit. Over 2024 to 2029 that reproduces all 50 supermoons in Espenak's published tables and no other new or full Moon (dated note). When the two rules disagree the response says so and by how much. The full Moon of 2026-11-24 is a worked example: at 360,754 km it is 754 km outside the fixed 360,000 km line but at a relative distance of 0.969 in its orbit, above Espenak's 0.9, so criteria_disagree is true and the response states in words that published lists following the two rules will disagree about this event.

The whole engine, against JPL Horizons

The arcminute claim at the top of this page is measured in public, in a reproducible study in the CycleCalcs data repository, alongside the event datasets archived under DOI 10.5281/zenodo.21905241: geocentric apparent places across 1700 to 2200, computed by the exact engine build this API serves and differenced against Horizons, with the raw reference tables and every residual committed. That study measured contract 2.1.0; the figures under Measured against JPL Horizons above recompute it on contract 2.2.0, as the report's section 8 describes; the method, the by-century tables, the delta-T analysis and the event timings are in the accuracy report (PDF, version 2026-10.2).

The study also surfaced two conventions this page now states rather than leaving discoverable. distance_au is the length of the aberration-corrected direction vector, which differs from the classical light-time range by up to about 1 part in 10,000, longer or shorter with the Earth's motion: for Mars on 2026-08-19 it exceeds JPL Horizons' light-time range by 24,752 km, 22,094 km of it this convention and the rest the difference between the two ephemerides. And the equatorial.j2000 block is that same aberrated direction expressed in the J2000 frame, so it sits about 20 arcsec of annual aberration away from a catalog-style astrometric place. Both are definitions; compare against Horizons quantity 2 (apparent, of date) to see the accuracy this page claims.

Light time and the other corrections

/v2/conventions publishes the correction register as data. Applied: aberration on every apparent place; light travel time on the Sun and the planets (the Moon's place is geometric, and its light time of about 1.3 s would move it by under 1 arcsec, while a star's catalog place is already the direction its light arrives from); nutation in every of-date frame; topocentric parallax whenever a latitude and longitude are supplied; and proper motion on named star targets. Not applied: polar motion, which is milliarcsecond class and an explicit non-goal, and light deflection, the bending of light by the Sun's gravity, which is about 0.004 arcsec at 90 deg from the Sun, about 1 arcsec at half a degree and 1.75 arcsec at the solar limb.

Topocentric parallax is worth stating in size because for the Moon it is enormous by the standards of everything else on this page. Measured at London through a full day, the largest geocentric-to-topocentric shift was 0.9279 deg, close to two lunar diameters.

Named stars: the catalog, proper motion and one apparent place

The star:<name> and hr:<n> targets are the 57 navigational stars of the Nautical Almanac, Polaris, and every star the IAU has named that is of V 3.0 or brighter and within 10.3 deg of the ecliptic, listed with their HR numbers at /v2/enums?set=stars. Their places come from the Bright Star Catalog, 5th revised edition (Hoffleit and Warren 1991), as served by NASA HEASARC, which asserts no restriction on its use, at the catalog's own precision: 0.1 s of right ascension and 1 arcsec of declination, equinox J2000 and epoch 2000.0. Their names are the IAU Working Group on Star Names'. A close double the catalog lists as two entries counts at its combined light, as the eye sees it: Porrima is two entries of V 3.65 and 3.68, which the eye sees as one star of V 2.9, and it carries that figure as combined_v_magnitude.

Proper motion is applied, and it matters: Arcturus alone moves about 11 arcmin between 1700 and 2000. Each star is moved from epoch 2000.0 to the requested instant by its catalog proper motion, parallax and radial velocity as straight-line motion in space, placed at the distance its catalog parallax implies, or at a nominal 1000 light-years where the catalog gives none, seen from the Earth's center, which is its annual parallax, given annual aberration, and precessed and nutated to the date.

No star distance is published. The catalog parallaxes predate Hipparcos and for distant stars are wrong by a factor of several: Rigel's 0.013 arcsec puts it at about 250 light-years, against about 860 in van Leeuwen's 2007 reduction. /v2/positions gives a star no distance, and /v2/separation, which must print one, marks it with distance_is_placeholder.

Against an independent reduction of the same catalog numbers in astropy 7.2 (ERFA), for every named star at eight instants from 1700 to 2200 (in 1700, 1750, 1850, 2000, 2026, 2100, 2150 and 2200), the apparent place of date agrees to a median of 0.04 arcsec, and to within 0.13 arcsec everywhere more than 10 deg from the Sun, well inside the catalog's own 1 arcsec. Nearer the Sun the difference grows, because light deflection is not applied: 0.23 arcsec for a star 2 deg from the Sun, and 0.99 arcsec for Regulus 0.47 deg from it on 23 August 2026.

A radec target you supply takes the same final steps with no proper motion: the pair is read as a J2000 place, is not moved, and is placed at a nominal 1000 light-years, a figure /v2/separation marks with distance_is_placeholder. Every endpoint gives a fixed target this one apparent place. /v2/positions once rotated a radec pair to the date with no aberration, about 20 arcsec from the place /v2/separation and /v2/rise-set used for the same pair.

Phase and season instants use the same apparent longitude

A new, first-quarter, full or last-quarter Moon is the instant the Moon's apparent ecliptic longitude minus the Sun's, both exactly as /v2/positions reports them, reaches 0, 90, 180 or 270 deg; an equinox, solstice or cross-quarter is the instant the Sun's reaches its defining value. That is the Astronomical Almanac definition the U.S. Naval Observatory publishes; JPL Horizons publishes no phase instants, but the apparent ecliptic longitudes it reports are this same quantity. It has been the API's since contract 2.1.0. Before that the phase search took the Sun without its 20.5 arcsec of annual aberration, so every quarter came out 34 to 46 s later than this definition gives, and the season search used a fixed one-AU light time, which put June solstices about 8 s early and December solstices about 8 s late against /v2/positions. The new moon of 2026-10-10 moved from 15:50:37 to 15:49:56 UTC, and on contract 2.2.0's IERS delta-T to 15:50:02; the Horizons longitudes cross at 15:50:05. Across 17 quarters of 2026 and 2027 the API falls 3.4 s before to 3.3 s after the Horizons crossing. Rounded to the minute, the phases match the U.S. Naval Observatory's published minute for 80 quarters out of 99. For 4 of the 17 sampled quarters USNO's minute is one earlier than the rounded Horizons crossing, which falls 31 to 41 seconds past the minute in each; if USNO rounds to the minute, its times run 11 to 15 s before Horizons'. Equinox and solstice instants are tens of seconds off: over the 8 seasons of 2026 and 2027 the rounded minute equals the Naval Observatory's for 5 of 8, 35.8 s RMS and 74.6 s at most, and the September equinox of 2026 prints 00:06 where the Naval Observatory prints 00:05 and the Horizons longitudes cross at 00:05:14; the 24 solar-term crossings of 2026 are 28.2 s RMS from Horizons, 49.9 s at most. That is the engine's solar theory: its longitude of the Sun is 1.15 arcsec RMS off at those instants, about 28 s of the Sun's motion. The accuracy report, sections 6.2 and 7.2, has the table. On contract 2.1.0 these figures were smaller: its delta-T ran about 6 s too large, which put every instant about 6 s early and offset part of that error. The site's pages, the paid calendar and the calendar feeds print the same instants, rounded to the nearest minute rather than truncated.

Galilean events are evaluated at the light departure instant

On /v2/jupiter-moons, transits and occultations are what the Earth sees; shadow transits and eclipses are physical events at Jupiter that the Earth sees later. Both views are projected at the light departure instant, and the two do not coincide.

The size of that correction is not subtle. Before it was applied, all four Sun-view contacts on 2026-11-15 solved 43.9 minutes late, against an Earth-Jupiter light time of 43.89 minutes that day. The decisive test needs no reference data: near opposition a moon's shadow falls almost exactly where the moon appears, so a shadow transit must very nearly coincide with the transit. An uncorrected implementation instead reports leads of 34.0, 35.0 and 32.7 minutes for Callisto, Io and Ganymede, three moons whose sky speeds differ by a factor of seven. A geometric offset cannot be near-identical for all three; a constant equal to that day's light time can only be an uncorrected branch.

The residual that remains is published in the response, at method.light_time_residual_km: the differential light time between Jupiter and a moon is not solved separately, which is at most 0.3 s of arrival time for Callisto and moves it under 5 km.

The same response also states that the umbra is treated as a cylinder with the Sun as a point source. The Sun's angular radius at Jupiter is about 0.05 deg, so an eclipse has a penumbral fringe (367 km for Io, up to 1649 km for Callisto) and a real disappearance is gradual over roughly two to four minutes rather than instantaneous.

The sub-solar point: aberration, and a correction that is easy to double-count

The engine back-dates every position it returns, so light travel time is already in the Sun vector before /v2/libration touches it. The extra step that endpoint applies re-forms the sub-solar direction as it stood from where the Earth was one light time ago, and that is annual aberration, not a second light-time correction.

Measured at the reference instant 2026-11-14T21:30Z: the raw engine vector gives a colongitude of 334.295147, and the step as applied gives 334.300903. That is 0.005756 deg of colongitude and a direction shift of 20.7142 arcsec, against an annual aberration constant of 20.4955 arcsec, the excess being the Moon's own velocity relative to the Earth.

The response reports method.annual_aberration_applied: true with a note saying exactly that. This field previously shipped under a name that said light time, which was worse than useless: a caller reconciling this colongitude against their own ephemeris would have applied light time a second time hunting for the 0.006 deg, and never accounted for it. The arithmetic never changed; only the name did.

Dated corrections

A correction ships in a minor contract version, and the changelog lists every change in each version with its old and new behavior. The corrections below are the ones it links here, each noted with its date: what was wrong, why, and what the API returns now.

2026-10-30, contract 2.7.0: Espenak's supermoon rule

What was wrong: classification.espenak on /v2/phases, and the tables on Supermoons, credited Fred Espenak with one supermoon line per calendar year, ten percent of the way from the year's closest perigee to its farthest apogee: 361,617.6 km in 2026. Espenak publishes no such line. His Full Moon at Perigee and New Moon at Perigee tables apply Nolle's 90 percent rule to each orbit and print the relative distance. Those tables list 50 supermoons for 2024 to 2029; the yearly line missed 18 and admitted none they leave out: the full Moon of 2026-01-03, at 362,300 km and listed at 0.956, read false. Why it was wrong: the line was never compared with his published tables. Now the rule is per orbit, with the apogee and perigee nearest in time, and it reproduces every relative distance in his 2024 to 2029 rows to within 0.001. Across 1700 to 2200, 1,372 new and full Moons gain the flag and none loses it.

2026-10-09, contract 2.2.0: delta-T

What was wrong: every instant was timed on the engine's Espenak and Meeus delta-T polynomial, whose branch after 2005 is an extrapolating parabola. It ran 6.2 s above the Earth's measured rotation in 2026 and 6.7 s above it on 2027-08-02, so every event computed for the present day came out about 6 s early in UTC: the 2027 eclipse contacts, phases, seasons, conjunctions and stations. Why it matters: an eclipse contact printed to the second came out about 7 s early, the direction in which "filters off" is unsafe. Now delta-T comes from the IERS from 1972, measured (EOP 20 C04, then the Rapid Service) and predicted (Bulletin A), and blends back to the polynomial by 2040, on the server, in the browser and in every baked page. The new moon of 2026-10-10 moved from 15:49:56 to 15:50:02 UTC; the Horizons longitudes cross at 15:50:05. The measurements are under time scales and phase and season instants.

2026-09-25, contract 2.1.0: phase and season instants

What was wrong: the phase search took the Sun's longitude without its 20.5 arcsec of annual aberration, so every new, first-quarter, full and last-quarter Moon came out 34 to 46 s later than the apparent-longitude definition gives, and the season search used a fixed one-AU light time, which put June solstices about 8 s early and December solstices about 8 s late against /v2/positions. Why it was wrong: a phase is the instant the Moon's apparent ecliptic longitude minus the Sun's reaches 0, 90, 180 or 270 deg, and a season the instant the Sun's reaches its defining value, on the longitudes /v2/positions reports; the old searches disagreed with the API's own positions. Now both searches use those longitudes, and the new moon of 2026-10-10 moved from 15:50:37 to 15:49:56 UTC. The comparison with the U.S. Naval Observatory and JPL Horizons is under phase and season instants.

2026-09-25, contract 2.1.0: conjunction or close approach

What was wrong: /v2/conjunctions and /v2/separation called a separation minimum a conjunction only when the two longitudes crossed within 24 hours of it. That window was written for the Moon, whose crossing always falls within about two hours of its minimum. When two bodies move slowly against each other, or one is near a station, the least separation can fall days or weeks from the crossing, and two or three crossings can share one minimum. Measured under the old rule, the 1993 Uranus-Neptune triple came back as one conjunction and a close approach, the 1988 Saturn-Uranus triple as one conjunction and two close approaches, and the April 2025 Venus-Saturn and March 2022 Venus-Mars pairings as close approaches, though their longitudes had crossed twice, 4 and 21 days before the Venus-Saturn minimum and 10 and 28 days before the Venus-Mars one. Now a minimum is a conjunction when the apparent longitudes cross anywhere in its basin, the span between the separation maxima either side, capped at 180 days. The basins divide time without overlap, so each crossing belongs to exactly one minimum. Among the six Jupiter-to-Neptune pairs over 1700 to 2200, with minima to 15 deg, the farthest crossing from its minimum is 73 days (Uranus-Neptune, 1993). Moon rows do not change.

2026-09-25, contract 2.1.0: the precession period and rate

What was wrong: /v2/cycles gave the period of axial precession as the traditional round figure, 360 x 72 years (9,467,280 days), with state.rate_arcsec_per_year 50.0. That is a round number, not a measurement. The rate this API uses everywhere else is the linear term of the IAU 2006 precession in longitude (Capitaine, Wallace and Chapront 2003), 50.288 arcseconds a year, which turns the equinox through a full circle in 1,296,000 / 50.28796195 = 25,771.6 years: 25,772 years, or 9,413,068 days. Now period_days is 9,413,068, period_text leads with about 25,772 years, and the rate is 50.288. The traditional figure is kept under names that say what it is: meta.conventions.precession_years_traditional, and traditional_period_days and traditional_period_years on the axial_precession row of an answer for one instant.

2026-09-25, contract 2.1.0: meta.frames on /v2/conjunctions and /v2/separation

What was wrong: meta.frames defines each frame a response uses, and every /v2/conjunctions and /v2/separation response listed six: equatorial_j2000, equatorial_of_date, ecliptic, ecliptic_j2000, galactic and horizontal, the last even with no location. Those routes never return a position in equatorial_j2000, ecliptic_j2000 or galactic, or in horizontal without a location, so the description named frames the answer did not contain. Now they list equatorial_of_date and ecliptic, plus horizontal with a location. No data field changed.

2026-09-25, contract 2.1.0: one apparent place for a fixed target

What was wrong: /v2/positions rotated a radec target to the date without annual aberration, while /v2/separation and /v2/rise-set gave the same pair its aberrated place, so one target had two places about 20 arcsec apart, depending on the endpoint asked. Now every endpoint gives a fixed target one apparent place: the pair is read as J2000, placed at a nominal 1000 light-years, seen from the Earth's center, given annual aberration, and precessed and nutated to the date, and equatorial.j2000 on a radec row is that direction in the J2000 frame. Values on /v2/positions move by up to about 20 arcsec, and the /v2/rarity dark-walk altitudes for radec targets use the same place. How a named star reaches the same place is under named stars.

What is not computed, and why

A stated absence is worth more than a plausible-looking number. Each of these is published in machine-readable form as well as here.

No minor planets, no Chiron, no asteroids

The engine's body enumeration is Sun, Moon, Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune, Pluto, the solar system barycentre, the Earth-Moon barycentre, and eight user-defined star slots. There is no Chiron, no Ceres, no asteroid and no comet, and there is no way to add one without a second ephemeris. The reason is published verbatim in non_goals at /v2/conventions: the MIT-licensed Astronomy Engine has no minor-planet ephemeris, and CycleCalcs will not add a copyleft or paid ephemeris to provide one.

That is also the answer to the Swiss Ephemeris question. It is not used here. The reason is licensing, not astronomy: a copyleft or commercially licensed ephemeris would change what a caller may do with the output of this API, and the whole point of the rights statement is that the numbers come back unrestricted.

No satellites, no polar motion, no interpretation

  • Satellite passes. They need TLEs that decay within days. Serving them is a permanent data-operations commitment, and a stale answer is worse than no answer.
  • Polar motion. Milliarcsecond-class corrections are beyond the accuracy this API claims. Adding them would improve nothing that refraction and UT1 have not already swamped.
  • Interpretation. This API returns angles, times and names. It attaches no meaning to any of them, and no field in it ever will.

Withheld pending a license review, and named rather than blank

  • The meteor catalog. The meteor_showers enumerated set and the next_events.meteor_peak field are withheld until the meteor catalog reduction clears its license review. Asking /v2/enums?set=meteor_showers returns an empty set object rather than an invented one, and /v2/today carries the omission by name in meta.not_computed with the reason attached.
  • Deep-sky catalogs. The catalogs enumerated set is withheld: it needs a reduced deep-sky catalog index, and that source has not cleared its license re-source. Stars are not part of this. The named stars are served from the cleared Bright Star Catalog, as above, and any other fixed point can be supplied as radec:6.7525,-16.716, which the full rise, set and transit machinery works on.
  • The Saros member number on /v2/eclipses. It needs a 223-row table of series-start epochs, which is catalog data, and the obvious source has not cleared a provenance check. The Saros series number is closed form and is published.
  • Constellation genitives and the constellations set, which need a reduced index that has not been generated. Returning a nominative in a field labeled genitive would be worse than an honest omission.

Check any of this yourself

Every figure above came out of the shipped system, and you can get most of them back out of it without leaving your terminal. The API describes its own conventions as data, which is the point.

# the refraction block, the event definitions, the corrections register,
# the date range and the non-goals, all as JSON
curl "https://www.cyclecalcs.com/v2/conventions"

# the delta-T model residual against the leap-second chain, at any instant
curl "https://www.cyclecalcs.com/v2/time?at=2026-07-27T12:00:00Z"

# the selenographic cross-check residual, in method.cross_check_residual_deg
curl "https://www.cyclecalcs.com/v2/libration?at=2026-11-14T21:30:00Z&include=basic,terminator"

# the Galilean light-time residual, in method.light_time_residual_km
curl "https://www.cyclecalcs.com/v2/jupiter-moons?at=2026-11-15T09:40:00Z&step=1m"

# the altitude a rise actually solved for, in the events array
curl "https://www.cyclecalcs.com/v2/rise-set?body=moon&at=2026-06-21&lat=51.4778&lon=-0.0015"

# the engine version, hash, license and every source behind every route
curl "https://www.cyclecalcs.com/v2/attribution"

The vendored engine sits in the repository unedited, so the refraction constants, the 34-arcminute allowance, the delta-T polynomial and the search tolerances quoted on this page can all be read directly from the source that computed your answer. Its hash is re-verified against the recorded upstream release on every build.

If you find a figure on this page that the shipped code does not reproduce, that is a defect and we want to hear about it: info@cyclecalcs.com. See also the API overview, the endpoint reference, the error catalog and the engine and sources.

All API documentation

  • Developer hub: What to build and where every tool lives: API, MCP server, widgets.
  • Overview and playground: What the API answers, the plans, and a live try-it panel.
  • Endpoint reference: Every route, its parameters and a real captured response.
  • Keys and plans: How to get a key, what each plan raises, and how quotas are counted.
  • Error codes: The registered problem documents, one entry per code.
  • Accuracy and residuals: Measured error against reference ephemerides, and the conventions every response states.
  • Versioning policy: What is frozen, what may change, and how a change is announced.
  • Status and limits: Rate windows, range caps and the current service position.
  • MCP server: The same engine as tools an AI assistant can call directly.
  • Version 1 (frozen): The original seven endpoints, still served and never changing shape.
  • Terms of use: What you may do with the answers, including in commercial products.

Cite this page

CycleCalcs, "API Accuracy and Residuals", CycleCalcs LLC, https://www.cyclecalcs.com/api/accuracy.html (add your access date). The underlying event datasets are archived with a permanent DOI: 10.5281/zenodo.21905241, which always resolves to the newest version. Since v1.1.0 that archived record also carries the engine's accuracy study against JPL Horizons, with every raw residual and the code that reproduces it; the same files are browsable in the data repository. To cite the accuracy report this page summarizes: CycleCalcs, "The CycleCalcs API against JPL Horizons, 1700 to 2200", CycleCalcs LLC, version 2026-10.2, https://www.cyclecalcs.com/api/accuracy-report-2026-10.2.pdf.

Every computed value on this page is public domain: CycleCalcs asserts no copyright in calculated astronomical facts and dedicates any such right to CC0, stated here. Quote the numbers freely, with or without credit. Conventions and sources: the engine and sources.

The study behind this page, measured on contract 2.1.0. Astronomy Engine 2.1.19 against JPL Horizons: geocentric apparent places and per-instant residuals, free and in the public domain under CC0: summary of residuals per body · complete archive (Zenodo, v1.1.0). The archive files are regenerated by a documented build and published with a SHA-256 manifest.