tej
Python SDK for tej-api: open, free, end-of-day market data for NSE and BSE. No signup, no keys, no catch.
Zero runtime dependencies. Sync and async clients with identical surface. Returns plain list[dict] rows so you can feed them straight to polars, pandas, or your own code without an extra conversion step.
Install
pip install tejhq
That is the entire install. No httpx, no aiohttp, no pydantic. Just stdlib. Distribution is tejhq; import as tej.
Quick start (sync)
from tej import Client
c = Client()
# Last 90 days of RELIANCE (default range)
rows = c.ohlcv("RELIANCE", "nse")
print(rows[-1])
# {'date': '2026-06-05', 'open': 1442.0, 'high': 1455.7, ...}
# Full history since 2010
rows = c.ohlcv("RELIANCE", "nse", "2010-01-04", "2026-06-05")
print(len(rows)) # ~4040
# Full-market snapshot for one trading date
snap = c.snapshot("nse", "2025-05-28")
print(len(snap)) # ~2700 symbols
# Corporate actions (dividends, splits, bonuses)
acts = c.actions("RELIANCE")
print(acts[0])
# {'exchange': 'NSE', 'type': 'dividend', 'ex_date': '2024-10-28', 'cash_amount': 10.0, ...}
Quick start (async)
import asyncio
from tej import AsyncClient
async def main():
async with AsyncClient() as c:
# Fan out 10 symbols concurrently
symbols = ["RELIANCE", "TCS", "INFY", "HDFCBANK", "ICICIBANK",
"SBIN", "HINDUNILVR", "ITC", "LT", "AXISBANK"]
results = await asyncio.gather(*[c.ohlcv(s, "nse") for s in symbols])
for sym, rows in zip(symbols, results):
print(sym, len(rows))
asyncio.run(main())
Use with polars / pandas
tej returns list[dict] so you bring your own dataframe library.
import polars as pl
from tej import Client
rows = Client().ohlcv("RELIANCE", "nse", "2010-01-04", "2026-06-05")
df = pl.DataFrame(rows)
df = df.with_columns(pl.col("date").str.to_date())
df.head()
import pandas as pd
from tej import Client
rows = Client().ohlcv("RELIANCE", "nse", "2025-01-01", "2025-06-01")
df = pd.DataFrame(rows)
df["date"] = pd.to_datetime(df["date"])
df.set_index("date", inplace=True)
df["close"].plot()
Optional extras install the dataframe library alongside the SDK:
pip install "tejhq[polars]"
pip install "tejhq[pandas]"
Free key endpoints
Get a key at tejhq.dev/keys, no card, no password.
from tej import Client
c = Client(api_key="tej_live_...")
# Back-adjusted prices, continuous through splits, bonuses and dividends
adj = c.adjusted("RELIANCE", "nse", "2024-10-01", "2024-11-30")
print(adj[-1]["adj_close"], adj[-1]["adj_factor_cumulative"])
# Symbol history by ISIN or symbol
c.symbols("nse", isin="INE040A01034")
c.symbols("nse", symbol="HDFCBANK")
# Account, keys, today's usage
c.me()
Pro endpoints
c = Client(api_key="tej_live_...") # a key on the Pro tier
# Derived metrics per day: returns, 52w high/low, average volume and turnover
m = c.metrics("RELIANCE", "nse", "2026-01-01")
# Point-in-time liquidity universe, survivorship-bias-free
members = c.universe("liquid500", "nse", as_of="2019-03-15")
symbols = [x["symbol"] for x in members]
# Up to 50 symbols in one request, dict keyed by symbol
bars = c.batch(["RELIANCE", "TCS", "INFY"], "nse", "2026-01-01")
# Free text to symbol
c.resolve("tata motors", exchange="nse", limit=3)
c.resolve("zomato") # former ticker resolves to the current one
# Screen the market: liquid names up more than 5% this month, within 5% of the 52w high
hits = c.screener(
"nse",
universe="liquid500",
filters={"ret_21d.gt": 0.05, "pct_off_52w_high.gte": -0.05, "avg_turnover_20d.gte": 5e7},
sort="ret_21d",
limit=20,
)
env = c.screener_envelope(date="2019-03-15", universe="liquid100")
env["meta"]["date"], env["meta"]["total"] # day actually used, matches before paging
API reference
| Method | Endpoint | Tier | Returns |
|---|---|---|---|
c.ohlcv(symbol, exchange, from_=None, to=None) |
GET /v1/ohlcv/{exchange}/{symbol} |
keyless | list[OHLCV] |
c.snapshot(exchange, date) |
GET /v1/snapshot/{exchange}?date= |
keyless | list[SnapshotRow] |
c.actions(symbol) |
GET /v1/actions/{symbol} |
keyless | list[Action] |
c.adjusted(symbol, exchange, from_=None, to=None) |
GET /v1/adjusted/{exchange}/{symbol} |
free key | list[AdjustedRow] |
c.symbols(exchange, symbol=None, isin=None) |
GET /v1/symbols/{exchange} |
free key | list[SymbolInterval] |
c.me() |
GET /v1/me |
free key | dict |
c.metrics(symbol, exchange, from_=None, to=None) |
GET /v1/metrics/{exchange}/{symbol} |
pro | list[MetricsRow] |
c.universe(name, exchange="nse", as_of=None) |
GET /v1/universe/{name} |
pro | list[UniverseMember] |
c.batch(symbols, exchange="nse", from_=None, to=None) |
GET /v1/batch |
pro | dict[str, list[OHLCV]] |
c.resolve(q, exchange="both", limit=5) |
GET /v1/resolve |
pro | list[ResolveHit] |
c.screener(exchange="nse", date=None, universe=None, filters=None, sort=None, order=None, limit=50, offset=0) |
GET /v1/screener |
pro | list[ScreenerRow] |
c.screener_envelope(...) |
same, with meta["total"] and meta["date"] |
pro | Envelope |
c.health(), c.ready() |
GET /health, GET /ready |
keyless | dict |
Need the response envelope (with meta)? Use c.ohlcv_envelope(...), which returns the raw {"data": [...], "meta": {...}} dict.
AsyncClient exposes the same surface with await.
Configuration
from tej import Client
c = Client(
base_url="https://api.tejhq.dev", # override for self-hosted or staging
api_key=None, # tej_live_... from https://tejhq.dev/keys; keyless works for ohlcv, snapshot, actions
timeout=30.0, # seconds
max_retries=3, # exponential backoff on 5xx/429/network
user_agent_suffix="my-app/1.0", # for attribution
default_headers={"X-My-Header": "hi"},
)
Errors
All errors inherit from tej.TejError. The specific subclass tells you what happened:
| Exception | When |
|---|---|
BadRequestError |
HTTP 400, bad path or query parameter (also raised locally on invalid args before the request goes out, as a plain ValueError) |
AuthError |
HTTP 401, key_required when no key was sent to a gated endpoint, invalid_key when the key is malformed, unknown, or revoked |
NotFoundError |
HTTP 404 |
ProRequiredError |
HTTP 402, the key's tier is too low for this endpoint (/v1/universe, /v1/metrics need Pro) |
TejError with error_code == "key_required" |
HTTP 401, the endpoint needs a key and none was sent (/v1/adjusted, /v1/symbols) |
TejError with error_code == "invalid_key" |
HTTP 401, the key is malformed, unknown, or revoked |
RateLimitError |
HTTP 429 |
ServerError |
HTTP 5xx |
NetworkError |
DNS, connection, TLS, or timeout failure |
TejError |
Anything else |
from tej import Client, ProRequiredError
try:
Client().ohlcv("RELIANCE", "nse")
except ProRequiredError as e:
print(e.error_code) # 'pro_required'
print(e.request_id) # tej-api request id from response headers
Why a separate package
tej-api is a Go service. tej-bazaar is the Python ingestion pipeline. This repo is the thin client that anyone can pip install without dragging in either, and that releases on its own cadence as the API evolves.
Coverage
- NSE bhavcopy + corp actions: 2010-01-04 to today, ~4,047 trading days, ~7M rows
- BSE bhavcopy + corp actions: 2024-07-08 to today (the SEBI CMTS cutover), ~470 days, ~1M rows
- Cron refresh: weekdays 20:00 IST
Keyless endpoints are served at the Cloudflare edge from pre-rendered JSON, so most requests return in well under a second and repeat requests are cache hits. Keyless access is rate limited to 100 requests per 10 seconds per IP at the edge and 120 per minute at origin; the SDK retries 429s with backoff. Keyed requests bypass the edge cache and carry X-RateLimit-Limit-Day and X-RateLimit-Remaining-Day headers: 1,000 a day and 300 a minute on a free key.
License
MIT. Use it, fork it, ship products on top of it.
Links
- API docs: tejhq.dev/docs
- OpenAPI spec: api.tejhq.dev/openapi.yaml
- TypeScript SDK, same surface: npmjs.com/package/tejhq
- Bulk parquet downloads: data.tejhq.dev and huggingface.co/datasets/tejhq/indian-markets
- Issues: github.com/tejhq/tej-sdk-py/issues
Metadata
Release files for tejhq 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| tejhq-0.3.0.tar.gz | 14.1 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tejhq-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 33.3 kB
Release files / tejhq-0.3.0.tar.gz
| Download URL | tejhq-0.3.0.tar.gz |
|---|---|
| Size | 14.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a8f7ceed5eed2397f57754c861a2cc826bd7870c10e27d0f299f49f74466f434
|
|
BLAKE2b-256 checksum How to use checksums |
5405faa649a8e726e8125deacc1f187166a9b67ebcf9b9447aa76909673799f8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency logRelease files / tejhq-0.3.0-py3-none-any.whl
| Download URL | tejhq-0.3.0-py3-none-any.whl |
|---|---|
| Size | 19.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d84d55aec449960a4949dc2b8636b935e062e6f31dba4da7cc420beb666efcd8
|
|
BLAKE2b-256 checksum How to use checksums |
cc651751fc95a801b7a8d1d27ceeae673f6b6f8afb70e5d2f5c109a1513c3cc2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency logSecurity for tejhq 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for tejhq-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.