# Vedic Astrology REST API — Jagannatha Hora calculation engine

> 17 HTTP endpoints returning Vedic astrology (Jyotish) calculations as JSON: birth chart / kundli, 23 divisional charts, dashas, yogas, doshas, ashtakavarga, panchang, gochar transits, muhurta and marriage matching. Sixteen of the 17 are open — no API key.

HTML version: https://horoscopes.today/api
MCP server:   https://horoscopes.today/mcp
OpenAPI 3.1:  https://horoscopes.today/openapi.json
Base URL:     https://jagannatha-hora-359167915530.europe-west1.run.app
Last verified against the service source: 2026-08-31

## Quick start

```bash
curl -X POST "https://jagannatha-hora-359167915530.europe-west1.run.app/horoscope" \
  -H "content-type: application/json" \
  -d '{"date":"1993-07-27","time":"13:51:00","place":"Hassan",
       "latitude":13.0,"longitude":76.1,"timezone":5.5}'
```

If you do not know the coordinates of a birth place, resolve it first with `GET /location/search`, which searches a local index of 162,000+ cities and returns latitude, longitude and timezone offset.

If you are wiring this into an AI agent rather than an application, use the MCP server instead — it exposes the same engine as 32 individually callable tools, so a model can ask for one section and get kilobytes rather than the megabytes `/horoscope` returns.

## Conventions

### No API key

Every endpoint on this list except POST /horoscope/pdf is open — no key, no token, no signup. CORS is wide open, so browser-side calls work too. The one gated endpoint uses a shared secret in the X-PDF-Secret header and is reserved for the site's own backend.

### Rate limits are real

Limits are enforced per client IP by the service itself. The app-wide default is 200 requests/minute per IP; several endpoints set a tighter one, listed with each endpoint below. Exceeding a limit returns 429.

### Everything is deterministic

The same inputs always produce the same output — these are astronomical calculations, not model inferences. Cache aggressively on your side. /panchang, /gochar and /gochar/events additionally send Cache-Control: public, max-age=21600.

### Dates and times

Dates are YYYY-MM-DD and times are HH:MM:SS on a 24-hour clock, always as strings, never epoch numbers. Timezones are a decimal hour offset from UTC (5.5, not "Asia/Kolkata"). For birth moments the service re-resolves the historically correct, DST-aware offset from the coordinates and date, so a slightly wrong offset is usually corrected for you.

### Responses are gzipped

Responses over 1 KB are gzip-compressed. This matters most for /horoscope, where it turns megabytes into hundreds of kilobytes. Send Accept-Encoding: gzip — every HTTP client does by default.

### Validation happens twice

FastAPI rejects a structurally wrong body with 422 before the handler runs; the field validators then reject out-of-range values with 400 and a message naming the field and the bound. Read the detail string — it tells you exactly which field failed and why.

## Shared birth-details block

Six endpoints take a birth chart as their subject: `/horoscope`, `/horoscope/pdf`, `/gochara`, `/gochara/range`, `/muhurta` and `/saturn-timeline`. They start from the same fields. Only the first six are required.

`/horoscope` and `/horoscope/pdf` accept every field below. The transit and muhurta endpoints take the first eight (through `ayanamsa_mode`) plus their own parameters; `/saturn-timeline` accepts a narrower `ayanamsa_mode` than the rest.

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `date` | string | Yes | `—` | Birth date, YYYY-MM-DD. Year must be 1900-2100, month 1-12, day 1-31. |
| `time` | string | Yes | `—` | Birth time, HH:MM:SS (24-hour). Hour 0-23, minute 0-59, second 0-59. |
| `place` | string | Yes | `—` | Birth place name. Cosmetic on the transit endpoints, where it defaults to an empty string; required on /horoscope. |
| `latitude` | number | Yes | `—` | Decimal degrees, -90 to 90. |
| `longitude` | number | Yes | `—` | Decimal degrees, -180 to 180. |
| `timezone` | number | Yes | `—` | UTC offset in hours, -12 to 14 (5.5 for IST). The service re-resolves the historical, DST-correct offset for the birth moment, so this is a hint rather than the last word. |
| `elevation` | number | No | `0.0` | Metres above sea level. |
| `ayanamsa_mode` | string | No | `"LAHIRI"` | One of 21 modes (see the ayanamsa list below). Case-insensitive; uppercased server-side. |
| `ayanamsa_value` | number | No | `null` | Custom ayanamsa in degrees, -30.0 to 30.0. Only meaningful with SIDM_USER, which is not honoured — see the ayanamsa note below. |
| `calculation_type` | string | No | `"drik"` | "drik" (modern) or "ss" (Surya Siddhanta). Case-insensitive. |
| `node_type` | string | No | `"mean"` | Rahu/Ketu node: "mean" or "true". |
| `bhava_madhya_method` | integer | No | `1` | House system, 1-5: 1=Equal-Lagna-Middle, 2=Equal-Lagna-Start, 3=Sripati, 4=KP, 5=Whole-Sign. |
| `language` | string | No | `"en"` | One of en, hi, ka, ta, te. |
| `pravesha_type` | integer | No | `0` | 0=Natal, 1=Annual, 2=Tithi-Pravesha. |
| `years` | integer | No | `1` | Year number for annual/progressed charts, 1-120. |
| `months` | integer | No | `1` | Month number for monthly charts, 1-12. |
| `sixty_hours` | integer | No | `1` | 60-hour period for hora progressions. The validator enforces 1-1000. |
| `divisional_chart_factor` | integer | No | `1` | 1-300. 1=D-1 (Rasi), 2=D-2 (Hora), 9=D-9 (Navamsa), and so on. |
| `chart_method` | integer | No | `1` | 1-6. Which classical construction to use for the divisional chart. |
| `base_rasi` | integer | No | `null` | 0-11 (0=Aries, 11=Pisces), for custom charts. |
| `count_from_end_of_sign` | boolean | No | `null` | Count divisions backward in even signs. |
| `varnada_method` | integer | No | `1` | Positive integer. |
| `tamil_month_method` | integer | No | `3` | 0=Ravi Annasamy, 1=V4.3.5, 2=10AM Start, 3=Midday UTC. |
| `kali_start_year` | integer | No | `13` | 1=Prabhava (South Indian) or 13=Pramaadhi (North Indian). |

