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.
The problem shape
Section titled “The problem shape”{ "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.
Conditional fields
Section titled “Conditional fields”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 codes
Section titled “Status codes”| 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 |
Catalog
Section titled “Catalog”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. |
Request errors
Section titled “Request errors”request.malformed
Section titled “request.malformed”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.
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'Authentication errors
Section titled “Authentication errors”auth.missing
Section titled “auth.missing”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.
auth.invalid_key
Section titled “auth.invalid_key”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.
Validation errors
Section titled “Validation errors”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.
Subject time
Section titled “Subject time”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.
subject.time.required
Section titled “subject.time.required”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.
subject.time.invalid_format
Section titled “subject.time.invalid_format”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.
subject.time.out_of_range
Section titled “subject.time.out_of_range”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.
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"}}'subject.time.conflicting
Section titled “subject.time.conflicting”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.
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"}}'subject.time.ambiguous_local_time
Section titled “subject.time.ambiguous_local_time”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.
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"}}'subject.time.nonexistent_local_time
Section titled “subject.time.nonexistent_local_time”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.
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
Section titled “Subject timezone”subject.timezone names the IANA zone a local_datetime resolves against. It travels only with local_datetime, never with datetime.
subject.timezone.required
Section titled “subject.timezone.required”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".
subject.timezone.not_applicable
Section titled “subject.timezone.not_applicable”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.
subject.timezone.unknown
Section titled “subject.timezone.unknown”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.
Subject location
Section titled “Subject location”subject.location.required
Section titled “subject.location.required”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.
subject.location.incomplete
Section titled “subject.location.incomplete”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.
subject.location.invalid_coordinates
Section titled “subject.location.invalid_coordinates”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.
Settings
Section titled “Settings”settings.ayanamsa.required
Section titled “settings.ayanamsa.required”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.
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"}}'settings.ayanamsa.not_applicable
Section titled “settings.ayanamsa.not_applicable”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.
settings.bodies.empty
Section titled “settings.bodies.empty”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.
settings.transit_bodies.empty
Section titled “settings.transit_bodies.empty”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.
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": []}}'settings.aspects.invalid_orb
Section titled “settings.aspects.invalid_orb”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.
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}}}'Window scans
Section titled “Window scans”/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.
transit.window.invalid
Section titled “transit.window.invalid”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.
transit.window.too_large
Section titled “transit.window.too_large”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.
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"}}'Server errors
Section titled “Server errors”server.internal
Section titled “server.internal”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.
server.overloaded
Section titled “server.overloaded”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.)
server.calc_timeout
Section titled “server.calc_timeout”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.
Reserved codes (place-name geocoding)
Section titled “Reserved codes (place-name geocoding)”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. Likesubject.time.ambiguous_local_time, it will carry acandidatesarray 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