Skip to content

Subjects & settings

Every computation endpoint shares two objects. subject says when and where a chart is for; settings say how to calculate it. There is one subject schema and one settings schema across the whole API — integrate them once and every endpoint follows. Both travel in a JSON POST body:

{
"subject": {
"datetime": "1990-05-15T14:30:00-04:00",
"location": { "latitude": 40.7128, "longitude": -74.0060 }
},
"settings": {
"zodiac": "tropical",
"node_type": "true"
}
}

subject is required; settings is optional and fully defaulted. Two-subject endpoints (synastry, composite) send subject_a and subject_b instead — the same subject schema in each slot — and the sky’s-Moon endpoint (moon) takes no subject at all.

Exactly one time field is required. Sending both is a 422 (subject.time.conflicting); sending neither is a 422 (subject.time.required).

datetime — an ISO 8601 timestamp with an explicit UTC offset (1990-05-15T14:30:00-04:00). It is authoritative: the API converts it to Universal Time for the ephemeris exactly as given, never reinterpreting it. No offset is a 422 (subject.time.invalid_format); a timezone alongside it is a 422 (subject.timezone.not_applicable), since the offset already states the zone.

local_datetime — a wall-clock time with no offset (1990-05-15T14:30:00) plus a required IANA timezone (America/New_York). A missing zone is a 422 (subject.timezone.required); an unknown name is subject.timezone.unknown; an offset or Z suffix here is a 422 (subject.time.invalid_format) — offset forms belong in datetime. Local times that occurred twice (DST fall-back) or never (spring-forward gap) are rejected with codes carrying the candidate instants to resubmit — see Timezones & ambiguity for the full DST contract.

Both forms name the same instant, and the echo proves it. The local-time version of the example above — "local_datetime": "1990-05-15T14:30:00" with "timezone": "America/New_York" — resolves to:

{
"coordinates": null,
"time_known": true,
"timezone": "America/New_York",
"utc_datetime": "1990-05-15T18:30:00Z",
"utc_offset": "-04:00"
}

The timezone field in meta.subject_resolved appears only on the local_datetime path. On the datetime path the offset is the whole story, and the field is absent.

Coordinates in decimal degrees: latitude from −90 to 90 (south negative), longitude from −180 to 180 (west negative). The two must travel together — a location with only one of them is a 422 (subject.location.incomplete). Omit the object entirely when the endpoint doesn’t need it, which depends on the endpoint:

Endpoint location
POST /v1/western/positions Optional — when present, each position gains a house placement (1–12).
POST /v1/western/houses Required — omitting it is a 422 (subject.location.required).
POST /v1/western/aspects Ignored — validated and echoed if present, but aspect math is purely relative.
POST /v1/western/natal Required — same contract as houses.
POST /v1/western/transits Optional — adds each transiting body’s natal house and natal ASC/MC as aspect targets.
POST /v1/western/transits/events Optional — adds natal ASC/MC to the scanned targets.
POST /v1/western/progressions Optional — progressed houses and angles compute only when present.
POST /v1/western/synastry Per subject, optional and independent — each chart’s ASC/MC targets and overlay direction need only that chart’s location.
POST /v1/western/composite Per subject, optional and independent — composite houses compute only when both subjects are located.
POST /v1/western/returns return.location, then subject.location, then neither — the return chart gets houses only when a location resolves.
POST /v1/western/moon Not accepted — this endpoint takes no subject.

Planetary positions depend on the moment, not on where the observer stands. If all you need is positions, omit location:

Terminal window
curl -s https://astral-external-api-iksoi6t3nq-ue.a.run.app/v1/western/positions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject": { "datetime": "1990-05-15T14:30:00-04:00" }
}' | jq .meta.subject_resolved

The echo reports "coordinates": null, so a stored response is unambiguous about whether a location was involved. Location-dependent outputs — cusps, angles, and per-body house placements — return subject.location.required rather than inventing a default location.

Every response echoes meta.subject_resolved.time_known. It is always true in v1, because a full timestamp is required. The field is forward-compatible plumbing for planned date-only subjects, where false will mark charts cast without a known time-of-day (houses, angles, a precise Moon degree omitted or degraded). Your response handling won’t need a schema change when that lands.

synastry and composite compare two people, so they replace subject with subject_a and subject_b — the same subject schema in each slot — and share one settings object (there is no settings_a/settings_b). Each subject’s location is optional and independent, per the table above. The echo splits to match: meta.subject_a_resolved and meta.subject_b_resolved, with no single subject_resolved. Subject errors carry a subject_a: or subject_b: prefix so you know which slot failed.