### Ayanamsa modes (18 honoured, 21 accepted)

```
LAHIRI, TRUE_CITRA, TRUE_LAHIRI, KP, FAGAN, RAMAN, USHASHASHI, YUKTESHWAR, SURYASIDDHANTA, SURYASIDDHANTA_MSUN, ARYABHATA, ARYABHATA_MSUN, SS_CITRA, TRUE_REVATI, SS_REVATI, TRUE_PUSHYA, TRUE_MULA, KP-SENTHIL
```

`TRUE_LAHIRI` and `TRUE_CITRA` are the same ayanamsa under two names and return
identical charts.

**Three further modes are accepted by the request validator but not applied by
the engine. Do not use them.**

| Mode | What actually happens |
|---|---|
| `SENTHIL` | The chart is computed with whichever mode was set previously — in practice `LAHIRI` — while `ayanamsa_value` reports the Senthil figure. The response looks correct and is not. |
| `SUNDAR_SS` | Same failure as `SENTHIL`. |
| `SIDM_USER` | The supplied degrees are passed in the Julian-day argument, so the result is roughly 93° out; requests generally fail with a 500 and no value. |

`SENTHIL` and `SUNDAR_SS` never reach the sidereal-mode setter, so positions are
still computed against the previous mode. The mismatch is invisible in the
response, which is why they are called out here rather than merely omitted:
`ayanamsa_value` is documented as the ayanamsa *actually applied*, and for these
two it is not. Jagannatha Hora's own desktop UI excludes the same three, and so
does this site's ayanamsa picker.

## Endpoints (17)

### POST /horoscope

**The complete Vedic birth chart for a birth moment.**

The workhorse endpoint. One call returns every calculation the engine performs for a chart: 23 divisional charts, nakshatras and padas, yogas, doshas, chara karakas, sahams, upagrahas, special lagnas, sphutas, arudhas, house relationships, planetary states, five kinds of bala, ashtakavarga, graha and rasi dashas, Saturn transits and a longevity band. The response is large — on the order of 3.5 MB of JSON — and gzip-compressed on the wire. Computed fresh every time; the same inputs always produce the same output, so cache it on your side.

- Rate limit: 30 requests/minute per IP
- Authentication: None. Open endpoint, no key required.
- Response keys: birth_details, horoscope

Takes the shared birth-details block.

Example request:

```bash
curl -X POST "https://jagannatha-hora-359167915530.europe-west1.run.app/horoscope" \
  -H "content-type: application/json" \
  -d '{
    "date": "1993-07-27",
    "time": "13:51:00",
    "place": "Hassan",
    "latitude": 13.0,
    "longitude": 76.1,
    "timezone": 5.5
  }'
```

Example response (TRUNCATED excerpt — the full response is ~3.5 MB; all 31 top-level keys are listed below):

```json
{
  "birth_details": {
    "date": "1993-07-27", "time": "13:51:00", "place": "Hassan",
    "latitude": 13.0, "longitude": 76.1, "timezone": 5.5
  },
  "horoscope": {
    "ayanamsa_value": 23.8117,
    "julian_day": 2449196.0770833,
    "nakshatra_pada": {
      "Ascendant": { "nakshatra": "Swati", "nakshatra_number": 15,
                     "pada": 2, "nakshatra_lord": "Rahu",
                     "degrees_in_nakshatra": 4.1832 }
    },
    "divisional_charts": {
      "D-1_rasi": { "Ascendant": { "sign": "Libra", "longitude": 10.8483 } }
    }
    /* ...29 more top-level keys, listed in full above... */
  }
}
```

- Ask for a divisional chart with divisional_chart_factor, but note that divisional_charts already contains all 23 vargas regardless.
- If you only need one slice of this — the dashas, the ashtakavarga, the yogas — the MCP server exposes each as its own tool and returns kilobytes instead of megabytes.

### POST /horoscope/pdf

**The same chart rendered as a downloadable PDF report.**

Runs the identical calculation path as POST /horoscope — there is no second computation and no divergence — then typesets it as a designed report and returns application/pdf with a Content-Disposition of inline; filename="janma-kundali.pdf". Dosha presence and the birth panchanga are computed additionally for the cover; the /horoscope JSON response is unaffected. This is the one gated endpoint on the service.

- Rate limit: 10 requests/minute per IP
- Authentication: X-PDF-Secret request header, compared against the server's HOROSCOPE_PDF_SECRET with a constant-time comparison. Fails closed — no secret configured on the server means every call is rejected. This endpoint is for the site's own backend, not for third-party integration.
- Response keys: (binary application/pdf)

Takes the shared birth-details block, plus the fields below.

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `name` | string | No | `null` | Name printed on the cover. Maximum 80 characters. |
| `chart_style` | string | No | `"north"` | "north" or "south". Anything else is rejected. |
| `photo` | string | No | `null` | Base64 cover photo; validated and re-encoded server-side. |
| `online_url` | string | No | `null` | Maximum 300 characters. Renders a "View online" button at the top of the PDF. |

Example request:

