openapi: 3.1.0

# ═══════════════════════════════════════════════════════════════════════════════════════════════
# Grahvani Public API — /v1 contract, Tier-1 slice
#
# This file is the SOURCE OF TRUTH for the public contract. SDKs, the docs portal, the Postman
# collection and the CI contract tests are all generated from it. Nothing is published that is
# not described here.
#
# Written deliberately BEFORE the edge is built. The 2026-08-04 readiness audit measured 553
# distinct response envelopes across 983 working engine endpoints — 495 of them used by exactly
# one endpoint — and 21 different error shapes. No SDK can be generated from that, and no customer
# can write generic retry logic against it. The engine keeps all of that internally; this contract
# is what strangers see, and it is designed once, on purpose.
#
# ⚠️ Everything described here FREEZES when the first customer integrates. Additive changes are
# allowed within /v1; anything breaking waits for /v2, with /v1 supported >= 12 months after.
# ═══════════════════════════════════════════════════════════════════════════════════════════════

info:
  title: Grahvani API
  version: "1.0.0"
  summary: Vedic astrological computation, metered by the call.
  description: |
    Charts, daśās, pañcāṅga, KP, doṣas and compatibility — computed from Swiss Ephemeris and
    classical rule sets, with the classical basis returned alongside every answer.

    **What makes this different.** Anyone can call an ephemeris. Every response carries a
    `provenance` block naming the method, the conventions applied, and — where one exists — the
    text and locus the rule comes from. Where the classical sources genuinely disagree, we say so
    rather than presenting one reading as settled.

    **Units, not calls.** Compute cost varies about twentyfold across this API: a rāśi chart is a
    handful of ephemeris calls, a festival calendar sweeps a year of pañcāṅga. Flat per-call
    pricing would either overcharge the cheap requests or lose money on the expensive ones, so
    every response reports `meta.units` and your plan is denominated in the same unit.

    **Ayanāṁśa.** Lahiri is the default and by far the deepest school — see
    `GET /v1/meta/capabilities` for exactly which operations exist in which school before you
    depend on one.
  contact:
    name: Grahvani API Support
    url: https://docs.grahvani.in
  x-billing-entity:
    legal_name: AstroCorp Techlab Pvt Ltd
    gstin: 36AAMCC3512G1ZU

servers:
  - url: https://api.grahvani.in
    description: Production

security:
  - ApiKeyAuth: []

tags:
  - name: Charts
    description: Rāśi, navāṁśa and the divisional (varga) charts.
  - name: Dashas
    description: Daśā systems. Vimśottarī is reckoned on the sidereal year — see `provenance`.
  - name: Panchang
    description: The five limbs of the Vedic day, and the calendar built from them.
  - name: KP
    description: Krishnamurti Paddhati. Sub-lord, cusps, ruling planets and horary.
  - name: Compatibility
    description: Aṣṭakūṭa (Guṇa Milan) and doṣa checks.
  - name: Meta
    description: Capability discovery and account usage.

