Home / API Documentation

REST API · v1

KP Stellar Astro API

The same deterministic KP engine the website runs on, callable from your own software. JSON in, JSON out, no SDK required.

Quick start

  1. Sign in and open the developer portal.
  2. Subscribe to developer access (₹1,999 for 30 days).
  3. Press Create app. You are shown an App ID and an App Secret. Copy the secret now — it is never shown again.
  4. Confirm the keys work before writing any real payload:
# Base URL: https://kpstellarastro.com
curl https://kpstellarastro.com/api/v1/ping \
  -H "X-App-Id: kpsa_app_3c5b9502b9690bbb" \
  -H "X-App-Secret: kpsa_sk_YOUR_SECRET_HERE"
{ "ok": true,
  "app_id": "kpsa_app_3c5b9502b9690bbb",
  "expires_at": 1789181891.98 }

If that returns ok: true, your credentials are good and anything failing after this point is the payload, not the keys.

Prefer not to hand-roll the HTTP? Official free clients: Python and JavaScript/TypeScript — one method per endpoint, typed 401/402/429 errors. Details on the developer portal.

Authentication

Every /api/v1 request carries two headers. There is no OAuth flow, no token exchange, and no expiry on the credentials themselves — only on the subscription behind them. (The Claude connector below is a separate family and does use OAuth.)

HeaderValue
X-App-IdYour App ID, e.g. kpsa_app_3c5b9502b9690bbb
X-App-SecretYour App Secret, e.g. kpsa_sk_…
Content-Typeapplication/json on every POST
The secret is stored only as a hash. We cannot recover it or email it to you — if you lose it, rotate it in the portal, which issues a new secret and keeps the same App ID so your integration keeps its identity. Never ship the secret in browser JavaScript or a mobile binary; call the API from your own server.

Check credentials

GET/api/v1/ping

Costs nothing, computes nothing, and confirms the credentials plus how long access runs. Use it in your deploy healthcheck.

Response 200

{ "ok": true,
  "app_id": "kpsa_app_3c5b9502b9690bbb",
  "expires_at": 1789181891.98 // unix seconds }

Birth chart

POST/api/v1/birth-chart

A full natal reading: planetary positions with their full KP sub-chains, twelve house cusps, dasha periods, significators, and a verdict for every life domain.

Request body

FieldTypeNotes
birth_datestringrequiredYYYY-MM-DD. Must not be later than today — a future birth date returns 422.
birth_timestringrequiredHH:MM, 24-hour, local to the birth place
latitudenumberrequiredDecimal degrees, north positive
longitudenumberrequiredDecimal degrees, east positive
placestringoptionalEchoed back; also resolves the historical time zone
namestringoptionalEchoed back for your own labelling

Request

curl -X POST https://kpstellarastro.com/api/v1/birth-chart \
  -H "X-App-Id: $APP_ID" \
  -H "X-App-Secret: $APP_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Asha",
    "birth_date": "1990-05-15",
    "birth_time": "10:30",
    "latitude": 13.0827,
    "longitude": 80.2707,
    "place": "Chennai, India"
  }'

Response 200 (abridged — real values)

