Skip to content
vikassharma545Public

About

Personal-use Python library with the Kite Connect interface, backed by your own Zerodha Kite web login. Unofficial, not affiliated with Zerodha.

Resources

Stars

26 stars

Watchers

3 watching

Forks

Latest commit

 

History

49 Commits

Folders and files

Repository files navigation

KiteWeb

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.


Contents

  1. KiteWeb vs Kite Connect
  2. Is this allowed?
  3. Requirements
  4. Install
  5. Setup: credentials and TOTP
  6. Quick start (5 minutes)
  7. Library reference
  8. Streaming ticks: KiteWebTicker
  9. Open the Kite website, logged in (open_browser)
  10. Drop-in server for existing Kite Connect code
  11. Trading safety
  12. Security
  13. Errors
  14. Configuration (environment variables)
  15. How it works and known limits
  16. Troubleshooting
  17. Running the tests
  18. Changelog
  19. License

KiteWeb vs Kite Connect

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)

Is this allowed?

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.

Requirements

  • 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.com and api.kite.trade.

Install

git clone https://github.com/vikassharma545/KiteWeb.git
cd KiteWeb
python -m pip install -e ".[server]"      # library + local server

Just 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__)"

Setup: credentials and TOTP

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)

Getting the TOTP secret

  1. Log in to kite.zerodha.com → click your user id (top right) → My profile → Password & Security.
  2. Enable External 2FA TOTP (authenticator-app based 2FA).
  3. 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).
  4. 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.

Setting the variables

# 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=JBSWY3DPEHPK3PXP

You 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.

What happens on first use

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.

Quick start (5 minutes)

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").

Library reference

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.

Function-by-function comparison with kiteconnect 5.2.1

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).

Market data

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.

Account (read-only)

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.

Orders, GTT, mutual funds, calculators (writes)

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.

Kite Connect auth-flow methods (compatibility)

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/.

Streaming ticks: KiteWebTicker

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_mode before connect(); 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 most reconnect_max_tries=50 consecutive 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_timeout seconds (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=None disables it.
  • Tick timestamps (exchange_timestamp, last_trade_time) are host-local naive datetimes, exactly like KiteTicker. (quote() converts them to IST like the REST API does.)
  • on_close(ws, code, reason) gets (1000, "closed") after your own close(); 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.

Open the Kite website, logged in (open_browser)

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=True opens the session without showing a window (useful for automation/screenshots).
  • path is where to land, e.g. "/orders", "/positions", "/holdings".
  • chrome_path / the KITEWEB_BROWSER env 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_dir uses 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, and open_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.

Opening the browser while your code is trading

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.)

Drop-in server for existing Kite Connect code

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 warning

Point 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 calculators POST /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 return 403 PermissionException until you start the server with KITEWEB_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 as ws.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.

Trading safety

Orders placed through KiteWeb are real orders on your account. Please read this once.

  • A failed write means "unknown", not "failed". A NetworkException on place_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: call orders() (or get_gtts() / mf_orders()) and look for your order. Pass tag="..." on place_order so you can recognise it.
  • A TokenException on 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: a GET is replayed once after re-login.)
  • session.post()/put() refuse retry_on_token_error=True on /oms/orders, /oms/gtt, /oms/mf/orders and /oms/mf/sips — a blind replay there could duplicate an order.
  • The server refuses every write route unless KITEWEB_ALLOW_TRADING=1 is 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.

Security

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.1 by default, refuses every write route unless KITEWEB_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 browser Origin header, so a web page in another browser on the same machine cannot attach to the logged-in session. The browser process is started without the ZERODHA_* / 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 next open_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 .gitignore excludes the usual suspects; still check git status before pushing.
  • Do not expose the server on a public interface without KITEWEB_TOKEN and TLS in front of it.
  • Do not paste an enctoken, a ws_url() (it embeds the token) or your TOTP secret anywhere public — redact tokens and your user id from any log you share.

Errors

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.

Configuration (environment variables)

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).

How it works and known limits

  • Login = Kite web login (/api/login + /api/twofa with a TOTP) → enctoken cookie. 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/ohlc are 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_limit are not in the tick feed → always 0;
    • an instrument that never ticks within snapshot_timeout is omitted from the result (and logged). Works after market hours too — Kite sends the last snapshot.
  • Timestamps: quote() and the server report IST like api.kite.trade, regardless of the machine's timezone. KiteWebTicker ticks carry host-local datetimes like KiteTicker.
  • trigger_range() is not available on Kite web (KiteWebError(501)).
  • instruments() returns exactly the types kiteconnect does (instrument_token/lot_size int, exchange_token str, prices float, expiry a date or "").
  • 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.

Troubleshooting

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.

Running the tests

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.

Changelog

0.3.1 (2026-09-06) — security audit fixes

  • Files holding secrets are owner-only on Windows too. enctoken.json, browser_cookies.json and the launcher's credentials.env now get an ACL with a single entry for your user (plain chmod 0600 is 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 the ZERODHA_* / KITEWEB_* environment variables, and the launcher passes credentials to the client directly instead of through os.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_close reports 1006 and the error text when the stream ended by error.
  • A malformed ZERODHA_TOTP_SECRET is reported as a TokenException before any request, not as a raw binascii traceback after the password has already been sent.
  • An unknown exchange (XYZ:FOO) is an InputException, not a download error; the server validates the historical interval path segment like the ids; server 500s no longer echo exception text.
  • Order writes whose response lacks an order_id raise KiteWebError instead of TypeError.
  • Leftover throwaway browser profiles are cleaned up; the in-memory instrument cache drops previous days.
  • Open-Kite.bat uses the py launcher when python is not on PATH.

0.3.0 (2026-08-28) — logged-in browser

  • 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_BROWSER overrides 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.

0.2.1 (2026-08-28) — account-safety hardening

  • 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 bad ZERODHA_PASSWORD could 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.
  • .gitignore excludes 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.

0.2.0 (2026-08-28) — audit release

Fixed:

  • Writes never recovered from an expired session: after a TokenException on place_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 KiteWebTicker and the server /ws relay looping forever with no ticks and no reconnect. Both now reconnect after stale_timeout (15 s) of silence (Kite heartbeats every ~3 s).
  • instruments() / mf_instruments() leaked raw requests exceptions instead of KiteWebError(504).
  • quote() timestamps are IST on every host (were host-local).
  • instruments(): exchange_token stays str like kiteconnect (was int); odd expiry strings no longer crash.
  • Websocket {"type":"error"} frames now reach on_error, like KiteTicker.
  • 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.

0.1.0 (2026-08-28)

First release of the kiteweb package, replacing the earlier Selenium-based PyKite scripts.

License

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.

About

Personal-use Python library with the Kite Connect interface, backed by your own Zerodha Kite web login. Unofficial, not affiliated with Zerodha.

Resources

Stars

26 stars

Watchers

3 watching

Forks

Releases

Packages

Contributors

Languages