How to use postback URLs on Kite Connect

From WebNotes, a public knowledge base. Last updated . Reading time ~9 min. Level: Intermediate.

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:

FieldTypeDescription
user_idstrZerodha client ID of the user who placed the order
unfilled_quantityintQuantity not yet filled
app_idintYour Kite Connect app ID
checksumstrSHA-256 integrity hash
placed_bystrClient ID who placed the order
order_idstrUnique order identifier
exchange_order_idstrExchange-assigned order ID
parent_order_idstr or nullParent order ID for CO/iceberg
statusstrOPEN, COMPLETE, REJECTED, CANCELLED, UPDATE
status_messagestrHuman-readable status message or rejection reason
tradingsymbolstrInstrument symbol
exchangestrNSE, BSE, NFO, etc.
transaction_typestrBUY or SELL
quantityintTotal order quantity
filled_quantityintQuantity filled so far
average_pricefloatAverage fill price
order_timestampstrTimestamp 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. The api_secret in your server’s environment does not match the one registered in the developer console. If you regenerated the api_secret, update the server’s .env file.
  • JSONDecodeError on the payload. Rarely, the postback arrives with an empty body or a non-JSON content type. The silent=True parameter in request.get_json() handles this gracefully; always guard against None.
  • Duplicate COMPLETE postbacks. Partial-fill orders trigger multiple UPDATE and a final COMPLETE. Use the filled_quantity field 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.

References

  1. Zerodha, Kite Connect postback documentation, kite.trade/docs/connect/v3/postbacks/, accessed 2024.
  2. kiteconnect Python SDK, github.com/zerodha/pykiteconnect, accessed 2024.
  3. Flask, Flask web framework, flask.palletsprojects.com, accessed 2024.
  4. SEBI, Guidelines on algorithmic trading, sebi.gov.in.
  5. Zerodha Support, Kite Connect webhooks and postbacks, support.zerodha.com.

Reviewed and published by

The WebNotes Editorial Team covers Indian capital markets, payments infrastructure and retail investor procedures. Every article is fact-checked against primary sources, principally SEBI circulars and master directions, NPCI specifications and the official support documentation published by the intermediary in question. Drafts go through a second-pair-of-eyes review and a separate compliance read before publication, and revisions are tracked against the SEBI and NPCI rule changes referenced in the methodology section.

Last reviewed
Conflicts of interest
WebNotes is independent. No relationship with any broker, registrar or bank named in this article.