{
  "name": "Asha",
  "place": "Chennai, India",
  "timezone": "Asia/Kolkata",
  "utc_offset_hours": 5.5,

  "positions": {
    "Sun": {
      "name": "Sun",
      "longitude": 30.485933,
      "speed": 0.96426,
      "retrograde": false,
      "sign": "Taurus",
      "sign_lord": "Venus",
      "nakshatra": "Krittika",
      "pada": 2,
      "star_lord": "Sun",
      "sub_lord": "Rahu",
      "sub_sub_lord": "Ketu"
    }, … 9 planets total
  },

  "cusps": [
    { "house": 1, "cusp": "06°58′",
      "sign_lord": "Moon", "star_lord": "Saturn",
      "sub_lord": "Mercury", "sub_sub_lord": "Jupiter" }
    , … 12 houses total
  ],

  "predictions": [
    {
      "event": "business",
      "title": "Business",
      "verdict": "WEAK_PROMISE",
      "confidence": "WEAK",
      "reasoning": "The CSL Mercury (sub lord of cusp 7) signifies
                    houses [3, 4, 9, 10, 11, 12], directly touching
                    [10, 11] the required houses [2, 7, 10, 11] …",
      "windows": [
        { "start": "2027-03-14", "end": "2027-08-21",
          "chain": "MD Jupiter / AD Venus / PD Moon",
          "houses": [2, 7, 11], "activation": "VERY_STRONG" }
      ],
      "timing_facts": {           // added 2026-10, one JSON shape everywhere it appears
        "mode": "BIRTH",
        "answer": "possible",     // strong|likely|possible|no_clear_period|not_indicated —
                                        // capped at "possible" here because the VERDICT above is
                                        // WEAK_PROMISE, whatever the row's own dasha strength is
        "with_hurdles": false,
        "rp_instant": "2026-09-30T14:35:00Z",  // null when not promised
        "rows": [
          { "start": "2027-03-14", "end": "2027-08-21",
            "chain": "MD Jupiter / AD Venus / PD Moon", "houses": [2, 7, 11],
            "activation": "VERY_STRONG",   // same keys "windows" above carries — the row's OWN dasha
                                            // strength, independent of the word's own cap below
            "word": "possible",             // strong|likely|possible — never a percentage. The stage-1
                                        // verdict (WEAK_PROMISE) caps every row's word at "possible",
                                        // however strong "activation" reads (decision 21's word table)
            "confirmed_by": ["Venus", "Moon"],  // [] = "not confirmed"; never omitted
            "month_focus": null,             // always null on a WEAK_PROMISE row — never computed for one.
                                        // When narrower than the row itself, an array of objects shaped
                                        // { "start": "2027-05-15", "end": "2027-06-14", "sign": "Taurus",
                                        //   "lord": "Venus", "label": "mid-May to mid-June 2027",
                                        //   "label_short": "mid-May to mid-June" } — only "label_short"
                                        // is new (added 2026-10); start/end/sign/lord/label already existed.
                                        // "label_short" is "label" with its trailing year dropped
            "age_at_start": null,           // the native's age when this window begins (BIRTH mode only);
                                        // always null for HORARY rows
            "hold": null                    // added 2026-10: null unless the row is held back, see the note below.
                                        // Always null on birth-chart responses.
          }
        ],
        "days_to_watch": null      // BIRTH is always null — see the horary example below
      },
      "rp_confirmed_window": { "chain": "…", "start": "…", "end": "…" }  // or null
    }, … 18 life domains total
  ],

  "dasha": { /* Vimshottari periods */ },
  "significators": [ /* per-planet A/B/C/D houses */ ],
  "chart_layout": { /* wheel geometry, if you draw it */ },
  "planetary_positions_table": [ … ],
  "house_cusps_table": [ … ],
  "cuspal_promise_table": [ … ],
  "house_significators_abcd": [ … ],
  "planetary_aspects": {   /* object: "western" and "vedic" (each keyed by planet name) plus "pairs" (a list) */
    "western": { "Sun": { "conjunction": ["Me"], "opposition": [], "benefic": ["Ju"], "malefic": [] }, … },  /* degree aspects on the KP orbs */
    "vedic": { "Mars": { "conjunction": [], "opposition": ["Sa"], "special": ["Ve"] }, … },   /* whole-sign */
    "pairs": [   /* additive: one entry per planet pair with a degree aspect, same pairs as "western" */
      { "a": "Mo", "b": "Ju", "aspect": "square", "angle": 90, "delta": 1.25, "motion": "applying" }, /* delta = degrees from exact; motion = applying|separating|stationary */
      …
    ]
  },
  "vedic_details": { … },
  "vedic_bhava": {          /* classical Parashari graha-in-bhava */
    "ascendant_sign": "Leo",
    "house_system": "whole-sign (Parashari)",
    "differ_count": 1,   // grahas whose bhava != their KP house
    "readings": [
      {
        "graha": "Sun", "sign": "Gemini", "degree": 0.5,
        "bhava": 11,       // WHOLE-SIGN house — what the reading describes
        "kp_house": 10,    // Placidus house, for comparison only
        "houses_differ": true,
        "reading": "Gains through rank and influential connections …",
        "dignity": "neutral",  // exalted|debilitated|moolatrikona|own|neutral
        "house_nature": ["upachaya"],
        "modifiers": [ /* classical rules that apply, as sentences */ ],
        "karakatva": "the soul, the father, authority and vitality",
        "lords": [1],      // bhavas this graha RULES (depends on the lagna)
        "lordship": "Sun rules your 1st (…). Sitting in the 11th, it carries
                     those matters into gains, income, networks …",
        "node_agency": "",  // Rahu/Ketu only: whose lord it acts for
        "dasha": { "start": "1990-01-16", "end": "1996-01-17",
                    "active": false, "phase": "past" /* past|active|future */ },
        "ayanamsa_sensitive": false
      }, … one per graha
    ]
  },
  "doshas": {               /* Kaal Sarpa + Mangal, same whole-sign houses */
    "ascendant_sign": "Cancer",
    "house_system": "whole-sign (Parashari)",
    "mangal": {
      "present": true,      // decided by the LAGNA reference only — see below
      "cancelled": true,    // present AND at least one classical parihara applies
      "severity": "mild",  // none|mild|moderate
      "mars_sign": "Aquarius",
      "mars_bhava": 8,      // WHOLE-SIGN house — what decides the verdict
      "mars_kp_house": 8,   // Placidus house, for comparison only
      "houses_differ": false,
      "houses": { "lagna": 8, "Moon": 3, "Venus": 12 },
      "afflicting": ["lagna", "Venus"],  // references that agree
      "contested_only": false, // true = rests solely on the disputed 2nd house
      "reasons": [ /* why, as sentences */ ],
      "cancellations": [ /* classical parihara that apply, as sentences */ ]
    },
    "kaal_sarpa": {
      "present": false, "partial": false,
      "type_name": "Kulik",  // one of the twelve, from Rahu's bhava
      "rahu_bhava": 2, "ketu_bhava": 8,
      "inside": [ /* grahas hemmed within the Rahu→Ketu arc */ ],
      "outside": ["Moon", "Saturn"],
      "on_axis": [ /* within 1° of a node — the partial (Aanshik) case */ ],
      "reason": "Moon, Saturn fall outside the Rahu–Ketu arc …"
    },
    "remedies": [ /* only for a dosha actually found; [] otherwise */ ],
    "base_rates": { "mangal_pct": 52.2, "kaal_sarpa_pct": 4.0, …  }
  }
}
timing_facts.rows[*].start/end (and windows[*].start/end, the same dates) are "YYYY-MM-DD" for most rows, but "YYYY-MM-DD HH:MM" whenever that row spans fewer than 3 days — a plain date there would make two consecutive short rows look like duplicate dates even though they cover different hours. Check the string's length (10 vs 16 characters) rather than assuming one format.
vedic_bhava uses a different house system from the rest of this response. Every other house number here is a KP Placidus house. Classical Parashari graha-in-bhava is read by whole sign — the Ascendant's entire sign is the 1st bhava — and the two disagree for roughly 58% of placements (measured over 2,700). Each reading therefore carries its own bhava alongside the kp_house, and houses_differ tells you when they part company. Do not join these readings onto a KP house number: the sentence belongs to the whole-sign bhava. This block is traditional correspondence, not a computed verdict, and never affects any predictions entry.
doshas follows the same whole-sign rule, and two things about it are easy to get wrong. First, Mangal dosha is decided from the lagna alone. Counting the Moon and Venus references as independent triggers marks 88.7% of charts Manglik; the lagna rule alone marks 52.2% (measured over 800 charts), which is what six-of-twelve houses predicts. The other references are returned in houses/afflicting as corroboration — do not treat them as triggers. Second, never render present without cancellations and base_rates: about 67% of Manglik charts carry a classical cancellation, and a dosha shown without either is the frightening half of the tradition rather than a neutral subset of it. Like vedic_bhava, this is traditional correspondence and never affects any predictions entry.

Verdict values

The verdict field on each prediction is one of a fixed set. Treat it as an enum, not as prose:

ValueMeaning
STRONGLY_PROMISEDSignified, with strong supporting significators
PROMISEDThe cuspal sub-lord signifies the required houses
PROMISED_WITH_DELAYPromised, but retrograde or obstructed — arrives later
PROMISED_WITH_OBSTRUCTIONPromised against a competing signification
WEAK_PROMISEThinly signified; timing is indicative only
REQUIRES_FURTHER_TIMING_CONFIRMATIONPromise stands, timing is not yet decidable
DENIEDNot signified. The chart does not promise it
Polarity matters. DENIED is not automatically bad news — for a benefic topic it means the event is not promised, but for a topic the querent does not want, denial is the good outcome. Do not render the raw enum to end users without interpreting it against the question.
Changed 2026-10: windows's meaning on each prediction. Previously the raw Antardasha-level candidate windows straight from the engine's own search, shown regardless of promise. It is now the SAME rows timing_facts.rows carries: Pratyantardasha-level, obstruction-screened, age-gated, nearest-first, and empty ([]) whenever the event isn't promised — a not-promised prediction no longer carries a dated window with no promise behind it.
timing_facts is the one JSON shape every dated fact in this API now uses (birth chart and horary alike). rows[*].word is the ONLY confidence wording to render — never a percentage, never a raw activation label. confirmed_by is [], never omitted, when the row is not confirmed by the Ruling Planets at the moment this chart was requested (rp_instant). month_focus is null whenever it doesn't narrow a row further.
Added 2026-10: timing_facts.rows[*].hold. Every row now carries a hold key. It is null when the row is not held back, and it is always null on birth-chart responses; it is filled only on horary rows (see the Horary section). The same key also appears on the website's own session endpoints: it is always null on /api/birth-chart, on /api/kp-detail and on the natal side of /api/natal-horary/compare, while the horary side of /api/natal-horary/compare carries hold exactly as /api/horary does. Additive: existing keys are unchanged.
Added 2026-10: label_short. month_focus's other fields — start, end, sign, lord, label — were already present; only label_short is new. It is the same text as label with its trailing year dropped (e.g. "mid-May to mid-June" vs "mid-May to mid-June 2027"), for rendering several spans from the same year without repeating it.
rp_confirmed_window is deprecated. Superseded by each row's own timing_facts.rows[*].confirmed_by. Still populated for one more release; do not build new integrations against it.

Horary (Prashna)

POST/api/v1/horary

Judges one question from the moment it is asked, using a number from 1 to 249. No birth details needed.