```bash
curl -X POST "https://jagannatha-hora-359167915530.europe-west1.run.app/horoscope/pdf" \
  -H "content-type: application/json" \
  -H "x-pdf-secret: $HOROSCOPE_PDF_SECRET" \
  -d '{
    "date": "1993-07-27", "time": "13:51:00", "place": "Hassan",
    "latitude": 13.0, "longitude": 76.1, "timezone": 5.5,
    "name": "Asha R", "chart_style": "south"
  }' --output janma-kundali.pdf
```

Example response:

```json
HTTP/1.1 200 OK
content-type: application/pdf
content-disposition: inline; filename="janma-kundali.pdf"

%PDF-1.4 ...binary...
```

### POST /marriage-match

**Ashtakoota (North Indian) and 10 Porutham (South Indian) compatibility for two charts.**

Takes two birth charts and scores them under both classical systems at once, then adds the Manglik-family dosha load for each person and the Beeja/Kshetra sphutas. The North Indian block scores the eight kutas out of 36; the South Indian block reports each of the ten poruthams as matched or not, with the minimum-porutham flag. It returns the computation, not a verdict.

- Rate limit: Inherits the app default (200 requests/minute per IP) — this endpoint declares no limiter of its own
- Authentication: None. Open endpoint, no key required.
- Response keys: boy, girl, north_indian, south_indian, boy_dosha_data, girl_dosha_data, beeja_rasi, kshetra_rasi

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `boy_birth_details` | object | Yes | `—` | date, time, latitude, longitude, timezone (all required) and elevation (default 0.0). Note this nested model has NO place field and takes no calculation settings. |
| `girl_birth_details` | object | Yes | `—` | Same shape as boy_birth_details. |

Example request:

```bash
curl -X POST "https://jagannatha-hora-359167915530.europe-west1.run.app/marriage-match" \
  -H "content-type: application/json" \
  -d '{
    "boy_birth_details": {
      "date": "1993-07-27", "time": "13:51:00",
      "latitude": 13.0, "longitude": 76.1, "timezone": 5.5
    },
    "girl_birth_details": {
      "date": "1995-11-02", "time": "06:20:00",
      "latitude": 12.9716, "longitude": 77.5946, "timezone": 5.5
    }
  }'
```

Example response:

```json
{
  "north_indian": {
    "match_system": "North Indian (Ashtakoota)",
    "overall_compatibility": {
      "total_score": 24.5, "max_possible_score": 36,
      "percentage": 68.06, "rating": "Good"
    }
  },
  "south_indian": {
    "match_system": "South Indian (10 Porutham)",
    "ten_porutham": { "dina_porutham": true, "gana_porutham": true,
                      "rajju_porutham": false },
    "overall_compatibility": {
      "matched_count": 7, "total_porutham": 10,
      "percentage": 70.0, "rating": "Good",
      "minimum_porutham_satisfied": true
    }
  },
  "boy_dosha_data": { "mars_dosha": 0.0, "sun_dosha": 0.0,
                      "saturn_dosha": 0.5, "rahu_ketu_dosha": 0.0,
                      "total_dosha": 0.5 },
  "beeja_rasi": "Scorpio",
  "kshetra_rasi": "Cancer"
}
```

- The two nested birth-detail objects are a different, smaller model than the one /horoscope takes — no place, no ayanamsa_mode, no chart settings.

### POST /panchang

**The full daily panchang for a date and place. No birth chart needed.**

The five limbs — vara, tithi, nakshatra, yoga, karana — plus sun and moon rise/set, day length, the auspicious and inauspicious muhurta windows, day and night Choghadiya, the lunar month in both Amanta and Purnimanta reckoning, ritu, ayana, Shaka and Vikram Samvat with the sixty-year cycle name, and the Moon and Sun signs. Deterministic for a given date and place; the response carries Cache-Control: public, max-age=21600.

- Rate limit: 60 requests/minute per IP
- Authentication: None. Open endpoint, no key required.
- Response keys: panchang

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `date` | string | Yes | `—` | YYYY-MM-DD. Rejects dates that do not exist on the calendar, e.g. 2026-02-31. |
| `latitude` | number | Yes | `—` | -90 to 90, and additionally \|latitude\| must be 66 or less — panchang is sunrise-anchored and undefined beyond the polar circles. |
| `longitude` | number | Yes | `—` | -180 to 180. |
| `timezone` | number | Yes | `—` | UTC offset, -12 to 14. |
| `place` | string | No | `""` | Cosmetic. |
| `ayanamsa_mode` | string | No | `"LAHIRI"` | Any allowed ayanamsa mode. |

Example request:

```bash
curl -X POST "https://jagannatha-hora-359167915530.europe-west1.run.app/panchang" \
  -H "content-type: application/json" \
  -d '{
    "date": "2026-07-13",
    "latitude": 28.6139, "longitude": 77.2090,
    "timezone": 5.5, "place": "New Delhi"
  }'
```

Example response:

```json
{
  "panchang": {
    "date": "2026-07-13",
    "vara": "Monday",
    "tithi": { "name": "Krishna Ekadashi", "ends": "2026-07-13 21:44" },
    "nakshatra": { "name": "Rohini", "pada": 3 },
    "yoga": { "name": "Vyaghata" },
    "karana": { "name": "Balava" },
    "sunrise": "05:32", "sunset": "19:19",
    "moonrise": "01:12", "moonset": "15:48",
    "rahu_kalam": { "start": "07:15", "end": "08:58" },
    "abhijit": { "start": "11:58", "end": "12:53" },
    "lunar_month": { "amanta": "Ashadha", "purnimanta": "Shravana" }
  }
}
```

