Instruments¶
The instrument routes let you browse and look up every instrument the ten brokers carry, merged into one list. Each real-world instrument appears once, however many brokers list it, and each one has a single instrument_id that the quote, history and order routes accept.
The table below lists the five routes on this page. The last two also take a list of instruments by POST, as described under Several instruments at once.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/instruments/segments |
Every exchange segment, its shape, the fields that name an instrument in it, and how many instruments it holds |
| GET | /api/instruments/master |
Every instrument in a segment, an exchange or everywhere, streamed as one JSON array |
| GET | /api/instruments/search |
Instruments in one segment whose symbol or underlying contains a search term |
| GET | /api/instruments/details |
One instrument, its lot and tick size, and how each broker names it |
| POST | /api/instruments/details |
The same for a list of instruments in one request |
| GET | /api/instruments/additional_details |
One instrument and the extra attributes each broker's instrument file carries |
| POST | /api/instruments/additional_details |
The same for a list of instruments in one request |
Glossary of constants¶
The routes on this page share a small vocabulary, listed in the table below. Constants has the full list of segment names.
| Parameter | Values | Meaning |
|---|---|---|
exchange |
nse, bse, mcx, ncdex, and unknown |
The exchange. unknown holds only the uncategorised segment. master also accepts all. |
segment |
An exchange-prefixed name such as nse_equities, or the bare name such as equities |
The exchange segment. A bare name is prefixed with the exchange for you. master also accepts all. |
shape |
security, future, option |
What kind of instrument the segment holds, which decides the identity fields |
option_type |
CE, PE |
Call or put |
Naming an instrument¶
Every route that acts on one instrument, which is details, additional_details, the market quote routes and the historical data routes, accepts the instrument in either of two spellings. You can send its instrument_id, or you can send its exchange, its segment and the identity fields that segment's shape calls for.
The table below shows the identity fields for each shape. The field names are the same ones every response uses.
| Shape | Segments with this shape (bare names) | Identity fields | Example |
|---|---|---|---|
security |
equities, equity_indices, fixed_income, currencies, commodities, mutual_funds, exchange_traded_funds, investment_trusts, the other *_indices, and uncategorised |
symbol |
exchange=nse&segment=equities&symbol=INFY |
future |
Every *_futures segment |
underlying_symbol, expiry_date |
exchange=mcx&segment=commodity_futures&underlying_symbol=CRUDEOIL&expiry_date=2026-10-19 |
option |
Every *_options segment |
underlying_symbol, expiry_date, strike_price, option_type |
exchange=nse&segment=equity_index_options&underlying_symbol=NIFTY&expiry_date=2026-09-29&strike_price=25000&option_type=CE |
The example dates and strikes above are illustrative, so check them against search before you use them.
What the instrument_id is¶
The instrument_id is a UUID version 5, computed from the instrument's natural key: its exchange, its segment, its shape and its identity field values, joined with | and hashed under a fixed project namespace. Because the id is computed rather than handed out, two brokers that list the same instrument arrive at the same id independently, and that is the whole mechanism that merges ten brokers' lists into one. It also means the same instrument keeps the same id from one day to the next.
Which spelling to use
Send instrument_id whenever you have it. It is one parameter, it cannot be misspelled, and it is what every response hands back. The identity spelling is for the first lookup, when all you know is a symbol.
How a request is turned into one instrument¶
The flowchart below shows how the API reads the parameters and finds the instrument. Every check that fails returns 400 with the message shown, before anything is read from Redis or the database.
flowchart TD
A["Query parameters"] --> B{"instrument_id<br/>given?"}
B -- yes --> C{"a valid UUID?"}
C -- no --> E1["400 instrument_id must be a UUID"]
C -- yes --> L["Look the id up"]
B -- no --> D{"exchange or<br/>segment given?"}
D -- no --> E2["400 give instrument_id, or exchange,<br/>segment and the identity fields"]
D -- yes --> F["Check exchange and segment,<br/>work out the shape"]
F --> G{"every identity field<br/>for that shape given?"}
G -- no --> E3["400 a future segment needs<br/>underlying_symbol, expiry_date"]
G -- yes --> H["Upper-case the symbol,<br/>parse expiry and strike"]
H --> L2["Look the name, expiry, strike<br/>and option type up in the segment"]
L --> R{"found on the<br/>mapping date?"}
L2 --> R
R -- no --> E4["404 no instrument ... is mapped on ..."]
R -- yes --> OK["The instrument's identity"]
The symbol and underlying are upper-cased before the lookup, so infy finds INFY. The option type is upper-cased as well, so ce is accepted.
Several instruments at once¶
The seven routes that act on one instrument (details, additional_details, ltp, ohlc and quote, and prices and ticks) also accept a list of instruments in one request. The list has no upper limit on its length. You send the same path with POST instead of GET, and put the instruments in a JSON body. The GET form is unchanged, so existing callers are not affected.
The rule to remember is that the method decides the shape of the answer, not the number of instruments. A GET always answers with one instrument's object, exactly as described on each route's page. A POST always answers {"results": [...]}, even when its list holds a single instrument, so client code never needs a special case for a list of one.
Request body¶
Each item of instruments names one instrument with the same fields the GET form takes in its query string, so an item is either {"instrument_id": ...} or the exchange, segment and identity fields described under Naming an instrument. The two spellings can be mixed in one list. Every other key in the body is a parameter shared by all the instruments, with the same name and spelling as in the query string.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
access-token |
header | string | Yes | The token from connect |
instruments |
body | array of objects | Yes | One or more instruments, each named as under Naming an instrument |
| Shared parameters | body | string, number or boolean | Depends on the route | Applied to every instrument; the table below lists them |
The table below lists the shared parameters each route reads. JSON numbers and booleans are accepted wherever the query string takes text, so "strike_price": 25000 and "adjusted": false both work.
| Route | Shared parameters |
|---|---|
details, additional_details |
date |
ltp, ohlc, quote |
none |
prices |
interval, then from and to or days, and adjusted, known_as_of |
ticks |
start, end, adjusted |
Example¶
curl -X POST "http://127.0.0.1:8080/api/instruments/details" \
-H "access-token: $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"instruments": [
{"instrument_id": "11111111-1111-5111-8111-000000000002"},
{"exchange": "nse", "segment": "equities", "symbol": "RELIANCE"},
{"exchange": "nse", "segment": "equity_index_futures", "underlying_symbol": "NIFTY", "expiry_date": "2026-10-27"},
{"exchange": "nse", "segment": "equities", "symbol": "NOSUCHSTOCK"}
]
}'
import os
import requests
response = requests.post(
'http://127.0.0.1:8080/api/instruments/details',
headers={
'access-token': os.environ['ACCESS_TOKEN'],
},
json={
'instruments': [
{
'instrument_id': '11111111-1111-5111-8111-000000000002',
},
{
'exchange': 'nse',
'segment': 'equities',
'symbol': 'RELIANCE',
},
],
},
timeout=10,
)
for result in response.json()['results']:
if result['status'] == 200:
print(result['request_index'], result['data']['symbol'], result['data']['lot_size'])
else:
print(result['request_index'], result['status'], result['error'])
Response¶
The example below shortens each data object, which in a real answer holds every field the GET form returns. The ids and dates are placeholders.
{
"results": [
{"request_index": 0, "status": 200, "data": {"instrument_id": "11111111-1111-5111-8111-000000000002", "symbol": "INFY", "lot_size": 1, "...": "..."}},
{"request_index": 1, "status": 200, "data": {"instrument_id": "11111111-1111-5111-8111-000000000003", "symbol": "RELIANCE", "lot_size": 1, "...": "..."}},
{"request_index": 2, "status": 200, "data": {"instrument_id": "11111111-1111-5111-8111-000000000007", "underlying_symbol": "NIFTY", "lot_size": 75, "...": "..."}},
{"request_index": 3, "status": 404, "error": "no instrument nse_equities NOSUCHSTOCK is mapped on 2026-09-26"}
]
}
Response attributes¶
| Attribute | Type | Description |
|---|---|---|
results |
array of objects | One entry per item of instruments, in the same order |
results[].request_index |
integer | The item's position in instruments, counting from 0 |
results[].status |
integer | The status the GET form would have answered this instrument with |
results[].data |
object | Present when status is 200: exactly the object the GET form returns |
results[].error |
string | Present otherwise: exactly the message the GET form returns |
results[].ticks |
array of objects | ticks only: the instrument's ticks, beside its data |
Match results to your request by request_index or by position, not by instrument_id. An instrument named by its fields has no id until it is found, and an instrument that is not found never gets one.
Status codes¶
The status of the whole response and the status of each entry answer different questions. The table below shows the statuses of the whole response.
| Status | When |
|---|---|
| 200 | The body was readable. Each entry then carries its own status, so a 200 can hold failed entries. |
| 400 | the body must be a JSON object with an instruments list, instruments must be a non-empty list, <name> must be text, a number or true or false for a shared parameter, or any message about a shared parameter that the GET form gives, such as interval is required or end must be after start. |
| 401 | Access token is required, Invalid access token or Access token has expired. |
| 404 | nothing had been mapped on or before <date>. |
| 503 | no instruments have been mapped yet. |
An entry's status is one of the statuses the GET form gives for one instrument. An item that is not an object gets 400 each entry of instruments must be an object, and an item whose field is an object or a list gets 400 <name> must be text, a number or true or false. The other items are still answered.
What a list costs¶
A list costs a fixed number of Redis round trips rather than a number that grows with the list, because each kind of read is sent for every instrument at once. The table below shows counts recorded by the offline suite test_runs/instrument_routes.py, including the access token check.
| Request | Instruments | Redis round trips |
|---|---|---|
GET /details by symbol |
1 | 7 |
POST /details, named by id, symbol, future fields and option fields |
4 | 7 |
GET /details by id |
1 | 6 |
POST /details, all by id |
3 | 6 |
GET /ltp, a stale quote fetched from a broker |
1 | 6 |
POST /ltp, cached, stale, uncached and unknown instruments mixed |
7 | 6 |
Two kinds of work still grow with the list. A broker quote is one HTTP call per instrument that has no usable cached quote, and a POST runs up to four of them at once; Where a quote comes from explains when that happens. prices reads each instrument's candles separately, and ticks runs one query per instrument while it streams.
Under the hood
- Body parsing and result entries:
InstrumentBatchinunified_broker_interface/utilities/instrument_batch.py. - Resolving the list:
InstrumentCatalogue.resolve_many. The catalogue look-ups share one pipeline throughMappingRedisTier.read_catalogue_for_prefixes, and every identity is read with oneHMGET. - An instrument that the cache cannot answer for, because of a past
date, a cold cache, or history of an instrument no longer mapped, is looked up in Postgres on its own.
Mapping dates¶
The merged instrument list is rebuilt every morning by bin/unified/instruments/map, and each rebuild is stored under its mapping date. Most routes answer for the latest mapping date. The listing and lookup routes also take a date parameter, which asks what the list looked like on an earlier day.
The diagram below shows where an answer comes from. The fast path is Redis, which holds only the latest mapping date. TimescaleDB is read for an earlier date, or when the Redis copy has not been built yet.
flowchart LR
Q["Request<br/>(date optional)"] --> D{"date before the<br/>latest mapping date?"}
D -- no --> C{"Redis catalogue<br/>for that date warm?"}
C -- yes --> R[("Redis<br/>unified:catalogue:*")]
C -- no --> P[("TimescaleDB<br/>unified.instruments<br/>unified.broker_mappings")]
D -- yes --> M["latest mapping date<br/>on or before date"] --> P
A date that falls between two mapping dates is answered from the latest mapping date on or before it, and every response says which mapping date it used, in mapping_date or in the X-Mapping-Date header. A date on or after the latest mapping date is treated as today.
Segments¶
GET /api/instruments/segmentsaccess-token
This route lists every segment that has instruments on the latest mapping date, in the project's fixed segment order, with the fields you need to name an instrument in each one. It is the place to start when you do not know what a segment is called. It reads the per-segment counts from Redis, or counts them in TimescaleDB when the Redis copy is cold.
Request parameters¶
| Name | In | Type | Required | Description |
|---|---|---|---|---|
access-token |
header | string | Yes | The token from connect |
Example¶
Response¶
The example below is shortened to three segments, and the counts are illustrative.
{
"mapping_date": "2026-09-26",
"exchanges": ["bse", "mcx", "ncdex", "nse", "unknown"],
"segments": [
{
"exchange": "nse",
"segment": "nse_equities",
"bare_segment": "equities",
"shape": "security",
"identity_fields": ["symbol"],
"instruments": 2400
},
{
"exchange": "mcx",
"segment": "mcx_commodity_futures",
"bare_segment": "commodity_futures",
"shape": "future",
"identity_fields": ["underlying_symbol", "expiry_date"],
"instruments": 300
},
{
"exchange": "unknown",
"segment": "uncategorised",
"bare_segment": "uncategorised",
"shape": "security",
"identity_fields": ["symbol"],
"instruments": 50
}
]
}
Response attributes¶
| Attribute | Type | Description |
|---|---|---|
mapping_date |
string | The mapping date answered, YYYY-MM-DD |
exchanges |
array of strings | Every exchange that has a segment, sorted, with unknown last |
segments[].exchange |
string | The segment's exchange |
segments[].segment |
string | The exchange-prefixed segment, as every other route returns it |
segments[].bare_segment |
string | The segment name without the exchange |
segments[].shape |
string | security, future or option |
segments[].identity_fields |
array of strings | The fields that name an instrument in this segment |
segments[].instruments |
integer | How many instruments the segment holds on the mapping date |
Status codes¶
| Status | When |
|---|---|
| 200 | The segments were listed. |
| 401 | Access token is required, Invalid access token or Access token has expired. |
| 503 | no instruments have been mapped yet. |
Under the hood
- Route:
InstrumentsBlueprint.segmentsinunified_broker_interface/blueprints/instruments.py, which callsInstrumentCatalogue.segments. - Redis: the hash
unified:catalogue:<mapping date>:segmentsholds the count per segment. - Fallback: a
count(*)overunified.instruments, limited to instruments with a row inunified.broker_mappingson the mapping date. Each fallback is logged. - The segment order is
CANONICAL_SEGMENTSinstock_brokers/instruments/mapping/utilities/segments.py.
Master¶
GET /api/instruments/masteraccess-token
This route returns every instrument in the scope you ask for, which can be one segment, every segment of one exchange, or the whole list. The whole list runs to over a hundred megabytes, so the answer is streamed as one JSON array in chunks of about 64 KB rather than built in memory first.
Request parameters¶
| Name | In | Type | Required | Description |
|---|---|---|---|---|
access-token |
header | string | Yes | The token from connect |
exchange |
query | string | Yes | An exchange, or all |
segment |
query | string | Yes | A segment of that exchange, or all. With exchange=all, only all or uncategorised is accepted. |
date |
query | string | No | YYYY-MM-DD, to list what was mapped on an earlier day |
The table below shows which combinations of exchange and segment are accepted.
exchange |
segment |
What you get |
|---|---|---|
nse |
nse_equities or equities |
One segment |
nse |
all |
Every segment of NSE |
all |
all |
Every instrument everywhere |
all |
nse_equities |
400 a single segment needs a single exchange |
Example¶
Response¶
The response carries an X-Mapping-Date header, and its body is an array of instrument identities. The example below is shortened to one element, and its values are illustrative.
[
{
"instrument_id": "11111111-1111-5111-8111-000000000001",
"exchange": "mcx",
"segment": "mcx_commodity_futures",
"shape": "future",
"symbol": null,
"underlying_symbol": "CRUDEOIL",
"expiry_date": "2026-10-19",
"strike_price": null,
"option_type": null,
"underlying_instrument_id": null
}
]
Response attributes¶
Every element of the array is an instrument identity, which has the nine fields below, followed by underlying_instrument_id. The same nine fields open the answers of search, details, additional_details, the quote routes and prices; underlying_instrument_id follows them only here and in details.
| Attribute | Type | Description |
|---|---|---|
instrument_id |
string | The UUID that names the instrument everywhere |
exchange |
string | The exchange |
segment |
string | The exchange-prefixed segment |
shape |
string | security, future or option |
symbol |
string or null | The symbol, for a security |
underlying_symbol |
string or null | The underlying, for a future or option |
expiry_date |
string or null | The expiry, YYYY-MM-DD, for a future or option |
strike_price |
number or null | The strike, for an option |
option_type |
string or null | CE or PE, for an option |
underlying_instrument_id |
string or null | For a future or option, the UUID of the instrument it is written on, as the brokers' own records say; null for a security and for a derivative whose underlying could not be decided. See Underlyings. |
Instruments come in segment order, and within a segment by name, expiry, strike and option type.
Status codes¶
| Status | When |
|---|---|
| 200 | The stream started. |
| 400 | exchange is required, exchange must be one of nse, bse, mcx, ncdex, all, segment is required, a single segment needs a single exchange, segment '<value>' is not a segment of <exchange>; see /api/instruments/segments, or date must be a date in YYYY-MM-DD format. |
| 401 | Access token is required, Invalid access token or Access token has expired. |
| 404 | nothing had been mapped on or before <date>. |
| 503 | no instruments have been mapped yet. |
A stream that breaks still says 200
Every check above happens before the first byte is sent. If Redis or the database fails part way through, the array is closed early and the status stays 200. Compare the number of elements you received with the segment's instruments count from segments if completeness matters.
Under the hood
- Route:
InstrumentsBlueprint.master, which callsInstrumentCatalogue.masterand streams throughjson_array_response. - Redis: reads the sorted set
unified:catalogue:<date>:catalogue:<segment>5,000 members at a time, and the identities for each batch from the hashunified:catalogue:<date>:identity. - Fallback: a server-side cursor over
unified.instruments, for a past date or a cold cache.
Search¶
GET /api/instruments/searchaccess-token
This route finds instruments in one segment whose symbol, or underlying for a derivative, contains the search term. It is the usual way to get an instrument_id from a name you already know.
Request parameters¶
| Name | In | Type | Required | Description |
|---|---|---|---|---|
access-token |
header | string | Yes | The token from connect |
exchange |
query | string | Yes | One exchange. all is not accepted. |
segment |
query | string | Yes | One segment of that exchange, prefixed or bare |
q |
query | string | No | The search term, matched anywhere in the name without regard to case. Empty returns the segment's first instruments. |
limit |
query | integer | No | How many instruments to return, from 1 to 200. The default is 50. |
date |
query | string | No | YYYY-MM-DD, to search what was mapped on an earlier day |
The results come in a fixed order, listed below.
- Names that equal the term exactly.
- Names that start with the term.
- Names that contain the term anywhere.
Within one name, contracts follow in expiry, strike and option type order. The limit counts contracts, so a search for NIFTY in an options segment can fill the whole limit with one underlying's strikes.
Example¶
import os
import requests
response = requests.get(
'http://127.0.0.1:8080/api/instruments/search',
headers={
'access-token': os.environ['ACCESS_TOKEN'],
},
params={
'exchange': 'nse',
'segment': 'equities',
'q': 'infy',
'limit': 5,
},
timeout=10,
)
instrument_id = response.json()['instruments'][0]['instrument_id']
Response¶
The example below is shortened to one match, and its id is a placeholder.
{
"mapping_date": "2026-09-26",
"instruments": [
{
"instrument_id": "11111111-1111-5111-8111-000000000002",
"exchange": "nse",
"segment": "nse_equities",
"shape": "security",
"symbol": "INFY",
"underlying_symbol": null,
"expiry_date": null,
"strike_price": null,
"option_type": null
}
]
}
Response attributes¶
| Attribute | Type | Description |
|---|---|---|
mapping_date |
string | The mapping date answered |
instruments |
array of objects | The matches, each an instrument identity as described under master |
Status codes¶
| Status | When |
|---|---|
| 200 | The search ran. An empty instruments array means nothing matched. |
| 400 | exchange is required, exchange must be one of nse, bse, mcx, ncdex, segment is required, segment '<value>' is not a segment of <exchange>; see /api/instruments/segments, limit must be a whole number, limit must be between 1 and 200, or date must be a date in YYYY-MM-DD format. |
| 401 | Access token is required, Invalid access token or Access token has expired. |
| 404 | nothing had been mapped on or before <date>. |
| 503 | no instruments have been mapped yet, or the instrument cache is unreachable. |
Under the hood
- Route:
InstrumentsBlueprint.search, which callsInstrumentCatalogue.search.SEARCH_LIMIT_DEFAULTis 50 andSEARCH_LIMIT_MAXIMUMis 200. - Redis: the sorted set
unified:catalogue:<date>:names:<segment>holds each distinct name, and each matching name's contracts are read from the segment's catalogue by prefix. - Fallback: a
LIKEquery overunified.instrumentswith the same ordering, for a past date or a cold cache.
Details¶
GET /api/instruments/detailsaccess-token
This route returns one instrument's identity together with what you need to trade it: the dates it has been seen, its lot size, its tick size and every broker's own name for it. The lot size and tick size are the brokers' consensus, and the per-broker handles keep each broker's own figures, because an order's quantity is checked against the broker it goes to.
Request parameters¶
| Name | In | Type | Required | Description |
|---|---|---|---|---|
access-token |
header | string | Yes | The token from connect |
instrument_id |
query | string | One spelling | The instrument's UUID |
exchange, segment and the identity fields |
query | string | The other spelling | See Naming an instrument |
date |
query | string | No | YYYY-MM-DD, to read the instrument as it was mapped on an earlier day |
Example¶
import os
import requests
response = requests.get(
'http://127.0.0.1:8080/api/instruments/details',
headers={
'access-token': os.environ['ACCESS_TOKEN'],
},
params={
'exchange': 'nse',
'segment': 'equities',
'symbol': 'RELIANCE',
},
timeout=10,
)
for handle in response.json()['carried_by']:
print(handle['broker'], handle['broker_token'], handle['order_symbol'])
Response¶
The example below is shortened to two brokers. The id is a placeholder, and the tokens, symbols, dates and tick size are illustrative.
{
"instrument_id": "11111111-1111-5111-8111-000000000003",
"exchange": "nse",
"segment": "nse_equities",
"shape": "security",
"symbol": "RELIANCE",
"underlying_symbol": null,
"expiry_date": null,
"strike_price": null,
"option_type": null,
"underlying_instrument_id": null,
"mapping_date": "2026-09-26",
"first_seen_date": "2026-06-01",
"last_seen_date": "2026-09-26",
"lot_size": 1,
"tick_size": "0.1",
"carried_by": [
{"broker": "dhan", "broker_token": "2885", "order_symbol": "RELIANCE", "lot_size": "1", "tick_size": "0.1"},
{"broker": "flattrade", "broker_token": "2885", "order_symbol": "RELIANCE-EQ", "lot_size": "1", "tick_size": "0.1"}
]
}
Response attributes¶
The answer starts with the nine identity fields described under master, followed by the fields below.
| Attribute | Type | Description |
|---|---|---|
underlying_instrument_id |
string or null | For a future or option, the UUID of the instrument it is written on; null for a security and for a derivative whose underlying could not be decided. See Underlyings. |
mapping_date |
string | The mapping date answered |
first_seen_date |
string or null | The first mapping date the instrument appeared on |
last_seen_date |
string or null | The last mapping date the instrument appeared on |
lot_size |
integer or null | Units of the underlying in one lot, decided as described below. null when the brokers tie or none sends one. |
tick_size |
string or null | The smallest price step in rupees, as text, that most brokers agree on. null when the two most common values tie or no broker sends one. |
carried_by |
array of objects | One handle per broker that lists the instrument, sorted by broker |
carried_by[].broker |
string | The broker's code |
carried_by[].broker_token |
string | The broker's own token for the instrument |
carried_by[].order_symbol |
string | The trading symbol the broker expects in an order |
carried_by[].lot_size |
string or null | That broker's own lot size, as text |
carried_by[].tick_size |
string or null | That broker's own tick size, as text |
The consensus lot_size is decided by the rules below, which are the same rules the live quote uses.
| Instrument | lot_size |
|---|---|
A security (shape security) |
Always 1 |
| A derivative on MCX | Groww's lot size, because Groww is the lot size authority on MCX |
| Any other derivative | The lot size most brokers agree on |
The sizes are sent as text in carried_by on purpose. A lot size or tick size that went through a floating-point number could come back with a rounding error, and in a quantity that error matters.
Status codes¶
| Status | When |
|---|---|
| 200 | The instrument was found. |
| 400 | Any of the parameter messages under How a request is turned into one instrument, such as instrument_id must be a UUID, a option segment needs underlying_symbol, expiry_date, strike_price, option_type, expiry_date must be a date in YYYY-MM-DD format, strike_price must be a number or option_type must be CE or PE. |
| 401 | Access token is required, Invalid access token or Access token has expired. |
| 404 | no instrument <id or fields> is mapped on <mapping date>, or nothing had been mapped on or before <date>. |
| 503 | no instruments have been mapped yet. |
Details for several instruments¶
POST /api/instruments/detailsaccess-token
The POST form takes a list of instruments in a JSON body and answers {"results": [...]}, with each answered entry's data exactly as above. The body can carry date, which applies to every instrument. Several instruments at once describes the body, the entries and the statuses, and uses this route as its example.
The seen dates and handles of every instrument found in the cache are read with one HMGET each, so a list costs the same Redis round trips as one instrument.
Under the hood
- Route:
InstrumentsBlueprint.details, which callsInstrumentCatalogue.details, orInstrumentCatalogue.details_manyfor aPOST. The single method passes a list of one to the list method, so both forms share one path. - Parameter parsing:
parse_instrumentinunified_broker_interface/utilities/instrument_identity.py. - Redis:
unified:catalogue:<date>:identity,unified:catalogue:<date>:order_handlesandunified:catalogue:<date>:seen. - Fallback:
unified.instrumentsfor the identity and seen dates, andunified.broker_mappingsfor the handles. - Consensus:
units_per_lotinstock_brokers/instruments/ticks/utilities/resolution.pyandInstrumentCatalogue.agreed_tick_size.
Underlyings¶
A future or option names what it is written on only by underlying_symbol, and that name does not always match the underlying's own symbol: the nse's NIFTYFPI contracts are written on the index stored as "Nifty FPI 150", and the bse's SENSEX50 contracts on "SNSX50". So each morning's mapping run decides every live derivative's underlying from the brokers' own records, and master and details answer it as underlying_instrument_id.
Dhan, Groww and Fyers each give the exchange's code for a derivative's underlying. The same codes identify the candidates: Dhan's and Groww's own token for a share or a future is the exchange's code, and Fyers records an index's and a share's own code. A derivative's codes are looked up only on its own exchange and in the segments its underlying can be in, which are the family's cash or index segment and, for an option, the family's futures segment, because an MCX option is written on a future. The table below lists the four outcomes.
| Status | Meaning | underlying_instrument_id |
|---|---|---|
resolved |
Every code leads to one and the same instrument | That instrument's id |
ambiguous |
The codes lead to more than one instrument | null |
unresolved |
The codes lead to no instrument | null |
no_code |
No broker gives a code | null |
The table below counts the decisions for 2026-09-28, the first date they were made, by family. Every resolved link was checked that day: its name equalled the derivative's underlying_symbol except for NIFTYFPI, SENSEX50 and the 91-day treasury bill 91DTB, whose underlyings carry other names, and every linked future expired on or after its option.
| Derivatives | Live | Resolved | Ambiguous | Unresolved | No code |
|---|---|---|---|---|---|
| Equity, nse and bse | 118,014 | 113,116 | 1,017 | 18 | 3,863 |
| Commodity, mcx, nse and ncdex | 48,699 | 34,254 | 0 | 1,976 | 12,469 |
| Currency, nse and bse | 25,184 | 0 | 0 | 11,770 | 13,414 |
| Fixed income, nse and bse | 6,225 | 4 | 0 | 54 | 6,167 |
A commodity or currency contract's own underlying is a reference record, so an option there resolves to the future it settles into, or not at all; the nse's currency codes lead to nothing in the right segments, and neither Dhan, Groww nor Fyers gives a code for NCDEX or most fixed income contracts. The ambiguous ones are the bse's FOCIT contracts, whose code names two bse indices.
Under the hood
stock_brokers/instruments/mapping/utilities/underlyings.py decides the date's underlyings straight after the contract sizes, and writes every decision, its status and each broker's code to unified.underlyings. The warm copies the resolved ones into the hash unified:catalogue:<date>:underlyings, keyed by the derivative's id. details reads it in the same pipelined round trip as the seen dates, so the route's cost is unchanged, and master reads it once per batch of 5,000. A past date or a cold cache reads unified.underlyings instead. Run python -m stock_brokers.instruments.mapping.utilities.underlyings --date YYYY-MM-DD --dry-run to see a date's decisions without writing them.
Additional details¶
GET /api/instruments/additional_detailsaccess-token
This route returns the extra columns each broker publishes about an instrument beyond what an order needs, such as the ISIN, the series, the freeze quantity and the price band. Every broker spells these columns differently, so the project maps them onto one shared set of 16 names. Every name is present for every broker, with null where that broker publishes nothing.
Request parameters¶
| Name | In | Type | Required | Description |
|---|---|---|---|---|
access-token |
header | string | Yes | The token from connect |
instrument_id |
query | string | One spelling | The instrument's UUID |
exchange, segment and the identity fields |
query | string | The other spelling | See Naming an instrument |
date |
query | string | No | YYYY-MM-DD, to read the attributes as they were on an earlier mapping day |
Example¶
import os
import requests
response = requests.get(
'http://127.0.0.1:8080/api/instruments/additional_details',
headers={
'access-token': os.environ['ACCESS_TOKEN'],
},
params={
'instrument_id': '11111111-1111-5111-8111-000000000003',
},
timeout=10,
)
for entry in response.json()['carried_by']:
print(entry['broker'], entry['isin'], entry['freeze_quantity'])
Response¶
The example below is shortened to one broker, and the attribute values are illustrative.
{
"instrument_id": "11111111-1111-5111-8111-000000000003",
"exchange": "nse",
"segment": "nse_equities",
"shape": "security",
"symbol": "RELIANCE",
"underlying_symbol": null,
"expiry_date": null,
"strike_price": null,
"option_type": null,
"mapping_date": "2026-09-26",
"attribute_names": [
"isin", "display_name", "instrument_type", "series", "freeze_quantity",
"price_band_high", "price_band_low", "multiplier", "underlying_token",
"surveillance_category", "permitted_to_trade", "buy_allowed", "sell_allowed",
"intraday_leverage", "margin_trading_leverage", "pledge_eligible"
],
"carried_by": [
{
"broker": "dhan",
"isin": "INE002A01018",
"display_name": "Reliance Industries",
"instrument_type": "EQUITY",
"series": "EQ",
"freeze_quantity": null,
"price_band_high": null,
"price_band_low": null,
"multiplier": null,
"underlying_token": null,
"surveillance_category": null,
"permitted_to_trade": null,
"buy_allowed": null,
"sell_allowed": null,
"intraday_leverage": null,
"margin_trading_leverage": null,
"pledge_eligible": null
}
]
}
Response attributes¶
The answer starts with the nine identity fields described under master, followed by the fields below.
| Attribute | Type | Description |
|---|---|---|
mapping_date |
string | The mapping date answered |
attribute_names |
array of strings | The 16 shared attribute names, in a fixed order |
carried_by |
array of objects | One entry per broker that published attributes, sorted by broker |
carried_by[].broker |
string | The broker's code |
carried_by[].<attribute name> |
string or null | The broker's value as text, stripped of surrounding spaces, or null when the broker does not publish it |
Values are always text, exactly as the broker's file held them, because the raw instrument tables store every column as text. Nothing here is converted into a number.
Status codes¶
| Status | When |
|---|---|
| 200 | The instrument was found. carried_by is empty when no broker publishes any of the attributes. |
| 400 | Any of the parameter messages listed under details. |
| 401 | Access token is required, Invalid access token or Access token has expired. |
| 404 | no instrument <id or fields> is mapped on <mapping date>, or nothing had been mapped on or before <date>. |
| 503 | no instruments have been mapped yet. |
Additional details for several instruments¶
POST /api/instruments/additional_detailsaccess-token
The POST form takes a list of instruments in a JSON body and answers {"results": [...]}, with each answered entry's data exactly as above. The body can carry date, which applies to every instrument. Several instruments at once describes the body, the entries and the statuses.
The attributes of every instrument found in the cache are read with one HMGET. When some come back empty, one EXISTS tells an instrument whose brokers publish nothing apart from a hash that was never warmed, and in the second case those instruments are read from TimescaleDB together, in one query.
Under the hood
- Route:
InstrumentsBlueprint.additional_details, which callsInstrumentCatalogue.additional_details, orInstrumentCatalogue.additional_details_manyfor aPOST. - Vocabulary:
RawAttributesinstock_brokers/instruments/mapping/utilities/raw_attributes.pymaps each broker's column names onto the shared names. Storage keeps only what a broker published, andRawAttributes.filladds the missing names back asnullfor the answer. - Redis:
unified:catalogue:<date>:additional_attributes, warmed by the same daily run as the rest of the catalogue. - Fallback: the mapping table in TimescaleDB, for a past date or when that hash is missing. Each fallback is logged.