Terminal window
curl -s https://astral-external-api-iksoi6t3nq-ue.a.run.app/v1/western/synastry \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject_a": {
"datetime": "1990-05-15T14:30:00-04:00",
"location": { "latitude": 40.7128, "longitude": -74.0060 }
},
"subject_b": {
"datetime": "1992-11-22T08:45:00+05:30",
"location": { "latitude": 28.6139, "longitude": 77.2090 }
}
}' | jq '{subject_a_resolved: .meta.subject_a_resolved, subject_b_resolved: .meta.subject_b_resolved}'

Some endpoints take one more block for the second moment a calculation needs — a transit instant, a scan window, a progression target, a return specification. Each sits alongside subject/settings and is echoed under its own meta.*_resolved key.

Endpoint Extra block Shape Moment echo
transits transit (optional) { datetime } — offset form only meta.transit_resolved
transits/events window (required) { from, to } — offset form, span ≤ 366 days events carry their own exact_at
moon moment (optional), window (optional) moment { datetime }; window { from, to }, span ≤ 92 days meta.moment_resolved
progressions target (optional) { datetime } or { date } meta.target_resolved
returns return (required) { body, around?, occurrence?, location? } meta.around_resolved

Two shapes recur. Transit and moon moments (transit.datetime, moment.datetime, window.from/to) are RFC 3339 with a mandatory offset and accept only that offset form — local_datetime/timezone here are unknown fields (400), because these are machine times, not birth times. Progressions’ target and returns’ around accept either datetime or a bare date (YYYY-MM-DD, midnight UTC assumed); sending both is a 422 (subject.time.conflicting), and omitting the block defaults to now (echoed, so replaying the echoed value reproduces the result).

The return block adds structure of its own: body is sun or moon (required; any other value is a 400 at the JSON layer, not a 422), occurrence is next (default) or previous, and location sets the return chart’s own coordinates.

All settings fields are optional, every field has a documented default, and the fully resolved set is echoed on every response.

Field Type Default Notes
zodiac string: tropical or sidereal tropical Sidereal requires ayanamsa.
ayanamsa string: lahiri, raman, or krishnamurti none Required if and only if zodiac is sidereal. Sending it with tropical is a 422 (settings.ayanamsa.not_applicable); omitting it with sidereal is a 422 (settings.ayanamsa.required).
node_type string: "true" or "mean" "true" A JSON string, not a boolean. Selects which node appears in the default bodies.
bodies array of body names ten planets plus the lunar node Default node is true_node, or mean_node when node_type is "mean". Must be non-empty — [] is a 422 (settings.bodies.empty).
house_system string: one of 22 systems placidus Drives houses, natal, and the house placement on positions. See the latitude note below.
lots_formula string: sect_aware or day_formula_always sect_aware Which day/night arithmetic computes the Part of Fortune. sect_aware follows the Sun’s sect at the subject moment; day_formula_always applies the day formula regardless.
aspects.set string: ptolemaic or ptolemaic_plus_minor ptolemaic The five major aspects, or the majors plus six minors.
aspects.orbs object: aspect name → degrees per-type defaults Per-type orb overrides in decimal degrees. Types outside the resolved set are ignored; the resolved map is echoed.
transit_bodies array of body names mars, jupiter, saturn, uranus, neptune, pluto, chiron Used only by transits/events; ignored elsewhere. Must be non-empty — [] is a 422 (settings.transit_bodies.empty).

Supported bodies and transit_bodies values (18): sun, moon, mercury, venus, mars, jupiter, saturn, uranus, neptune, pluto, true_node, mean_node, chiron, lilith_mean, ceres, pallas, juno, vesta.

House systems. All 22: placidus, koch, whole_sign, equal, equal_mc, aries_equal, campanus, regiomontanus, porphyry, alcabitius, morinus, topocentric, krusinski, vehlow, sripati, meridian, horizon, carter, sunshine, apc, pullen_sd, pullen_sr. Nine are defined at every latitude: whole_sign, equal, equal_mc, aries_equal, vehlow, sripati, porphyry, morinus, meridian. Where the engine cannot compute a system — currently placidus and koch above the polar circles — the response is a 422 (settings.house_system.undefined_at_latitude) carrying the failing latitude and an available_systems array of those nine, never a silent substitution.

