openapi: 3.1.0
info:
  title: Skylit Public API
  version: "1.0.0"
  summary: Options-Greeks heatmaps as a public HTTP API.
  description: |
    Skylit exposes its real-time options-Greeks (gamma / vanna) heatmaps as a
    versioned public HTTP API, per strike, with the live velocity metric and
    Skylit's node classification (King / Gatekeeper / etc.).

    **Authentication.** Send your Skylit API key as a bearer token:

        Authorization: Bearer <key>

    A request without an `Authorization` header gets `401`; an invalid,
    revoked or expired key gets `403`.

    **Credit metering.** Every chargeable request debits a fixed cost from
    your credit balance:

    | Endpoint        | Cost |
    |-----------------|-----:|
    | `/v1/heatmap`    | 1   |
    | `/v1/historical` | 5   |
    | `/v1/stream`     | 1 per minute open |
    | `/v1/openapi.json` | 0 |

    New customers are seeded with 5,000 credits. Every chargeable response
    carries `X-Credits-Remaining: <balance>`. Out of credits → `402`
    `insufficient_credits`; an admin-suspended account → `403`
    `account_suspended`. Top up via your account console.

    **Rate limits.** A safety ceiling of 600 requests / minute is enforced
    by the Skylit gateway and surfaced via `X-RateLimit-Limit`,
    `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. `429` includes
    `Retry-After`. This is a runaway-protection ceiling, not a quota —
    credit metering does the per-customer accounting.

    Live streams (`/v1/stream`) cost 1 credit to open plus 1 per minute
    connected, and close after one hour.

    **Resolution.** The heatmap is a 1-second time series. Endpoints return the
    single snapshot nearest the requested instant — poll `/v1/heatmap` every
    60-90s for live, or query `/v1/historical` at any minute boundary for a
    1-minute replay grid (no server-side downsampling needed).

    **Velocity is live-only.** The `velocityPct` field is present on
    `/v1/heatmap` (live) and absent on `/v1/historical` (replay).

    **Response shape.** Success → `{ "data": ..., "meta": { ... } }`; errors →
    `{ "error": { "code": "...", "message": "..." } }`. camelCase throughout.
    `data.symbols` is always an array (one element per requested symbol), so a
    single-symbol call and a multi-symbol "Trinity" call share one shape.

servers:
  - url: https://api.skylit.ai
    description: Production

security:
  - bearerApiKey: []

tags:
  - name: Heatmap
  - name: Meta

paths:
  /v1/heatmap:
    get:
      summary: Live per-strike heatmap (one or more symbols)
      operationId: getHeatmap
      tags: [Heatmap]
      description: |
        Current per-strike heatmap for one or more symbols at the latest
        snapshot. Includes the live `velocityPct` per strike. Pass multiple
        comma-separated symbols for a single cross-asset (Trinity) call, and
        `expirations` to net each strike over specific expiration dates instead
        of the nearest `maxExpirations`.
      parameters:
        - $ref: "#/components/parameters/Symbols"
        - $ref: "#/components/parameters/Metric"
        - $ref: "#/components/parameters/MaxStrikes"
        - $ref: "#/components/parameters/MaxExpirations"
        - $ref: "#/components/parameters/Expirations"
        - $ref: "#/components/parameters/Layout"
      responses:
        "200":
          description: Live heatmap snapshot(s).
          headers:
            Cache-Control: { schema: { type: string, example: "private, max-age=5" } }
            X-RateLimit-Limit: { schema: { type: integer } }
            X-RateLimit-Remaining: { schema: { type: integer } }
            X-RateLimit-Reset: { schema: { type: integer } }
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: object
                    required: [symbols]
                    properties:
                      symbols:
                        type: array
                        items: { $ref: "#/components/schemas/SymbolHeatmap" }
                  meta: { $ref: "#/components/schemas/Meta" }
              examples:
                live:
                  summary: SPY live (truncated)
                  value:
                    data:
                      symbols:
                        - symbol: SPY
                          asOf: "2026-05-22T14:31:00Z"
                          spot: 591.23
                          previousClose: 589.10
                          priceChange: 2.13
                          priceChangePercent: 0.36
                          expirations: ["2026-05-22", "2026-05-23", "2026-05-30"]
                          strikes:
                            - { strike: 590, value: 1894300.4, nodeType: king, velocityPct: 12.4 }
                            - { strike: 595, value: 642100.2, nodeType: gatekeeper, velocityPct: -3.1 }
                            - { strike: 585, value: 88010.0, nodeType: normal, velocityPct: 0.4 }
                    meta: { metric: gamma, resolution: "1m", mode: live, cached: false }
                singleExpiration:
                  summary: SPY netted over one expiration (`expirations=2026-05-23`)
                  value:
                    data:
                      symbols:
                        - symbol: SPY
                          asOf: "2026-05-22T14:31:00Z"
                          spot: 591.23
                          previousClose: 589.10
                          priceChange: 2.13
                          priceChangePercent: 0.36
                          expirations: ["2026-05-23"]
                          strikes:
                            - { strike: 590, value: 412880.1, nodeType: king, velocityPct: 8.2 }
                            - { strike: 595, value: 121400.7, nodeType: gatekeeper, velocityPct: -1.4 }
                    meta: { metric: gamma, resolution: "1m", mode: live, cached: false }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/historical:
    get:
      summary: Replay per-strike heatmap at a past instant (one or more symbols)
      operationId: getHistoricalHeatmap
      tags: [Heatmap]
      description: |
        The snapshot nearest `at` for one or more symbols — same shape as
        `/v1/heatmap` minus `velocityPct` (velocity is live-only). `at` may be
        up to 365 days in the past; if no snapshot exists at/near that instant
        the response is `404` with `code: no_data`.
      parameters:
        - $ref: "#/components/parameters/Symbols"
        - name: at
          in: query
          required: true
          description: RFC3339 instant to replay (e.g. `2026-03-05T10:01:00Z`). Up to 365 days back.
          schema: { type: string, format: date-time }
        - $ref: "#/components/parameters/Metric"
        - $ref: "#/components/parameters/MaxStrikes"
        - $ref: "#/components/parameters/MaxExpirations"
        - $ref: "#/components/parameters/Expirations"
      responses:
        "200":
          description: Historical heatmap snapshot(s).
          headers:
            X-RateLimit-Limit: { schema: { type: integer } }
            X-RateLimit-Remaining: { schema: { type: integer } }
            X-RateLimit-Reset: { schema: { type: integer } }
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: object
                    required: [symbols]
                    properties:
                      symbols:
                        type: array
                        items: { $ref: "#/components/schemas/SymbolHeatmap" }
                  meta: { $ref: "#/components/schemas/Meta" }
              examples:
                replay:
                  summary: SPY at a past minute (truncated)
                  value:
                    data:
                      symbols:
                        - symbol: SPY
                          asOf: "2026-03-05T10:01:00Z"
                          spot: 512.40
                          previousClose: 510.02
                          priceChange: 2.38
                          priceChangePercent: 0.47
                          expirations: ["2026-03-05", "2026-03-06"]
                          strikes:
                            - { strike: 512, value: 1500200.0, nodeType: king }
                            - { strike: 515, value: 410000.0, nodeType: gatekeeper }
                    meta: { metric: gamma, resolution: "1m", mode: historical, cached: false }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404":
          description: |
            No snapshot at/near the requested instant, unknown symbol, or none
            of the requested `expirations` exist in that snapshot.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              examples:
                noData:
                  value: { error: { code: no_data, message: "No snapshot available for SPY at 2025-01-01T10:01:00Z." } }
                expirationNotFound:
                  value:
                    error:
                      code: expiration_not_found
                      message: "None of the requested expirations are available for SPY. Available: 2026-03-05, 2026-03-06, 2026-03-07."
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/stream:
    get:
      summary: Live SSE stream (one symbol per connection)
      operationId: streamHeatmap
      tags: [Heatmap]
      description: |
        A **Server-Sent Events** (`text/event-stream`) feed of live per-strike
        heatmap updates for **one** symbol. Open one connection per symbol.

        **Events:**
        - `connected` — handshake, payload `{symbol, creditsRemaining}`.
        - `initial_data` — current heatmap snapshot on connect.
        - `snapshot_update` — full heatmap on each change.
        - `velocity_update` — per-strike % change.
        - `credits` — emitted every minute boundary, payload `{remaining}`.
        - `closed` — stream terminates with `{reason: "insufficient_credits"
          | "account_suspended" | "credit_check_failed"}`.
        - `reconnect` — server is recycling the connection (after ~1h),
          payload `{reason: "max_duration"}`. Reconnect to continue.
        - `: keepalive` comment every 30s for proxy keepalive.

        **Pricing.** 1 credit on connect (charged before the SSE upgrade —
        an under-funded client gets a clean `402` HTTP response, not a
        half-open stream), then 1 credit per minute open. The per-minute
        ticker emits `event: credits {remaining: N}` after each successful
        debit so clients can budget the next minute.

        **Concurrency.** Up to 5 concurrent streams per customer per pod.
        Exceeding the cap returns `429` `stream_limit_reached`.

        **Expirations.** Frames carry every expiration in the window (an
        `Expirations` array plus one matrix column per expiration), so select
        expirations client-side. The `expirations` parameter is snapshot-only —
        sending it here returns `400` `invalid_parameter` rather than quietly
        meaning something narrower than it does on `/v1/heatmap`.

        > OpenAPI is request/response-oriented and can't fully model an event
        > stream. The connection costs 1 credit to open plus 1 per minute
        > connected and closes after one hour; reconnect to continue.
      parameters:
        - name: symbol
          in: query
          required: true
          description: Single ticker to stream (e.g. `SPY`). One symbol per connection.
          schema: { type: string, example: SPY }
        - $ref: "#/components/parameters/Metric"
        - $ref: "#/components/parameters/MaxStrikes"
        - $ref: "#/components/parameters/MaxExpirations"
      responses:
        "200":
          description: An SSE stream of heatmap events.
          content:
            text/event-stream:
              schema:
                type: string
                description: |
                  SSE frames, e.g. `event: snapshot_update` then `data: {SymbolHeatmap}`.
                  The snapshot_update `data` payload matches #/components/schemas/SymbolHeatmap.
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/gex/levels:
    get:
      operationId: getLevels
      summary: Key levels (classified nodes) for one or more symbols
      description: |
        The strikes Skylit classifies as nodes (king, gatekeeper, pika, barney,
        significant) for up to 10 symbols, strongest first, with each level's
        distance from spot. Same live source, filters and 5-second cache as
        `/v1/heatmap`; 1 credit per request.
      tags: [Heatmap]
      parameters:
        - $ref: "#/components/parameters/Symbols"
        - $ref: "#/components/parameters/Metric"
        - $ref: "#/components/parameters/MaxStrikes"
        - $ref: "#/components/parameters/MaxExpirations"
        - $ref: "#/components/parameters/Expirations"
      responses:
        "200":
          description: Levels per symbol.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      symbols:
                        type: array
                        items:
                          type: object
                          properties:
                            symbol: { type: string }
                            asOf: { type: string, format: date-time }
                            spot: { type: number }
                            previousClose: { type: number }
                            kingNode:
                              description: The strongest king node, or null when none is classified.
                              nullable: true
                              allOf:
                                - $ref: "#/components/schemas/Level"
                            levels:
                              type: array
                              items: { $ref: "#/components/schemas/Level" }
                  meta: { $ref: "#/components/schemas/Meta" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/account:
    get:
      operationId: getAccount
      summary: Your balance and limits
      description: |
        The calling key's account: status, balance (credits and US dollars at
        $0.001 per credit), whether usage is unlimited, and the limits that
        apply. Free.
      tags: [Account]
      responses:
        "200":
          description: Account.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      customerId: { type: string }
                      status: { type: string, enum: [active, suspended] }
                      apiEligible: { type: boolean }
                      unlimited: { type: boolean }
                      creditsBalance: { type: integer }
                      balanceUsd: { type: number }
                      limits:
                        type: object
                        properties:
                          requestsPerMinute: { type: integer }
                          symbolsPerHeatmapCall: { type: integer }
                          symbolsPerStream: { type: integer }
                          historicalInFlight: { type: integer }
                          activeKeys: { type: integer }
                          streamMaxDurationMinutes: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/openapi.json:
    get:
      summary: This OpenAPI specification, as JSON.
      operationId: getOpenAPI
      tags: [Meta]
      security: []
      responses:
        "200":
          description: OpenAPI 3.1 document for the Skylit Public API.
          content:
            application/json:
              schema: { type: object }

components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      description: |
        Skylit API key in the `Authorization` header
        (`Authorization: Bearer <key>`).

  parameters:
    Symbols:
      name: symbols
      in: query
      required: true
      description: |
        One ticker, or a comma-separated list for a single cross-asset call
        (e.g. `SPY` or `SPY,SPX,QQQ`). Each is returned as an element of
        `data.symbols`.
      schema:
        type: string
        example: SPY,SPX,QQQ
    Metric:
      name: metric
      in: query
      required: false
      description: Which Greek exposure to return per strike.
      schema:
        type: string
        enum: [gamma, vanna]
        default: gamma
    MaxStrikes:
      name: maxStrikes
      in: query
      required: false
      description: Maximum number of strikes around spot to return.
      schema: { type: integer, default: 92, minimum: 1, maximum: 400 }
    Expirations:
      name: expirations
      in: query
      required: false
      description: |
        Net each strike over exactly these expirations (`YYYY-MM-DD`,
        comma-separated) — one for a single-expiration heatmap (`2026-05-22`)
        or several for a custom set (`2026-05-22,2026-06-19`). Supersedes
        `maxExpirations`, and reaches any expiration the snapshot has, not just
        the nearest ones. Requested dates the symbol does not have are ignored;
        the `expirations` array in the response lists what was actually used.
        If none of them match, the response is `404` with
        `code: expiration_not_found` and the available dates in the message.
        On `/v1/heatmap`, expirations that have already expired are not
        available (they are trimmed from the live snapshot) — replay them with
        `/v1/historical` instead.
      schema:
        type: string
        example: "2026-05-22"
    MaxExpirations:
      name: maxExpirations
      in: query
      required: false
      description: |
        How many of the nearest expirations to net into each strike's `value`.
        Ignored when `expirations` is set.
      schema: { type: integer, default: 5, minimum: 1, maximum: 60 }
    Layout:
      name: layout
      in: query
      required: false
      description: |
        `net` (default) returns one net value per strike. `matrix` also returns
        `matrix`, the per-expiration grid those values are summed from.
      schema: { type: string, enum: [net, matrix], default: net }

  schemas:
    Level:
      type: object
      required: [strike, value, nodeType, distancePct]
      properties:
        strike: { type: number }
        value: { type: number, description: Net exposure at this strike for the selected metric. }
        nodeType: { type: string, enum: [king, gatekeeper, pika, barney, significant] }
        distancePct: { type: number, description: Percent distance from spot (negative = below). }
    SymbolHeatmap:
      type: object
      required: [symbol, asOf, spot, previousClose, priceChange, priceChangePercent, expirations, strikes]
      properties:
        symbol: { type: string, description: Canonical ticker for the returned data. }
        asOf:
          type: string
          format: date-time
          description: RFC3339 timestamp of the snapshot actually returned (nearest to the requested instant).
        spot: { type: number, description: Spot price at the snapshot. }
        previousClose: { type: number }
        priceChange: { type: number, description: Spot minus previous close. }
        priceChangePercent: { type: number }
        expirations:
          type: array
          description: Expiration dates (YYYY-MM-DD) contributing to each strike's net value.
          items: { type: string, format: date }
        strikes:
          type: array
          description: Per-strike nodes, ordered by strike ascending.
          items: { $ref: "#/components/schemas/StrikeNode" }
        matrix:
          type: array
          description: |
            Present only with `layout=matrix`. One row per entry in `strikes`,
            one column per entry in `expirations`: `matrix[i][j]` is the
            exposure at strike i for expiration j.
          items: { type: array, items: { type: number } }

    StrikeNode:
      type: object
      required: [strike, value, nodeType]
      properties:
        strike: { type: number, description: Strike price. }
        value:
          type: number
          description: Net exposure for the selected `metric` at this strike (summed across the returned expirations).
        nodeType:
          type: string
          description: Skylit's node classification for this strike.
          enum: [king, gatekeeper, pika, barney, significant, normal]
        velocityPct:
          type: number
          description: |
            Live percent change of this strike's value over the velocity window.
            Present on `/v1/heatmap` only; omitted on `/v1/historical`.

    Meta:
      type: object
      required: [metric, resolution, mode, cached]
      properties:
        metric: { type: string, enum: [gamma, vanna] }
        resolution: { type: string, example: "1m" }
        mode: { type: string, enum: [live, historical] }
        cached: { type: boolean, description: True if served from the in-process cache. }

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Stable, machine-readable error code.
              example: no_data
            message:
              type: string
              description: Human-readable explanation.

  responses:
    PaymentRequired:
      description: Out of credits (`insufficient_credits`).
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    BadRequest:
      description: Request validation failed.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Unauthorized:
      description: Missing or invalid API key.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Forbidden:
      description: The API key is invalid, revoked or expired, or the account is suspended.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    NotFound:
      description: |
        Unknown symbol, no data available, or none of the requested
        `expirations` exist for the symbol (`code: expiration_not_found`).
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    RateLimited:
      description: Per-minute rate limit exceeded.
      headers: { Retry-After: { schema: { type: integer } } }
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Unavailable:
      description: Heatmap data is temporarily unavailable.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
