How to use postback URLs on Kite Connect
The Kite Connect API includes a postback (webhook) mechanism that pushes order update notifications to your server the moment an order changes state. Instead of polling GET /orders repeatedly, your server receives an HTTP POST at the registered postback URL each time an order moves from OPEN to COMPLETE, REJECTED, CANCELLED, or any intermediate state. This guide covers registration, checksum verification, a Python server implementation, and production hardening.
What is a postback URL?
A postback URL is an HTTP POST endpoint you register in the Kite Connect developer console. Whenever an order placed through your app changes state at Zerodha’s OMS (Order Management System), Zerodha sends an HTTP POST request to your postback URL with the updated order details in the request body.
This is similar to a webhook in modern web services. The key difference from the Kite Connect WebSocket stream (which delivers market data) is that postbacks carry order-level events, not market price events.
Typical use cases include:
- Triggering the next leg of a multi-legged strategy when the first leg is confirmed
COMPLETE. - Updating a trade log database in real time without polling.
- Sending a Telegram or email alert when an order is rejected.
- Reconciling internal position state with the actual OMS state.
Step-by-step procedure
Register the postback URL
Log in to the Kite Connect developer console at kite.trade/developers/apps. Open your app and click Edit. In the Postback URL field, enter the full HTTPS URL of your endpoint, for example https://yourdomain.com/kite/postback. Click Save.
Zerodha begins sending postbacks to this URL immediately for any new orders placed through your app. There is no separate activation step.
During local development, you can use a tunnelling service such as ngrok to expose a local server to the internet:
ngrok http 5000
# ngrok prints: Forwarding https://abc123.ngrok.io -> localhost:5000
# Use https://abc123.ngrok.io/kite/postback as the temporary postback URL
Understand the postback payload
Zerodha sends a JSON body with the following fields:
| Field | Type | Description |
|---|---|---|
user_id | str | Zerodha client ID of the user who placed the order |
unfilled_quantity | int | Quantity not yet filled |
app_id | int | Your Kite Connect app ID |
checksum | str | SHA-256 integrity hash |
placed_by | str | Client ID who placed the order |
order_id | str | Unique order identifier |
exchange_order_id | str | Exchange-assigned order ID |
parent_order_id | str or null | Parent order ID for CO/iceberg |
status | str | OPEN, COMPLETE, REJECTED, CANCELLED, UPDATE |
status_message | str | Human-readable status message or rejection reason |
tradingsymbol | str | Instrument symbol |
exchange | str | NSE, BSE, NFO, etc. |
transaction_type | str | BUY or SELL |
quantity | int | Total order quantity |
filled_quantity | int | Quantity filled so far |
average_price | float | Average fill price |
order_timestamp | str | Timestamp of the order event |
Verify the checksum
Every postback includes a checksum field computed by Zerodha as:
checksum = SHA-256(order_id + user_id + api_secret)
You must verify this before processing the payload. An invalid checksum means the request did not originate from Zerodha, or the payload was tampered with.
import hashlib
def verify_checksum(order_id: str, user_id: str, received_checksum: str, api_secret: str) -> bool:
expected = hashlib.sha256(
(order_id + user_id + api_secret).encode("utf-8")
).hexdigest()
return expected == received_checksum
Implement the postback server
Here is a minimal Flask server:
"""postback_server.py, Kite Connect postback receiver."""
import os
import hashlib
import json
import logging
from flask import Flask, request, jsonify
from dotenv import load_dotenv
load_dotenv()
API_SECRET = os.environ["KITE_API_SECRET"]
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def verify_checksum(order_id: str, user_id: str, received: str) -> bool:
expected = hashlib.sha256(
(order_id + user_id + API_SECRET).encode("utf-8")
).hexdigest()
return expected == received
@app.route("/kite/postback", methods=["POST"])
def kite_postback():
try:
payload = request.get_json(force=True, silent=True)
if payload is None:
logger.warning("Received non-JSON postback body")
return jsonify({"status": "error", "message": "Invalid JSON"}), 400
order_id = payload.get("order_id", "")
user_id = payload.get("user_id", "")
checksum = payload.get("checksum", "")
if not verify_checksum(order_id, user_id, checksum):
logger.warning("Checksum mismatch for order_id=%s", order_id)
return jsonify({"status": "error", "message": "Checksum mismatch"}), 403
status = payload.get("status")
symbol = payload.get("tradingsymbol")
filled_qty = payload.get("filled_quantity", 0)
avg_price = payload.get("average_price", 0)
logger.info(
"Order %s | %s | %s | filled=%s avg=%.2f",
order_id, symbol, status, filled_qty, avg_price,
)
# Dispatch to your strategy logic here
handle_order_event(payload)
# Always respond 200 quickly; do not block on slow processing
return jsonify({"status": "ok"}), 200
except Exception as e:
logger.exception("Postback handler error: %s", e)
return jsonify({"status": "error"}), 500
def handle_order_event(payload: dict) -> None:
"""Route order events to strategy logic."""
status = payload.get("status")
order_id = payload.get("order_id")
if status == "COMPLETE":
logger.info("Order %s COMPLETE, triggering next leg if applicable", order_id)
# Your logic here: place hedge, update position tracker, send alert, etc.
elif status == "REJECTED":
reason = payload.get("status_message", "Unknown")
logger.error("Order %s REJECTED: %s", order_id, reason)
# Your logic here: retry, alert, log to DB
elif status == "CANCELLED":
logger.info("Order %s was CANCELLED", order_id)
elif status in ("OPEN", "UPDATE"):
logger.info("Order %s is %s (filled: %s)", order_id, status, payload.get("filled_quantity"))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Install Flask: pip install flask.
Run: python postback_server.py.
Use a background task for slow processing
Postback handler latency directly affects Zerodha’s retry behaviour. If your handler takes longer than a few seconds (for example, due to a database write or an outbound API call), process the payload in a background thread or queue:
import threading
@app.route("/kite/postback", methods=["POST"])
def kite_postback():
payload = request.get_json(force=True, silent=True)
# ... checksum verification ...
# Offload slow work; respond immediately
thread = threading.Thread(target=handle_order_event, args=(payload,), daemon=True)
thread.start()
return jsonify({"status": "ok"}), 200
Postback delivery behaviour
Zerodha delivers postbacks on a best-effort basis. There are several points to be aware of.
No guaranteed redelivery. If your server returns a non-200 response or is unreachable, Zerodha may retry a limited number of times, but delivery is not guaranteed. Your system should not depend solely on postbacks for order reconciliation. Always cross-check with kite.orders() at the start of each session.
Multiple postbacks per order. An order may trigger several postbacks as it progresses through states: OPEN when accepted, one or more UPDATE events as partial fills arrive, and COMPLETE or REJECTED as the final state.
Order of delivery. Postbacks may occasionally arrive out of sequence under high load. Always check the order_timestamp field and reconcile against your own internal state if order is important.
Testing postbacks locally
Use a request inspection tool to examine incoming payloads during development:
# Forward ngrok tunnel to local Flask server
ngrok http 5000
# Send a test postback manually
curl -X POST https://abc123.ngrok.io/kite/postback \
-H "Content-Type: application/json" \
-d '{"order_id":"1234567890","user_id":"AB1234","status":"COMPLETE","checksum":"<compute locally>","tradingsymbol":"RELIANCE","filled_quantity":1,"average_price":2850.0}'
To compute the correct test checksum:
import hashlib
checksum = hashlib.sha256(("1234567890" + "AB1234" + "your_api_secret").encode()).hexdigest()
print(checksum)
What can go wrong
- Postbacks not arriving. The postback URL may not be saved correctly in the console, or your server may not be accessible from the public internet. Verify the URL in app settings and test the endpoint with a manual
curl. - All postbacks return
403 Checksum mismatch. Theapi_secretin your server’s environment does not match the one registered in the developer console. If you regenerated theapi_secret, update the server’s.envfile. JSONDecodeErroron the payload. Rarely, the postback arrives with an empty body or a non-JSON content type. Thesilent=Trueparameter inrequest.get_json()handles this gracefully; always guard againstNone.- Duplicate
COMPLETEpostbacks. Partial-fill orders trigger multipleUPDATEand a finalCOMPLETE. Use thefilled_quantityfield rather than reacting to the status alone to avoid double-counting fills. - Server down during trading hours. If your postback server restarts mid-session, you will miss postbacks for that period. On restart, call
kite.orders()to reconcile any missed state changes.
Related guides
- How to place an order via the Kite Connect REST API
- How to write a basic Python script using kiteconnect
- How to generate a Kite Connect API key
- How to generate the request_token and access_token
- Kite Connect API overview
- Kite Connect ecosystem
- Zerodha overview
References
- Zerodha, Kite Connect postback documentation, kite.trade/docs/connect/v3/postbacks/, accessed 2024.
- kiteconnect Python SDK, github.com/zerodha/pykiteconnect, accessed 2024.
- Flask, Flask web framework, flask.palletsprojects.com, accessed 2024.
- SEBI, Guidelines on algorithmic trading, sebi.gov.in.
- Zerodha Support, Kite Connect webhooks and postbacks, support.zerodha.com.