{
  "openapi": "3.1.0",
  "info": {
    "title": "IDS — Indian Data Service API",
    "version": "1.0.0",
    "summary": "Local last-traded-price service (IDS) for a fixed watchlist.",
    "description": "IDS (Indian Data Service) serves live last-traded prices (LTP) for the symbols this process is watching. No authentication, no API key: bind to 127.0.0.1 (default) and keep it local.\n\nPrices are pushed by the upstream feed; this API only reads the latest value it holds. `ltp` is the last traded price in the instrument's currency (INR), `t` is the epoch second of that trade, `age` is how many seconds old it is when you ask (use it, not the HTTP status, to detect a stale feed).\n\nSymbols are the exact watched keys: `RELIANCE`, `Nifty 50`, `Nifty Bank`, `SENSEX`, `India VIX`. Names containing spaces must be percent-encoded in a path (`Nifty%2050`). The aliases `NIFTY`, `BANKNIFTY`, `FINNIFTY`, `MIDCPNIFTY` are accepted wherever a symbol is taken and resolve to the index.\n\nStreaming: connect to `ws://127.0.0.1:8787/feed` (WebSocket, no subprotocol). On connect the server sends one `init` message with the catalogue and every price it already has, then `quotes` messages 4 times per second containing only the symbols that changed. Send `{\"type\":\"ping\"}` to get `{\"type\":\"pong\",\"at\":<epoch ms>,\"updates\":<n>}`. Message shapes are in the `x-websocket` block below.",
    "x-websocket": {
      "url": "ws://127.0.0.1:8787/feed",
      "clientToServer": {
        "ping": {
          "type": "ping"
        }
      },
      "serverToClient": {
        "init": {
          "type": "init",
          "meta": [
            {
              "s": "RELIANCE",
              "n": "Reliance Industries Limited",
              "mk": "cash",
              "prev": null
            }
          ],
          "snapshot": {
            "RELIANCE": {
              "ltp": 1169.2,
              "chg": null,
              "vol": 1234567,
              "t": 1793344512
            }
          },
          "updates": 1204,
          "servers": 1
        },
        "quotes": {
          "type": "quotes",
          "data": {
            "RELIANCE": {
              "ltp": 1169.4,
              "chg": null,
              "vol": 1234700,
              "t": 1793344513
            }
          },
          "updates": 1205
        },
        "pong": {
          "type": "pong",
          "at": 1793344513456,
          "updates": 1205
        }
      }
    },
    "license": {
      "name": "Proprietary — local use only"
    }
  },
  "servers": [
    {
      "url": "http://127.0.0.1:8787",
      "description": "local instance"
    }
  ],
  "security": [],
  "paths": {
    "/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Liveness and feed status",
        "responses": {
          "200": {
            "description": "Always 200 while the process lives; check `lastUpdate` for feed freshness.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "4XX": {
            "description": "Client error (unknown symbol, bad body, wrong method).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/quotes": {
      "get": {
        "operationId": "listQuotes",
        "summary": "Latest price for every watched symbol",
        "parameters": [
          {
            "name": "symbols",
            "in": "query",
            "required": false,
            "description": "Comma-separated subset, e.g. `RELIANCE,NIFTY`. Omit for all.",
            "schema": {
              "type": "string"
            },
            "example": "RELIANCE,TCS,NIFTY"
          }
        ],
        "responses": {
          "200": {
            "description": "Quotes currently held. Symbols that have not ticked yet are omitted from `quotes` and listed in `missing`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotesResponse"
                }
              }
            }
          },
          "4XX": {
            "description": "Client error (unknown symbol, bad body, wrong method).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/quotes/{symbol}": {
      "get": {
        "operationId": "getQuote",
        "summary": "Latest price for one symbol",
        "parameters": [
          {
            "name": "symbol",
            "in": "path",
            "required": true,
            "description": "Watched symbol or index alias. Encode spaces: `Nifty%2050`.",
            "schema": {
              "type": "string"
            },
            "example": "RELIANCE"
          }
        ],
        "responses": {
          "200": {
            "description": "Watched and already ticking.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteDetail"
                }
              }
            }
          },
          "404": {
            "description": "Not in the watchlist, or known but no tick received yet (`ltp` is then null with status 200).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/watch": {
      "get": {
        "operationId": "listWatch",
        "summary": "Current watchlist",
        "responses": {
          "200": {
            "description": "Watched symbols with display names.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatchList"
                }
              }
            }
          },
          "4XX": {
            "description": "Client error (unknown symbol, bad body, wrong method).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "changeWatch",
        "summary": "Add or remove symbols while running",
        "description": "Accepts JSON (`{\"add\":[...],\"remove\":[...]}`) or plain text: one symbol per line or comma-separated, with a leading `-`, `!` or `~` to remove. Applied in place: no restart, existing sockets stay connected, every open `/feed` client receives a fresh `init`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WatchChange"
              },
              "example": {
                "add": [
                  "HDFCBANK",
                  "NIFTY"
                ],
                "remove": [
                  "TCS"
                ]
              }
            },
            "text/plain": {
              "schema": {
                "type": "string"
              },
              "example": "HDFCBANK\nTCS\n-INFY"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied; `ignored` lists names that did not resolve (unknown ticker, or an option contract — options are not served).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatchResult"
                }
              }
            }
          },
          "400": {
            "description": "Body could not be parsed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Method other than GET/POST.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/feed": {
      "get": {
        "operationId": "streamFeed",
        "summary": "WebSocket stream (upgrade required)",
        "description": "Not a REST endpoint: send `Upgrade: websocket`. Anything else returns 400 `upgrade failed`. See the `x-websocket` block in `info` for message shapes.",
        "responses": {
          "101": {
            "description": "Switching protocols; binary-free JSON text frames."
          },
          "400": {
            "description": "Missing upgrade header.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApi",
        "summary": "This document",
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 description of this API.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "4XX": {
            "description": "Client error (unknown symbol, bad body, wrong method).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Quote": {
        "type": "object",
        "required": [
          "ltp",
          "chg",
          "vol",
          "t",
          "age"
        ],
        "properties": {
          "ltp": {
            "type": "number",
            "description": "Last traded price.",
            "example": 1169.2
          },
          "chg": {
            "type": [
              "number",
              "null"
            ],
            "description": "Percent change against the previous daily close, or null when no seed was taken (`--no-seed`).",
            "example": 0.16
          },
          "vol": {
            "type": "number",
            "description": "Cumulative volume for the current session.",
            "example": 1234567
          },
          "t": {
            "type": "integer",
            "description": "Epoch second of the tick.",
            "example": 1793344512
          },
          "age": {
            "type": "integer",
            "description": "Seconds between the tick and this response. Treat > 60 as stale.",
            "example": 3
          }
        }
      },
      "QuoteDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Quote"
          },
          {
            "type": "object",
            "required": [
              "symbol",
              "name",
              "mk"
            ],
            "properties": {
              "symbol": {
                "type": "string",
                "example": "RELIANCE"
              },
              "name": {
                "type": "string",
                "example": "Reliance Industries Limited"
              },
              "mk": {
                "type": "string",
                "enum": [
                  "cash",
                  "index"
                ],
                "example": "cash"
              }
            }
          }
        ],
        "description": "When the symbol is watched but has not ticked yet, `ltp`/`vol`/`t` are null and `age` is null."
      },
      "QuotesResponse": {
        "type": "object",
        "required": [
          "at",
          "count",
          "quotes"
        ],
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "Server time (UTC) when the snapshot was taken."
          },
          "count": {
            "type": "integer",
            "description": "Number of quotes returned."
          },
          "quotes": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/Quote"
            }
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Requested symbols that are watched but have no tick yet. Omitted when empty."
          },
          "unknown": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Requested symbols that are not in the watchlist. Omitted when empty."
          }
        }
      },
      "WatchItem": {
        "type": "object",
        "required": [
          "s",
          "n",
          "mk",
          "prev"
        ],
        "properties": {
          "s": {
            "type": "string",
            "description": "Symbol key used everywhere else.",
            "example": "RELIANCE"
          },
          "n": {
            "type": "string",
            "description": "Display name.",
            "example": "Reliance Industries Limited"
          },
          "mk": {
            "type": "string",
            "enum": [
              "cash",
              "index"
            ]
          },
          "prev": {
            "type": [
              "number",
              "null"
            ],
            "description": "Previous daily close, null with `--no-seed`."
          }
        }
      },
      "WatchList": {
        "type": "object",
        "required": [
          "watching",
          "symbols"
        ],
        "properties": {
          "watching": {
            "type": "integer",
            "example": 64
          },
          "symbols": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WatchItem"
            }
          }
        }
      },
      "WatchChange": {
        "type": "object",
        "properties": {
          "add": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "remove": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "WatchResult": {
        "type": "object",
        "required": [
          "ok",
          "watching",
          "added",
          "removed",
          "ignored"
        ],
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "watching": {
            "type": "integer"
          },
          "added": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "removed": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "ignored": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "ok",
          "uptime",
          "symbols",
          "live",
          "updates",
          "sockets",
          "clients",
          "lastUpdate"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "uptime": {
            "type": "integer",
            "description": "Seconds since the process started."
          },
          "symbols": {
            "type": "integer",
            "description": "Watchlist size."
          },
          "live": {
            "type": "integer",
            "description": "Symbols with at least one tick."
          },
          "updates": {
            "type": "integer",
            "description": "Ticks accepted since start."
          },
          "sockets": {
            "type": "integer",
            "description": "Upstream sockets in use."
          },
          "clients": {
            "type": "integer",
            "description": "Connected `/feed` clients."
          },
          "lastUpdate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Timestamp of the newest tick, or null before the first one."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      }
    }
  }
}
