Skip to content

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: InstrumentBatch in unified_broker_interface/utilities/instrument_batch.py.
  • Resolving the list: InstrumentCatalogue.resolve_many. The catalogue look-ups share one pipeline through MappingRedisTier.read_catalogue_for_prefixes, and every identity is read with one HMGET.
  • 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

curl http://127.0.0.1:8080/api/instruments/segments \
  -H "access-token: $ACCESS_TOKEN"
import os

import requests

response = requests.get(
    'http://127.0.0.1:8080/api/instruments/segments',
    headers={
        'access-token': os.environ['ACCESS_TOKEN'],
    },
    timeout=10,
)
for segment in response.json()['segments']:
    print(segment['segment'], segment['identity_fields'])

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.segments in unified_broker_interface/blueprints/instruments.py, which calls InstrumentCatalogue.segments.
  • Redis: the hash unified:catalogue:<mapping date>:segments holds the count per segment.
  • Fallback: a count(*) over unified.instruments, limited to instruments with a row in unified.broker_mappings on the mapping date. Each fallback is logged.
  • The segment order is CANONICAL_SEGMENTS in stock_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

curl -i "http://127.0.0.1:8080/api/instruments/master?exchange=mcx&segment=commodity_futures" \
  -H "access-token: $ACCESS_TOKEN"
import os

import requests

response = requests.get(
    'http://127.0.0.1:8080/api/instruments/master',
    headers={
        'access-token': os.environ['ACCESS_TOKEN'],
    },
    params={
        'exchange': 'mcx',
        'segment': 'commodity_futures',
    },
    timeout=300,
)
print(response.headers['X-Mapping-Date'], len(response.json()))

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.

HTTP/1.1 200 OK
Content-Type: application/json
X-Mapping-Date: 2026-09-26
[
  {
    "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 calls InstrumentCatalogue.master and streams through json_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 hash unified:catalogue:<date>:identity.
  • Fallback: a server-side cursor over unified.instruments, for a past date or a cold cache.

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.

  1. Names that equal the term exactly.
  2. Names that start with the term.
  3. 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

curl "http://127.0.0.1:8080/api/instruments/search?exchange=nse&segment=equities&q=infy&limit=5" \
  -H "access-token: $ACCESS_TOKEN"
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 calls InstrumentCatalogue.search. SEARCH_LIMIT_DEFAULT is 50 and SEARCH_LIMIT_MAXIMUM is 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 LIKE query over unified.instruments with 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

curl "http://127.0.0.1:8080/api/instruments/details?exchange=nse&segment=equities&symbol=RELIANCE" \
  -H "access-token: $ACCESS_TOKEN"
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 calls InstrumentCatalogue.details, or InstrumentCatalogue.details_many for a POST. The single method passes a list of one to the list method, so both forms share one path.
  • Parameter parsing: parse_instrument in unified_broker_interface/utilities/instrument_identity.py.
  • Redis: unified:catalogue:<date>:identity, unified:catalogue:<date>:order_handles and unified:catalogue:<date>:seen.
  • Fallback: unified.instruments for the identity and seen dates, and unified.broker_mappings for the handles.
  • Consensus: units_per_lot in stock_brokers/instruments/ticks/utilities/resolution.py and InstrumentCatalogue.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

curl "http://127.0.0.1:8080/api/instruments/additional_details?instrument_id=11111111-1111-5111-8111-000000000003" \
  -H "access-token: $ACCESS_TOKEN"
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.

curl -X POST "http://127.0.0.1:8080/api/instruments/additional_details" \
  -H "access-token: $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"instruments": [{"instrument_id": "11111111-1111-5111-8111-000000000002"}, {"instrument_id": "11111111-1111-5111-8111-000000000003"}]}'

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 calls InstrumentCatalogue.additional_details, or InstrumentCatalogue.additional_details_many for a POST.
  • Vocabulary: RawAttributes in stock_brokers/instruments/mapping/utilities/raw_attributes.py maps each broker's column names onto the shared names. Storage keeps only what a broker published, and RawAttributes.fill adds the missing names back as null for 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.