Annotated walkthrough of the official example_orders.py and example_market.py scripts shipped in the Shoonya_API_OAuth repo — the real reference implementation behind the Python SDK, not illustrative pseudocode.
Overview
The Shoonya_API_OAuth repo ships two runnable CLI scripts — example_orders.py and example_market.py — that exercise every major call through a simple input()-driven menu loop. They're the fastest way to confirm your credentials and see real request/response shapes before wiring anything into a strategy. Both scripts import the same wrapper class, ShoonyaApiPy, from api_helper.py.
This is the actual SDK surfaceEarlier pages on this site show a simplified/illustrative ShoonyaClient for readability. The real class is ShoonyaApiPy (also aliased NorenApiPy in some forks), and its method names — place_order, get_quotes, searchscrip, start_websocket — are what you'll actually import and call.
Shared setup: OAuth login
Unlike the password/TOTP-based fork, this repo authenticates via the OAuth flow documented on Manual Login (OAuth) — redirect to the authorize URL, capture the code, then exchange it for a token:
python
from api_helper import NorenApiPy
import logging
logging.basicConfig(level=logging.DEBUG)
api = NorenApiPy()
uid = "ABC123"# your Shoonya user ID
client_id = "ABC123_U"
secret_key = ""#your Secret_Code# Step 1 — get the OAuth login URL, visit it, log in, capture 'code' from the redirect
oauth_url = api.getOAuthURL(client_id)
print("Visit and login:", oauth_url)
auth_code = input("Paste the 'code' from the redirect URL: ")
# Step 2 — exchange the auth code for an access token
acc_tok, usrid, ref_tok, actid = api.getAccessToken(auth_code, secret_key, client_id, uid)
print("Access Token:", acc_tok)
print("User ID:", usrid)
print("Refresh Token:", ref_tok)
print("Account ID:", actid)
# Step 3 — inject the token into the session, along with UID and AID
api.injectOAuthHeader(acc_tok, usrid, actid)
print("Login successful, session ready.")
No cred.yml in this forkThe password/TOTP fork stores credentials in a cred.yml file loaded at startup. This OAuth fork has no equivalent file — the only long-lived secret is secret_key, which should be loaded from an environment variable or secrets manager, never hardcoded as shown above. Treat it exactly like the checksum inputs on the Manual Login (OAuth) calculator.
example_orders.py — full script
A menu-driven loop covering the full order lifecycle. Reproduced with the OAuth login block collapsed (see above):
python
socket_opened = Falsedefevent_handler_order_update(message):
print("order event: " + str(message))
defevent_handler_quote_update(message):
print("quote event: " + str(message))
defopen_callback():
global socket_opened
socket_opened = Trueprint('app is connected')
api.subscribe('NSE|22')
# api.subscribe(['NSE|22', 'BSE|522032']) # multiple tokens at once# ... api = ShoonyaApiPy(); OAuth login via getOAuthURL/getAccessToken/injectOAuthHeader (see Setup section) ...if ret != None:
whileTrue:
print('p => place order')
print('m => modify order')
print('c => cancel order')
print('y => order history')
print('o => get order book')
print('h => get holdings')
print('l => get limits')
print('k => get positions')
print('d => get daily mtm')
print('s => start_websocket')
print('q => quit')
prompt1 = input('what shall we do? ').lower()
if prompt1 == 'p':
ret = api.place_order(
buy_or_sell='B', product_type='C',
exchange='NSE', tradingsymbol='INFY-EQ',
quantity=1, discloseqty=0,
price_type='LMT', price=1500.00, trigger_price=None,
retention='DAY', remarks='my_order_001',
)
print(ret)
elif prompt1 == 'm':
orderno = input('Enter orderno:').lower()
ret = api.modify_order(
exchange='NSE', tradingsymbol='INFY-EQ', orderno=orderno,
newquantity=2, newprice_type='LMT', newprice=1505.00,
)
print(ret)
elif prompt1 == 'c':
orderno = input('Enter orderno:').lower()
ret = api.cancel_order(orderno=orderno)
print(ret)
elif prompt1 == 'y':
orderno = input('Enter orderno:').lower()
ret = api.single_order_history(orderno=orderno)
print(ret)
elif prompt1 == 'o':
ret = api.get_order_book()
print(ret)
elif prompt1 == 'h':
ret = api.get_holdings()
print(ret)
elif prompt1 == 'l':
ret = api.get_limits()
print(ret)
elif prompt1 == 'k':
ret = api.get_positions()
print(ret)
elif prompt1 == 'd':
# contributed by Aromal P NairwhileTrue:
ret = api.get_positions()
mtm, pnl = 0, 0for i in ret:
mtm += float(i['urmtom'])
pnl += float(i['rpnl'])
day_m2m = mtm + pnl
print(day_m2m)
elif prompt1 == 's':
if socket_opened:
print('websocket already opened')
continue
ret = api.start_websocket(
order_update_callback=event_handler_order_update,
subscribe_callback=event_handler_quote_update,
socket_open_callback=open_callback,
)
print(ret)
else:
print('Fin')
break
Two things worth noticing
The 'd' (Daily MTM) branch is an infinite loop with no sleep or break condition — it's a quick-and-dirty demo, not something to run unmodified. Add a poll interval and an exit condition (see Daily MTM) before reusing it.
modify_order's success payload returns the order number under result, not norenordno — the same field-naming quirk documented on Modify Order. The example script just prints the raw dict, so this is easy to miss until you try to parse it.
place_order — real parameter mapping
The wrapper's Python keyword arguments don't share names with the raw JSON fields documented on Place Order. This is the actual mapping used by api_helper.py:
Python kwarg
Raw JSON field
Notes
buy_or_sell
trantype
B / S
product_type
prd
C / M / I / H / B
exchange
exch
—
tradingsymbol
tsym
URL-encode symbols with &, e.g. M&M
quantity
qty
—
discloseqty
dscqty
—
price_type
prctyp
Wrapper-level enum includes MKT/SL-MKT; whether the broker's OMS actually accepts them is a separate, account-level restriction — see the warning on Place Order.
price
prc
0.00 is valid only for MKT
trigger_price
trgprc
None unless price_type is an SL variant
retention
ret
DAY / IOC / EOS
remarks
remarks
Free text — tag every order uniquely here for reconciliation, per Place Order best practices
uid, actid, ordersource
uid, actid, ordersource
Filled in automatically by the wrapper from the logged-in session — you never pass these yourself
example_market.py — full script
The market-data counterpart, covering symbol search, quotes, contract info, historical candles, and the option chain:
python
defget_time(time_string):
data = time.strptime(time_string, '%d-%m-%Y %H:%M:%S')
return time.mktime(data)
# ... api = ShoonyaApiPy(); OAuth login via getOAuthURL/getAccessToken/injectOAuthHeader ...if ret != None:
whileTrue:
print('f => find symbol')
print('m => get quotes')
print('p => contract info n properties')
print('v => get 1 min market data')
print('t => get today 1 min market data')
print('d => get daily data')
print('o => get option chain')
print('s => start_websocket')
print('q => quit')
prompt1 = input('what shall we do? ').lower()
if prompt1 == 'v':
ret = api.get_time_price_series(
exchange='NSE', token='22',
starttime=1642265814, endtime=1642438794, interval=240,
)
df = pd.DataFrame.from_dict(ret)
print(df)
elif prompt1 == 't':
ret = api.get_time_price_series(exchange='NFO', token='71321')
df = pd.DataFrame.from_dict(ret)
print(df)
elif prompt1 == 'f':
exch, query = 'MCX', 'CRUDEOIL FEB'
ret = api.searchscrip(exchange=exch, searchtext=query)
print(ret)
if ret != None:
for symbol in ret['values']:
print('{0} token is {1}'.format(symbol['tsym'], symbol['token']))
elif prompt1 == 'd':
ret = api.get_daily_price_series(
exchange='NSE', tradingsymbol='RELIANCE-EQ', startdate=0,
)
print(ret)
elif prompt1 == 'p':
ret = api.get_security_info(exchange='NSE', token='22')
print(ret)
elif prompt1 == 'm':
ret = api.get_quotes(exchange='NSE', token='22')
print(ret)
elif prompt1 == 'o':
exch, tsym = 'MCX', 'CRUDEOIL18FEB22'
chain = api.get_option_chain(exchange=exch, tradingsymbol=tsym, strikeprice=4150, count=2)
chainscrips = []
for scrip in chain['values']:
scripdata = api.get_quotes(exchange=scrip['exch'], token=scrip['token'])
chainscrips.append(scripdata)
print(chainscrips)
elif prompt1 == 's':
if socket_opened:
print('websocket already opened')
continue
ret = api.start_websocket(
order_update_callback=event_handler_order_update,
subscribe_callback=event_handler_quote_update,
socket_open_callback=open_callback,
)
print(ret)
else:
ret = api.logout()
print(ret)
print('Fin')
break
The option chain branch makes N+1 callsThe 'o' handler fetches the chain once via get_option_chain, then loops and calls get_quotes per strike returned. For a chain with a real strike count (10–20+ per side), that's 10–20+ sequential REST calls purely for a live price refresh — exactly the polling pattern Rate Limits warns against. Resolve tokens once with get_option_chain, then subscribe to those tokens on the WebSocket feed instead of re-polling quotes per strike.
get_time_price_series — the two calling patterns
The script shows both an explicit-range call and a defaults-only call, and the difference matters:
Explicit epoch-second window and candle size in minutes — used by the 'v' branch above.
get_time_price_series(exchange, token)
Omitting starttime/endtime/interval falls back to wrapper defaults (effectively "today, 1-minute candles") — used by the 't' branch.
The included helper get_time(time_string) converts a '%d-%m-%Y %H:%M:%S' string to epoch seconds via time.mktime — useful for building starttime/endtime without hand-computing timestamps. Note this uses local system time, not UTC/IST explicitly, so keep the host clock's timezone in mind (see the NTP note on TOTP Setup for a related clock-drift issue elsewhere in this API).
Best practices
Run these scripts against a small, disposable order (1 share, a liquid large-cap) before pointing any automation at your real strategy — they're unmodified from the repo and will place a live order the moment you hit p.
logging.basicConfig(level=logging.DEBUG) at the top of both scripts prints every raw HTTP request/response — keep this on while debugging, but turn it off (or route to a file) in anything long-running, since it will log your access token on every call.
The 'd' Daily MTM branch's unbounded while True loop is illustrative only — see Daily MTM for a bounded version with a recompute interval.
Both scripts call api.subscribe(...) from inside open_callback, never before — the socket has to confirm open before a subscribe frame is meaningful. Structure your own WebSocket code the same way; see Streaming Code Examples.
Never hardcode secret_key in the login block as shown for readability above — load it from an environment variable, matching the pattern warned about on Manual Login (OAuth).
Notes
Source: example_orders.py and example_market.py on GitHub. This fork swaps the TOTP/password login block used by other Shoonya SDK forks for getOAuthURL → getAccessToken → injectOAuthHeader — see Manual Login (OAuth) for the underlying HTTP flow this wraps.
Overview
The Shoonya_API_OAuth repo ships two runnable CLI scripts —
example_orders.pyandexample_market.py— that exercise every major call through a simpleinput()-driven menu loop. They're the fastest way to confirm your credentials and see real request/response shapes before wiring anything into a strategy. Both scripts import the same wrapper class,ShoonyaApiPy, fromapi_helper.py.ShoonyaClientfor readability. The real class isShoonyaApiPy(also aliasedNorenApiPyin some forks), and its method names —place_order,get_quotes,searchscrip,start_websocket— are what you'll actually import and call.Shared setup: OAuth login
Unlike the password/TOTP-based fork, this repo authenticates via the OAuth flow documented on Manual Login (OAuth) — redirect to the authorize URL, capture the
code, then exchange it for a token:cred.ymlfile loaded at startup. This OAuth fork has no equivalent file — the only long-lived secret issecret_key, which should be loaded from an environment variable or secrets manager, never hardcoded as shown above. Treat it exactly like the checksum inputs on the Manual Login (OAuth) calculator.example_orders.py — full script
A menu-driven loop covering the full order lifecycle. Reproduced with the OAuth login block collapsed (see above):
'd'(Daily MTM) branch is an infinite loop with no sleep or break condition — it's a quick-and-dirty demo, not something to run unmodified. Add a poll interval and an exit condition (see Daily MTM) before reusing it.modify_order's success payload returns the order number underresult, notnorenordno— the same field-naming quirk documented on Modify Order. The example script just prints the raw dict, so this is easy to miss until you try to parse it.place_order — real parameter mapping
The wrapper's Python keyword arguments don't share names with the raw JSON fields documented on Place Order. This is the actual mapping used by
api_helper.py:B/SC/M/I/H/B&, e.g.M&MMKT/SL-MKT; whether the broker's OMS actually accepts them is a separate, account-level restriction — see the warning on Place Order.MKTNoneunlessprice_typeis an SL variantDAY/IOC/EOSexample_market.py — full script
The market-data counterpart, covering symbol search, quotes, contract info, historical candles, and the option chain:
'o'handler fetches the chain once viaget_option_chain, then loops and callsget_quotesper strike returned. For a chain with a real strike count (10–20+ per side), that's 10–20+ sequential REST calls purely for a live price refresh — exactly the polling pattern Rate Limits warns against. Resolve tokens once withget_option_chain, then subscribe to those tokens on the WebSocket feed instead of re-polling quotes per strike.get_time_price_series — the two calling patterns
The script shows both an explicit-range call and a defaults-only call, and the difference matters:
get_time_price_series(exchange, token, starttime, endtime, interval)'v'branch above.get_time_price_series(exchange, token)starttime/endtime/intervalfalls back to wrapper defaults (effectively "today, 1-minute candles") — used by the't'branch.The included helper
get_time(time_string)converts a'%d-%m-%Y %H:%M:%S'string to epoch seconds viatime.mktime— useful for buildingstarttime/endtimewithout hand-computing timestamps. Note this uses local system time, not UTC/IST explicitly, so keep the host clock's timezone in mind (see the NTP note on TOTP Setup for a related clock-drift issue elsewhere in this API).Best practices
p.logging.basicConfig(level=logging.DEBUG)at the top of both scripts prints every raw HTTP request/response — keep this on while debugging, but turn it off (or route to a file) in anything long-running, since it will log your access token on every call.'d'Daily MTM branch's unboundedwhile Trueloop is illustrative only — see Daily MTM for a bounded version with a recompute interval.api.subscribe(...)from insideopen_callback, never before — the socket has to confirm open before a subscribe frame is meaningful. Structure your own WebSocket code the same way; see Streaming Code Examples.secret_keyin the login block as shown for readability above — load it from an environment variable, matching the pattern warned about on Manual Login (OAuth).Notes
Source: example_orders.py and example_market.py on GitHub. This fork swaps the TOTP/password login block used by other Shoonya SDK forks for
getOAuthURL→getAccessToken→injectOAuthHeader— see Manual Login (OAuth) for the underlying HTTP flow this wraps.