Website session only (2026-10): kp_working, ruling_planets, dasha, house_significators_abcd. The website's own POST /api/horary (signed-in session or guest, no X-App-Id) also returns four additive keys: kp_working (the cusp's sub-lord facts: cusp, sub-lord, star lord, the houses each signifies, the houses that grant and the houses that hold back the matter, and which of them the sub-lord touches), ruling_planets (the Ruling Planets at the moment asked: day lord, the seven factors, the de-duplicated list) and dasha (the Vimshottari tree as it ran when asked, the same shape as /api/birth-chart) and house_significators_abcd (the A/B/C/D significators of each house, the same table and shape as /api/birth-chart). The same kp_working object also rides on every predictions[] item of the website's /api/birth-chart. /api/v1/horary and /api/v1/birth-chart are unchanged: these keys are absent there, and no existing key changed.
Response field reading_basis. POST /api/v1/horary may include reading_basis, a short string saying how the reading was reached. It is present only when our question-reading service is in use for the call, and absent otherwise (so a response without it is normal). Current values: exact_keyword, keyword_conflict, engine_specific, engine_kept, seed, lexicon_alias, alias_cell, composed, llm_decided, last_resort or grammar_unavailable. The list may grow: treat an unknown value as informational, never as an error. A question that is not about anything a chart can judge is still answered with the unchanged "reason": "off_topic" body, which carries no reading_basis. tag_pick in the request is ignored, and picked never appears in reading_basis.

Request body

FieldTypeNotes
questionstringrequiredOne answerable question. Either/or questions are rejected
horary_numberintegerrequired1–249
judgment_datestringrequiredYYYY-MM-DD — the moment of asking
judgment_timestringrequiredHH:MM, local to the place below
latitudenumberrequiredWhere the question is asked
longitudenumberrequired
placestringoptionalResolves the time zone

Request

curl -X POST https://kpstellarastro.com/api/v1/horary \
  -H "X-App-Id: $APP_ID" \
  -H "X-App-Secret: $APP_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "question": "Will I get the job?",
    "horary_number": 137,
    "judgment_date": "2026-08-13",
    "judgment_time": "14:20",
    "latitude": 13.0827,
    "longitude": 80.2707,
    "place": "Chennai, India"
  }'

Response 200 (abridged)

{
  "question": "Will I get the job?",
  "horary_number": 137,
  "category": "Job / Employment",
  "event_id": "job",
  "primary_cusp": 6,
  "required_houses": [2, 6, 10, 11],
  "verdict": "PROMISED",
  "confidence": "STRONG",
  "reasoning": "…",
  "windows": [ { "start": "…", "end": "…" } ],  // changed 2026-10, see the callout below
  "timing_facts": {            // added 2026-10 — same shape as /api/v1/birth-chart's "timing_facts";
                                     // the pre-existing "timing" key further down is the UNRELATED, pre-B2
                                     // Predictive Timing (dasha) breakdown on /api/v1/horary, untouched here.
    "mode": "HORARY", "answer": "strong", "with_hurdles": false,
    "rp_instant": "2026-08-13T08:50:00Z",
    "rows": [ /* same row shape as /api/v1/birth-chart's "timing_facts.rows", plus "band" -- MODERATE or stronger, at most one per band, in band order (up to 3) */ ],
    "bands": [ { "band": "nearest", "from": "2026-10-04", "to": "2028-10-03", "row_index": 0 } ],  // added 2026-10 (U5f): always three entries, nearest / mid / long
    "horizon_end": "2036-10-03",   // added 2026-10 (U5f): no row ends after this date
    "asked_period": { "kind": "this", "count": 0, "unit": "year", "year": 0, "month": 0, "age": 0, "from": "2026-10-05", "to": "2026-12-31", "clipped_to": null, "answer": "yes", "row_index": 0, "row": { /* same shape as rows[*] */ }, "after_index": null, "after_start": null },  // added 2026-10 (U5a): ONLY when the question names a period we recognise
    "days_to_watch": {
      "days": [ { "date": "2026-08-19", "strength": "strong", "row": 0 } ],
      "note": null,
      "note_code": null,     // added 2026-10: "no_standout" | "frame_too_far" | null — a
                                // client-safe identity against "note"'s own free text
      "star_lords": ["Saturn", "Mercury"]  // added 2026-10: the planets this scan confirmed days against
    }
  },
  "fruitful_star_days": [
    { "date": "2026-08-19", "strength": "strong",
      "matched_steps": [ "Moon in the star of Saturn", "Moon in the sign of Mercury" ],
      "fruitful_planets": [ "Saturn", "Mercury" ],
      "reasoning": "…",
      "stretch_start": "2026-08-19", "stretch_end": "2026-08-20" }
  ],
  "fruitful_star_days_note": null,
  "rp_confirmed_window": { "chain": "…", "start": "…", "end": "…" },  // deprecated, see below
  "classification_confidence": "…",
  "verified": true,        // added 2026-10: true when the matter-and-intent check ran (or you passed locked_event_id)
  "timezone": "Asia/Kolkata",
  "timing": { /* unchanged Predictive Timing (MD/AD/PD/SD/PRD) breakdown -- NOT the new "timing_facts" object above */ },
  "positions": { … }, "cusps": [ … ], "significators": [ … ]
}
Added 2026-10: verified. verified is true when the check ran on the question (or you named the matter with locked_event_id); it does not by itself mean a matter was found, and false when the reading rests on the engine's own match because the check could not run this time; treat a false as a lower-trust reading. The question is still judged and the chart is returned, and it counts as a question like any other. Success responses carry no ok field at all, so test for ok === false rather than for a truthy ok. A question that was checked and is not about anything a chart can judge returns 200 with { "ok": false, "reason": "off_topic", "retry": false, "verified": true, "message": "..." } and no cusp; do not retry the same text. Branch on reason: off_topic (reword) when ok is false.
multi_question_notice is not an error. When the question contains more than one ask, the field is populated and you should surface it — the verdict then applies to the primary question only.
fruitful_star_days is optional, local-dated and short-horizon only. Dates are on or after the question date, never before it, and only within about the next 3 months — windows above is always the answer regardless; treat these as up to 10 days, in at most 3 short stretches, worth watching inside it. Often empty ([]), in which case fruitful_star_days_note gives the customer-safe reason. Changed 2026-09-30: previously scanned from the running dasha period's own start (which could be years before the question) and rarely narrowed at all.
Changed 2026-10: windows's meaning. Now the SAME rows timing_facts.rows carries (MODERATE confidence or stronger), instead of every candidate window at any confidence.
Added 2026-10: horary periods are read in three bands from the moment of asking. timing_facts.bands always lists nearest (running now, or starting within 2 years), mid (2 to 5 years) and long (5 to 10 years), each with its from/to dates and a row_index into rows (null when no clear period falls in that band). Each filled band carries one row, and rows[*].band names it. windows and timing_facts.rows are now at most these three rows, in band order, and no row ends after timing_facts.horizon_end (10 years after the question). Rows are scored on the matter's timing houses. When the matter is promised but all three bands are empty, answer is "no_clear_period" and no date is given. days_to_watch only ever refers to the nearest band's row (row 0). Additive: bands, horizon_end and rows[*].band are absent or null on birth-chart responses.
Added 2026-10: timing_facts.rows[*].hold says when a period that would already have begun is held back. Every row carries the key (additive). It is null when the row is not held back. When the row is held back it is an object: until (ISO date, YYYY-MM-DD) is the day the period can first begin and is the same day the row's own start shows; planet is the planet whose backward motion causes the hold, named in English ("Saturn"); period_start (ISO date, or null) is the day the period would have begun without the hold; by is "lord" when the planet is the period's own ruling planet, or "sub" when it is the planet in whose sub-division that ruling planet sits. Show it as one plain line, for example "held back until 11 Dec 2026, when Saturn stops moving backward". When the held start falls inside the period a question asked about, asked_period.row carries the held start: its start moves to the held date, and asked_period.answer is unchanged. A period the chart does not let through is simply absent from rows; no key says why. The hold only appears when the service's timing setting for it is switched on, and a saved reading never changes. rp_confirmed_window (deprecated, below) is not screened or held: it always shows the unmodified window.
Added 2026-10: timing_facts.asked_period answers the period the question asked about. It is present only when the question names a period in a recognised English phrase ("this year", "within six months", "in 2028", "soon"); otherwise the key is absent. It is read the same way on /api/horary and /api/v1/horary. kind (this, next, in, by, before, within, soon) with count/unit/year/month/age (age is always 0 here) says what was read; from and to are the period's first and last day in the place's local calendar. answer is "yes" (a period that switches the matter on overlaps it; row is the earliest such period), "no" (none does), "not_promised" (the matter is not promised in this chart) or "beyond" (the period lies past horizon_end, so the reading does not cover it). clipped_to is set when only part of the period lies inside the reading's 10 years (the date it was clipped to), and on "beyond" (to the horizon_end date). row has the same shape as rows[*]; row_index is null when that row is not one of rows, and after_index (for "no") points at the first shown row that starts after the period. after_start is that row's start, exactly as rows[after_index].start prints it (null when there is none, and for every answer other than "no"). rows, bands and answer are never changed by it. Additive, and absent from birth-chart responses.
timing_facts is the one JSON shape every dated fact in this API now uses — see the identical timing_facts object on /api/v1/birth-chart for the full field-by-field description. rows[*].word is the ONLY confidence wording to render. days_to_watch is populated only when the frame actually narrows; days_to_watch.days[*].row indexes into rows above.
Added 2026-10: days_to_watch.note_code and days_to_watch.star_lords. note_code is a client-safe identity against note's own free text — "no_standout", "frame_too_far", or null when days is populated. star_lords is the list of planets this request's own scan confirmed — the Moon's star lord on each returned day is always one of these.
rp_confirmed_window is deprecated. Superseded by each row's own timing_facts.rows[*].confirmed_by. Still populated for one more release; do not build new integrations against it.

