A personal-use Python library with the same interface as Zerodha's Kite Connect SDK — powered by your own Kite web login instead of an API key.
KiteWeb exposes the same Python API as kiteconnect 5.2.1 (KiteConnect + KiteTicker: same method
names, arguments, return shapes and constants) — quotes, ticks, historical candles, portfolio, orders, GTT,
mutual funds — backed by the same endpoints the Kite website itself uses. It also ships a local server
that speaks the Kite Connect wire protocol, so code written against Kite Connect in any language can be
pointed at it. All it needs is your own Kite user id, password and TOTP.
It is a personal project for personal use: read your own portfolio, pull market data for research and small tools, prototype against the Kite Connect interface. Zerodha's official, supported route for programmatic access — and the right one for any serious or automated trading — is Kite Connect (execution APIs free for a personal app; market data ₹500/month per app as of 2025).
from kiteweb import KiteWebConnect
kw = KiteWebConnect() # logs in once, caches the session in ~/.kiteweb/
kw.ltp("NSE:INFY") # {'NSE:INFY': {'instrument_token': 408065, 'last_price': 1144.0}}
kw.holdings() # same JSON Kite Connect returns
kw.place_order("regular", "NSE", "INFY", "BUY", 1, "CNC", "LIMIT", price=1100) # real order!Unofficial. Not open source. Use entirely at your own risk. KiteWeb is an independent project, not affiliated with or endorsed by Zerodha Broking Ltd. It talks to undocumented endpoints that Zerodha may change or block at any time. You are responsible for complying with Zerodha's terms of service and with applicable law (including SEBI's rules on algorithmic trading); the author accepts no liability for losses, blocked accounts or anything else. Read Is this allowed?, Trading safety and the LICENSE before you use it.
- KiteWeb vs Kite Connect
- Is this allowed?
- Requirements
- Install
- Setup: credentials and TOTP
- Quick start (5 minutes)
- Library reference
- Streaming ticks:
KiteWebTicker - Open the Kite website, logged in (
open_browser) - Drop-in server for existing Kite Connect code
- Trading safety
- Security
- Errors
- Configuration (environment variables)
- How it works and known limits
- Troubleshooting
- Running the tests
- Changelog
- License
| Kite Connect (official) | KiteWeb | |
|---|---|---|
| What it is | Zerodha's supported, documented API product | Independent personal project using the Kite website's own endpoints |
| Authentication | API key + daily request_token → access_token login flow |
Your own user id + password + TOTP; session cached and reused |
| Real-time ticks (WebSocket) | Yes (Connect plan) | Yes — same binary protocol, same tick dicts |
| Historical candles | Yes (Connect plan) | Yes |
| Quotes / LTP / OHLC | Yes (Connect plan) | Yes (built from a WebSocket snapshot; circuit limits not available) |
| Orders, GTT, MF, margins, portfolio | Yes | Yes — same methods, same payloads |
| Python client | kiteconnect |
kiteweb — same API, verified method-by-method |
| Other languages | Official SDKs against api.kite.trade |
Point a Kite Connect SDK at the local kiteweb server |
| Support / stability | Documented and supported by Zerodha | Undocumented endpoints; best-effort; may stop working at any time |
| Suitable for | Any programmatic use, including automated trading under Zerodha's and SEBI's rules | Personal, non-commercial use on your own account, at your own risk |
| Licence | Zerodha's terms | Source-available, personal use only (see LICENSE) |
Please read this honestly before you start.
- Zerodha's terms of service apply to you. KiteWeb uses your own credentials to talk to the endpoints the Kite website uses. Zerodha does not sanction or support that; it may change the endpoints, add bot detection, or take action on accounts at any time. Using KiteWeb in a way that breaches Zerodha's terms is not permitted by this project's licence — it is your responsibility to check and comply.
- Regulation applies to you. Automated or algorithmic order placement by retail investors in India is governed by SEBI's framework and by your broker's rules. Zerodha's official and supported route for programmatic and automated trading is Kite Connect. If you want to automate trading, use Kite Connect and follow those rules. KiteWeb's order methods exist for interface parity and personal experimentation on your own account; they are not a way around any rule, and you must not use them as one.
- Your account, your money, your risk. Any order KiteWeb places is a real order. The author is not responsible for losses, blocked accounts, or any consequence of your use, and provides no support or guarantee. See the LICENSE for the full terms.
- Not affiliated with Zerodha. This is an independent project. If you represent Zerodha and have a concern about it, contact the author through the repository's GitHub page.
- Python 3.10 or newer (
python --version). Developed and tested on Windows with Python 3.12; it is pure Python and has no OS-specific code, so Linux and macOS work the same way. - A Zerodha Kite account with TOTP-based two-factor authentication enabled (the "external 2FA / authenticator app" option — not the SMS/email OTP). You need the TOTP secret key, not the 6-digit code. See Setup.
- Internet access to
kite.zerodha.com,ws.zerodha.comandapi.kite.trade.
git clone https://github.com/vikassharma545/KiteWeb.git
cd KiteWeb
python -m pip install -e ".[server]" # library + local serverJust the library (no server): python -m pip install -e .
Everything including the test tools: python -m pip install -e ".[server,test]"
Check it worked:
python -c "import kiteweb; print(kiteweb.__version__)"KiteWeb logs in exactly like you do in the browser: user id + password + TOTP code. It reads them from three environment variables.
| Variable | What it is |
|---|---|
ZERODHA_USER_ID |
Your Kite client id, e.g. AB1234 |
ZERODHA_PASSWORD |
Your Kite login password |
ZERODHA_TOTP_SECRET |
The base32 secret behind your authenticator app (e.g. JBSWY3DPEHPK3PXP) |
- Log in to kite.zerodha.com → click your user id (top right) → My profile → Password & Security.
- Enable External 2FA TOTP (authenticator-app based 2FA).
- When Kite shows the QR code, choose the option to view/copy the setup key instead of scanning.
That alphanumeric string is your
ZERODHA_TOTP_SECRET. Store it somewhere safe (a password manager). - You can also scan the same QR code with Google Authenticator/Authy — the app and KiteWeb will generate identical codes, so your normal browser login keeps working.
If you already use an authenticator app and never saved the key, disable and re-enable TOTP to get a new one.
# Windows PowerShell (current window only)
$env:ZERODHA_USER_ID = "AB1234"
$env:ZERODHA_PASSWORD = "your-password"
$env:ZERODHA_TOTP_SECRET = "JBSWY3DPEHPK3PXP":: Windows cmd
set ZERODHA_USER_ID=AB1234
set ZERODHA_PASSWORD=your-password
set ZERODHA_TOTP_SECRET=JBSWY3DPEHPK3PXP# Linux / macOS / Git Bash
export ZERODHA_USER_ID=AB1234
export ZERODHA_PASSWORD='your-password'
export ZERODHA_TOTP_SECRET=JBSWY3DPEHPK3PXPYou can also pass them in code instead: KiteWebConnect(user_id, password, totp_secret).
Never commit these to git — the shipped .gitignore excludes .env, config.ini, *.cred and
~/.kiteweb/, but check git status before you push.
The first call logs in (two HTTPS requests) and saves the resulting session token (enctoken) to
~/.kiteweb/enctoken.json (%USERPROFILE%\.kiteweb\ on Windows). Every later run reuses it — no
login, no TOTP — until Zerodha rejects it, at which point KiteWeb logs in again automatically.
Set KITEWEB_CACHE_DIR to put the cache somewhere else.
KiteWeb protects your account from itself: a rejected login starts a 60 s cooldown, and after 3 consecutive rejections it refuses to try again until you restart with fixed credentials (Zerodha blocks the account after 5 wrong password attempts). A TOTP code is never reused within its 30 s window.
Prefer a notebook? examples/kiteweb_tutorial.ipynb walks through everything below interactively — market data, your account, live ticks, errors — and keeps the order-placing section disabled behind a flag.
import datetime as dt
from kiteweb import KiteWebConnect
kw = KiteWebConnect()
# --- market data ---------------------------------------------------------------
kw.ltp("NSE:INFY", "NSE:NIFTY 50", "BSE:SENSEX")
# {'NSE:INFY': {'instrument_token': 408065, 'last_price': 1144.0}, 'NSE:NIFTY 50': {...}, ...}
q = kw.quote("NSE:INFY")["NSE:INFY"]
# {'instrument_token': 408065, 'timestamp': datetime(2026, 8, 28, 17, 34, 26),
# 'last_trade_time': datetime(2026, 8, 28, 15, 59, 52), 'last_price': 1144.0, 'last_quantity': 1,
# 'buy_quantity': 0, 'sell_quantity': 2401, 'volume': 9621969, 'average_price': 1136.55, 'oi': 0,
# 'oi_day_high': 0, 'oi_day_low': 0, 'net_change': 1.0, 'lower_circuit_limit': 0,
# 'upper_circuit_limit': 0, 'ohlc': {'open': 1123.6, 'high': 1145.0, 'low': 1123.3, 'close': 1143.0},
# 'depth': {'buy': [{'quantity': ..., 'price': ..., 'orders': ...} x5], 'sell': [... x5]}}
kw.ohlc("MCX:CRUDEOIL26SEPFUT") # {'MCX:...': {'instrument_token': ..., 'last_price': ..., 'ohlc': {...}}}
kw.historical_data(408065, dt.date(2026, 8, 1), dt.date(2026, 8, 28), "day")
# [{'date': datetime(2026, 8, 1, 0, 0, tzinfo=+05:30), 'open': ..., 'high': ..., 'low': ..., 'close': ..., 'volume': ...}, ...]
rows = kw.instruments("NSE") # list of dicts, same columns as Kite Connect's instrument CSV
infy = next(r for r in rows if r["tradingsymbol"] == "INFY")
infy["instrument_token"] # 408065
# --- account -------------------------------------------------------------------
kw.profile()["user_name"]
kw.margins()["equity"]["available"]["live_balance"]
kw.holdings(); kw.positions(); kw.orders(); kw.trades()Instruments are always written EXCHANGE:TRADINGSYMBOL exactly as they appear in the instrument
master — NSE:INFY, NSE:NIFTY 50 (with the space), BSE:SENSEX, NFO:NIFTY26SEP24500CE,
MCX:CRUDEOIL26SEPFUT. Unknown symbols raise KiteWebError(400, ..., "InputException").
KiteWebConnect has every public method of kiteconnect.KiteConnect with the same name,
arguments, return shape and constants (kw.VARIETY_REGULAR, kw.PRODUCT_CNC, kw.ORDER_TYPE_LIMIT,
kw.GTT_TYPE_SINGLE, …). This is verified by the test suite against the installed kiteconnect.
Compared against the official pykiteconnect 5.2.1
(latest on PyPI, released 23 Jul 2026; master has no newer methods). Every one of its 45 KiteConnect
methods and 9 KiteTicker methods exists here; all 42 constants have identical values.
✅ identical behaviour · ≈ works, with the noted difference · ✖ not available on Kite web
KiteConnect → KiteWebConnect
| Group | Method | Status | Note |
|---|---|---|---|
| Session | set_session_expiry_hook(method) |
✅ | fires when a TokenException reaches you |
| Session | login_url() |
≈ | returns https://kite.zerodha.com/ — there is no OAuth redirect flow |
| Session | generate_session(request_token, api_secret) |
≈ | arguments optional; logs in with password + TOTP, returns a Kite-shaped dict with dummy access_token |
| Session | renew_access_token(refresh_token, api_secret) |
≈ | same as generate_session() |
| Session | set_access_token(access_token) |
≈ | no-op (token is obtained by the web login) |
| Session | invalidate_access_token(access_token=None) |
≈ | local logout: deletes the cached enctoken and the saved browser cookies (the Kite server-side session is not ended) |
| Session | invalidate_refresh_token(refresh_token) |
≈ | no-op, returns True |
| User | profile() |
✅ | |
| User | margins(segment=None) |
✅ | |
| Orders | place_order(...) (18 args incl. algo_id, market_protection) |
✅ | returns order_id |
| Orders | place_autoslice_order(...) |
✅ | returns full dict with children |
| Orders | modify_order(...) |
✅ | |
| Orders | cancel_order(variety, order_id, parent_order_id=None) |
✅ | |
| Orders | exit_order(variety, order_id, parent_order_id=None) |
✅ | |
| Orders | orders() |
✅ | timestamps as datetime |
| Orders | order_history(order_id) |
✅ | |
| Orders | trades() |
✅ | |
| Orders | order_trades(order_id) |
✅ | |
| Portfolio | positions() |
✅ | |
| Portfolio | holdings() |
✅ | |
| Portfolio | get_auction_instruments() |
✅ | |
| Portfolio | convert_position(...) |
✅ | |
| Mutual funds | mf_orders(order_id=None) |
✅ | |
| Mutual funds | place_mf_order(...) |
✅ | |
| Mutual funds | cancel_mf_order(order_id) |
✅ | |
| Mutual funds | mf_sips(sip_id=None) |
✅ | |
| Mutual funds | place_mf_sip(...) |
✅ | |
| Mutual funds | modify_mf_sip(...) |
✅ | |
| Mutual funds | cancel_mf_sip(sip_id) |
✅ | |
| Mutual funds | mf_holdings() |
✅ | |
| Mutual funds | mf_instruments() |
✅ | public CSV from api.kite.trade |
| Market data | instruments(exchange=None) |
✅ | public CSV, cached per day; identical column types |
| Market data | quote(*instruments) |
≈ | built from a WebSocket snapshot; lower/upper_circuit_limit are always 0 |
| Market data | ohlc(*instruments) |
≈ | WebSocket snapshot |
| Market data | ltp(*instruments) |
≈ | WebSocket snapshot |
| Market data | historical_data(...) |
✅ | all intervals, continuous, oi |
| Market data | trigger_range(transaction_type, *instruments) |
✖ | raises KiteWebError(501); Kite web dropped the cover-order route |
| GTT | get_gtts() |
✅ | |
| GTT | get_gtt(trigger_id) |
✅ | |
| GTT | place_gtt(...) |
✅ | same validation as the official _get_gtt_payload |
| GTT | modify_gtt(...) |
✅ | |
| GTT | delete_gtt(trigger_id) |
✅ | |
| Margins | order_margins(params) |
✅ | |
| Margins | basket_order_margins(params, consider_positions=True, mode=None) |
✅ | |
| Margins | get_virtual_contract_note(params) |
✅ |
Constructor: KiteConnect(api_key, access_token=None, root=None, debug=False, timeout=None, proxies=None, pool=None, disable_ssl=False) becomes KiteWebConnect(user_id=None, password=None, totp_secret=None, session=None, snapshot_timeout=5.0) — there is no API key. Extras only in KiteWeb: quote_raw(),
orders_raw(), trades_raw(), order_history_raw(), order_trades_raw(), mf_orders_raw(),
mf_sips_raw() (wire-format strings) and historical_candles() (raw candle arrays).
KiteTicker → KiteWebTicker
| Method / attribute | Status | Note |
|---|---|---|
connect(threaded=False, disable_ssl_verification=False, proxy=None) |
≈ | disable_ssl_verification/proxy are ignored with a warning |
is_connected() |
✅ | |
close(code=None, reason=None) / stop() |
✅ | |
stop_retry() |
✅ | |
subscribe(tokens) / unsubscribe(tokens) / set_mode(mode, tokens) / resubscribe() |
✅ | may also be called before connect() |
on_ticks, on_open, on_close, on_error, on_connect, on_message, on_reconnect, on_noreconnect, on_order_update |
✅ | same signatures; tick dicts parsed by the official parser |
MODE_FULL/QUOTE/LTP, EXCHANGE_MAP, CONNECT_TIMEOUT, RECONNECT_MAX_DELAY, RECONNECT_MAX_TRIES |
✅ | |
ROOT_URI |
≈ | wss://ws.zerodha.com/ instead of wss://ws.kite.trade |
Constructor: KiteTicker(api_key, access_token, debug=False, root=None, reconnect=True, reconnect_max_tries=50, reconnect_max_delay=60, connect_timeout=30) becomes
KiteWebTicker(session=None, reconnect=True, reconnect_max_tries=50, reconnect_max_delay=60, connect_timeout=30, debug=False, stale_timeout=15).
| Method | Notes |
|---|---|
quote(*instruments) |
Full quote with depth. Built from a one-shot WebSocket snapshot (see limits). |
ohlc(*instruments) |
Open/high/low/close + last price. |
ltp(*instruments) |
Last price only. |
historical_data(token, from, to, interval, continuous=False, oi=False) |
interval: minute, 3minute, 5minute, 10minute, 15minute, 30minute, 60minute, day. Dates as date, datetime or "YYYY-MM-DD HH:MM:SS" strings. date values are IST-aware datetimes. |
instruments(exchange=None) |
Instrument master, cached on disk for the day. Exchanges: NSE, BSE, NFO, BFO, CDS, BCD, MCX. |
mf_instruments() |
Mutual fund master. |
trigger_range(...) |
Not available on Kite web — raises KiteWebError(501) without a request. |
All three quote methods also accept a list (kw.quote(["NSE:INFY", "BSE:SENSEX"])) like the official client.
profile(), margins(segment=None), holdings(), positions(), orders(), trades(),
order_history(order_id), order_trades(order_id), get_auction_instruments(), get_gtts(),
get_gtt(trigger_id), mf_orders(order_id=None), mf_sips(sip_id=None), mf_holdings().
Like the official client, timestamp fields (order_timestamp, exchange_timestamp, fill_timestamp,
created, last_instalment, timestamp, last_trade_time) come back as datetime objects. The *_raw
variants (orders_raw(), quote_raw(), …) keep Kite's wire strings — that is what the server serves.
| Method | Returns |
|---|---|
place_order(variety, exchange, tradingsymbol, transaction_type, quantity, product, order_type, price=None, validity=None, validity_ttl=None, disclosed_quantity=None, trigger_price=None, iceberg_legs=None, iceberg_quantity=None, auction_number=None, algo_id=None, tag=None, market_protection=None) |
order_id (str) |
place_autoslice_order(...) (same args) |
full dict: parent order_id + children |
modify_order(variety, order_id, parent_order_id=None, quantity=None, price=None, order_type=None, trigger_price=None, validity=None, disclosed_quantity=None, market_protection=None) |
order_id |
cancel_order(variety, order_id, parent_order_id=None) / exit_order(...) |
order_id |
convert_position(exchange, tradingsymbol, transaction_type, position_type, quantity, old_product, new_product) |
True |
place_gtt(trigger_type, tradingsymbol, exchange, trigger_values, last_price, orders) |
{"trigger_id": ...} |
modify_gtt(trigger_id, ...), delete_gtt(trigger_id) |
as Kite Connect |
place_mf_order(...), cancel_mf_order(id), place_mf_sip(...), modify_mf_sip(...), cancel_mf_sip(id) |
as Kite Connect |
order_margins(params), basket_order_margins(params, consider_positions=True, mode=None), get_virtual_contract_note(params) |
margin / charge calculators (no order is placed) |
oid = kw.place_order(kw.VARIETY_REGULAR, "NSE", "INFY", kw.TRANSACTION_TYPE_BUY, 1,
kw.PRODUCT_CNC, kw.ORDER_TYPE_LIMIT, price=1100, tag="mybot")
kw.modify_order(kw.VARIETY_REGULAR, oid, price=1090)
kw.cancel_order(kw.VARIETY_REGULAR, oid)
kw.place_gtt(kw.GTT_TYPE_SINGLE, "INFY", "NSE", [1000], 1144.0,
[{"transaction_type": "BUY", "quantity": 1, "order_type": "LIMIT", "product": "CNC", "price": 1000}])
kw.order_margins([{"exchange": "NSE", "tradingsymbol": "INFY", "transaction_type": "BUY", "variety": "regular",
"product": "CNC", "order_type": "MARKET", "quantity": 1, "price": 0, "trigger_price": 0}])Zerodha's own rejections (insufficient funds, RMS, market closed, …) surface verbatim as KiteWebError.
There is no API key / request-token flow here, but code written for KiteConnect often calls these,
so they exist and are harmless:
| Method | Behaviour in KiteWeb |
|---|---|
set_session_expiry_hook(fn) |
fn() is called whenever a TokenException reaches you (i.e. automatic re-login did not rescue the call). |
generate_session(request_token=None, api_secret=None) |
Logs in if needed; returns a Kite-shaped session dict with a dummy access_token of "kiteweb". |
renew_access_token(...) |
Same as generate_session(). |
set_access_token(token) |
No-op. |
invalidate_access_token() |
Local logout: deletes the cached enctoken and the saved browser cookies. The next call logs in again. Zerodha's server-side session is not ended; it lapses when Kite expires it. |
invalidate_refresh_token() |
No-op, returns True. |
login_url() |
https://kite.zerodha.com/. |
Same callbacks, modes and tick dicts as kiteconnect.KiteTicker (the binary frames are parsed by the
official parser, so CDS/BCD price divisors, index packets and depth are identical).
from kiteweb import KiteWebTicker
kt = KiteWebTicker() # shares the same cached login
def on_ticks(ws, ticks):
for t in ticks:
print(t["instrument_token"], t["last_price"])
def on_connect(ws, response):
ws.subscribe([408065, 256265]) # INFY, NIFTY 50
ws.set_mode(ws.MODE_FULL, [408065]) # MODE_LTP / MODE_QUOTE / MODE_FULL
kt.on_ticks = on_ticks
kt.on_connect = on_connect
kt.on_order_update = lambda ws, data: print("order update:", data)
kt.on_error = lambda ws, code, reason: print("error:", code, reason)
kt.connect() # blocks; kt.connect(threaded=True) runs in a daemon thread- Callbacks:
on_ticks(ws, ticks),on_connect(ws, response),on_open(ws),on_close(ws, code, reason),on_error(ws, code, reason),on_message(ws, payload, is_binary)(raw frames),on_reconnect(ws, attempt),on_noreconnect(ws),on_order_update(ws, data). - Methods:
subscribe(tokens),unsubscribe(tokens),set_mode(mode, tokens),resubscribe(),is_connected(),close()/stop(),stop_retry(). - You may call
subscribe/set_modebeforeconnect(); the subscriptions are sent as soon as the socket opens, and re-sent after every reconnect. - Reconnect: exponential backoff (2 s, 4 s, … up to
reconnect_max_delay=60), at mostreconnect_max_tries=50consecutive failures. A handshake rejected with 401/403 triggers a fresh login. - Dead-connection detection: Kite's server sends a heartbeat every few seconds. If nothing arrives for
stale_timeoutseconds (default 15) the socket is treated as dead and reconnected — so a laptop sleep or a dropped NAT mapping cannot leave you silently receiving no ticks.stale_timeout=Nonedisables it. - Tick timestamps (
exchange_timestamp,last_trade_time) are host-local naive datetimes, exactly likeKiteTicker. (quote()converts them to IST like the REST API does.) on_close(ws, code, reason)gets(1000, "closed")after your ownclose(); when the stream ended because of an error (or reconnects were exhausted) it gets(1006, <error text>).- A websocket handshake rejected with 401/403 goes through the same recovery as a REST call: a fresher token written to the shared cache by another process (e.g. the browser) is adopted first, and only otherwise is a fresh login made.
Kite normally allows only one active web session per user, so logging in through the browser can
knock out the token kiteweb cached, and vice versa. open_browser() solves that: it opens your own
Chrome/Edge, logs in to kite.zerodha.com for you, and hands the resulting session back to kiteweb —
so the website and your Python code share one login. You watch the dashboard while your code runs.
Double-click launcher. On Windows, double-click Open-Kite.bat in the project folder. The
first run asks for your credentials (and offers to remember them); after that it just opens Kite,
logged in. (Under the hood it runs examples/open_kite.py, which you can
also run directly: python examples/open_kite.py.)
From your code:
from kiteweb import KiteWebConnect
kw = KiteWebConnect()
browser = kw.open_browser() # your Chrome opens, already on the logged-in dashboard
kw.ltp("NSE:INFY") # the API uses the very same session as that window
# ... do your thing; the window stays open ...
browser.close() # closes the window (saving the session for next time)How it works: it drives the browser over Chrome's built-in DevTools Protocol (no Selenium, no
chromedriver — it reuses the websocket-client kiteweb already depends on). The first time, it
performs Kite's own /api/login + /api/twofa flow inside the page (so Zerodha's server sets the
session exactly as a normal login would) and saves the resulting cookies. Later opens replay those
saved cookies — no login, no TOTP, and the running session is not disturbed — falling back to a real
login only once the saved session has expired. It uses the same ZERODHA_* credentials as the API.
open_browser(headless=False, path="/dashboard", chrome_path=None, user_data_dir=None).headless=Trueopens the session without showing a window (useful for automation/screenshots).pathis where to land, e.g."/orders","/positions","/holdings".chrome_path/ theKITEWEB_BROWSERenv var point at a specific browser if it is not auto-detected (Chrome, Edge and Chromium are found automatically on Windows, macOS and Linux).user_data_diruses a specific browser profile directory instead of a throwaway one. It must not be a profile that a running Chrome/Edge is already using: Chrome hands the request to that instance and exits, andopen_browser()then times out waiting for its DevTools endpoint.- A saved session is reused only after Zerodha confirms the saved token is still valid (a profile call); otherwise a real login runs. A saved session cannot be mistaken for a live one.
Because a fresh Kite login invalidates the previous session (Kite's one-session-per-user rule), two
separate processes logging in independently would keep logging each other out. kiteweb avoids that:
the saved cookies mean a repeat open_browser() reuses the existing session instead of creating a
new one, and if a token ever is rotated, the other process automatically adopts the newer token from
the shared cache on its next call rather than logging in again. So you can double-click the launcher
mid-run and both your code and the website keep working on one session. (A write that happens to be
in flight at that exact moment can still return a TokenException — treat it as "unknown", reconcile
with orders(), then continue; see Trading safety.)
Start a local server that mirrors https://api.kite.trade and wss://ws.kite.trade:
python -m kiteweb.server # http://127.0.0.1:8000 (or: kiteweb-server --port 8000)
python -m kiteweb.server --port 9000 --log-level warningPoint any Kite Connect client at it. The api key / access token can be anything:
from kiteconnect import KiteConnect, KiteTicker
kc = KiteConnect(api_key="x", access_token="y", root="http://127.0.0.1:8000")
kc.quote("NSE:INFY") # unchanged code
kc.historical_data(408065, "2026-08-01", "2026-08-28", "day")
kt = KiteTicker("x", "y", root="ws://127.0.0.1:8000/ws")# any language: same paths, same JSON envelope
curl "http://127.0.0.1:8000/quote/ltp?i=NSE:INFY&i=BSE:SENSEX"
# {"status":"success","data":{"NSE:INFY":{"instrument_token":408065,"last_price":1144.0}, ...}}Routes (identical to api.kite.trade):
- Read (always on):
/quote,/quote/ltp,/quote/ohlc,/instruments[/{exchange}](CSV),/instruments/historical/{token}/{interval}?from&to&continuous&oi,/user/profile,/user/margins[/{segment}],/portfolio/holdings,/portfolio/holdings/auctions,/portfolio/positions,/orders,/orders/{order_id},/orders/{order_id}/trades,/trades,/gtt/triggers[/{id}],/mf/instruments,/mf/orders[/{id}],/mf/sips[/{id}],/mf/holdings, and the calculatorsPOST /margins/orders,POST /margins/basket,POST /charges/orders. - Write (off by default):
POST /orders/{variety},PUT|DELETE /orders/{variety}/{order_id},PUT /portfolio/positions,POST|PUT|DELETE /gtt/triggers[/{id}],POST|DELETE /mf/orders[/{id}],POST|PUT|DELETE /mf/sips[/{id}]. They return403 PermissionExceptionuntil you start the server withKITEWEB_ALLOW_TRADING=1. - Session stubs so old code does not crash:
POST /session/token,DELETE /session/token,POST /session/refresh_token. - WebSocket relay:
/ws(and/) — the same binary tick protocol asws.kite.trade.
Security. The server holds your logged-in Zerodha session. Keep it on 127.0.0.1 (the default).
If it must be reachable from elsewhere, set KITEWEB_TOKEN=<secret> and send it as the
X-Kiteweb-Token: <secret> header (or ?token=<secret>, which ends up in access logs — avoid it), and
put TLS in front of it. The /ws relay rejects an unauthenticated socket with close code 1008.
Responses use Kite's wire format (timestamps as "YYYY-MM-DD HH:MM:SS" strings, IST) and Kite's error
envelope ({"status":"error","message":...,"error_type":...}), so the official clients parse them normally.
Orders placed through KiteWeb are real orders on your account. Please read this once.
- A failed write means "unknown", not "failed". A
NetworkExceptiononplace_order,modify_order,cancel_order,place_gtt, … means the request may or may not have reached Zerodha. KiteWeb never retries a write automatically. Before you retry, reconcile: callorders()(orget_gtts()/mf_orders()) and look for your order. Passtag="..."onplace_orderso you can recognise it. - A
TokenExceptionon a write means the session had expired. The dead token is discarded, so simply reconciling and calling again logs in afresh and works. (Reads recover on their own: aGETis replayed once after re-login.) session.post()/put()refuseretry_on_token_error=Trueon/oms/orders,/oms/gtt,/oms/mf/ordersand/oms/mf/sips— a blind replay there could duplicate an order.- The server refuses every write route unless
KITEWEB_ALLOW_TRADING=1is set, so a stray local process cannot trade. The library has no such gate: whoever holds the credentials can trade. - Order placement / modification / cancellation and the GTT lifecycle are unit-tested and parity-checked
against
kiteconnect; the GTT lifecycle has been confirmed on a real account. The optional live test (KITEWEB_LIVE_TRADING=1, see tests) exercises an AMO order end to end — run it only with the account owner's consent.
KiteWeb logs in to your Zerodha account with your real credentials, so know exactly what it does with them.
What is stored where
| Data | Where | Notes |
|---|---|---|
ZERODHA_USER_ID, ZERODHA_PASSWORD, ZERODHA_TOTP_SECRET |
environment variables (or constructor args) only | never written to disk, never logged, only ever sent to kite.zerodha.com over HTTPS |
Session token (enctoken) |
~/.kiteweb/enctoken.json (or KITEWEB_CACHE_DIR) |
restricted to your user account: mode 0600 on Linux/macOS; on Windows the file's ACL is replaced with a single entry for your user (plain chmod does nothing there). Anyone holding this file can act as you on Kite until the session expires. |
Browser cookies (from open_browser()) |
~/.kiteweb/browser_cookies.json |
the full Kite web session (incl. kf_session); restricted to your user the same way. Same sensitivity as the token file. invalidate_access_token() deletes it together with the token file; delete it by hand to force a fresh login next time. |
| Saved launcher credentials (optional) | ~/.kiteweb/credentials.env |
only if you answer "yes" to the launcher's save prompt; holds your password and TOTP secret in plain text, restricted to your user the same way. Anything running as you can still read it. Delete it to be asked again. |
| Instrument master CSVs | same directory | public data |
Nothing KiteWeb logs or raises contains the enctoken, your password or the TOTP secret. Error messages carry Zerodha's response text (e.g. "Invalid password"), not your credentials.
Built-in safeguards
- Login happens only when there is no valid cached session. A rejected login (wrong password/TOTP) starts a 60 s cooldown; after 3 consecutive rejections KiteWeb refuses to try again until the process is restarted, because Zerodha blocks the account after 5 wrong attempts.
- A TOTP code is never reused within its 30 s window.
- Writes (orders, GTT, MF) are never replayed automatically after a network or token error.
- The server binds to
127.0.0.1by default, refuses every write route unlessKITEWEB_ALLOW_TRADING=1, and can require a token (KITEWEB_TOKEN) on every request and websocket. Its error responses never echo internal exception text (that goes to the server log). open_browser()starts Chrome with a DevTools port on localhost that refuses connections carrying a browserOriginheader, so a web page in another browser on the same machine cannot attach to the logged-in session. The browser process is started without theZERODHA_*/KITEWEB_*environment variables, and the launcher passes credentials to the client directly rather than via the environment.- Throwaway browser profiles (
kiteweb-browser-*in your temp directory) are removed at the nextopen_browser()once their Chrome has exited, and immediately if the open fails.
Do not
- Do not commit
.env,config.ini,~/.kiteweb/or any file containing credentials. The.gitignoreexcludes the usual suspects; still checkgit statusbefore pushing. - Do not expose the server on a public interface without
KITEWEB_TOKENand TLS in front of it. - Do not paste an
enctoken, aws_url()(it embeds the token) or your TOTP secret anywhere public — redact tokens and your user id from any log you share.
Every failure is a kiteweb.KiteWebError with the same fields as Kite Connect's exceptions:
from kiteweb import KiteWebError
try:
kw.ltp("NSE:NOPE")
except KiteWebError as e:
print(e.status_code, e.error_type, e.message) # 400 InputException unknown instrument 'NSE:NOPE'error_type |
When |
|---|---|
TokenException |
Login failed (wrong password/TOTP, missing env vars, login cooldown/refusal) or the session expired and could not be recovered for this call. |
InputException |
Bad arguments: unknown instrument, bad GTT payload, invalid path segment (server). |
PermissionException |
Server write route hit without KITEWEB_ALLOW_TRADING=1, or Zerodha refused. |
NetworkException |
Connection/timeout errors, upstream 5xx, no ticks within snapshot_timeout, instrument download failed. |
GeneralException |
Anything else, including Zerodha's own messages and the 501 for trigger_range. |
| Variable | Default | Purpose |
|---|---|---|
ZERODHA_USER_ID, ZERODHA_PASSWORD, ZERODHA_TOTP_SECRET |
— | Login credentials (or pass to KiteWebConnect(...)). |
KITEWEB_CACHE_DIR |
~/.kiteweb |
Where the session token and the daily instrument CSVs are cached. |
KITEWEB_ALLOW_TRADING |
unset | Set to 1 to enable the server's write routes. |
KITEWEB_TOKEN |
unset | If set, every server request / websocket must present it. |
KITEWEB_BROWSER |
auto-detect | Path to the Chrome/Edge/Chromium executable for open_browser(). |
Constructor knobs: KiteWebConnect(snapshot_timeout=5.0) — how long quote/ltp/ohlc wait for the first
tick of every instrument; KiteWebTicker(reconnect, reconnect_max_tries, reconnect_max_delay, connect_timeout, stale_timeout).
- Login = Kite web login (
/api/login+/api/twofawith a TOTP) →enctokencookie. Cached on disk, re-used across runs, refreshed only when Zerodha rejects it. - Ticks come from
wss://ws.zerodha.com— the same binary protocol as Kite Connect's ticker. quote/ltp/ohlcare a one-shot WebSocket snapshot (subscribe → first tick per instrument → disconnect), because Kite web's REST quote route rejects web sessions. Consequences:- each call takes a fraction of a second and opens a socket (at most 2 concurrently);
lower_circuit_limit/upper_circuit_limitare not in the tick feed → always0;- an instrument that never ticks within
snapshot_timeoutis omitted from the result (and logged). Works after market hours too — Kite sends the last snapshot.
- Timestamps:
quote()and the server report IST likeapi.kite.trade, regardless of the machine's timezone.KiteWebTickerticks carry host-local datetimes likeKiteTicker. trigger_range()is not available on Kite web (KiteWebError(501)).instruments()returns exactly the typeskiteconnectdoes (instrument_token/lot_sizeint,exchange_tokenstr, prices float,expiryadateor"").- Kite web may keep only one active web session per user: logging in from the browser can invalidate the token KiteWeb cached (it just logs in again on the next read), and vice versa.
- Heavy dependency note:
kiteconnect(and therefore Twisted) is installed only to reuse its tick parser.
| Symptom | Fix |
|---|---|
TokenException: ZERODHA_USER_ID, ZERODHA_PASSWORD and ZERODHA_TOTP_SECRET must be set |
The env vars are not visible to the Python process — set them in the same terminal, or pass them to KiteWebConnect(...). |
TokenException: ZERODHA_TOTP_SECRET is not a valid base32 TOTP key |
The value is not the secret key (base32: letters A-Z and digits 2-7). You probably pasted a 6-digit code, or the key with spaces/punctuation. Nothing was sent to Zerodha. |
InputException: unknown exchange 'XYZ' |
The part before the colon must be one of NSE, BSE, NFO, BFO, CDS, BCD, MCX. |
TokenException: Invalid TOTP / Invalid password |
Wrong secret (it must be the base32 key, not a 6-digit code), or your clock is off by more than 30 s (TOTP is time-based — sync the system clock). |
TokenException: login on cooldown for Ns… |
A login was just rejected; KiteWeb waits 60 s before trying again so your account is not blocked. Fix the credentials first. |
TokenException: login refused: 3 consecutive rejected logins… |
Three rejections in a row. Fix ZERODHA_* and restart the process. If you also failed in the browser, use "Forgot password" on Kite before the 5th attempt locks the account. |
InputException: unknown instrument 'NSE:XYZ' |
Use the exact tradingsymbol from kw.instruments("NSE") — indices need the space: NSE:NIFTY 50, NSE:NIFTY BANK. Derivatives: NFO:NIFTY26SEP24500CE. |
NetworkException: no ticks received within 5.0s |
The socket connected but Kite sent nothing for those tokens: check the symbol/exchange, try again, or raise snapshot_timeout. |
PermissionException: trading disabled (server) |
Start the server with KITEWEB_ALLOW_TRADING=1. |
pip install fails on Twisted (Windows) |
Upgrade pip (python -m pip install -U pip) so a prebuilt wheel is used. |
| Ticks stop and never resume | Fixed in 0.2.0 (dead-connection detection). If you set stale_timeout=None, that disabled it. |
| Session works in one script but a second script logs in again | Both use ~/.kiteweb/enctoken.json; make sure both see the same KITEWEB_CACHE_DIR and the same ZERODHA_USER_ID. |
Enable logging to see what KiteWeb is doing: logging.basicConfig(level=logging.INFO) — warnings go to the
kiteweb logger. Nothing KiteWeb logs contains your credentials or token.
python -m pytest -q # offline unit tests, no account needed
KITEWEB_LIVE=1 python -m pytest tests/test_live.py -v -s # real login + quotes (read-only)
KITEWEB_LIVE_TRADING=1 python -m pytest tests/test_live_trading.py -v -s # places/cancels a real 1-share AMO + GTT. Opt-in, DANGEROUS.The offline suite (215 tests) fakes the HTTP session and the websocket, and checks every method, payload
and constant against the installed kiteconnect package for drop-in parity. The live tests need real
credentials and are opt-in only.
- Files holding secrets are owner-only on Windows too.
enctoken.json,browser_cookies.jsonand the launcher'scredentials.envnow get an ACL with a single entry for your user (plainchmod 0600is a no-op on Windows and the previous release relied on it). - DevTools port hardened.
open_browser()no longer starts Chrome with--remote-allow-origins=*, so web pages cannot attach to the debugging port; the browser process is started without theZERODHA_*/KITEWEB_*environment variables, and the launcher passes credentials to the client directly instead of throughos.environ. - Logout is complete.
invalidate_access_token()/Session.logout()also deletes the saved browser cookies. - Saved browser session is validated with Zerodha (profile call) before being reused, instead of inspecting the page; a stale token can no longer be adopted as valid.
- Ticker handshake 401/403 now adopts a fresher token from the shared cache before logging in, like
REST calls do (a direct re-login used to log the browser out).
on_closereports1006and the error text when the stream ended by error. - A malformed
ZERODHA_TOTP_SECRETis reported as aTokenExceptionbefore any request, not as a rawbinasciitraceback after the password has already been sent. - An unknown exchange (
XYZ:FOO) is anInputException, not a download error; the server validates the historicalintervalpath segment like the ids; server 500s no longer echo exception text. - Order writes whose response lacks an
order_idraiseKiteWebErrorinstead ofTypeError. - Leftover throwaway browser profiles are cleaned up; the in-memory instrument cache drops previous days.
Open-Kite.batuses thepylauncher whenpythonis not onPATH.
KiteWebConnect.open_browser()opens your own Chrome/Edge on the logged-in Kite website and shares that session with the API client, so the two no longer log each other out. Drives the browser over Chrome's DevTools Protocol (no Selenium/chromedriver); runs Kite's real login inside the page so the server sets the session correctly, then saves the cookies so later opens skip the login entirely (no TOTP) and reuse the session instead of rotating it.KITEWEB_BROWSERoverrides browser auto-detection.Open-Kite.bat/examples/open_kite.py: a double-click launcher that opens Kite logged in, reading credentials from the environment, a saved file, or a one-time prompt.- Faster login, and cross-process token sharing: a stale token is re-adopted from the shared cache (e.g. the token the browser just refreshed) before falling back to a fresh login.
- Login throttling. A rejected login (wrong password/TOTP) starts a 60 s cooldown; after 3 consecutive
rejections
login()refuses further attempts until the process is restarted. Zerodha blocks the account after 5 wrong password attempts — a script retrying in a loop with a badZERODHA_PASSWORDcould previously trigger that. - TOTP codes are never reused. A second login inside the same 30 s window (e.g. a websocket handshake rejected right after a REST re-login) now waits for the next code instead of sending one Zerodha rejects as already used.
.gitignoreexcludes credential files (.env,config.ini,*.cred,.kiteweb/); package metadata added.- Licence changed from MIT to a source-available personal-use licence (see LICENSE).
- README rewritten for a public audience: terms/regulation notice, security section, Kite Connect comparison.
Fixed:
- Writes never recovered from an expired session: after a
TokenExceptiononplace_order& co. the dead enctoken was kept, so every later write failed. The token is now discarded (never replayed) and the next call logs in again;set_session_expiry_hook()fires in that case. - Dead websocket was never detected: a silently dropped connection left
KiteWebTickerand the server/wsrelay looping forever with no ticks and no reconnect. Both now reconnect afterstale_timeout(15 s) of silence (Kite heartbeats every ~3 s). instruments()/mf_instruments()leaked rawrequestsexceptions instead ofKiteWebError(504).quote()timestamps are IST on every host (were host-local).instruments():exchange_tokenstaysstrlikekiteconnect(wasint); oddexpirystrings no longer crash.- Websocket
{"type":"error"}frames now reachon_error, likeKiteTicker. - Old daily instrument CSVs (~10 MB each) are pruned from the cache dir instead of accumulating.
KiteWebTicker.connect(proxy=..., disable_ssl_verification=...)warns instead of silently ignoring.
Added: stop_retry(), the KiteTicker constants, the KiteConnect auth-compat methods,
python -m kiteweb.server / kiteweb-server entry point, and a function-by-function parity check
against pykiteconnect 5.2.1.
First release of the kiteweb package, replacing the earlier Selenium-based PyKite scripts.
KiteWeb is not open source. The source is published so you can read it and use it yourself, under a source-available licence: personal, non-commercial use on your own Zerodha account only; no redistribution, resale, hosting for others or commercial use; no warranty and no liability. See LICENSE for the full terms.
Not affiliated with or endorsed by Zerodha Broking Ltd. "Zerodha", "Kite" and "Kite Connect" are their trademarks, used here only to describe compatibility.