Skip to content

Errors

Every non-2xx response is an RFC 9457 problem document, served as application/problem+json. Errors are part of the contract: each failure mode has a stable, namespaced code, and the type URI of every problem — https://docs.astralastrologyapi.com/errors/<code> — resolves to that code’s section on this page.

{
"type": "https://docs.astralastrologyapi.com/errors/settings.ayanamsa.required",
"title": "ayanamsa is required when zodiac is sidereal",
"status": 422,
"code": "settings.ayanamsa.required",
"detail": "zodiac is sidereal but no ayanamsa was provided",
"request_id": "req_8fK2x1"
}
Field Contract
type URI identifying the error; links to the matching section here. Mirrors code.
title Short human-readable summary. Stable per code.
status The HTTP status, repeated in the body.
code The field to branch on. Stable, namespaced, never renamed within v1.
detail Human-readable explanation of this occurrence. Human-only — the wording may change at any time; never parse it.
request_id Correlates this response with our server logs. Log it, and include it in any support request.

Branch on code, log request_id, show or discard detail, and never string-match title or detail in program logic.

A few codes attach extra machine-readable members, so the error itself carries what you need to recover. Each field appears only on the code listed for it.

Field Appears on Contract
candidates subject.time.ambiguous_local_time Array of { "utc_datetime", "utc_offset" } — the UTC instants an ambiguous local time could resolve to. Pick one and resubmit it as datetime.
suggestion subject.time.nonexistent_local_time One { "utc_datetime", "utc_offset" } — the first valid instant after the DST gap the local time fell into.
latitude settings.house_system.undefined_at_latitude The latitude at which the requested house system could not be computed.
available_systems settings.house_system.undefined_at_latitude The house systems defined at every latitude — safe fallbacks.
Status Meaning Codes
400 The body is not valid JSON or does not match the schema (unknown field, bad enum, missing required field). request.malformed
401 Authentication failed. auth.missing, auth.invalid_key
422 The request parsed, but the input is semantically invalid. subject.*, settings.*, transit.*
500 A bug on our side. It pages us. server.internal
503 The server is shedding load or a calculation timed out. Safe to retry with backoff. server.overloaded, server.calc_timeout

Every code the API can return, with the section that documents its trigger and handling. All codes are stable within v1 — branch on code.

Code Status Meaning
request.malformed 400 Body is not valid JSON or violates the schema.
auth.missing 401 No bearer token.
auth.invalid_key 401 Unknown or inactive API key.
subject.time.required 422 Neither datetime nor local_datetime given.
subject.time.invalid_format 422 Time is not valid for its field.
subject.time.out_of_range 422 Datetime is outside 1800–2400 CE.
subject.time.conflicting 422 datetime and local_datetime both given.
subject.time.ambiguous_local_time 422 Local time occurred twice (DST fall-back).
subject.time.nonexistent_local_time 422 Local time never occurred (DST gap).
subject.timezone.required 422 local_datetime needs a timezone.
subject.timezone.not_applicable 422 timezone sent alongside datetime.
subject.timezone.unknown 422 Not a recognized IANA zone name.
subject.location.required 422 Endpoint needs coordinates.
subject.location.incomplete 422 Only one of latitude/longitude sent.
subject.location.invalid_coordinates 422 Coordinates out of range.
settings.ayanamsa.required 422 Sidereal zodiac needs an ayanamsa.
settings.ayanamsa.not_applicable 422 ayanamsa sent with a tropical zodiac.
settings.bodies.empty 422 bodies is an empty array.
settings.transit_bodies.empty 422 transit_bodies is an empty array.
settings.aspects.invalid_orb 422 An aspect orb is ≤ 0 or > 30°.
settings.house_system.undefined_at_latitude 422 House system undefined at this latitude.
transit.window.invalid 422 Window unparseable, or from not before to.
transit.window.too_large 422 Window exceeds the endpoint’s maximum span.
server.internal 500 A bug on our side.
server.overloaded 503 Server is shedding load.
server.calc_timeout 503 Calculation timed out.

400 — Body is not valid JSON or violates the schema.

When you get it: the body is not valid JSON; carries a field the schema does not define (the schema is strict — additionalProperties: false — so a typo like datetme is rejected, not silently applied to the wrong moment); names an unknown enum value (e.g. a return.body other than sun/moon); or omits a required field (e.g. subject_b on a two-subject endpoint).

How to fix: validate your JSON against the endpoint schema and drop any field it does not define. subject.location.place is reserved for place-name geocoding and is rejected as an unknown field today.

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 '{not json'

401 — Missing bearer token.

When you get it: the request has no Authorization header, or the header is not a bearer scheme.