Panchang

POST/api/v1/panchang

The five limbs for a date and place, plus the inauspicious and auspicious windows.

Request

curl -X POST https://kpstellarastro.com/api/v1/panchang \
  -H "X-App-Id: $APP_ID" \
  -H "X-App-Secret: $APP_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "date": "2026-08-13",
    "latitude": 13.0827,
    "longitude": 80.2707,
    "place": "Chennai, India"
  }'

Response 200 (real values)

{
  "date": "2026-08-13",
  "place": "Chennai, India",
  "timezone": "Asia/Kolkata",
  "utc_offset_hours": 5.5,
  "vara": "Thursday",
  "sunrise": "05:56",
  "sunset": "18:29",
  "tithi":     { "index": 1, "name": "Pratipada", "paksha": "Shukla" },
  "nakshatra": { "name": "…", "lord": "…" },
  "yoga":      { "name": "…" },
  "karana":    { "name": "…" },
  "inauspicious": {
    "rahu_kalam":   { "start": "13:48", "end": "15:22" },
    "yamagandam":   { "start": "05:56", "end": "07:30" },
    "gulika_kalam": { "start": "09:04", "end": "10:39" }
  },
  "auspicious": { … }
}

Compatibility

POST/api/v1/compatibility

Ashtakoot Guna matching for two charts, scored out of 36, with each of the eight koots broken out separately.

Request body

FieldTypeNotes
boyobjectrequiredA birth object, same fields as birth-chart
girlobjectrequiredLikewise

Request

curl -X POST https://kpstellarastro.com/api/v1/compatibility \
  -H "X-App-Id: $APP_ID" -H "X-App-Secret: $APP_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "boy":  { "birth_date": "1990-05-15", "birth_time": "10:30",
              "latitude": 13.0827, "longitude": 80.2707, "place": "Chennai, India" },
    "girl": { "birth_date": "1992-08-02", "birth_time": "04:15",
              "latitude": 19.0760, "longitude": 72.8777, "place": "Mumbai, India" }
  }'
Read the per-koot breakdown, not only the total. A strong total can hide one weak koot that matters to the couple, and a middling total is often fine once you see where the points went. Rendering only the /36 figure throws away the part that is actually actionable.

Muhurta

GET/api/v1/muhurta/events

The event ids /api/v1/muhurta accepts. Call this rather than hard-coding the list — it is engine-defined and would rot silently the first time an event is added or renamed.

Response 200

{ "events": [
    { "id": "property", "label": "Property" },
    { "id": "vehicle",  "label": "Vehicle" }
    , … also: foreign_travel, marriage, business, education,
         higher_education, job, loan, surgery
] }
POST/api/v1/muhurta

Auspicious windows for a personal event, ranked, each carrying the reason it scored where it did.

FieldTypeNotes
birthobjectrequiredBirth object, as above
event_namestringrequiredAn id from /api/v1/muhurta/events
max_windowsintegeroptional1–20, default 8
from_date, to_datestringoptionalYYYY-MM-DD. Filters which qualifying windows come back
viewer_utc_offset_hoursnumberoptional−12 to 14. The UTC offset of the person reading the result. Defaults to the birth place's offset
No elected date is ever in the past. A muhurta that names a date already gone is useless, so today is an absolute floor — applied to the transit scan itself, not just its result, so a window that is currently RUNNING is kept and its best still-available day returned rather than the window being dropped. A from_date in the past does not reopen this. "Today" means the reader's calendar day: send viewer_utc_offset_hours if your user may be in a different time zone from the chart's birth place, or dates that are yesterday to them can appear as today.
A bounded range never lowers the bar. from_date/to_date filter windows that already qualify; they do not relax the promise, confidence or Moon-transit requirements to fill an empty range. An empty result means no window qualified — present it as such rather than widening the range until something appears.
POST/api/v1/muhurta/check-date

Judge one real-life date for a personal event: a date the person was handed (an interview, a registration slot) rather than one they chose. Returns the running period lords and whether each signifies the matter, the Moon's lords that day, the day's ruling planets against the matter, panchanga notes and the best hora, and one plain verdict.