- The polar restriction is a hard 400, not a degraded result: beyond ±66° the solar-crossing search would not terminate.

### POST /gochar

**Live planetary transits for a date and place. Birthless.**

Where the natal chart is irrelevant: the nine grahas with sign and degree, nakshatra and pada, direct or retrograde motion, combustion flag, classical dignity (exalted, debilitated, moolatrikona, own), daily speed, and the date each next changes sign, nakshatra and pada — plus the Moon and Sun signs and the set of planets currently retrograde. Positions are geocentric and therefore identical worldwide at an instant; the place's timezone governs only what counts as "today" and how the dates are displayed. Cached 6 hours.

- Rate limit: 60 requests/minute per IP
- Authentication: None. Open endpoint, no key required.
- Response keys: gochar

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `date` | string | Yes | `—` | YYYY-MM-DD; must be a real calendar date. |
| `latitude` | number | Yes | `—` | -90 to 90. No polar restriction — gochar is sunrise-free and well defined at every latitude. |
| `longitude` | number | Yes | `—` | -180 to 180. |
| `timezone` | number | Yes | `—` | UTC offset, -12 to 14. |
| `place` | string | No | `""` | Cosmetic. |
| `ayanamsa_mode` | string | No | `"LAHIRI"` | Any allowed ayanamsa mode. |

Example request:

```bash
curl -X POST "https://jagannatha-hora-359167915530.europe-west1.run.app/gochar" \
  -H "content-type: application/json" \
  -d '{
    "date": "2026-07-14",
    "latitude": 28.6139, "longitude": 77.2090,
    "timezone": 5.5, "place": "New Delhi"
  }'
```

Example response:

```json
{
  "gochar": {
    "date": "2026-07-14",
    "planets": [
      { "name": "Saturn", "sign": "Pisces", "degree": 8.42,
        "nakshatra": "Uttara Bhadrapada", "pada": 1,
        "retrograde": true, "combust": false,
        "dignity": "own", "daily_speed": -0.019,
        "next_sign_change": "2027-03-29" }
    ],
    "moon_sign": "Taurus",
    "sun_sign": "Gemini",
    "retrograde_now": ["Saturn", "Rahu", "Ketu"]
  }
}
```

### POST /gochar/events

**Upcoming transit events over a date range. Birthless.**

One merged, date-sorted list of sign changes for every requested planet, nakshatra changes for all of them except the Moon (which changes nakshatra roughly daily and would drown the list), and retrograde stations for Mars, Mercury, Jupiter, Venus and Saturn. Event dates are expressed in the requested timezone. Computed fresh.

- Rate limit: 60 requests/minute per IP
- Authentication: None. Open endpoint, no key required.
- Response keys: gochar_events

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `from_date` | string | Yes | `—` | YYYY-MM-DD, a real calendar date. |
| `to_date` | string | Yes | `—` | YYYY-MM-DD. The range is capped at 90 days. |
| `latitude` | number | Yes | `—` | -90 to 90. Used for the display timezone. |
| `longitude` | number | Yes | `—` | -180 to 180. |
| `timezone` | number | Yes | `—` | UTC offset used to express event dates, -12 to 14. |
| `place` | string | No | `""` | Cosmetic. |
| `planets` | string[] | No | `null` | Planet names. Defaults to all nine: Sun, Moon, Mars, Mercury, Jupiter, Venus, Saturn, Rahu, Ketu. |
| `ayanamsa_mode` | string | No | `"LAHIRI"` | Any allowed ayanamsa mode. |

Example request:

```bash
curl -X POST "https://jagannatha-hora-359167915530.europe-west1.run.app/gochar/events" \
  -H "content-type: application/json" \
  -d '{
    "from_date": "2026-07-14", "to_date": "2026-08-13",
    "latitude": 28.6139, "longitude": 77.2090, "timezone": 5.5,
    "planets": ["Jupiter", "Saturn", "Mercury"]
  }'
```

Example response:

```json
{
  "gochar_events": [
    { "date": "2026-07-18", "planet": "Mercury",
      "type": "nakshatra_change", "to": "Pushya" },
    { "date": "2026-07-26", "planet": "Mercury",
      "type": "retrograde_station", "direction": "retrograde" },
    { "date": "2026-08-04", "planet": "Jupiter",
      "type": "sign_change", "from": "Cancer", "to": "Leo" }
  ]
}
```

### POST /gochara

**A transit snapshot read against a natal chart, for one target date.**

The natal counterpart to /gochar. Returns the panchanga for the target moment; the muhurta windows (sunrise, sunset, solar noon, Abhijit, and the Rahu, Gulika and Yamaganda periods to avoid); all nine planets with sign, degree, nakshatra and pada, retrograde and combust flags, and their house from both the natal Moon and the natal Lagna; and the person-relative strengths — Tara Bala from Moon, Sun and Lagna, Chandra Bala, and Sade Sati state. The event location is optional and defaults to the birth location. Computed fresh, no cache.

- Rate limit: 60 requests/minute per IP
- Authentication: None. Open endpoint, no key required.
- Response keys: birth_details, gochara

Takes the shared birth-details block, plus the fields below.

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `target_date` | string | Yes | `—` | YYYY-MM-DD — the transit moment. |
| `target_time` | string | No | `"12:00:00"` | HH:MM:SS. Defaults to local noon. |
| `event_place` | string | No | `null` | Falls back to the birth place. |
| `event_latitude` | number | No | `null` | -90 to 90. Falls back to the birth latitude. |
| `event_longitude` | number | No | `null` | -180 to 180. Falls back to the birth longitude. |
| `event_timezone` | number | No | `null` | -12 to 14. Falls back to the birth timezone. |
| `event_elevation` | number | No | `null` | Metres. Falls back to the birth elevation. |
| `include` | string[] | No | `null` | Enrichment sections. "ashtakavarga" is the recognised value; omit the field for the default, which includes it. |

