Update for API Traders: New Changes in DhanHQ API Authentication Process and Updates

@Hardik This issue has been an absolute nightmare. Your latest change has completely broken the Orders API endpoint.

I’ve repeatedly explained the problems with the place_order endpoint to your team. I spent over three hours debugging, providing undeniable proof, even adding debug statements in the dhanhq library and showing logs. Yet, the only responses I get are dismissive - like telling me to remove parameter names while calling the function, or the typical ā€œit’s working fine on our endā€ excuse. Meanwhile, the endpoint keeps throwing the same error:

{'status': 'failure', 'remarks': {'error_code': 'DH-905', 'error_type': 'Input_Exception', 'error_message': 'Missing required fields, bad values for parameters etc.'}, 'data': ''}

How do you justify pushing updates without even basic testing? It’s been two days and there’s still no fix. A year ago, Dhan was one of the best trading paltform- fast APIs, fewer bugs than other platfoms. Now it’s just having more and more issues day by day.

And the same acces token works for other endpoints suh as get funds, get order list etc.

CC @PravinJ

@Dev_Goswami2 You can use this sample code. It works as this

  • First checks if a saved token (dhan_token.json) exists and is valid for at least 7 more hours; if yes, it reuses it.
  • If not, it calls Dhan’s /generate-consent API to get a consentAppId.
  • Using this, it prints a login URL (from https://auth.dhan.co/login/consentApp-login?...) which you must open in your browser and log in with your Dhan account + 2FA.
  • After login, Dhan redirects you to your configured redirect URL (e.g., http://localhost:3000) with a tokenId in the query - you copy this tokenId and paste it into the script.
  • The script then exchanges this tokenId for a valid accessToken + expiry time, saves them in dhan_token.json, and returns them for your API calls.
import json
import logging
from datetime import datetime, timedelta, timezone
from pathlib import Path
import requests
from typing import Optional, Tuple

########################################################################
# ----------------------
# Config & Constants
# ----------------------
API_KEY = ""
API_SECRET = ""
DHAN_CLIENT_ID = ''

AUTH_BASE = "https://auth.dhan.co"
REDIRECT_URL = "http://localhost:3000"  # whatever you configured when generating the API key/secret
API_BASE = "https://api.dhan.co"

# Save token JSON alongside this script
TOKEN_FILE: Path = Path(__file__).resolve().parent / "dhan_token.json"
# How much validity time (buffer) we require to reuse the token
REUSE_BUFFER = timedelta(hours=7)


logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s"
)
#########################################################################


# ----------------------
# Helpers: Token IO
# ----------------------
def _parse_expiry(expiry_str: str) -> datetime:
    """
    Parse expiry returned by API. Dhan usually returns ISO like '2025-09-23T12:37:23'.
    Treat it as UTC if tzinfo is missing.
    """
    dt = datetime.fromisoformat(expiry_str)
    if dt.tzinfo is None:
        # Assume UTC if tz not provided
        dt = dt.replace(tzinfo=timezone.utc)
    return dt


def _load_existing_token() -> Optional[Tuple[str, datetime]]:
    """
    Returns (access_token, expiry_dt) if file exists and is valid JSON with one key.
    Otherwise returns None.
    """
    if not TOKEN_FILE.exists():
        logging.info("Token file not found at %s", TOKEN_FILE)
        return None

    try:
        with TOKEN_FILE.open("r", encoding="utf-8") as f:
            data = json.load(f)

        if not isinstance(data, dict) or len(data) != 1:
            logging.warning("Token file format invalid; expected single {expiry: token} entry.")
            return None

        expiry_str, access_token = next(iter(data.items()))
        if not isinstance(expiry_str, str) or not isinstance(access_token, str):
            logging.warning("Token file content types invalid.")
            return None

        expiry_dt = _parse_expiry(expiry_str)
        return access_token, expiry_dt

    except Exception as e:
        logging.error("Failed to read/parse token file: %s", e)
        return None



def _save_token(expiry_str: str, access_token: str) -> None:
    """
    Save token as { "expiry": "token" } overwriting any previous content.
    """
    try:
        with TOKEN_FILE.open("w", encoding="utf-8") as f:
            json.dump({expiry_str: access_token}, f, indent=2)
        logging.info("Token saved to %s (expires at %s).", TOKEN_FILE, expiry_str)
    except Exception as e:
        logging.error("Failed to save token: %s", e)


# ----------------------
# Dhan Auth API Calls
# ----------------------
def _generate_consent() -> str:
    """
    Step 1: POST /app/generate-consent?client_id={dhanClientId}
    Headers: app_id, app_secret
    Returns consentAppId
    """
    url = f"{AUTH_BASE}/app/generate-consent"
    headers = {"app_id": API_KEY, "app_secret": API_SECRET}
    params = {"client_id": DHAN_CLIENT_ID}
    logging.info("Requesting consent for client_id %s ...", DHAN_CLIENT_ID)
    resp = requests.post(url, headers=headers, params=params, timeout=30)
    resp.raise_for_status()
    data = resp.json()
    consent_app_id = data["consentAppId"]
    logging.info("Consent generated. consentAppId=%s", consent_app_id)
    return consent_app_id


def _prompt_user_for_token_id(consent_app_id: str) -> str:
    """
    Step 2 (user in browser): Open login URL, complete login/2FA.
    Copy tokenId from your redirect URL and paste here.
    """
    login_url = f"{AUTH_BASE}/login/consentApp-login?consentAppId={consent_app_id}"
    logging.info("Open this URL in your browser and complete login/2FA:")
    logging.info(login_url)
    token_id = input("Paste tokenId from your redirect URL here: ").strip()
    if not token_id:
        raise ValueError("Empty tokenId provided.")
    logging.info("tokenId received.")
    return token_id


def _consume_consent(token_id: str) -> Tuple[str, str]:
    """
    Step 3: POST /app/consumeApp-consent?tokenId={tokenId}
    Headers: app_id, app_secret
    Returns (accessToken, expiryTimeStr)
    """
    url = f"{AUTH_BASE}/app/consumeApp-consent"
    headers = {"app_id": API_KEY, "app_secret": API_SECRET}
    params = {"tokenId": token_id}
    logging.info("Consuming consent to obtain access token...")
    resp = requests.post(url, headers=headers, params=params, timeout=30)
    resp.raise_for_status()
    data = resp.json()

    # Validate presence
    if "accessToken" not in data or "expiryTime" not in data:
        raise KeyError("Response missing 'accessToken' or 'expiryTime'.")

    logging.info("Access token obtained successfully.")
    return data["accessToken"], data["expiryTime"]


# ----------------------
# Public: Get Valid Token
# ----------------------
def get_dhan_token():
    """
    Returns a valid Dhan access token.
    - If a saved token exists and has >= 7 hours remaining, reuse it.
    - Otherwise, runs the 3-step flow and saves the new token.
    """
    # Try reuse
    loaded = _load_existing_token()
    now_utc = datetime.now(timezone.utc)

    if loaded:
        token, expiry_dt = loaded
        remaining = expiry_dt - now_utc
        if remaining >= REUSE_BUFFER:
            logging.info(
                "Reusing existing token (remaining: %s >= buffer: %s).",
                remaining, REUSE_BUFFER
            )
            return token, str(expiry_dt)
        else:
            logging.info(
                "Existing token expires soon (remaining: %s < buffer: %s). Generating new token.",
                remaining, REUSE_BUFFER
            )

    # Fresh flow
    consent_app_id = _generate_consent()
    token_id = _prompt_user_for_token_id(consent_app_id)
    access_token, expiry_str = _consume_consent(token_id)

    # Persist
    _save_token(expiry_str, access_token)

    return access_token, expiry_str


def get_access_token():
    try:
        token, expiry_str = get_dhan_token()
        logging.info(f"Access Token: {token} and expiry {expiry_str}...")
        return token, expiry_str
    except Exception as e:
        logging.exception("Failed to obtain Dhan access token: %s", e)
        return '', ''

get_access_token()

@Titamazon

On the TOTP issue, are you getting it intermittently? Also, what do you mean by the web process?

Yes, this is something we are working on.

@Hans_Singh This is fixed and you shouldn’t be facing this issue now

@shyam_Darade Welcome to MadeForTrade community!
Yes, for now, each user needs to have a distinct IP address associated.

This will be updated, we are revisiting the rate limits given the requests that we are getting from users.

Yes, it is allowed for you to have a primary and a secondary IP address.

Yes @prashantcafe for now. We are trying to get things moving on the family sharing of IP, but it will take a while.

Hey @Sunil_S2
This has been resolved. Do let us know if you are still getting this error.

Hey @vinay_rana8727 Can you please share your client ID over DM, will get this checked. I understand the issues that the changes is causing and the team is here to ensure we can make the transition smooth.

@Hardik This is the issue I have - Documentation is bare minimum and missing information, receiving no clear instruction on modification needed in the flow due to API key, static IP rule unclear on family accounts.

Tomorrow these new regulations are supposed to kick in and there is still no clarity. I am not receiving support over email as well.

1 Like

hi @Hardik ,

Approx timeline , so to plan accordingly.

API w/o static IPs will not work from 1st Oct ( tommorrow ) , any extension / relaxation expected ?

Rgds,

@Hardik Initially, I’m getting the error {ā€˜status’: ā€˜failure’, ā€˜remarks’: {ā€˜error_code’: ā€˜DH-905’, ā€˜error_type’: ā€˜Input_Exception’, ā€˜error_message’: ā€˜Invalid IP’}}. After 2-3 attempts, I’m getting same old error: {ā€˜errorType’: ā€˜Input_Exception’, ā€˜errorCode’: ā€˜DH-905’, ā€˜errorMessage’: ā€˜Missing required fields, bad values for parameters etc.’}

I’m also facing similar issue. There is no proper documentation for the new updates.

@Hardik kindly do the documention process before rollout the changes. There are lot of gaps with the existing system wrt document.

1 Like

Hey @Azure_Sphere @Ramreddy_Kadathala

There is going to be an extension of this. We are going to add about the same.

Meanwhile, we are making changes to incorporate all feedback and ensure the transition is much smoother.

Hello @Hardik ,
Please note the issue:

  • Token generated manually from Dhan portal → works (REST + WS).
  • Token generated via consentAppId → token_id → access_token flow → works for REST, fails for WS.
    Is it that,
    Portal-generated token is a full access token (usable for REST + WebSocket).
  • API-generated token (via consent flow) is:
    Limited-scope (REST only), or
    Requires an additional exchange step before WS use.

Please clarify and confirm.

@Hardik

Yes, I am getting this error intermittently. Once it occurs, the same error persists for about a minute. After that, it starts working and the step passes successfully.

By ā€œweb process,ā€ I mean that I have automated the token generation using the REST API. I encountered this error during the automated process, and the same error also occurred during the manual browser-based process.

I’m just a small retail trader who spent months learning Python, hoping to build a simple automation to help me trade better. My bot was working fine with 30-day tokens, letting me trade steadily without constant manual work.

Now after the new SEBI rules and your changes, I find myself forced to manually login and generate new tokens every day or when they expire. There’s no refresh token to automate this important step. This completely breaks the automation I worked so hard to build and deeply worries me about sustaining 24/7 trading.

Please provide an alternative if there is , if not then, I humbly request Dhan to please consider a solution that allows retail traders like me to keep our bots running automatically without daily manual intervention. This small fix would mean the world to us who rely on your API for our livelihoods. Please hear our plea and help us keep trading smooth and uninterrupted.

2 Likes

THANKS FOR UPDATE
So it is not really compulsory to get and use static ip from today (1st oct 25).
we got some more time to be compatible and compliance for static ip??
like angelone and upstox supports both for trade but us encourage to use static ip.

1 Like

You can use OAuth based method to auto generate the token Authentication - DhanHQ Ver 2.0 / API Document.

Yes as per latest circular that came yesterday

@Hardik If one is forced to do a browser login each time for Oauth, how exactly is that different from generating a 24 hour access key token? It defeats the point of a headless server that trades by itself. Did none from the broker community/Dhan make a representation to SEBI how ill thought and impractical their suggested changes are?

1 Like

@Hardik Updating PIN since last evening is not working. During login, it says invalid pin. I have tried couple of times to update the pin and yet somehow Dhan is not taking neither the new pin nor the old. Kindly check and rectify.

Hello @AmitKAswani

We are working on fixing this and you will be able to use the same token for all DhanHQ services moving forward. The fix for this will be deployed by this week itself.

Noted. Will try to replicate this and also check on server side whether such issues are getting captured or not. If it is possible for you to share your Client ID over DM, it will be helpful.

@Ashutosh_Raina we understand the issue that you are facing, having started on the algo trading journey and having to add a new token daily. There is a way out for this, wherein you can automate the API key based login process. Someone shared a snippet above:

Yes, the whole industry did make representation on this. Having said that, the idea is to validate every user on each trading day to discourage scenarios wherein user accounts may get compromised/managed by someone else. An individual who is doing algo trading for his own account can still use API key based login. I have added a community member’s method of using it in automation.

Can you write a note on help@dhan.co. Since you have recently updated the PIN, it shouldn’t have been happening.

Hey @Titamazon

We checked this and the error is usually thrown when there are 5 consecutive incorrect TOTP attempts. Can you check once at your end, by adding logs to see how many times was the TOTP entered?

If you are getting this at random and it is not following this pattern, do DM your Client ID.

It’s working now, thanks!

Hi @Hardik. now the api authentication is working but every once in a while it is still failing with below message

400 - "{\"errorType\":\"Input_Exception\",\"errorCode\":\"DH-905\",\"errorMessage\":\"Missing required fields, bad values for parameters etc.\"}"

and the next attempt is being successful. and one other observation the very first attempt when i use the auth token its failing. Kindly get this issue fixed ASAP.

@Hardik Order placement is acting all weird. Status was failure but order was executed. Below is the log.
2025-10-01 09:37:28,770 341 INFO: {ā€˜status’: ā€˜failure’, ā€˜remarks’: {ā€˜error_code’: ā€˜DH-905’, ā€˜error_type’: ā€˜Input_Exception’, ā€˜error_message’: ā€˜Missing required fields, bad values for parameters etc.’}, ā€˜data’: {ā€˜errorType’: ā€˜Input_Exception’, ā€˜errorCode’: ā€˜DH-905’, ā€˜errorMessage’: ā€˜Missing required fields, bad values for parameters etc.’}}
2025-10-01 09:37:30,687 341 INFO: {ā€˜status’: ā€˜failure’, ā€˜remarks’: {ā€˜error_code’: ā€˜DH-905’, ā€˜error_type’: ā€˜Input_Exception’, ā€˜error_message’: ā€˜Missing required fields, bad values for parameters etc.’}, ā€˜data’: {ā€˜errorType’: ā€˜Input_Exception’, ā€˜errorCode’: ā€˜DH-905’, ā€˜errorMessage’: ā€˜Missing required fields, bad values for parameters etc.’}}
2025-10-01 09:37:34,625 341 INFO: {ā€˜status’: ā€˜failure’, ā€˜remarks’: {ā€˜error_code’: ā€˜DH-905’, ā€˜error_type’: ā€˜Input_Exception’, ā€˜error_message’: ā€˜Missing required fields, bad values for parameters etc.’}, ā€˜data’: {ā€˜errorType’: ā€˜Input_Exception’, ā€˜errorCode’: ā€˜DH-905’, ā€˜errorMessage’: ā€˜Missing required fields, bad values for parameters etc.’}}
2025-10-01 09:37:39,769 341 INFO: {ā€˜status’: ā€˜failure’, ā€˜remarks’: {ā€˜error_code’: ā€˜DH-905’, ā€˜error_type’: ā€˜Input_Exception’, ā€˜error_message’: ā€˜Missing required fields, bad values for parameters etc.’}, ā€˜data’: {ā€˜errorType’: ā€˜Input_Exception’, ā€˜errorCode’: ā€˜DH-905’, ā€˜errorMessage’: ā€˜Missing required fields, bad values for parameters etc.’}}
2025-10-01 09:37:46,675 341 INFO: {ā€˜status’: ā€˜failure’, ā€˜remarks’: {ā€˜error_code’: ā€˜DH-905’, ā€˜error_type’: ā€˜Input_Exception’, ā€˜error_message’: ā€˜Missing required fields, bad values for parameters etc.’}, ā€˜data’: {ā€˜errorType’: ā€˜Input_Exception’, ā€˜errorCode’: ā€˜DH-905’, ā€˜errorMessage’: ā€˜Missing required fields, bad values for parameters etc.’}}

Below is the screenshot from the orders page

Since the first attempt had status as failure, script tried again. Any idea why is this happening?