FieldTypeNotes
birthobjectrequiredBirth object, as above
event_namestringrequiredAn id from /api/v1/muhurta/events
check_datestringrequiredYYYY-MM-DD. Today (the reader's calendar day) or later, up to the same calendar date 30 years ahead. Events on the sensitive-matter list (health, surgery, deaths and similar) are refused with 400
viewer_utc_offset_hoursnumberoptional−12 to 14. Decides what "today" is. Defaults to the birth place's offset

Response 200 (abridged)

{ "verdict": "supporting",   // supporting | neutral | outside | not_promised
  "headline": "A supporting day",
  "period": { "inside": true, "all_lords_favour": true, "lords": [ … ] },
  "moon": { "shown": true, "lords": [ … ] },
  "ruling_planets": { "endorsing_count": 2, … },
  "panchanga": { … }, "hora": { … }, "cautions": [ … ] }
The verdict is deliberately modest. There is no "auspicious" or "inauspicious" value: the engine backs only supporting, neutral inside a good period, or outside the period the chart gives. moon.shown is false when the Moon's day-by-day grade does not narrow anything for this chart, and "supporting" is never returned on signals that would pass on nearly every day. and "supporting" additionally needs every running Dasa/Bhukti/Antara lord to signify the matter. A matter the chart does not promise returns verdict: "not_promised" with event_label, date, headline and a plain-language reasoning; period, moon, ruling_planets and the rest are absent. ruling_planets.informative is false when the ruling planets endorse the matter on nearly every day for this chart.

TSP quick question

POST/api/v1/tsp

Theory of Short Prediction — a fast single-question read from its own 2193-segment number table. Deliberately narrower than horary, and not a substitute for it: no chart wheel is produced.

FieldTypeNotes
tsp_numberintegerrequired1–2193 — not the 1–249 horary number
judgment_datestringrequiredYYYY-MM-DD
judgment_timestringrequiredHH:MM
latitude, longitudenumberrequiredWhere the question is asked
querystringoptionalThe question text, echoed back for your own labelling
namestringoptionalEchoed back
placestringoptionalResolves the time zone
The verdict comes from the number, not the text. query is optional and is echoed back rather than parsed — unlike horary, where the question is classified to pick the cusp. Do not infer from a TSP response that the question text was read.
curl -X POST https://kpstellarastro.com/api/v1/tsp \
  -H "X-App-Id: $APP_ID" -H "X-App-Secret: $APP_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "tsp_number": 137,
    "query": "Will I get the job?",
    "judgment_date": "2026-08-13",
    "judgment_time": "14:20",
    "latitude": 13.0827,
    "longitude": 80.2707,
    "place": "Chennai, India"
  }'

Cuspal interlinks

POST/api/v1/cuspal-interlinks

Cuspal Interlinks — the star / sub / sub-sub chain linking a cusp to the houses that fulfil it. Same request body as birth-chart. This is the technique behind the "why" panel in the web app, exposed on its own.

Question cusp

POST/api/v1/question-cusp

Which house cusp a horary question is judged from, and which houses must be signified — the same classification horary runs internally, returned directly with no chart cast. Useful for previewing how a question will be judged before spending a horary number on it.

Response field reading_basis. POST /api/v1/question-cusp may include reading_basis, a short string saying how the reading was reached. It is present only when our question-reading service is in use for the call, and absent otherwise (so a response without it is normal). Current values: exact_keyword, keyword_conflict, engine_specific, engine_kept, seed, lexicon_alias, alias_cell, composed, llm_decided, last_resort or grammar_unavailable. The list may grow: treat an unknown value as informational, never as an error. A question that is not about anything a chart can judge is still answered with the unchanged "reason": "off_topic" body, which carries no reading_basis. tag_pick in the request is ignored, and picked never appears in reading_basis.

Request body

FieldTypeNotes
questionstringrequiredUp to 2000 characters.
in_indiabooleanoptionalDefault true. Disambiguates domestic vs. foreign travel questions only — has no other effect on classification.
curl -X POST https://kpstellarastro.com/api/v1/question-cusp \
  -H "X-App-Id: $APP_ID" -H "X-App-Secret: $APP_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"question": "will I win the lottery"}'

Response 200

{ "ok": true,
  "event_id": "lottery",
  "category": "Lottery / Prize Bond Win",
  "primary_cusp": 5,
  "required_houses": [5, 9, 10, 11],
  "verified": true,
  "multi_question_notice": null,
  "notice_kind": null,
  "doctrine": { "corroboration": "mixed", "note": "Classical texts admit more than one reading here" } }

Added 2026-10: verified is true when the matter-and-intent check ran, and false when the cusp rests on the engine's own match because the check could not run this time; the question is still judged. Success responses carry no ok field, so test for ok === false. A question that was checked and is not about anything a chart can judge returns 200 with { "ok": false, "reason": "off_topic", "retry": false, "verified": true, "message": "..." } and no cusp; do not retry the same text.

A question that fails validation (gibberish, too short, not a real question) returns 200 with "ok": false and a message — not an error status, since asking "can this be judged?" isn't itself a failure.

Descriptive Indicators

POST/api/v1/descriptive

Table-lookup traits about whatever the question's cusp actually describes — a person, a place, or a matter — plus, where the event supports it, a facet reading: which of several recognised shapes (a court case settling vs. being appealed vs. a favorable judgment; a property matter being a purchase vs. a sale vs. a tenancy) the chart leans toward. Both are advisory evidence read off the cuspal sub-lord and its star lord — never a PROMISED/DENIED verdict, and never invented: a chart that shows nothing returns an empty list, which is a legitimate, common answer. Same table lookup the website's own Descriptive Indicators panel uses, on the caller-supplied chart in this request rather than a signed-in account's saved one — but this endpoint decides which cusp a question is about by deterministic word matching only, with no AI reading of the question, while the website may use an AI-checked reading for the same wording. So the same question can land on a different cusp here than it does on the site.

Added 2026-10: a question that matches no recognised topic is not described. Because this endpoint does no AI reading, it cannot work out what such a question is about, so it returns 200 with "cusp": 0, empty indications and can_ask, and an empty event and subject, rather than a general reading of the 11th house. Name the matter in the question to get a description. A question that is not about the querent's life at all returns the same shape with "off_topic": true.

Request body

FieldTypeNotes
modestringoptionalDefault "BIRTH". "HORARY" uses the horary object below instead of birth.
questionstringrequired for BIRTH modeThe judged question — fixes which cusp is being described. For HORARY mode the question travels inside horary instead.
askstringoptionalA specific descriptive question ("what will their profession be") to filter to. Omit to get every indication the chart supports plus the list of questions that CAN be asked (can_ask).
birthobjectrequired for BIRTH modeSame shape as /api/v1/birth-chart's body.
horaryobjectrequired for HORARY modeSame shape as /api/v1/horary's body.
curl -X POST https://kpstellarastro.com/api/v1/descriptive \
  -H "X-App-Id: $APP_ID" -H "X-App-Secret: $APP_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"mode": "BIRTH", "question": "will there be a court case over the property?",
       "birth": {"name": "Test", "birth_date": "1990-06-15", "birth_time": "10:30",
                 "latitude": 12.9716, "longitude": 77.5946, "place": "Bengaluru"}}'

Response 200

{ "subject": "the opposing party",
  "cusp": 6,
  "is_person": false,
  "indications": [ ... ],
  "can_ask": [ { "kind": "work", "question": "What kind of matter is this?" }, ... ],
  "event": "Litigation",
  "facets": [
    { "id": "settlement", "label": "settlement / compromise",
      "agreement": 1, "because": "the 6th cuspal sub-lord Saturn and its star lord Sun between them signify house 7" }
  ] }

facets is empty for events with no defined facet table, or when the chart shows none of a facet's houses. It is a small, growing set — currently litigation, bail, property, health, foreign travel, loan, donation/charity, promotion, workplace trouble, vehicle, surgery, missing person, stock market, inheritance, gratuity/insurance, and lost property/theft recovery.

Unlike the signed-in web app, there is no Premium-plan gate here — the developer subscription is already the paywall, same as every other /api/v1 endpoint.

Financial astrology

Four endpoints for money questions — an entity chart from a founding moment, its key windows, a head-to-head comparison of two candidate moments, and the astronomical events in a range.

POST/api/v1/financial/chart