paths:

  # ── Charts ────────────────────────────────────────────────────────────────────────────────
  /v1/charts/rasi:
    post:
      tags: [Charts]
      summary: Rāśi (D1) chart
      description: |
        The birth chart: planetary longitudes, signs, nakṣatras, houses and the ascendant.
        **1 unit.**
      operationId: chartsRasi
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirthRequest'
      responses:
        '200':
          description: The chart.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/RasiChart'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/charts/divisional/{varga}:
    post:
      tags: [Charts]
      summary: Divisional (varga) chart
      description: |
        A divisional chart — D2 through D60. **3 units.**

        This is one of the 41 operations that genuinely exist in more than one ayanāṁśa school,
        so `ayanamsa` is honoured here. Most of this API is Lahiri-only; check
        `GET /v1/meta/capabilities` rather than assuming.
      operationId: chartsDivisional
      parameters:
        - name: varga
          in: path
          required: true
          description: The division. `d9` is the navāṁśa.
          schema:
            type: string
            enum: [d2, d3, d4, d7, d9, d10, d12, d16, d20, d24, d27, d30, d40, d45, d60]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirthRequest'
      responses:
        '200':
          description: The divisional chart.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Envelope'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '501': { $ref: '#/components/responses/NotAvailableInSchool' }

  # ── Dashas ────────────────────────────────────────────────────────────────────────────────
  /v1/dashas/vimshottari:
    post:
      tags: [Dashas]
      summary: Vimśottarī daśā
      description: |
        The 120-year Vimśottarī cycle, to the level of nesting you ask for. **3 units.**

        Reckoned on the **sidereal year (365.256363 days)**. That is a measured choice, not a
        preference: across nine mahādaśās, Drik Pañcāṅga's published 120-year cycle comes to
        43830.7639 days against sidereal's 43830.7636 — a 26-second match. The Julian year is
        0.76 d out and the Gregorian 1.66 d. The principled argument agrees, since the daśā is
        anchored to the Moon's nakṣatra, which is a sidereal position.

        ⚠️ Boundary dates will not match every other tool, and the dominant cause is **not** this
        constant — it is the ayanāṁśa variant, which outweighs year length by roughly 40×.
      operationId: dashasVimshottari
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/BirthRequest'
                - type: object
                  properties:
                    levels:
                      type: integer
                      minimum: 1
                      maximum: 5
                      default: 2
                      description: 1 = mahādaśā, 2 = +antara, 3 = +pratyantara, and so on.
      responses:
        '200':
          description: The daśā tree.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Envelope'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ── Panchang ──────────────────────────────────────────────────────────────────────────────
  /v1/panchang/day:
    post:
      tags: [Panchang]
      summary: Pañcāṅga for a date and place
      description: |
        Tithi, nakṣatra, yoga, karaṇa and vāra with their end times, plus sunrise and sunset.
        **1 unit.**

        ⚠️ Pañcāṅga is reckoned at the **place of observance**, which is not necessarily a
        birthplace. Send where the person *is*.
      operationId: panchangDay
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlaceDateRequest'
      responses:
        '200':
          description: The five limbs.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Envelope'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ── KP ────────────────────────────────────────────────────────────────────────────────────
  /v1/kp/horary:
    post:
      tags: [KP]
      summary: KP horary (praśna) chart, numbers 1–249
      description: |
        A horary chart from the querent's number. **3 units.**

        The number fixes the ascendant at the **commencement** of its division in the 249-fold
        lattice — the 243 sub-divisions together with the six sign cusps that fall inside a sub
        and split it. The remaining eleven cusps are the Placidus cusps *for that ascendant*; the
        moment you supply governs the planets and the ruling planets, not the house framework.

        Verified against Krishnamurti's own published values in *KP Reader 3*: number 37 gives
        Taurus 22°40′ and number 206 gives Capricorn 26°6′40″, both exact.
      operationId: kpHorary
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [horary_number, moment, place]
              properties:
                horary_number:
                  type: integer
                  minimum: 1
                  maximum: 249
                  description: The querent's number.
                  example: 37
                moment:
                  type: string
                  format: date-time
                  description: The moment of judgment, ISO 8601 with offset.
                place:
                  $ref: '#/components/schemas/Place'
                question:
                  type: string
                  description: Optional. Used only to select the house of the matter.
      responses:
        '200':
          description: The horary chart.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Envelope'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/kp/cusps:
    post:
      tags: [KP]
      summary: KP cuspal chart with sign, star and sub lords
      description: |
        The twelve cusps with their four-tier lordship. **3 units.**

        Computed on the **KP New (Krishnamurti)** ayanāṁśa. Sub-lord boundaries are built in exact
        integer arcseconds, so a longitude sitting precisely on a boundary is decidable: it
        belongs to the sub that **begins** there, per KSK's own horary examples.
      operationId: kpCusps
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirthRequest'
      responses:
        '200':
          description: The cuspal chart.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Envelope'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ── Compatibility ─────────────────────────────────────────────────────────────────────────
  /v1/compatibility/ashtakoota:
    post:
      tags: [Compatibility]
      summary: Aṣṭakūṭa (Guṇa Milan), 36 points
      description: |
        The eight kūṭas and the composite score. **10 units.**

        **Read `provenance` before you present this to a user.** Two of the eight tables are
        verified against primary sources: Graha Maitrī matches *Bṛhat Parāśara Horā Śāstra*
        Ch. 3 śl. 55 on all seven planets, and Nāḍī follows the boustrophedon formula rather than
        a transcribed table. The yoni grid is **disputed** between published sources and the
        response says so per-kūṭa rather than presenting a contested number as settled.

        Worth knowing: the 36-point system is **not** in BPHS. It comes from the Muhūrta
        literature; BPHS supplies only the friendship table that Graha Maitrī consumes. Sources
        attributing the system itself to BPHS are mistaken.
      operationId: compatibilityAshtakoota
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [bride, groom]
              properties:
                bride: { $ref: '#/components/schemas/BirthRequest' }
                groom: { $ref: '#/components/schemas/BirthRequest' }
                bhakoot_rule:
                  type: string
                  enum: [strict, lenient]
                  default: strict
                  description: |
                    How Bhakūṭa doṣa cancellation is applied. Under `lenient` — same lord **or**
                    a friendly relationship on either side — Nava-Pañcama (5–9) doṣa can never
                    arise, because all twelve pairs cancel. That is a defensible classical
                    position and a surprising one, so it is explicit rather than implied.
      responses:
        '200':
          description: The kūṭa breakdown and composite score.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Envelope'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ── Meta ──────────────────────────────────────────────────────────────────────────────────
  /v1/meta/capabilities:
    get:
      tags: [Meta]
      summary: Which operations exist in which ayanāṁśa school
      description: |
        **Free — 0 units.** Read this before building against a non-default school.

        The schools are not equal in depth, and the gap is large enough that assuming otherwise
        will break an integration. Measured on 2026-08-04:

        | School | Operations | Share of Lahiri |
        |---|---:|---:|
        | **lahiri** (default) | **668** | — |
        | yukteswar | 48 | 7.2% |
        | bhasin | 47 | 7.0% |
        | raman | 35 | 5.2% |
        | kp | 17 | 2.5% |
        | true_chitra | 12 | 1.8% |

        Lahiri carries the yogas, doṣas, Jaimini, Nāḍī, balas and remedies. The other schools
        carry charts and daśās. Requesting an operation a school does not implement returns
        **501** naming the schools that do — never a 404, and never a silent fallback to Lahiri.
      operationId: metaCapabilities
      responses:
        '200':
          description: The capability matrix.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Envelope'

  /v1/meta/usage:
    get:
      tags: [Meta]
      summary: Units consumed in the current billing period
      description: '**Free — 0 units.** Plan, units used, units remaining, period end.'
      operationId: metaUsage
      responses:
        '200':
          description: Current usage.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Envelope'