Example request:

```bash
curl -X POST "https://jagannatha-hora-359167915530.europe-west1.run.app/gochara" \
  -H "content-type: application/json" \
  -d '{
    "date": "1991-08-18", "time": "11:13:00", "place": "Hassan",
    "latitude": 13.0071, "longitude": 76.0962, "timezone": 5.5,
    "target_date": "2026-06-25",
    "event_place": "Bangalore",
    "event_latitude": 12.9716, "event_longitude": 77.5946
  }'
```

Example response:

```json
{
  "birth_details": { "date": "1991-08-18", "time": "11:13:00",
                     "place": "Hassan", "latitude": 13.0071,
                     "longitude": 76.0962, "timezone": 5.5 },
  "gochara": {
    "panchanga": { "vara": "Thursday", "tithi": "Shukla Dasami",
                   "nakshatra": "Hasta", "yoga": "Siddha",
                   "karana": "Taitila", "paksha": "Shukla",
                   "rikta": false },
    "muhurta": { "sunrise": "05:58", "sunset": "18:49",
                 "abhijit": { "start": "12:01", "end": "12:53" },
                 "rahu_kalam": { "start": "13:47", "end": "15:20" } },
    "planets": [
      { "name": "Jupiter", "sign": "Cancer", "degree": 14.7,
        "nakshatra": "Aslesha", "pada": 1, "retrograde": false,
        "combust": false, "house_from_moon": 11, "house_from_lagna": 9 }
    ],
    "person": { "tara_bala": { "from_moon": "Sadhaka" },
                "chandra_bala": 6,
                "sade_sati": { "active": false } }
  }
}
```

### POST /gochara/range

**The same natal transit snapshot for every day in a range, in one call.**

Built for the case where an AI agent should do the deciding. It returns the full per-day gochara detail — panchanga, muhurta windows including a clean Abhijit, planets, person-relative strength, ashtakavarga, gochara-phala and vedha — with no scoring and no "best date" verdict, so the caller reasons over the raw data itself. The natal reference is computed once and reused across the range. Computed fresh; the range is capped at 60 days.

- Rate limit: 30 requests/minute per IP
- Authentication: None. Open endpoint, no key required.
- Response keys: gochara_range

Takes the shared birth-details block, plus the fields below.

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `from_date` | string | Yes | `—` | YYYY-MM-DD. |
| `to_date` | string | Yes | `—` | YYYY-MM-DD. Maximum 60 days from from_date. |
| `time_of_day` | string | No | `"12:00:00"` | HH:MM:SS used for each day. Defaults to local noon. |
| `event_place` | string | No | `null` | Falls back to the birth place. |
| `event_latitude` | number | No | `null` | Falls back to the birth latitude. |
| `event_longitude` | number | No | `null` | Falls back to the birth longitude. |
| `event_timezone` | number | No | `null` | Falls back to the birth timezone. |
| `event_elevation` | number | No | `null` | Falls back to the birth elevation. |
| `include` | string[] | No | `null` | Enrichment sections; the documented values are "ashtakavarga" and "gochara_phala". Omit for all of them, or pass [] for the lean snapshot. |

Example request:

```bash
curl -X POST "https://jagannatha-hora-359167915530.europe-west1.run.app/gochara/range" \
  -H "content-type: application/json" \
  -d '{
    "date": "1991-08-18", "time": "11:13:00", "place": "Hassan",
    "latitude": 13.0071, "longitude": 76.0962, "timezone": 5.5,
    "from_date": "2026-06-24", "to_date": "2026-06-30",
    "include": []
  }'
```

Example response:

```json
{
  "gochara_range": {
    "from_date": "2026-06-24",
    "to_date": "2026-06-30",
    "days": [
      { "date": "2026-06-24",
        "panchanga": { "vara": "Wednesday", "tithi": "Shukla Navami" },
        "planets": [ { "name": "Moon", "sign": "Virgo", "degree": 2.9 } ],
        "person": { "chandra_bala": 4 } }
    ]
  }
}
```

- Pass include: [] when you want the lean snapshot — omitting the field is not the same thing, and gives you everything.

### POST /saturn-timeline

**Sade Sati and Dhaiyya timing, with month-by-month pressure and relief bands.**

Returns the sign-based Sade Sati cycles for a chart, whether one is running right now and which of the three phases it is in, how many months remain, and — derived from Saturn's kakshya in the Ashtakavarga — the classical relief share. detail="full" adds the month-by-month bands, the interruptions and the segments. This is computation, not interpretation: dates and supported/unsupported flags, nothing else.

- Rate limit: 60 requests/minute per IP
- Authentication: None. Open endpoint, no key required.
- Response keys: saturn_timeline

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `date` | string | Yes | `—` | Birth date, YYYY-MM-DD. |
| `time` | string | Yes | `—` | Birth time, HH:MM:SS. |
| `place` | string | No | `""` | Cosmetic. |
| `latitude` | number | Yes | `—` | -90 to 90. |
| `longitude` | number | Yes | `—` | -180 to 180. |
| `timezone` | number | Yes | `—` | UTC offset, -12 to 14. |
| `ayanamsa_mode` | string | No | `"LAHIRI"` | ONLY "LAHIRI" or "RAMAN" here — narrower than every other endpoint. The two subsystems this endpoint drives honour only those two labels identically; anything else would silently compute a mismatched chart, so it is rejected with a 400 instead. |
| `detail` | string | No | `"summary"` | "summary" or "full". |