How to fix: send Authorization: Bearer ak_... on every /v1/* request. See Authentication.

401 — Unknown or inactive API key.

When you get it: the bearer token is not an active key — mistyped, revoked, or issued for a different environment.

How to fix: confirm the deployed key matches what was provisioned, and that you are using the intended ak_test_ / ak_live_ key.

All validation errors are 422 Unprocessable Content: the request was well-formed, but the input cannot produce a correct calculation. Two-subject endpoints (/synastry, /composite) and the transit, moment, window, and return blocks reuse these same codes — detail is prefixed to name the failing field (subject_a:, subject_b:, transit.datetime:, return.around.date:), but the code is unchanged, so one branch-on-code handler works everywhere. The catalog below is grouped by namespace.

Exactly one of subject.datetime (ISO 8601 with an explicit UTC offset) or subject.local_datetime (wall-clock time plus an IANA timezone) is required. Timezones & ambiguity covers both forms; the codes below are their failure modes.

422 — Neither datetime nor local_datetime was given.

When you get it: neither subject.datetime nor subject.local_datetime is present. Every calculation is anchored to a moment.

How to fix: supply exactly one — datetime as ISO 8601 with an offset (e.g. "1990-05-15T14:30:00-04:00"), or local_datetime plus an IANA timezone.

422 — Time is not valid for its field.

When you get it: datetime is not parseable as ISO 8601 with a UTC offset — a bare YYYY-MM-DDTHH:MM:SS in datetime fails, and is not reinterpreted as a local time — or local_datetime carries an offset or Z suffix, which belongs in datetime.

How to fix: datetime takes YYYY-MM-DDTHH:MM:SS±HH:MM (or Z for UTC); local_datetime takes the bare YYYY-MM-DDTHH:MM[:SS], with the zone named in timezone.

422 — Datetime is outside the supported range (1800–2400 CE).

When you get it: the datetime parsed, but falls outside the range the ephemeris is validated for.

How to fix: supply a datetime between 1800 and 2400 CE. If you need a wider range, tell us.

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": "1499-01-01T00:00:00Z"}}'

422 — datetime and local_datetime are mutually exclusive.

When you get it: both subject.datetime and subject.local_datetime were provided. Accepting both would make one silently authoritative, so the pair is rejected. (It also fires when a progressions target or returns around block carries both datetime and date.)

How to fix: send exactly one time form — datetime if you know the offset, or local_datetime + timezone to have the API resolve it.

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": "1992-11-22T08:45:00+05:30", "local_datetime": "1992-11-22T08:45:00"}}'

422 — Local time is ambiguous (DST fall-back).

When you get it: the local_datetime occurred twice in the given timezone, because clocks fell back across it. The problem carries a candidates array listing every instant the input could mean; the API refuses to pick one for you.

How to fix: choose the candidate you mean — programmatically, or by presenting the choice to a user — and resubmit it as datetime. See Timezones & ambiguity.

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": {"local_datetime": "1992-10-25T01:30:00", "timezone": "America/New_York"}}'

422 — Local time never occurred (DST spring-forward gap).

When you get it: clocks skipped over the local_datetime in the given timezone — no such instant exists. The problem carries a suggestion: the first valid instant after the gap.

How to fix: a nonexistent time is usually an upstream data or conversion bug — correct it at the source. If the record genuinely states a time inside the gap, resubmit suggestion as datetime.

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": {"local_datetime": "2026-03-08T02:30:00", "timezone": "America/New_York"}}'

subject.timezone names the IANA zone a local_datetime resolves against. It travels only with local_datetime, never with datetime.

422 — local_datetime requires a timezone.

When you get it: subject.local_datetime is present without subject.timezone. A wall-clock time resolves to nothing without a zone.

How to fix: add an IANA zone name, e.g. "timezone": "Asia/Kolkata".

422 — timezone applies only to local_datetime.

When you get it: subject.timezone was sent alongside subject.datetime, whose offset already states the zone.

How to fix: drop timezone — or, to have the API resolve the offset, switch to local_datetime.

422 — Unrecognized IANA timezone.

When you get it: subject.timezone is not a name in the IANA database; detail echoes the value. Common causes: abbreviations like EST or CET, and raw offsets like +05:30 (an offset belongs in datetime).

How to fix: use a canonical IANA name such as America/New_York or Asia/Kolkata.

422 — Subject location is required on this endpoint.

When you get it: the endpoint cannot compute without coordinates and subject.location was omitted. House cusps need a place on Earth, so /v1/western/houses and /v1/western/natal require a location; /v1/western/positions and /v1/western/aspects do not.

How to fix: supply subject.location with both latitude and longitude.

422 — Latitude and longitude must be provided together.

When you get it: subject.location has one of latitude / longitude but not the other. Half a pair is almost always a bug, so it is rejected on every endpoint — including those that would not otherwise use the location.

How to fix: send both, or omit location entirely where it is optional.

422 — Coordinates out of range.

When you get it: latitude is outside −90…90 or longitude is outside −180…180. A common cause is swapped latitude/longitude.

How to fix: send decimal degrees within range, positive north/east.

422 — ayanamsa is required when zodiac is sidereal.

When you get it: settings.zodiac is "sidereal" but no settings.ayanamsa was given. There is no universal default — the choice shifts every longitude — so the API will not pick one for you.

How to fix: pass one of lahiri, raman, or krishnamurti.

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": "2000-01-01T00:00:00Z"}, "settings": {"zodiac": "sidereal"}}'

422 — ayanamsa applies only to the sidereal zodiac.

When you get it: settings.ayanamsa was given while zodiac is "tropical" (explicit or default), where it would have no effect.

How to fix: remove ayanamsa, or set "zodiac": "sidereal" if a sidereal chart was the intent.

422 — bodies must not be empty.

When you get it: settings.bodies was provided as an empty array. An empty selection cannot mean “use the defaults” — it usually means a list-building bug — so it is rejected rather than returning an empty chart.

How to fix: omit bodies to get the default set, or list at least one body.

422 — transit_bodies must not be empty.

When you get it: settings.transit_bodies was provided as an empty array to /v1/western/transits/events. An empty set cannot mean “use the defaults” and would scan nothing while still charging, so it is rejected. Only /v1/western/transits/events reads this field; other endpoints ignore it.

How to fix: omit transit_bodies for the default set (mars, jupiter, saturn, uranus, neptune, pluto, chiron), or list at least one body.

Terminal window
curl -s https://astral-external-api-iksoi6t3nq-ue.a.run.app/v1/western/transits/events \
-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}}, "window": {"from": "2026-01-01T00:00:00Z", "to": "2026-06-01T00:00:00Z"}, "settings": {"transit_bodies": []}}'

422 — An aspect orb is out of range.

When you get it: a settings.aspects.orbs override is ≤ 0 or greater than 30°; detail names the aspect type and the offending value. It fires wherever an aspect scan runs — /v1/western/aspects, /natal, /synastry, /composite, /transits, and /transits/events.

How to fix: send each orb greater than 0 and at most 30 decimal degrees.

settings.house_system.undefined_at_latitude

Section titled “settings.house_system.undefined_at_latitude”

422 — House system is undefined at this latitude.

When you get it: the requested house system (or the default, placidus) cannot be computed at the subject’s latitude — in the current engine, placidus and koch above the polar circles. It fires anywhere houses are computed: /v1/western/houses, /v1/western/natal, and /v1/western/positions when a location is supplied. The whole request fails; house fields are never silently omitted.

The problem carries the failing latitude and an available_systems array — the nine systems defined at every latitude: whole_sign, equal, equal_mc, aries_equal, porphyry, morinus, vehlow, sripati, meridian.

How to fix: pick a system from available_systems for polar-latitude subjects. Other quadrant systems compute at all latitudes but degrade numerically near the poles.

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": "2000-06-21T12:00:00Z", "location": {"latitude": 78.22, "longitude": 15.65}}}'

/v1/western/transits/events and /v1/western/moon each take a window; these two codes are its failure modes. Both bounds are RFC 3339 with a mandatory UTC offset, and the maximum span differs by endpoint: 366 days for transit events, 92 days (“a season”) for the moon.

422 — Window is unparseable, or from is not before to.

When you get it: a bound could not be parsed or is out of range, or window.from is not strictly before window.to.

How to fix: send both bounds as RFC 3339 with an offset, from strictly before to.

422 — Window exceeds the endpoint’s maximum span.

When you get it: the span from from to to is over the endpoint’s cap — 366 days on /v1/western/transits/events, 92 days on /v1/western/moon. detail states the cap that was exceeded.

How to fix: shorten the window, or page through several requests.

Terminal window
curl -s https://astral-external-api-iksoi6t3nq-ue.a.run.app/v1/western/transits/events \
-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}}, "window": {"from": "2024-01-01T00:00:00Z", "to": "2026-01-01T00:00:00Z"}}'

500 — Internal error.

When you get it: a bug on our side. These page us automatically.

What to do: retry once; if it persists, contact us with the request_id.

503 — Server is shedding load.

When you get it: the calculation pool is saturated and the API is refusing work rather than queueing it into timeout territory.

What to do: retry with exponential backoff and jitter. The request is fine — resubmit it unchanged. (Retry-After is planned; see the note above.)

503 — Calculation timed out.

When you get it: the calculation exceeded its per-request time budget, usually under heavy load.

What to do: same as server.overloaded — the request is valid, retry with backoff.

One code is reserved and not yet live. It ships with place-name geocoding, which will let a subject’s location be a place name instead of coordinates. It is documented now so the contract is stable before you need it.

  • subject.location.ambiguous_place (422) — a place name matched more than one location. Like subject.time.ambiguous_local_time, it will carry a candidates array of each valid resolution, so a client can disambiguate programmatically or present the choice to a user.

Until geocoding ships, subject.location.place is rejected as an unknown field with request.malformed.

API spec v0.1.0 · docs 1123b41