Default orbs. ptolemaic: conjunction 8°, opposition 8°, trine 7°, square 7°, sextile 5°. ptolemaic_plus_minor adds quincunx (3°), semisextile (2°), semisquare (2°), sesquiquadrate (2°), quintile (2°), and biquintile (2°). Any type you don’t override uses its default, and the resolved map — covering exactly the types in the resolved set — is echoed in meta.settings_resolved.aspects.orbs. The transit endpoints resolve a different default: a flat 3.0° for transits, a flat 1.0° for transits/events, with any per-type override you send still winning.

Requesting the sidereal zodiac means naming the ayanamsa explicitly — there is no implied default, because the choice moves every longitude:

Terminal window
curl -s https://astral-external-api-iksoi6t3nq-ue.a.run.app/v1/western/positions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject": { "datetime": "1990-05-15T14:30:00-04:00" },
"settings": { "zodiac": "sidereal", "ayanamsa": "lahiri" }
}' | jq .meta.settings_resolved

Both request objects are strict: an unknown field is rejected with 400 (request.malformed) naming the offending key, never silently ignored. If a request appears to accept a setting, it took effect. The codes quoted on this page are contractual — see Errors for the full catalog.

Every successful response has the same two-key shape: data carries the endpoint-specific result, and meta carries provenance.

{
"data": {
"positions": [
{
"body": "sun",
"longitude": 54.6572,
"sign": "taurus",
"sign_longitude": { "degrees": 24, "minutes": 39, "seconds": 26 },
"element": "earth",
"modality": "fixed",
"speed": 0.9641,
"retrograde": false,
"declination": 18.936,
"out_of_bounds": false,
"house": 9
}
]
},
"meta": {
"calc_version": "0.1.1",
"settings_resolved": {
"aspects": { "set": "ptolemaic", "orbs": {
"conjunction": 8.0, "opposition": 8.0, "trine": 7.0, "square": 7.0, "sextile": 5.0
} },
"bodies": ["sun", "moon", "mercury", "venus", "mars", "jupiter",
"saturn", "uranus", "neptune", "pluto", "true_node"],
"house_system": "placidus",
"lots_formula": "sect_aware",
"node_type": "true",
"zodiac": "tropical"
},
"subject_resolved": {
"coordinates": { "latitude": 40.7128, "longitude": -74.0060 },
"time_known": true,
"utc_datetime": "1990-05-15T18:30:00Z",
"utc_offset": "-04:00"
},
"credits_charged": 1,
"request_id": "req_933ebab3d3294301afa1c8fe8c36636d"
}
}

(The positions array is truncated to one body; a real response carries one entry per requested body, each with a house placement here because the subject included a location.)

meta always carries calc_version (identical inputs under one calc_version reproduce byte-identical data), the full settings_resolved, credits_charged (in-band metering — see Credits & metering for the per-endpoint cost), and a request_id to quote in support requests. The time echo varies by endpoint family:

Endpoint family Subject echo Extra moment echo
positions, houses, aspects, natal subject_resolved —
synastry, composite subject_a_resolved, subject_b_resolved —
transits, transits/events subject_resolved transit_resolved (snapshot only)
progressions subject_resolved target_resolved
returns subject_resolved around_resolved
moon — (no subject) moment_resolved

Errors use RFC 9457 application/problem+json with a stable, namespaced code field — branch on code, never on the human-readable detail.

meta.settings_resolved and the subject echo appear on every response, not only when you ask — so a default you didn’t know about, or an offset conversion you couldn’t see, identifies itself. Compare settings_resolved with what you intended and the subject echo’s utc_datetime with the moment you meant, and any discrepancy is a documentation lookup rather than a support ticket. See Determinism & calc_version for the byte-identical guarantee behind it.

Send only a subject and watch the defaults come back resolved:

Terminal window
curl -s https://astral-external-api-iksoi6t3nq-ue.a.run.app/v1/western/positions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject": {
"datetime": "1990-05-15T14:30:00-04:00",
"location": { "latitude": 40.7128, "longitude": -74.0060 }
}
}' | jq .meta.settings_resolved

Override several at once and the echo tracks each one — here the orb map also switches to the wider aspect set:

Terminal window
curl -s https://astral-external-api-iksoi6t3nq-ue.a.run.app/v1/western/houses \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject": {
"datetime": "1990-05-15T14:30:00-04:00",
"location": { "latitude": 40.7128, "longitude": -74.0060 }
},
"settings": {
"house_system": "whole_sign",
"aspects": { "set": "ptolemaic_plus_minor" }
}
}' | jq .meta.settings_resolved

API spec v0.1.0 · docs 1123b41