An entity chart: a company from its incorporation moment, a listing, a deal. Takes birth (the moment) and event_name.

POST/api/v1/financial/muhurta

Key time windows for a financial event. Same body, plus optional max_windows.

POST/api/v1/financial/compare

Rank two candidate moments against each other. Takes original, candidate (each wrapping a birth object) and event_categories.

Two full chart computations per call. Comparing a long shortlist pairwise multiplies quickly — budget for it against your daily limit rather than looping blindly.
GET/api/v1/financial/events

Astronomical events in a date range. Query parameters start_date and end_date, both YYYY-MM-DD.

curl "https://kpstellarastro.com/api/v1/financial/events?start_date=2026-08-01&end_date=2026-08-31" \
  -H "X-App-Id: $APP_ID" -H "X-App-Secret: $APP_SECRET"

Question classification (session auth)

Part of the same developer subscription as /api/v1 above, but authenticated differently: it does not take X-App-Id / X-App-Secret at all. It uses a signed-in session bearer token instead — the same token the website itself uses — so a standalone server integration authenticating purely by App-Id/Secret cannot call it; the caller must be a signed-in account with active developer access.

POST/api/question/cusp

Which house cusp a horary question is judged from, and which houses must be signified — the same classification /api/horary runs internally, returned directly with no chart cast. Useful for previewing how a question will be judged before spending a horary number on it.

Auth

HeaderValue
AuthorizationBearer <session token> — the same token a signed-in browser session uses, obtained via the site's own sign-in flow, not the developer portal.
Included in the same developer subscription as the /api/v1 endpoints above (₹1,999 / 30 days) — no separate charge. A signed-in account without active developer access gets 402; there is no free or unauthenticated tier for this endpoint.

Request body

FieldTypeNotes
questionstringrequiredUp to 2000 characters.
in_indiabooleanoptionalDefault true. Disambiguates domestic vs. foreign travel questions only — has no other effect on classification.
curl -X POST https://kpstellarastro.com/api/question/cusp \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"question": "will I win the lottery"}'

Response 200

{ "ok": true,
  "event_id": "lottery",
  "category": "Lottery / Prize Bond Win",
  "primary_cusp": 5,
  "required_houses": [5, 9, 10, 11],
  "multi_question_notice": null,
  "notice_kind": null,
  "doctrine": { "corroboration": "mixed", "note": "Classical texts admit more than one reading here" } }

A question that fails validation (gibberish, too short, not a real question) returns 200 with "ok": false and a message, the same shape /api/question/check uses — not an error status, since asking "can this be judged?" isn't itself a failure.

Birth-time proof (signed-in app, session auth)

Used by the website itself. These take the signed-in session bearer token (Authorization: Bearer <session token>), not X-App-Id / X-App-Secret, and are not part of the developer subscription: they read and write one person's stored life events. They are free to use (no credits). Adding events and the proof follow the Birth Rectification on/off switch (503 when it is off); removing an event is never switched off. Events can only be added to a birth chart (400 otherwise); dates use ASCII digits only. The events are personal data: owner-only, never sent to an AI model, deleted with the chart or the account, and included in Download my data.

POST/api/charts/{chart_id}/known-events

Record one dated life event on a saved chart. Body: category (one of marriage, children, job, promotion, business, property, vehicle, foreign_settlement, foreign_travel, higher_education; there is no free text), date (YYYY-MM or YYYY-MM-DD; not in the future, not before the birth date), optional source (manual, life_events or unverified_card). Returns the stored item {id, category, date, source, added_at}. 400 invalid category/date, 404 not your chart, 409 duplicate or more than 12 events on the chart, 429 over 60 adds per hour.

DELETE/api/charts/{chart_id}/known-events/{event_id}

Remove one recorded event. Returns {"deleted": true}; 404 if it is not yours.

GET/api/charts/{chart_id}/birth-time-proof

Does the saved birth chart's time fit the person's own dated events (known events, plus tracked predictions answered "it happened" with a month)? Computed on read with the Birth Rectification scorer; nothing is stored. Returns status (matches, mismatch or not_enough; two or more scorable events are needed), scored, matched, approximate (some dates are month-only), suggest_rectify, max_shift_minutes (30), birth_time_confidence and events (id, title, when, matched, source). Birth charts only (400 otherwise). Limited to 30 calls per 5 minutes per account.

PATCH/api/predictions/{prediction_id}

Records the outcome of a tracked prediction (outcome: happened, did_not_happen, happened_later, too_early or reopen; optional note). New optional field outcome_month (YYYY-MM, not in the future): the month it happened. Accepted only with outcome = happened or happened_later, or alone on a row already answered either way, and only for prediction kinds the birth-time proof can score (others get 400). Answering happened_later keeps a stored month; changing the outcome to anything else, or reopen, clears it (a reopened row loses its month). Never included in any aggregate or public figure.

Saved charts returned by GET /api/charts/{chart_id} include known_events, as do PUT /api/charts/{chart_id} and POST /api/charts/{chart_id}/apply-rectified-time; the list view GET /api/charts omits it. GET /api/auth/export-data now also returns tracked_predictions (with outcome_month) and muted_topics.

Calendar export (website session)

GET/api/me/calendar.ics

Website session endpoint, the same family as /api/me/home: it needs a signed-in session bearer token, it is not part of the developer /api/v1 family and takes no X-App-Id / X-App-Secret. It exports the caller's own followed questions as an iCalendar file: each followed period as an all-day span, each stored day to watch as an all-day event (only when the Moon-note switch is on), and one all-day "did it happen?" check-in (only when the closing switch is on) the day after a period closes, or dated today when the period has already closed and is still unanswered. A period with no end date is one all-day event on its start date, or on today while it runs. Day and check-in events carry a morning display alarm. Event ids are keyed hashes and the link is the Home page, so no stored id appears in the file; SEQUENCE rises with each download so changes replace earlier events. Past dates, dismissed, answered, snoozed and muted-topic rows are left out, and so are health, longevity and death matters. No chart is cast, no model is called and no credit is used.

Query

ParameterTypeNotes
titlesbooleanDefault false: event titles are neutral ("KP window: a question you follow"). true adds the stored matter title. The question text, birth details and names are never written.
todaystringThe caller's local YYYY-MM-DD; used only within one day of UTC, as on /api/me/home.
curl "https://kpstellarastro.com/api/me/calendar.ics?titles=false" \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" -o kp-followed-windows.ics

Returns 200 with text/calendar and a Content-Disposition attachment named kp-followed-windows.ics. 401: not signed in. 404: nothing to add yet (no followed question with a current or future date). 429: more than 30 downloads an hour per account.

Claude connector (MCP, OAuth)

A remote Model Context Protocol server, so Claude (and other MCP clients) can call the KP engine on a signed-in person's behalf. It is its own authentication family: OAuth 2.1 with PKCE (S256) and a Bearer access token, not X-App-Id / X-App-Secret, and not part of the developer /api/v1 subscription. The person signs in with their KP Stellar account on the website's consent page; the client then holds a token that is only good for /mcp (a website session token is never accepted there, and an MCP token is never a session token). Scope: kp.read. All sixteen tools are read-only.