# ═══════════════════════════════════════════════════════════════════════════════════════════════

components:

  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        `gv_live_…` or `gv_test_…`. Test-mode keys return deterministic fixtures, are never
        billed, and are the right way to build an integration before spending live units.

  responses:
    BadRequest:
      description: |
        Your request. Always carries `error.field` when a single input is at fault.

        This API does **not** return 5xx for input problems, and does not answer a question it
        was not asked — a calculator with no usable input refuses rather than returning a result
        computed from defaults.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Missing, malformed, revoked or out-of-scope API key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Rate limit or monthly unit quota exhausted. Always carries `Retry-After`.
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds to wait.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotAvailableInSchool:
      description: |
        The operation exists, but not in the ayanāṁśa school you asked for. The body names the
        schools that do implement it. Deliberately **not** a 404 — the resource is real — and
        deliberately not a silent fallback to Lahiri, which would return a chart you did not ask
        for and could not tell apart from one you did.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:

    Envelope:
      type: object
      required: [data, meta]
      description: |
        Every successful response has this shape. `data` is the only part that varies by endpoint.
      properties:
        data:
          type: object
          description: The computation.
        meta:
          $ref: '#/components/schemas/Meta'
        provenance:
          $ref: '#/components/schemas/Provenance'

    Meta:
      type: object
      required: [request_id, endpoint, units, computed_at]
      properties:
        request_id:
          type: string
          description: Quote this in any support request.
          example: req_01JQ8ZK3M4N5P6Q7R8S9T
        endpoint:
          type: string
          example: charts.rasi
        units:
          type: integer
          description: What this call cost. Never a surprise, never retroactive.
          example: 1
        ayanamsa:
          type: string
          example: lahiri
        computed_at:
          type: string
          format: date-time
        engine_version:
          type: string
          description: The computation build. Changes here can move published values.

    Provenance:
      type: object
      description: |
        Present wherever we have something citable. **Omitted rather than invented** — an absent
        `provenance` means we have no source to name, not that none was checked.
      properties:
        method:
          type: string
          example: Vimśottarī daśā on the sidereal year (365.256363 d)
        conventions:
          type: object
          additionalProperties: { type: string }
          description: The choices that would otherwise be invisible.
          example:
            ayanamsa: Lahiri (Chitrapakṣa)
            sub_lord_boundary: half-open [start, end)
        sources:
          type: array
          items:
            type: object
            properties:
              work: { type: string, example: Bṛhat Parāśara Horā Śāstra }
              locus: { type: string, example: Ch. 3, śl. 55 }
              note: { type: string }
        verified_against:
          type: string
          description: An independent check, where one was performed.
          example: 'Drik Pañcāṅga, 120-year cycle, Δ 26 s'
        disputed:
          type: array
          description: |
            Where published sources genuinely disagree and we have not been able to settle it from
            a printed source. Saying so is the point — a number presented as settled when it is
            not is the failure this API exists to avoid.
          items:
            type: object
            properties:
              element: { type: string, example: yoni kūṭa grid }
              note: { type: string }

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [type, code, message, request_id]
          properties:
            type:
              type: string
              enum: [invalid_request, authentication, permission, quota, rate_limit, not_implemented, internal]
            code:
              type: string
              example: missing_field
            message:
              type: string
              example: birth.time is required (HH:MM:SS).
            field:
              type: string
              description: Present when one input is at fault.
              example: birth.time
            doc_url:
              type: string
              format: uri
            request_id:
              type: string

    Place:
      type: object
      required: [latitude, longitude, timezone]
      properties:
        latitude:  { type: number, minimum: -90,  maximum: 90,  example: 27.1767 }
        longitude: { type: number, minimum: -180, maximum: 180, example: 78.0081 }
        timezone:
          type: string
          description: |
            IANA name, e.g. `Asia/Kolkata`. **Not** a fixed offset — an offset cannot represent
            historical daylight-saving transitions, and a birth in 1980 may need one.
          example: Asia/Kolkata

    BirthRequest:
      type: object
      required: [birth]
      properties:
        birth:
          type: object
          required: [date, place]
          properties:
            date:
              type: string
              format: date
              example: '1980-05-01'
            time:
              type: string
              pattern: '^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$'
              description: |
                Local clock time at the place of birth. **If unknown, omit it** — we use 12:00
                noon and set `meta.birth_time_assumed`, and still return the full chart. We do
                not refuse a reading for want of a birth time.
              example: '05:16:00'
            place:
              $ref: '#/components/schemas/Place'
        ayanamsa:
          type: string
          enum: [lahiri, raman, yukteswar, bhasin, true_chitra, kp]
          default: lahiri
          description: |
            Honoured only where the operation exists in that school — 41 of them do. Everything
            else is Lahiri-only and returns 501 for another school. See
            `GET /v1/meta/capabilities`.

    PlaceDateRequest:
      type: object
      required: [date, place]
      description: |
        A date and a place — for computations that are about a moment rather than a person.

        Kept deliberately separate from `BirthRequest`. Pañcāṅga is reckoned at the **place of
        observance**, which is not the birthplace, and collapsing the two schemas is how that
        distinction gets quietly lost by an integrator reusing their birth payload.
      properties:
        date:
          type: string
          format: date
          example: '2026-08-04'
        place:
          $ref: '#/components/schemas/Place'
        ayanamsa:
          type: string
          enum: [lahiri, raman, yukteswar, bhasin, true_chitra, kp]
          default: lahiri

    RasiChart:
      type: object
      description: Illustrative; the full schema is generated from the implementation.
      properties:
        ascendant:
          type: object
          properties:
            longitude: { type: number, example: 36.6166 }
            sign:      { type: string, example: Taurus }
            nakshatra: { type: string, example: Rohiṇī }
        planets:
          type: array
          items:
            type: object
            properties:
              name:        { type: string, example: Moon }
              longitude:   { type: number, example: 204.7769 }
              sign:        { type: string, example: Libra }
              nakshatra:   { type: string, example: Viśākhā }
              house:       { type: integer, example: 6 }
              retrograde:  { type: boolean, example: false }
        houses:
          type: array
          items:
            type: object
            properties:
              number: { type: integer }
              cusp:   { type: number }
              sign:   { type: string }