Example request:

```bash
curl -X POST "https://jagannatha-hora-359167915530.europe-west1.run.app/saturn-timeline" \
  -H "content-type: application/json" \
  -d '{
    "date": "1990-05-15", "time": "14:30:00", "place": "Bangalore",
    "latitude": 12.9716, "longitude": 77.5946, "timezone": 5.5,
    "detail": "summary"
  }'
```

Example response:

```json
{
  "saturn_timeline": {
    "moonSign": 7,
    "moonDegree": 18.44,
    "sadeSati": {
      "active": true,
      "currentPhase": "1st",
      "cycle": {
        "startDate": "2025-03-29", "endDate": "2032-06-03",
        "phases": [
          { "house": "12th", "startDate": "2025-03-29", "endDate": "2027-06-12" },
          { "house": "1st",  "startDate": "2027-06-12", "endDate": "2029-09-01" },
          { "house": "2nd",  "startDate": "2029-09-01", "endDate": "2032-06-03" }
        ]
      },
      "monthsRemaining": 70,
      "reliefShare": 0.38
    },
    "dhaiyya": { "active": false, "kind": null,
                 "startDate": null, "endDate": null },
    "nextCycle": null
  }
}
```

### POST /planet-ingress

**Every sidereal sign change for a set of planets across a date range.**

Includes retrograde back-outs and re-entries, each crossing flagged so you can tell a temporary ingress from the one that sticks: a final_settling flag marks the last crossing of each sign boundary inside the window. Data only, computed fresh.

- Rate limit: 60 requests/minute per IP
- Authentication: None. Open endpoint, no key required.
- Response keys: planet_ingress

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `planets` | string[] | No | `null` | Planet names. Defaults to the slow movers: Saturn, Jupiter, Rahu, Ketu. |
| `from_date` | string | Yes | `—` | YYYY-MM-DD. |
| `to_date` | string | Yes | `—` | YYYY-MM-DD. The field description states a maximum of about 5 years. |
| `timezone` | number | No | `0.0` | UTC offset used to express the crossing dates, -12 to 14. Defaults to UTC, not to IST. |
| `ayanamsa_mode` | string | No | `"LAHIRI"` | Any allowed ayanamsa mode. |

Example request:

```bash
curl -X POST "https://jagannatha-hora-359167915530.europe-west1.run.app/planet-ingress" \
  -H "content-type: application/json" \
  -d '{
    "planets": ["Saturn", "Jupiter"],
    "from_date": "2025-01-01", "to_date": "2027-12-31",
    "timezone": 5.5
  }'
```

Example response:

```json
{
  "planet_ingress": [
    { "planet": "Saturn", "from_sign": "Aquarius", "to_sign": "Pisces",
      "date": "2025-03-29", "retrograde": false, "final_settling": true },
    { "planet": "Jupiter", "from_sign": "Gemini", "to_sign": "Cancer",
      "date": "2025-10-18", "retrograde": false, "final_settling": false },
    { "planet": "Jupiter", "from_sign": "Cancer", "to_sign": "Gemini",
      "date": "2025-12-05", "retrograde": true, "final_settling": false }
  ]
}
```

- timezone defaults to 0.0 (UTC). A crossing near local midnight will look like it is on the previous day unless you pass your own offset.

### POST /muhurta

**Score every day in a range for an event, against classical muhurta rules and the native's own chart.**

Scores each day by the classical factors — vara, nakshatra, tithi, paksha — and by the person's strength on that day: Tara Bala, Chandra Bala and Sade Sati state, per the rule table for the chosen event type. It returns the per-date factor data, a deterministic score with its breakdown and the weights used, and the auspicious Abhijit window plus the Rahu, Gulika and Yamaganda windows to avoid. It does not pick a date; the caller composes the recommendation. Computed fresh.

- Rate limit: 30 requests/minute per IP
- Authentication: None. Open endpoint, no key required.
- Response keys: muhurta

Takes the shared birth-details block, plus the fields below.

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `event_type` | string | Yes | `—` | Must be one of the tier-1 event types. Fetch the list from GET /muhurta/events; an unknown value is a 400 that names the valid set. |
| `from_date` | string | Yes | `—` | Scan start, YYYY-MM-DD. |
| `to_date` | string | Yes | `—` | Scan end, YYYY-MM-DD. Maximum 90 days. |
| `event_place` | string | No | `null` | Falls back to the birth place. |
| `event_latitude` | number | No | `null` | Falls back to the birth latitude. |
| `event_longitude` | number | No | `null` | Falls back to the birth longitude. |
| `event_timezone` | number | No | `null` | Falls back to the birth timezone. |
| `event_elevation` | number | No | `null` | Falls back to the birth elevation. |
| `day_part_resolution_minutes` | integer | No | `15` | Time-of-day granularity, 5 to 60 minutes. |
| `respect_inauspicious_windows` | boolean | No | `true` | Report the Rahu, Gulika and Yamaganda windows to avoid. |

Example request:

```bash
curl -X POST "https://jagannatha-hora-359167915530.europe-west1.run.app/muhurta" \
  -H "content-type: application/json" \
  -d '{
    "date": "1991-08-18", "time": "11:13:00", "place": "Hassan",
    "latitude": 13.0071, "longitude": 76.0962, "timezone": 5.5,
    "event_type": "griha_pravesha_rented",
    "from_date": "2026-06-24", "to_date": "2026-06-30",
    "event_place": "Bangalore",
    "event_latitude": 12.9716, "event_longitude": 77.5946
  }'
```

Example response:

```json
{
  "muhurta": {
    "event_type": "griha_pravesha_rented",
    "weights": { "vara": 2, "nakshatra": 3, "tithi": 2,
                 "tara_bala": 3, "chandra_bala": 2 },
    "dates": [
      { "date": "2026-06-26",
        "score": 11,
        "breakdown": { "vara": 2, "nakshatra": 3, "tithi": 2,
                       "tara_bala": 3, "chandra_bala": 1 },
        "factors": { "vara": "Friday", "nakshatra": "Uttara Phalguni",
                     "tithi": "Shukla Ekadashi", "paksha": "Shukla" },
        "windows": { "abhijit": { "start": "12:00", "end": "12:52" },
                     "avoid": [ { "name": "Rahu Kalam",
                                  "start": "10:40", "end": "12:16" } ] } }
    ]
  }
}
```

### GET /muhurta/events

**The event types POST /muhurta accepts, with their rule sources.**

Call this first. It is the authoritative list of event_type values; sending anything not in it is a 400. Each entry carries a human label and the classical source the rule table came from.

- Rate limit: Inherits the app default (200 requests/minute per IP)
- Authentication: None. Open endpoint, no key required.
- Response keys: events

Example request:

```bash
curl "https://jagannatha-hora-359167915530.europe-west1.run.app/muhurta/events"
```

Example response:

```json
{
  "events": [
    { "event_type": "griha_pravesha_rented",
      "label": "Griha Pravesha (rented home)",
      "source": "Muhurta Chintamani" }
  ]
}
```

- The page does not reproduce the event list because it is defined in the service's rule table (gochara_rules.EVENT_RULES) and changes there, not here. Fetch it.

### GET /location/search

**Resolve a place name to coordinates and a timezone offset.**

Resolves against a local index of 162,000+ world cities — instant, free, offline, and deterministic. No external geocoder is contacted. The single best match is returned. For "City, Country" inputs that do not match directly, the trailing qualifier is dropped and retried, so "Springfield, IL, US" falls back to "Springfield, IL". Use this to turn a user's typed birth place into the latitude, longitude and timezone the calculation endpoints need.

- Rate limit: Inherits the app default (200 requests/minute per IP)
- Authentication: None. Open endpoint, no key required.
- Response keys: place, latitude, longitude, timezone, source, country, displayName, state (when known)

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `query` | string | Yes | `—` | The place name. Minimum 2 characters after trimming, or 400. |
| `q` | string | No | `null` | Alias for query, accepted because external callers commonly send ?q=. If both are present, query wins. |

Example request:

```bash
curl "https://jagannatha-hora-359167915530.europe-west1.run.app/location/search?query=Chennai,%20India"
```

Example response:

```json
{
  "place": "Chennai",
  "latitude": 13.0475,
  "longitude": 80.2087,
  "timezone": 5.5,
  "source": "database",
  "country": "India",
  "displayName": "Chennai, Tamil Nadu, India",
  "state": "Tamil Nadu"
}
```

- Comma handling is bounded at 6 segments, so a hostile many-comma query cannot trigger unbounded index scans on this public endpoint.
- A miss is a 404 whose message suggests adding a country name or ISO code.

### GET /location/autocomplete

**Type-ahead suggestions from the same 162,000-city index.**

Substring search over the local city index, for populating a birth-place picker as the user types. Much faster than an external geocoding API and free.

- Rate limit: Inherits the app default (200 requests/minute per IP)
- Authentication: None. Open endpoint, no key required.
- Response keys: query, count, results

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `query` | string | Yes | `—` | Partial place name. Minimum 2 characters, or 400. |
| `q` | string | No | `null` | Alias for query. |
| `limit` | integer | No | `10` | Maximum results. Values above 50 are clamped to 50 rather than rejected. |

Example request:

```bash
curl "https://jagannatha-hora-359167915530.europe-west1.run.app/location/autocomplete?query=Chenn&limit=3"
```

Example response:

```json
{
  "query": "Chenn",
  "count": 3,
  "results": [
    { "place": "Chennai", "displayName": "Chennai, Tamil Nadu, India",
      "latitude": 13.0827, "longitude": 80.2707, "timezone": 5.5,
      "country": "India", "state": "Tamil Nadu" }
  ]
}
```

### GET /

**Service banner: name, version and a short endpoint index.**

A cheap identity check. Returns the API's own name and version string plus a hand-maintained index of its headline endpoints — useful for confirming which deployment you are talking to.

- Rate limit: Inherits the app default (200 requests/minute per IP)
- Authentication: None. Open endpoint, no key required.
- Response keys: message, version, endpoints

Example request:

```bash
curl "https://jagannatha-hora-359167915530.europe-west1.run.app/"
```

Example response:

```json
{
  "message": "Kundli Studio Complete Horoscope API",
  "version": "1.0.0",
  "endpoints": {
    "POST /horoscope": "Generate complete horoscope from birth details",
    "POST /marriage-match": "Calculate marriage compatibility (Ashtakoota/10 Porutham)",
    "GET /location/search": "Search for location and get coordinates + timezone (162K+ cities)",
    "GET /location/autocomplete": "Get location suggestions for autocomplete",
    "GET /docs": "Interactive API documentation",
    "GET /health": "Health check endpoint"
  }
}
```

- The index here is a literal dict in the handler, not generated — it lists six entries while the service actually exposes seventeen. Treat this page or /docs as the endpoint inventory, not that field.

### GET /health

**Liveness probe.**

Returns a fixed healthy payload. Excluded from the service's request logging so that probing it every few seconds costs nothing in log volume.

- Rate limit: Inherits the app default (200 requests/minute per IP)
- Authentication: None. Open endpoint, no key required.
- Response keys: status, service

Example request:

```bash
curl "https://jagannatha-hora-359167915530.europe-west1.run.app/health"
```

