An astrological charting library for JavaScript and TypeScript. Give it a name, a moment, and a place; get back body positions, house cusps, angles, and the aspects between them.
import { configure, createPerson, createChart } from 'astrologyjs';
configure({ apiKey: process.env.MORPHEMERIS_API_KEY });
const person = await createPerson(
'Subject',
{ local: '1990-06-15T14:30' }, // local wall clock at the place
{ lat: 51.5074, lng: -0.1278 }, // London
);
const chart = await createChart('Subject — natal', person);
for (const planet of chart.planets) {
console.log(`${planet.name}: ${planet.signDegree.toFixed(2)}° ${planet.sign}`);
}You were right. Versions up to 1.3.1 computed charts by calling a free ephemeris service that no
longer exists, so every chart operation fails — usually with Unexpected token <, which is the
library trying to parse an HTTP redirect page as JSON. Issues reporting this have been open since
2017.
The short version of what happened: this was a part-time project that outran its author's bandwidth, and the backend went away. There is a real, maintained service behind it now.
Do not install 1.x. Beyond the dead backend, some of its arithmetic was wrong even while the
service was alive. The reported orb of an aspect was the fractional part of the angular separation
rather than the distance from exactness, so a trine at 118.5° reported 0.5 instead of 1.5 — an
aspect looked three times tighter than it was. And Person.create() accepted a Date, which meant
the host process's timezone silently decided what moment the chart was cast for: the same code
produced different charts on a laptop and in a UTC container, with no error either time.
If you stored charts produced by 1.x, treat their orbs as unreliable and re-derive them.
npm install astrologyjsESM only. 2.x ships no CommonJS build. Node 20.19+ or 22.12+ (both support require(esm)),
or any modern browser — read the browser note first.
astrologyjs needs an ephemeris. It uses Morphemeris, which wraps the
Swiss Ephemeris behind a REST API. You bring your own key — the library ships no shared key and
runs no free proxy, because a shared free endpoint is exactly the arrangement that broke last time
and stranded everyone using it.
- Create an account at morphemeris.com.
- Generate an API key.
- Give it to the library, by any of:
// Explicitly, per call — wins over everything else
await createChart('natal', person, { apiKey: 'mk_live_…' });
// Once, for the process
configure({ apiKey: 'mk_live_…' });
// Or set MORPHEMERIS_API_KEY in the environment (Node only; never read in a browser build)Disclosure: Morphemeris is run by this library's maintainer. It is a young service — newer than the library it now backs — so judge it on that basis rather than on a track record it does not yet have. It is not a hobby endpoint on shared hosting, which is what the original was, and it does not depend on anyone's spare weekend to stay up. That is the honest extent of the claim.
The free tier is 500 credits per calendar month, replenished on the 1st, with no payment method
required. One /v1/chart request is one credit, and a chart costs as many credits as it needs
requests:
| Chart type | Credits | Charts per free month |
|---|---|---|
| Basic (natal) | 1 | 500 |
| Davison | 1 | 500 |
| Transits | 2 | 250 |
| Synastry | 2 | 250 |
| Combined | 2 | 250 |
| DavisonTransits | 2 | 250 |
| CombinedTransits | 3 | 166 |
Identical requests inside one process are deduplicated, so building the same chart twice — or a synastry chart for two people born at the same instant in the same place — costs one credit, not two.
refreshTransits() is the one thing that can surprise you: it is cheap to call in a loop, and at
millisecond precision an animated transit clock would drain the free tier in minutes. Transit
instants are therefore rounded to 60 seconds by default, which costs at most half an arcminute on
the Moon — below the resolution a chart displays. Set transitGranularitySec: 0 to disable it, and
watch your balance.
const chart = await createChart('natal', person, {
houseSystem: 'placidus', // default
sidereal: 'lahiri', // omit for tropical
node: 'true', // or 'mean'
});
chart.planets; // 20 bodies: Sun–Pluto, nodes, Lilith, Chiron, Pholus, Ceres, Pallas, Juno, Vesta
chart.houses; // 12 cusps
chart.ascendant;
chart.midheaven;
chart.aspects; // every pair in aspect, with orb measured from exactness
chart.warnings; // anything the library assumed on your behalfAspects, midpoints, composites, and Davison charts are computed locally, so a chart is fully usable offline once fetched. The service supplies ephemeris and house cusps; nothing else.
There is deliberately no way to pass a Date or a bare date-time string. Both carry an implicit
zone, and every chart cast from one is a coin flip. A moment enters the library in exactly one of
three forms:
{ utc: '1815-12-10T19:00:00Z' } // already unambiguous
{ local: '1815-12-10T19:00' } // wall clock at the place
{ date: '1815-12-10', timeUnknown: true } // the date is known, the time is notThe zone comes from the coordinates, and the historical offset from the platform's own timezone
database — so wartime DST, half-hour and quarter-hour zones, and pre-standard-time local mean time
all resolve correctly. Every assumption is readable afterwards on the Person: zone,
utcOffsetMinutes, timeKnown, and zoneFromDefaultResolver.
When a local time is genuinely ambiguous — the hour that repeats when clocks go back — the library refuses rather than guessing, and hands you both candidates:
try {
await createPerson('Subject', { local: '2023-11-05T01:30' }, { lat: 40.7128, lng: -74.006 });
} catch (error) {
if (error instanceof AmbiguousTimeError) {
error.candidates; // both instants, with their offsets — pick one
}
}
// Then answer it:
await createPerson('Subject', { local: '2023-11-05T01:30', offsetMinutes: -240 }, place);An unknown birth time omits the houses and angles by default rather than inventing them: an
ascendant sweeps the whole zodiac in 24 hours, so a noon value is not an approximation of the true
one, it is unrelated to it. If you prefer the common noon convention, ask for it explicitly with
{ unknownTime: 'noon' } — body positions are the same either way, and the chart tells you which
you got in warnings.
Every failure is a typed class extending AstrologyError, each with a code and a retryable
flag: ConfigurationError, ValidationError, AmbiguousTimeError, NonexistentTimeError,
UnsupportedBodyError, AuthError, OriginError, InsufficientCreditsError, RateLimitError,
UpstreamError, ServiceUnavailableError, AdapterError, TransportError.
InsufficientCreditsError and RateLimitError are separate types on purpose — one needs a
purchase, the other needs a wait — and a response that is not JSON raises a TransportError naming
the status, the content type, and the first bytes of what actually came back. Unexpected token <
does not happen here.
The library runs in a browser, and process.env is never read in a browser build.
That is tested rather than asserted: test/browser/chart.browser.test.ts bundles the library for
platform=browser, serves it from a real origin, and casts a natal chart in Chromium against the
live API — then casts the same chart under Node and requires every planet's longitude, latitude,
speed and declination to match to ten decimal places. A missing key surfaces as ConfigurationError
rather than ReferenceError: process is not defined, and the cross-origin request is a real one.
But any key in a browser bundle is public — this library does not obfuscate it, because obfuscation implies a protection that does not exist.
If you ship a key to browsers, restrict it with Morphemeris's require_origin setting. Note that
origin restriction alone does not protect a leaked key, since a non-browser caller simply omits the
header. For anything beyond a demo, proxy through your own backend and keep the key there.
- Declination and out-of-bounds are computed locally, not read from the API response. The derivation uses true obliquity — mean plus nutation — and agrees with the engine's own equatorial output to within 0.1 arcsecond. It began as a workaround for morphemeris#83, since fixed upstream, and stays because a number the library computes is one it can verify.
orbis the distance from exactness, always non-negative. This is cross-checked against Morphemeris's independently implemented server-side aspect endpoint.- Two charts are comparable only if their frames match. House system, zodiac, and node choice
travel with the chart on
chart.optionsrather than being ambient. - A body has no
symbolproperty; see docs/glyphs.md for Unicode and Kairon Semiserif glyph tables, including the ASCII mapping 1.x encoded.
- Glyph tables — Unicode codepoints and the Kairon Semiserif ASCII map
- API reference — generated with
pnpm docs - Specification — the binding contract, if you want to know why something behaves the way it does
Tests come before code, and a failing test means the code is wrong until proven otherwise. Run
pnpm test — that is unit tests plus the Gherkin acceptance suite, and both must pass. Tests
against the live API read MORPHEMERIS_API_KEY from the environment and no-op cleanly without it.
The browser suite additionally needs a Chromium binary, which is a deliberate one-off rather than something every install pays for:
pnpm exec playwright install chromiumWithout it — and with an API key present — that suite fails with an error telling you to run exactly that.
MIT. See LICENSE.