Most readers only need the setup steps: in Claude, Settings → Connectors → Add custom connector, enter https://kpstellarastro.com/mcp, sign in and allow. A plain-words setup guide with prompts to copy is at /use-in-claude. The endpoints below are for client authors.
ItemValue
Access tokenOpaque, valid 1 hour. Refresh tokens rotate on every use and last 30 days; a connection ends after 180 days and the person consents again.
Request limits60 calls a minute per account on /mcp (429 with Retry-After), plus a free daily allowance per account (resets at midnight IST) that the connector's tools count against. Over the allowance a tool returns a normal tool error with the upgrade link; initialize and tools/list are free.
CreditsThe connector's engine tools use no credits today. Credit packs work on the same account as on the website.
Redirect URIshttps://claude.ai/api/mcp/auth_callback, or loopback http://localhost / 127.0.0.1 / [::1] with a path (any port) for local clients.
POST/mcp

The MCP endpoint (Streamable HTTP, JSON-RPC 2.0). Also answers at /mcp/. Stateless: no session id is issued or needed, so any call can be the first. Send Authorization: Bearer <access token>, Content-Type: application/json and Accept: application/json, text/event-stream. Methods: initialize, tools/list (16 tools, each marked readOnlyHint: true) and tools/call. Replies are plain JSON.

GET and DELETE return 405 (there is no server-to-client stream). 401 without a valid token, with WWW-Authenticate: Bearer resource_metadata="https://kpstellarastro.com/.well-known/oauth-protected-resource", scope="kp.read" (and error="invalid_token" when a token was sent). 503 while the connector is switched off. 429 over 60 calls a minute.

curl -X POST https://kpstellarastro.com/mcp \
  -H "Authorization: Bearer YOUR_MCP_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"birth_chart","arguments":{"birth_date":"1990-05-15","birth_time":"10:30","latitude":13.0827,"longitude":80.2707}}}'
GET/.well-known/oauth-protected-resource

