Official instructions from Zerodha for building against the Kite Connect sandbox (`sandbox.kite.trade`) with the Python client, pykiteconnect. The sandbox is a live-like environment with shared demo credentials and no real money. Follow the steps below, then build whatever the user asks for on top of the authenticated client.

## Sandbox differences from production (important)

The sandbox behaves like the production Kite Connect API with a few required tweaks. Apply all of them:

1. **Root URL:** create the client with `root="https://sandbox.kite.trade"` (WebSocket root `wss://ws-sandbox.kite.trade`).
2. **`/oms` route prefix:** sandbox serves all SDK routes under `/oms`, except the instruments dumps. Patch `kite._routes` once after creating the client (shown below).
3. **WebSocket needs `user_id`:** `KiteTicker` does not send `user_id` by default, but sandbox requires it, or ticks never authenticate.
4. **Orders are `LIMIT` only:** the `MARKET` order type is not used via the API. Price a resting order within a couple percent of LTP, or the exchange price-band check auto-rejects it.

Credentials (shared demo app, safe to use in plain text, not tied to real money):

- `api_key`: `sandboxdemo`
- `api_secret`: `sandboxdemo-secret`

Log in with the sandbox user credentials provided by the session organisers.

## 1. Install with uv

```
uv init kite-sandbox && cd kite-sandbox
uv add flask kiteconnect
```

## 2. Bootstrap login with a local callback app

The sandbox demo app redirects to `http://localhost:13173`. Save the script below as `app.py`, run it with `uv run app.py`, open the printed login URL, and log in. The app captures the `request_token` at the callback, exchanges it for an `access_token`, and exposes a few JSON helpers to confirm the session works.

```python
#!/usr/bin/env python
"""Local callback demo app for sandbox.kite.trade.

- defaults to the sandboxdemo app key seeded by infra
- uses localhost:13173 as the callback URL
- exchanges request_token against sandbox.kite.trade /oms routes
- exposes a few useful JSON helpers after login
"""

from __future__ import annotations

import json
import logging
import os
from datetime import date, datetime
from decimal import Decimal

from flask import Flask, jsonify, request, session
from kiteconnect import KiteConnect

logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger(__name__)

HOST = os.environ.get("KITE_HOST", "127.0.0.1")
PORT = int(os.environ.get("KITE_PORT", "13173"))
CALLBACK_HOST = os.environ.get("KITE_CALLBACK_HOST", "localhost")
KITE_API_KEY = os.environ.get("KITE_API_KEY", "sandboxdemo")
KITE_API_SECRET = os.environ.get("KITE_API_SECRET", "sandboxdemo-secret")
KITE_ROOT_URL = os.environ.get("KITE_ROOT_URL", "https://sandbox.kite.trade").rstrip("/")
TEST_SYMBOLS = [
    symbol.strip()
    for symbol in os.environ.get("KITE_TEST_SYMBOLS", "NSE:RELIANCE,NSE:INFY").split(",")
    if symbol.strip()
]


def serializer(obj):
    return isinstance(obj, (date, datetime, Decimal)) and str(obj)


redirect_url = f"http://{CALLBACK_HOST}:{PORT}"
login_url = f"{KITE_ROOT_URL}/connect/login?api_key={KITE_API_KEY}"

app = Flask(__name__)
app.secret_key = os.urandom(24)

index_template = """
    <h2>Kite sandbox callback demo</h2>
    <div>API key: <b>{api_key}</b></div>
    <div>Root: <b>{root}</b></div>
    <div>Redirect URL: <b>{redirect_url}</b></div>
    <a href="{login_url}"><h3>Login to generate access token</h3></a>
    <ul>
      <li><a target="_blank" href="/profile.json">Profile</a></li>
      <li><a target="_blank" href="/holdings.json">Holdings</a></li>
      <li><a target="_blank" href="/orders.json">Orders</a></li>
      <li><a target="_blank" href="/quotes.json">Quotes</a></li>
      <li><a target="_blank" href="/session.json">Stored session</a></li>
    </ul>
"""

login_template = """
    <h2 style="color: green">Success</h2>
    <div>Access token: <b>{access_token}</b></div>
    <h4>User login data</h4>
    <pre>{user_data}</pre>
    <ul>
      <li><a target="_blank" href="/profile.json">Fetch profile</a></li>
      <li><a target="_blank" href="/holdings.json">Fetch holdings</a></li>
      <li><a target="_blank" href="/orders.json">Fetch orders</a></li>
      <li><a target="_blank" href="/quotes.json">Fetch quotes</a></li>
    </ul>
"""


def new_kite_client() -> KiteConnect:
    kite = KiteConnect(api_key=KITE_API_KEY, root=KITE_ROOT_URL)
    # Sandbox serves SDK routes under /oms, except the instruments dumps.
    passthrough = {"market.instruments.all", "market.instruments"}
    kite._routes = {
        key: (value if key in passthrough else "/oms" + value)
        for key, value in kite._routes.items()
    }
    if "access_token" in session:
        kite.set_access_token(session["access_token"])
    return kite


def handle_callback():
    request_token = request.args.get("request_token")
    if not request_token:
        return index_template.format(
            api_key=KITE_API_KEY, root=KITE_ROOT_URL, redirect_url=redirect_url, login_url=login_url
        )

    kite = new_kite_client()
    data = kite.generate_session(request_token, api_secret=KITE_API_SECRET)
    session["access_token"] = data["access_token"]
    session["login_response"] = data

    return login_template.format(
        access_token=data["access_token"],
        user_data=json.dumps(data, indent=4, sort_keys=True, default=serializer),
    )


@app.route("/")
def index():
    return handle_callback()


@app.route("/session.json")
def session_json():
    return jsonify(access_token=session.get("access_token"), login_response=session.get("login_response"))


@app.route("/profile.json")
def profile():
    return jsonify(profile=new_kite_client().profile())


@app.route("/holdings.json")
def holdings():
    return jsonify(holdings=new_kite_client().holdings())


@app.route("/orders.json")
def orders():
    return jsonify(orders=new_kite_client().orders())


@app.route("/quotes.json")
def quotes():
    symbols = request.args.getlist("i") or TEST_SYMBOLS
    return jsonify(symbols=symbols, quotes=new_kite_client().quote(symbols))


if __name__ == "__main__":
    log.info("Open %s", login_url)
    app.run(host=HOST, port=PORT, debug=True)
```