Example response:

```json
{ "status": "healthy", "service": "Kundli Studio API" }
```

### GET /debug/meminfo

**Operational memory observability. Not a product endpoint.**

Reports the answering worker's resident set size and peak RSS from /proc, the configured allocator, and the MCP horoscope cache occupancy. It exists to watch memory behaviour under load. With multiple workers each request may land on a different one, so a single sample means little. Read-only and cheap, but treat it as an internal detail that can change without notice.

- Rate limit: Inherits the app default (200 requests/minute per IP)
- Authentication: None. Open endpoint, no key required.
- Response keys: allocator, rss_mb, rss_peak_mb, horoscope_cache

Example request:

```bash
curl "https://jagannatha-hora-359167915530.europe-west1.run.app/debug/meminfo"
```

Example response:

```json
{
  "allocator": "/usr/lib/x86_64-linux-gnu/libjemalloc.so.2",
  "rss_mb": 512.4,
  "rss_peak_mb": 690.1,
  "horoscope_cache": { "size": 3, "max_size": 8, "hits": 41, "misses": 12 }
}
```

- The rss fields read /proc and are Linux-only; off Linux the response carries an rss_error field instead.

## What POST /horoscope returns

The response is `{ "birth_details": {...}, "horoscope": {...} }`. The real payload is roughly 3.5 MB of JSON, so here are all 31 top-level keys inside `horoscope`, in the order the engine assigns them. That is the complete shape.

| Key | What it holds |
| --- | --- |
| `calendar_info` | Panchanga and calendar context for the birth moment |
| `bhava_chart` | House (bhava) cusps under the selected bhava_madhya_method |
| `ayanamsa_value` | The ayanamsa in degrees actually applied |
| `julian_day` | Julian day number of the birth moment |
| `divisional_charts` | 23 vargas, D-1 to D-144, each planet with sign and longitude |
| `nakshatra_pada` | Nakshatra, pada, lord and degrees-in-nakshatra per planet |
| `amsa_rulers` | D-150 Nadiamsa amsa rulers across planets, lagnas and sphutas |
| `yogas` | Classical yoga screening |
| `doshas` | Manglik, Kaal Sarp and the other classical doshas |
| `chara_karakas` | The eight variable karakas (Atma, Amatya, …) |
| `sahams` | The 36 sahams (Arabic parts) |
| `upagrahas` | The 11 shadowy sub-planets |
| `special_lagnas` | Bhava, Hora, Ghati and the other special ascendants |
| `house_varnadas` | Varnada lagna per house |
| `graha_arudhas` | Arudha padas computed from the grahas |
| `surya_arudhas` | Arudhas measured from the Sun |
| `chandra_arudhas` | Arudhas measured from the Moon |
| `house_relationships` | Argala and Vedha between houses |
| `planetary_states` | Retrograde, combustion, dignity and related states |
| `shad_bala` | The six-fold planetary strength |
| `bhava_bala` | House strength |
| `other_bala` | Harsha, pancha and dwadhasa vargeeya bala |
| `vimsopaka_bala` | Twenty-point varga strength |
| `vaiseshikamsa_bala` | Vaiseshikamsa strength |
| `ashtakavarga` | BAV, SAV, SoAV and PAV grids |
| `arudha_padhas` | Arudha padas (note the source spelling of this key) |
| `sphuta` | The 14 sphutas |
| `graha_dashas` | Graha dasha systems, Vimshottari included |
| `rasi_dashas` | Rasi dasha systems, Chara and Narayana included |
| `saturn_transits` | Sade Sati plus 4th/8th-house Saturn transits from Moon and Lagna |
| `longevity_prediction` | Classical longevity band with the combinations that produced it |

Note the spelling of `arudha_padhas` — that is the engine's key, not a typo, and the API returns it exactly that way.

## Errors

Failures come back as a JSON object with a single `detail` field naming the field and the constraint it broke.

| Status | Meaning | When |
| --- | --- | --- |
| 400 Bad Request | Invalid input | A validator rejected a field. The body is {"detail": "Invalid input: <reason>"}. /location/search and /location/autocomplete also return 400 when the query is under 2 characters. |
| 401 Unauthorized | Missing or wrong shared secret | POST /horoscope/pdf only, when the X-PDF-Secret header is absent or does not match. Fails closed: if the server has no secret configured, every request is rejected. |
| 404 Not Found | No location matched | GET /location/search when the query resolves to nothing in the world-cities index. |
| 422 Unprocessable Entity | Schema violation | FastAPI's own validation, raised before the handler runs — a missing required field or a wrong JSON type. |
| 429 Too Many Requests | Rate limited | The per-endpoint limit below, or the app-wide 200/minute default, was exceeded for your IP. |
| 500 Internal Server Error | Calculation failed | An unexpected engine error. /location/search deliberately returns a generic message here and logs the trace server-side rather than leaking internals. |

## Before you build on this

- There is no versioned URL prefix and no deprecation policy today. The service reports version 1.0.0 at GET / and the shapes documented here are the shapes it returns, but a breaking change would not announce itself with a new path.
- There is no uptime SLA, no support contract and no status page. It is the same deployment that serves Horoscopes Today's own products.
- The service returns computation, never interpretation. Nothing here decides whether a match is good or a date is auspicious — that judgement is yours, or your model's.

## See also

- MCP server reference: https://horoscopes.today/mcp (Markdown: https://horoscopes.today/mcp.md, manifest: https://horoscopes.today/mcp-tools.json)
- Free birth chart workspace: https://horoscopes.today/birth-chart
- The service's own live OpenAPI UI: https://jagannatha-hora-359167915530.europe-west1.run.app/docs