RFC 9728 metadata. Returns resource (https://kpstellarastro.com/mcp), authorization_servers and scopes_supported. The path-suffixed form /.well-known/oauth-protected-resource/mcp returns the same document. No auth.

GET/.well-known/oauth-authorization-server

RFC 8414 metadata: issuer, the authorize, token, registration and revocation endpoints, code_challenge_methods_supported: ["S256"], public clients only (token_endpoint_auth_methods_supported: ["none"]), client_id_metadata_document_supported and authorization_response_iss_parameter_supported both true. No auth.

POST/oauth/register

Dynamic client registration (RFC 7591). JSON body: redirect_uris (required; the allowed forms are in the table above) and optional client_name. Public clients only; asking for a client secret returns invalid_client_metadata. 201 with client_id; a bad redirect URI returns 400 invalid_redirect_uri. Clients may instead use an HTTPS client-metadata-document URL as their client_id. Limited per IP.

GET/oauth/authorize

Authorization endpoint; a browser page, not an API call. Query: client_id, redirect_uri, response_type=code, code_challenge with code_challenge_method=S256 (required), state, resource=https://kpstellarastro.com/mcp and scope=kp.read. It serves the consent page, which signs the person in with their KP Stellar account, shows what is being allowed and returns to the client's redirect_uri with code, state and iss (or error=access_denied). Codes are single use and expire after 60 seconds. The page cannot be framed.

POST/oauth/token

Form-encoded (application/x-www-form-urlencoded). grant_type=authorization_code with client_id, code, code_verifier, redirect_uri and resource; or grant_type=refresh_token with client_id and refresh_token. Returns access_token, refresh_token, token_type: "Bearer", expires_in and scope. Refresh rotates both tokens. Replaying an already-used refresh token returns 400 invalid_grant, and if the replay comes after a few seconds the whole connection is ended as presumed theft. Errors use the standard OAuth error / error_description body. Limited per IP.

POST/oauth/revoke

RFC 7009. Form body token (access or refresh) and client_id. Always answers 200, whether or not the token existed; revoking either token ends the whole connection.

GET/api/mcp/connections

The signed-in account's connected apps. This one takes the website session bearer token (Authorization: Bearer <session token>), not an MCP token. Returns {"connections": [{"id", "client_name", "redirect_host", "scope", "connected_at", "last_used_at"}]}, newest first. 401 without a session. Limited to 60 calls a minute (429).

POST/api/mcp/connections/{id}/revoke

Disconnect one app (session bearer token). Ends its access and refresh tokens at once, so the app's next /mcp call is 401. Returns {"revoked": true}; 404 if the id is not this account's. Signing out everywhere or changing the password on the website also ends every connection.

Privacy: the connector keeps a usage record of the account, the tool name, the time and a status, and nothing else (no birth details, places, questions or IP address).

Public endpoints (no key)

A few read-only, non-personal endpoints back the free tool pages and need no App ID/Secret — call them directly from a browser or your own script. They are throttled per IP (see limits) rather than per app, since there is no key to count against.

GET /api/public/ruling-planets

The classical KP Ruling Planet set (the local sunrise-to-sunrise day lord, Ascendant and Moon sign/star/sub lords, ranked Ascendant sub, Ascendant star, Ascendant sign, Moon sub, Moon star, Moon sign, day lord) cast for this moment at a place, with factors (which planet fills each of those seven places, keyed LAGNA_SUB, LAGNA_STAR, LAGNA_SIGN, MOON_SUB, MOON_STAR, MOON_SIGN, DAY_LORD, in that order; ruling_planets is these with repeats removed), Rahu/Ketu listed as node_agents when they act for a ruling planet and per-planet flags (retrograde delays, in_retro_star is a downgrade, not a rejection), plus today's KP ayanamsa and sky, where the twelve cusps and nine planets stand at that moment (cusp_lons: 12 sidereal longitudes, house 1 first; cusp_lords: [sign lord, star lord, sub lord] for each cusp; positions: per planet longitude, retrograde, star_lord, sub_lord and house), read off the same chart. Powers /ruling-planets and the embeddable widget.

ParamTypeRequired
latitudefloatyes
longitudefloatyes
placestringno — echoed back, not used in the calculation
GET /api/public/ruling-planets?latitude=13.0827&longitude=80.2707&place=Chennai

{
  "place": "Chennai",
  "timezone": "Asia/Kolkata",
  "utc_offset_hours": 5.5,
  "local_time": "2026-09-15 14:32",
  "day_lord": "Venus",
  "moon": { "sign": "Taurus", "star_lord": "Moon", "sub_lord": "Rahu" },
  "ascendant": { "sign": "Libra", "star_lord": "Saturn", "sub_lord": "Mercury" },
  "ruling_planets": ["Mercury", "Saturn", "Venus", "Rahu", "Moon"],
  "factors": { "LAGNA_SUB": "Mercury", "LAGNA_STAR": "Saturn", "LAGNA_SIGN": "Venus", "MOON_SUB": "Rahu", "MOON_STAR": "Moon", "MOON_SIGN": "Venus", "DAY_LORD": "Venus" },
  "node_agents": ["Rahu"],
  "flags": { "Mercury": { "retrograde": false, "in_retro_star": false }, "Saturn": { "retrograde": false, "in_retro_star": false }, "Venus": { "retrograde": false, "in_retro_star": false }, "Rahu": { "retrograde": false, "in_retro_star": false }, "Moon": { "retrograde": false, "in_retro_star": false } },
  "kp_ayanamsa": 24.156789,
  "generated_at": "2026-09-15T09:02:11.123456+00:00",
  "sky": { "cusp_lons": [12 sidereal longitudes, house 1 first], "cusp_lords": [["Venus", "Jupiter", "Saturn"], "...eleven more"], "positions": { "Sun": { "longitude": 183.2, "retrograde": false, "star_lord": "Mars", "sub_lord": "Saturn", "house": 1 }, "...the other eight planets" } }
}
Cached per rounded location (~1 km) and current minute — repeated calls for the same city inside one minute return the same cached result rather than recomputing.

GET /api/public/where-am-i

The visitor's own coarse, city-level place, from the site's local GeoIP database (no third-party call; the address itself is never returned or stored). The app uses it to pre-fill the Horary place when the visitor has no saved place. No parameters. Per visitor, so never cached (Cache-Control: no-store), and throttled per IP like the other public endpoints. detected is false, with no other field, when the database cannot place the visitor (private network, unknown address, not configured).

GET /api/public/where-am-i

{
  "detected": true,
  "city": "Pune",
  "region": "Maharashtra",
  "country": "India",
  "latitude": 18.52,
  "longitude": 73.86,
  "timezone": "Asia/Kolkata"
}

GET /api/public/kp-sub-lord-table

The full 243-row zodiac → nakshatra → sub-lord reference table, straight from the engine's recursive subdivision — not location or time dependent, so the same response every time. Powers /kp-sub-lord-table.

No parameters. Returns {"count": 243, "rows": [...]}", each row: sign, sign_lord, nakshatra, pada, star_lord, sub_lord, start_dms/end_dms and their float-degree equivalents.

GET /api/public/kp-horary-number

The full 1-249 KP horary number reference table, straight from kp_astro.horary.horary_number.horary_table() — the same table the horary judgment endpoints use internally. Powers /kp-horary-number.

No parameters. Returns {"count": 249, "rows": [...]}", each row: number, sign, sign_lord, nakshatra, star_lord, sub_lord, start_dms/end_dms.

GET /api/desktop-app/latest-version

The desktop app's startup check for a newer build. Public, no auth. Returns latest_version (null until one is published), release_notes and verdict_basis — "v1" or "v2", an opaque label for the verdict basis currently in force (treat it as an opaque string; it never names the underlying method).

GET /api/prediction-diary

The public KP Prediction Diary — anonymised horary judgments published before their outcome is known, plus a running count of how many resolved inside their window. Powers /predictions.

No parameters. Returns count, unresolved_count, resolved_count, hit_count, hit_rate (null until at least one entry is resolved) and items — each with question, horary_number, category, verdict, confidence, windows, outcome and its immutable content_hash.

Added 2026-10: banded diary entries. An entry published from now on carries horizon_years (10) and each item in its windows carries a band ("nearest", "mid" or "long"), at most one window per band and in that order; windows is empty when no clear period falls within the 10 years. Entries published before this change have neither field and are unchanged. horizon_years is part of the hashed content_hash material only when present, so every earlier entry still verifies.

What is not on the API

Three things the web app does are deliberately absent, and will stay absent until each has an answer to its own problem rather than being bolted onto this subscription:

Not exposedWhy
Birth-time rectification (auto-search)By far the heaviest operation on the platform — seconds per request, scoring many candidate birth times, each a full chart plus significators plus dasha. In the web app it is signed-in only and already held to roughly 4 calls/minute per account with at most 3 running concurrently site-wide. The developer tier's 60/minute is an order of magnitude beyond that, and the concurrency cap means one integration looping it would starve real users. It needs its own pricing and its own limits, not this subscription's.
AI explanationCosts real money per call against a third-party model. It belongs on the credit system, not a flat monthly fee.
Saved charts, clients, tracked predictionsApp credentials authenticate an application, not one of its end users. Serving a specific person's stored data through them would make one developer key a master key to their whole user base — that needs a per-user authorization flow, which is separate work.

Errors

Errors are JSON with a detail string. The status code tells you which problem you have, and they are deliberately distinct — do not collapse them into one retry path.

StatusMeaningWhat to do
401Invalid App ID or App Secret, or the app was revokedCheck both headers. Rotate the secret if it may have leaked. Do not retry — it will not start working
402Developer access has expiredRenew in the portal. Your keys are still valid; only the subscription lapsed
422The payload failed validationRead field_errors — it names the exact field and problem
429Over the rate limitBack off and retry. See limits
503The endpoint is temporarily disabledRetry later; this is a server-side switch, not your request

A validation error, verbatim

{
  "detail": "Date is required.",
  "field_errors": [
    { "field": "judgment_date", "message": "Date is required." },
    { "field": "judgment_time", "message": "Time is required." }
  ]
}
401 does not distinguish a wrong secret from an unknown App ID. That is deliberate — a distinct answer would let anyone enumerate valid App IDs. When debugging, check both values.

Rate limits

 Public endpoints (no key)Developer API
AccessWebsite endpoints only — no /api/v1All /api/v1 endpoints
Per minute1060  6× more
Per day1001,000  10× more
Counted againstYour IP, shared with everyone on the same networkEach app separately, up to 5 apps

The per-app scope is often the bigger win: five apps at 1,000 calls each is 5,000 a day, and none of them is affected by other traffic from your network.

Limits are counted per app, not per IP, so several developers behind one office network do not throttle each other, and running your integration from a serverless platform with rotating addresses is fine. Create a separate app per environment (staging, production) to keep their budgets independent — you can have up to five.

Going over returns 429. Back off and retry; nothing is banned or charged extra.

Pricing

PlanPriceIncludes
Developer access₹1,999 / 30 daysAll /api/v1 endpoints, up to 5 apps, 1,000 calls per app per day

Billed as a one-off 30-day period, not an auto-renewing mandate — nothing recurs without you buying it again. Renewing before expiry adds to the time you have left rather than replacing it. When the period ends, calls answer 402 and your apps are not deleted: renew and the same credentials resume working.

Notes & guarantees

  • Same engine as the website. These endpoints are thin authenticated wrappers over the exact handlers the app itself calls, so the API cannot drift from what the site shows.
  • Deterministic. Once the first check for a question has been remembered, the same input returns the same verdict every time. The only language-model step is the single matter-and-intent check described below, which decides which matter a question is about; it never produces a figure, a date or a verdict.
  • Every question is checked, once. Deciding which matter a question is about (and therefore which cusp it judges from) runs on the keyword and grammar tiers, and then a single matter-and-intent check confirms or corrects that reading. Nothing else about your call uses a language model. The check is remembered, so a question already asked (here or on the website) is not checked again. Changed 2026-10: this used to be keyword and grammar only. The response field verified says whether the check ran: true when it did, or when you named the matter with locked_event_id (no check is needed then); false when the reading rests on the engine alone because the check could not run this time (a short outage, for example). When the check cannot run, the question is still judged on the engine's own reading, with verified set to false. If you need a specific matter, name it: pass locked_event_id (for example "job_change", "property_sale") on /api/v1/horary and that classification is used directly, with no keyword matching involved. Call /api/v1/question-cusp first if you want to see what a question resolves to before casting a chart. A question that was checked and is not about anything a chart can judge is answered with "reason": "off_topic" and no cusp (do not retry the same text).
  • Nothing is stored from your API calls. Charts computed through /api/v1 are not saved to any account; we record only the call count and last-used time against your app.
  • Time zones are historical. Supplying place resolves the zone that was actually in force on that date, including past DST rules — this matters for a 1970s birth time.
  • Birth time precision. KP is minute-sensitive; the cuspal sub-lord can change within four minutes. If your users supply approximate times, present the results with that caveat.
  • Versioning. Breaking changes go to a new path prefix. /api/v1 keeps its response shape.