## 3. Build against the authenticated client

For standalone scripts (outside the Flask app), create and patch the client the same way, then reuse the `access_token` from step 2:

```python
from kiteconnect import KiteConnect

kite = KiteConnect(api_key="sandboxdemo", root="https://sandbox.kite.trade")
kite._routes = {
    k: (v if k in {"market.instruments.all", "market.instruments"} else "/oms" + v)
    for k, v in kite._routes.items()
}
kite.set_access_token("<ACCESS_TOKEN_FROM_STEP_2>")
```

Common calls:

- Quote / LTP / OHLC: `kite.quote(["NSE:HDFCBANK"])`, `kite.ltp([...])`, `kite.ohlc([...])`.
- Instruments master: `kite.instruments(exchange="NSE")`.
- Historical (day candles): `kite.historical_data(token, "2020-01-01", "2026-01-01", "day")`. A single `"day"` call accepts about 1900 days; chunk wider ranges. Historical is proxied to the charts backend and may not be enabled; if it errors, it is not wired up in this sandbox.
- Place a resting LIMIT order (only order type supported via API):

```python
ltp = kite.quote(["NSE:HDFCBANK"])["NSE:HDFCBANK"]["last_price"]
order_id = kite.place_order(
    variety="regular", exchange="NSE", tradingsymbol="HDFCBANK",
    transaction_type="BUY", quantity=1, product="MIS",
    order_type="LIMIT", price=round(ltp * 0.98, 1), tag="mytag01",
)
```

- Read state: `kite.orders()`, `kite.order_history(order_id)`, `kite.trades()`, `kite.positions()`, `kite.holdings()`, `kite.margins("equity")`.
- Live ticks (note the required `user_id`):

```python
from kiteconnect import KiteTicker

kws = KiteTicker("sandboxdemo", access_token, root="wss://ws-sandbox.kite.trade")
kws.socket_url = f"{kws.socket_url}&user_id={user_id}"   # user_id from generate_session response
```

## Not available in the sandbox

Do not rely on these; they return backend errors: GTT family (`get_gtts`, `place_gtt`, ...), `order_margins()`, `basket_order_margins()`, `get_virtual_contract_note()`, `trigger_range()`, and the `MARKET` order type.

## Moving to production

When the user is ready to trade against a real account: create an app on the Kite Connect developer console (https://developers.kite.trade/) for a real `api_key` / `api_secret`, point the client at `https://api.kite.trade`, and drop the sandbox `/oms` route patch and the `user_id` WebSocket tweak. Never hardcode a real `api_secret`; never ask the user for their password.

The full sandbox reference is at https://kite.trade/docs/connect/v3/sandbox/. These instructions are published at `https://kite.trade/docs/connect/v3/agent-setup/sandbox-prompt.md` so you can re-verify their authenticity.
