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.
The subject object
Section titled “The subject object”The moment: datetime or local_datetime
Section titled “The moment: datetime or local_datetime”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.
location (both or neither)
Section titled “location (both or neither)”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. |
Datetime-only subjects
Section titled “Datetime-only subjects”Planetary positions depend on the moment, not on where the observer stands. If all you need is positions, omit location:
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_resolvedThe 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.
What time_known means
Section titled “What time_known means”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.
Two-subject endpoints
Section titled “Two-subject endpoints”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.
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}'Endpoint-specific request blocks
Section titled “Endpoint-specific request blocks”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.
The settings object
Section titled “The settings object”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:
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_resolvedBoth 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.
The response envelope
Section titled “The response envelope”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.
The echo contract
Section titled “The echo contract”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:
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_resolvedOverride several at once and the echo tracks each one — here the orb map also switches to the wider aspect set:
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_resolvedAPI spec v0.1.0 · docs 1123b41