JSON output
Add -o json (or IDX_OUTPUT=json, or output = "json" in the config file)
and every command prints one JSON document to stdout. Errors go to stderr as
JSON too. Nothing else is ever written to stdout in JSON mode, so
idx -o json ... | jq is safe. -o jsonl prints one record per line instead
(see JSON Lines). -o auto prints a table when stdout
is a terminal and this JSON document when it is piped or captured; it is
opt-in, the default stays table. Commands without an envelope (version,
schema, config get, cache stats|clear, mcp setup, stockbit status|logout) follow the same flag, IDX_OUTPUT and config file.
Machine-readable schemas: idx schema <name> with quotes, history,
intraday, profile, fundamentals, growth, valuation, risk,
compare, financials, earnings, dividends, returns, market,
brief, technical, screen, news, sentiment, insights,
foreign-value, astro, ownership-holders, ownership-entity, ownership-changes,
ownership-foreign, ownership-foreign-flow, ownership-releases,
ownership-import, ownership-discover, ownership-search,
ownership-cross-holders, ownership-concentration, ownership-graph,
ownership-resolve-candidates, ownership-resolution,
ownership-resolutions, ownership-composition, ownership-snapshot-sync,
ownership-status, ownership-snapshot, ownership-institutions,
watchlist, stockbit-actions, stockbit-movements, stockbit-status,
error, quote, candle.
-o csv prints the payload's main list (candles, rows, items, holders,
changes, dividends, ...) as comma-separated rows with a header; nested objects
become dotted columns (valuation.pe_trailing), arrays of scalars join with
|, and a payload with no list becomes one row. Warnings still go to stderr.
JSON Lines (-o jsonl)
-o jsonl (or IDX_OUTPUT=jsonl, or output = "jsonl") writes one compact
JSON value per line to stdout, each ending in \n, and nothing else. Each
line is exactly the object -o json carries at that path: same keys, same
order, nulls kept. No new schema: every line is a shape idx schema <name>
already documents.
- Which list becomes lines is fixed per schema (table below), not guessed from the data. An empty list prints nothing and exits 0.
- The envelope is not printed. Its
warningsand the stale notice go to stderr aswarning: ...lines, as in table mode (--quiethides them). Provenance (provider,fetched_at,cached,stale) and header fields such as a screen's funnel counts or the changes summary are only in-o json. - Errors are the same one-line JSON on stderr as with
-o json. - Commands without an envelope print one line per item of an array, else one
line.
*pathcommands andcompletionskeep their plain output. - Table-only flags (
history --limit,broker --top,dividends --limit,financials --statement) never trim JSON, JSONL or CSV.
| schema | one line per |
|---|---|
idx.quotes.v1, idx.compare.v1, idx.returns.v1, idx.broker.v1, idx.bandar.v1, idx.ownership.releases.v1, idx.ownership.import.v1, idx.ownership.discover.v1, idx.ownership.resolution.v1, idx.ownership.resolutions.v1 | item of data |
idx.history.v1 | data.candles[] |
idx.intraday.v1 | data.bars[] (the quote and the summary stay in -o json) |
idx.financials.v1 | data.periods[] |
idx.dividends.v1 | data.dividends[] (yearly totals, splits and TTM figures stay in -o json) |
idx.screen.v1, idx.bandar.screen.v1, idx.ownership.foreign-flow.v1, idx.ownership.concentration.v1 | data.rows[] |
idx.watchlist.v1 | data.symbols[], JSON strings (idx -o jsonl watch list | jq -r .) |
idx.holidays.v1 | data.holidays[] |
idx.instruments.v1 | data.instruments[] |
idx.members.v1 | data.members[] |
idx.indices.v1 | data.indices[] |
idx.calendar.v1 | data.events[] |
idx.foreign.v1, idx.foreign.value.v1 | data.days[] |
idx.market.brokers.v1 | data.brokers[] |
idx.news.v1 | data.items[] |
idx.insights.v1 | data.insights[] (the peers stay in -o json) |
idx.ownership.holders.v1, idx.ownership.institutions.v1, idx.ownership.cross-holders.v1 | data.holders[] |
idx.ownership.entity.v1 | data.holdings[] |
idx.ownership.changes.v1 | data.changes[] |
idx.ownership.foreign.v1 | data.points[] |
idx.ownership.search.v1 | data.matches[] |
idx.ownership.resolve-candidates.v1 | data.candidates[] |
idx.stockbit.actions.v1 | data.actions[] |
idx.stockbit.movements.v1 | data.movements[] |
idx.profile.v1, idx.fundamentals.v1, idx.growth.v1, idx.valuation.v1, idx.risk.v1, idx.technical.v1, idx.brief.v1, idx.sentiment.v1, idx.ownership.snapshot.v1 | data on one line (a single record) |
idx.earnings.v1, idx.actions.v1, idx.market.v1, idx.market.foreign.v1, idx.market.foreign.value.v1, idx.bandar.broker.v1, idx.orderbook.v1, idx.ownership.graph.v1, idx.ownership.composition.v1, idx.ownership.snapshot-sync.v1, idx.ownership.status.v1, idx.astro.v1 | data on one line (several peer lists kept together) |
idx -o jsonl stocks history BBCA --period 1mo | jq -c '{time, close}'
idx -o jsonl market holidays --year 2026 | wc -l # 22
The same rule reads idx's own output back: - in a ticker list accepts an
-o json envelope or -o jsonl lines on stdin and takes each row's symbol
(JSON strings are tickers), so
idx -o jsonl stocks screen -f "pe<10" -n 5 | idx stocks compare - works
without jq. A bare ticker read this way is an IDX one whatever IDX_EXCHANGE
says, because that is what idx meant when it wrote it (D05.SI keeps its
suffix); the same holds for a watchlist file (first line # idx watchlist).
Other text on stdin is read like typed arguments and follows IDX_EXCHANGE.
Envelope
Every successful command wraps its payload:
{
"schema": "idx.quotes.v1",
"provider": "yahoo",
"fetched_at": "2026-10-16T17:45:07Z",
"cached": false,
"stale": false,
"warnings": ["no data for ZZZZ"],
"data": ...
}
| field | type | meaning |
|---|---|---|
schema | string | Payload shape and version. Changes only when a field is renamed or removed. |
provider | string | Source that produced the data (yahoo, later msn, ...). |
fetched_at | RFC 3339 UTC | When the underlying response left the network. For cached responses this is the original fetch time. |
cached | bool | Served from the local cache. |
stale | bool | The cached copy was older than its TTL and was used anyway (--offline, or the network failed or the source refused the request). |
stale_reason | string? | Why stale is true: offline (--offline: the network was not tried) or fetch_failed (the live request failed, or the source's bot protection refused it, such as an idx.co.id challenge that stayed through the retries; a live fetch failed (...); showing cached copy warning names the failure). Omitted when stale is false. |
warnings | string[] | Non-fatal notes. Omitted when empty. |
data | object or array | The payload, see below. |
An answer built from several responses (a brief's sections, several
tickers, a range read one session at a time) carries all of them: cached
is true only when every response came from the cache, stale when any was,
and fetched_at is the oldest one's fetch time (a screen's per-company
fundamentals count toward cached and stale only). So cached: false
with a fetched_at from before the call means some parts were fetched for
it and the others came from the cache; the web terminal labels such an
answer partly cached.
idx.quotes.v1 (stocks quote)
data is an array of quote objects, one per symbol the source knew. Symbols
it did not know appear in warnings; if none were known the command fails
with exit code 4.
| field | type | notes |
|---|---|---|
symbol | string | IDX ticker without suffix, e.g. BBCA; a ticker from another Yahoo Finance market keeps its suffix, e.g. D05.SI (see below) |
name | string? | Company name |
currency | string? | IDR |
financial_currency | string? | Currency of the financial statements when the source names it (Yahoo's financialCurrency; null from MSN, Stockbit and the chart fallback). Issuers such as BSSR, GEMS and ITMG report in USD: their price_to_book then divides the IDR price by a USD book value (BSSR 48,921.57 on 2026-11-06), published as reported, never converted. P/E, EPS and the dividend yield are in the price's terms |
price | number? | Last trade |
change, change_pct | number? | Versus previous close; change_pct is in percent (-14.81) |
previous_close, open, day_high, day_low | number? | |
volume, avg_volume_3m | integer? | Shares |
market_cap | number? | In currency |
pe_trailing, pe_forward, eps_ttm, price_to_book | number? | |
dividend_yield_pct | number? | Percent, e.g. 6.27 |
week52_high, week52_low, sma50, sma200 | number? | |
shares_outstanding | integer? | |
market_state | string? | REGULAR, CLOSED, ... |
market_time | RFC 3339? | Last trade time in WIB (+07:00) |
source | string | Endpoint that produced the row: yahoo:quote (full) or yahoo:chart (reduced fallback) |
behind_session | bool? | true when market_time is dated (WIB) before the latest IDX session that has ended (16:15 WIB, the end of post-closing; weekends and the bundled IDX holidays are skipped): the stock is suspended or did not trade, or the source lags the close. false when the last trade is from that session or later. null when market_time is unknown (Stockbit, MSN rows without a trade) or the symbol does not trade on IDX (^GSPC, ^N225, another market's D05.SI). Computed on every call, never cached. For years the holiday list does not cover only weekends are skipped, and unscheduled closures read as behind. |
? means the value is null when the source did not return it. Nothing is
estimated. When source is yahoo:chart, expect market_cap, P/E, dividend
and average-volume fields to be null.
Other markets: the Yahoo-backed commands (stocks quote, history,
profile, financials, earnings, dividends, returns, fundamental,
technical, compare) also take a ticker with another Yahoo Finance suffix,
or a bare one with IDX_EXCHANGE set. In every payload an IDX ticker stays
bare (BBCA) and another market's keeps its suffix (D05.SI); currency is
what Yahoo reports (SGD), and times are still given in WIB (+07:00): the
instant is right, the wall clock is Jakarta's, not the exchange's. Dates that
name a trading session (the chart fallback's previous_close, and price_date
and the dividend dates in idx.dividends.v1) are taken on the exchange's own
clock, so a Toronto close at 16:00 EST counts for that day, not the next.
idx.history.v1 (stocks history)
{
"symbol": "TLKM",
"name": "...",
"currency": "IDR",
"period": "1y",
"interval": "1wk",
"candles": [
{ "time": "2025-10-13T09:00:00+07:00", "open": 2900.0, "high": 2960.0, "low": 2850.0,
"close": 2880.0, "adj_close": 2804.6, "volume": 512345600 }
],
"summary": {
"start": "2025-10-13T09:00:00+07:00", "end": "2026-10-16T09:00:00+07:00",
"first_close": 2880.0, "last_close": 1900.0, "change": -980.0, "change_pct": -34.03,
"high": 3990.0, "low": 1900.0, "avg_volume": 578600000, "candles": 54
}
}
candlesis oldest first and is never truncated by--limit(that flag only affects the table).- Bars with a missing open/high/low/close (holidays, or the current session before its first trade) are dropped.
intervalis the bar size the source actually returned. Yahoo can answer with coarser bars than asked; the payload then carries its real size and a warning says so.--period maxwith1dor1wkasks Yahoo for the whole history (period1=0), so it is daily or weekly (about 9,200 daily bars for^JKSE, from 1990-04-06), not the monthly barsrange=maxreturns. The web terminal caches a history in one D1 row (2 MB): one that would not fit (all daily bars of an index older than IHSG) keeps its most recent candles,summarystill covers them all, and a warning says so.adj_closeis present when the source supplies split/dividend adjusted closes.summaryisnullwhen there are no candles.intervalis one of1m 2m 5m 15m 30m 1h 1d 1wk 1mo. One-minute bars cover at most 5 days (period1dor5d) and two-minute bars at most1mo; a longer window is a usage error (exit 2), because Yahoo answers HTTP 422 beyond 8 days of 1m and 60 days of 2m data.
idx.intraday.v1 (stocks intraday)
The latest session of one stock or index by the minute (or the bar size asked
for): today's session while the market trades, otherwise the last one.
Yahoo's chart runs about 10 minutes behind the market (its quote says
exchangeDataDelayedBy: 10; on 2026-11-06 at 09:13:21 WIB the last row was
stamped 09:03:20), so its one-day window is empty for the first minutes after
the 09:00 open (no bars, and the previous session has dropped out; the first
row of the day came at 09:10 WIB). idx then reads the five-day window and
shows its last session, with a warning naming the date.
{
"symbol": "BBCA",
"name": "PT Bank Central Asia Tbk",
"session": "2026-11-05",
"interval": "1m",
"quote": { "symbol": "BBCA", "price": 5125.0, "change": 195.0, "change_pct": 3.955,
"previous_close": 4930.0, "open": 5050.0, "volume": 166336900,
"market_state": "PREPRE", "market_time": "2026-11-05T16:14:43+07:00", "...": "..." },
"bars": [
{ "time": "2026-11-05T09:00:00+07:00", "open": 5025.0, "high": 5100.0, "low": 5025.0,
"close": 5100.0, "adj_close": null, "volume": 0 }
],
"summary": {
"first_bar": "2026-11-05T09:00:00+07:00", "last_bar": "2026-11-05T16:14:00+07:00",
"bars": 333, "open": 5025.0, "high": 5125.0, "low": 5025.0, "close": 5125.0,
"bars_volume": 155142100, "vwap": 5089.98
}
}
| field | type | notes |
|---|---|---|
session | date? | WIB date of the bars; null when there are none |
interval | string | Bar size asked for: 1m (default), 2m, 5m, 15m, 30m, 1h |
quote | object? | The idx.quotes.v1 quote object as its source reports it; null (with a warning) when the quote could not be fetched |
bars | array | Candles as in idx.history.v1, oldest first; -o json carries every bar (--limit only trims the table), the MCP tool keeps the last 120 by default |
summary | object? | Over every bar of the session; null without bars |
summary.open, close | number | First bar's open, last bar's close |
summary.high, low | number | Highest high, lowest low |
summary.bars_volume | integer | Sum of the bars' volume, shares |
summary.vwap | number? | Volume-weighted average of each bar's typical price, (high + low + close) / 3, computed by idx and rounded to 2 decimals; null when no bar has volume |
- The summary describes the bars, the quote the session as the source reports
it, and the two differ: on Yahoo the opening auction's volume is in no bar
(the 09:00 bar shows volume 0, so
bars_volumeis belowquote.volume), the first bar's open is not always the opening price, the pre-closing auction lands in the 16:00 bar, and 16:01 to 16:14 are trades at the closing price. - Minutes without a trade have no bar; the lunch break has none (Monday to Thursday 12:00 to 13:30, Friday 11:30 to 14:00).
- VWAP is an approximation: IDX's average price is the regular market's value divided by its volume, which needs every trade. For BBCA on 2026-11-05 the bars give 5,089.98 against IDX's 5,088.5.
- Indices carry no volume on Yahoo, so their
vwapisnullandbars_volumeis 0. - Warnings: no quote (and why), the five-day retry when the 1-day window is empty (for some minutes after the open, or no trade today), no bars (the stock did not trade, or the source has none outside trading hours), and bars and quote from different dates.
- In session, and in multi-day windows after the close, Yahoo ends with a row
stamped at the last trade time (e.g. 09:02:16 or 16:14:43); it is a snapshot,
not a bar, and is left out, in
idx.history.v1too. Bars sit on the interval's grid from the window's first bar (09:00), so a row off that grid, or one that repeats the time of the row before it, is the snapshot. - With
--provider stockbitthe bars are Stockbit's realtime one-minute candles for the day (none outside the session).
idx.screen.v1 (stocks screen)
data.rows[] carry the quote-level fields (price, change_pct,
market_cap, pe_trailing, pe_forward, price_to_book,
dividend_yield_pct, eps_ttm, avg_volume_3m, 52-week range) and, when
fundamentals_checked is true, sector, industry, roe_pct, roa_pct,
net_margin_pct, operating_margin_pct, debt_to_equity,
revenue_growth_pct, earnings_growth_pct, peg, price_to_sales.
data.universe, quoted, matched_quote_level, fundamentals_checked,
fundamentals_capped and total_matches describe the funnel, so a reader can
tell a genuine "no match" from a capped fundamentals pass.
rows[].currency and rows[].financial_currency (optional) are the price's
currency and the statements' when the source names them: Yahoo's currency
and financialCurrency from the quote, else from the fundamentals, as under
idx.quotes.v1 and idx.fundamentals.v1. A row's price_to_book and
price_to_sales are both in financial_currency terms: a fundamentals report
over statements in another currency fills neither (under auto, an MSN report
for a company whose quote names USD statements, since MSN names no statement
currency).
Filters apply to the values as published, with one exception below.
price_to_book comes from the quote and is negative for a company whose
shareholders' equity is below zero, so pb<1 matches those too; add pb>0
(pb>0,pb<1) to keep only companies trading below a positive book value. A
negative debt_to_equity means the same, so de<0.5 matches it too; add
de>0.
The exception: when a row's financial_currency differs from its currency
(issuers such as BSSR, GEMS and ITMG report in USD), its price_to_book and
price_to_sales divide the IDR price by USD figures (BSSR's P/B 48,921.57 on
2026-11-06). The row keeps them as reported, but they are not ratios, so pb
and ps read them as missing: no pb or ps clause matches the row, low or
high, and a pb or ps sort puts it last, as for a row without the value.
Through 0.8.2 they were compared as published, so such a row passed pb>3
and led --sort pb. P/E, EPS and the dividend yield are in the price's terms
and filter as before. The table marks such a P/B with * and names those
companies under it.
Added in 0.8.2, all optional:
data.preset: the preset name (top-performers,worst-performers,high-dividend,low-pe,52w-high,52w-low,high-volume,large-cap) ornull. Presets are idx's own definitions: a clause list plus a sort key and direction (top-performerssorts byret1ydescending,low-peispe>0ascending,52w-highisrange>=95,52w-lowrange<=5, ...;idx stocks screen --helplists them).data.filterslists every clause applied, the preset's first.rows[].range_52w_pct: where the price sits in the 52-week range, computed by idx from the quote, 0 at the low and 100 at the high (not clamped;nullwithout all three figures or when the high equals the low). Filter and sort keyrange.rows[].return_1w_pct,return_1m_pct,return_3m_pct,return_6m_pct,return_ytd_pct,return_1y_pct: price returns in percent as MSN reports them (keysret1w,ret1m,ret3m,ret6m,retytd,ret1y). They come from one request for MSN's whole-market screener list, made only when a filter, the sort or--returnsneeds them, and arenullotherwise or when MSN lists no value. A filter or sort that needs them fails rather than returning an empty screen when the list cannot be fetched.data.returns_source:msn:screenerwhen the return windows were joined, elsenull.
idx.news.v1 (stocks news) and idx.sentiment.v1 (stocks sentiment)
News items: title, url, source (kontan/investasi,
cnbcindonesia/market, ...), published (WIB), summary, matched_by
(ticker | name | all), scope (company when the item mentions the
requested ticker, else market), sentiment (-1..+1), positive_terms,
negative_terms. With a symbol, items lists company headlines first and
then market headlines up to the limit; company_matches counts the company
headlines before the limit and company_only records whether market context
was suppressed. The sentiment report (company headlines only) adds label
(positive | negative | neutral | no_coverage), score, counts, the
covered period, top terms and the strongest headlines, plus method
describing the lexicon rule. MSN Money's crowd sentiment is not part of it:
Finance/SentimentBrowser returns no data for IDX tickers in any market
tried (checked 2026-11-06), so idx.sentiment.v1 stays headline-based.
idx.insights.v1 (stocks insights)
MSN Money's rule-based statements about one company:
{
"symbol": "BBCA", "name": "Bank Central Asia Tbk PT", "updated": "2026-11-05T17:51:31Z",
"good": 17, "bad": 4, "neutral": 0, "not_rated": 9,
"peers": ["BBKP", "BABP", "SUPA", "INPC"],
"insights": [
{ "category": "Valuation", "name": "P/B ratio",
"statement": "BBCA is trading at a P/B multiple (2.10x) above weighted average of its peers (0.56x)",
"short_statement": "...", "status": "bad", "criteria": "lower_is_better",
"value": 2.09778355810005, "benchmark_value": 0.564114114036551, "compared_with": "peers" }
]
}
insights[]keeps MSN's order:Valuation,Earnings,Growth,Performance,Health. The samenamecan appear twice with different benchmarks (peers and industry).status:good,bad,neutralornot_rated(MSN'sNA, usually missing data; an unknown rating is also listed asnot_rated, with a warning).criteria:lower_is_betterorhigher_is_better.valueandbenchmark_valueare as MSN reports them and their units differ by statement: multiples (P/E 10.2), fractions (growth 0.038), rupiah (revenue). For a statement MSN could not rate, a 0/0 pair is its placeholder for missing data and is reported asnull.compared_withis derived by idx from MSN's benchmark object:peers(the peer group's aggregate),industry(the industry aggregate),own_history(the company's earlier quarters or years),price(analyst target against the current price) orthreshold(a fixed level, e.g. the analyst rating scale);nullfor a benchmark idx does not recognise.peers: the IDX tickers in MSN's peer group, resolved with oneFinance/Quotescall; empty with a warning when that lookup fails.updated: when MSN last recomputed the statements (UTC), about once a day.
An unknown ticker exits 4; a rejected MSN key exits 3 and names
IDX_MSN_API_KEY.
idx.dividends.v1 (stocks dividends)
dividends[] (date = ex-date, amount per share as reported, split-adjusted
on Yahoo) and splits[] (date, numerator, denominator, ratio) oldest
first, with price and price_date from the same response. Computed by idx
and documented as such: years[] (year, payments, total, change_pct),
last, ttm_total, ttm_payments, ttm_yield_pct (= TTM total / price),
consecutive_years, as_of.
idx.broker.v1 (broker)
Array, one summary per requested ticker, from Indo Premier (IPOT) or, with a
working Stockbit session, from Stockbit (--source auto, the default; see
below): symbol, name (bundled universe), from and to (the range the
source applied, WIB dates), investor (all | foreign | domestic), board
(all | regular | cash | negotiated), buyers[] and sellers[] (each
broker's gross buying or selling, ranked by lots, most first: rank, code,
name from the bundled IDX member list or null, kind = local |
foreign | state_owned or null, lots (1 lot = 100 shares), value in
IDR, average_price), and the range totals total_value, total_lots,
foreign_net_value (foreign buy minus sell), average_price (null when
nothing traded), buy_value and sell_value (what the selected investors
bought and sold, IDR). Added in place, after rounded: buyer_count and
seller_count (brokers on each side as the source counts them, or null),
buyers_complete and sellers_complete (true when the list holds every
broker on its side) and source (ipot | stockbit, the source of this
summary; --source auto can mix them in one call, and the envelope's
provider is the first summary's). What IPOT's totals mean depends on the
filters (Stockbit's follow the list):
- All investors:
total_value,total_lotsandaverage_priceare the range's;buy_valueandsell_valueboth equal the value traded. - One side (
investorforeignordomestic): the source's totals are that side's buying, sototal_value,total_lotsandaverage_pricearenull,buy_valueis the side's buying as published, andsell_valueis computed by idx from the foreign net: foreignbuy_value - foreign_net_value, domesticbuy_value + foreign_net_value. BBCA on 2026-11-05: foreign investors bought 605.4 B and sold 301.5 B, domestic investors bought 242.8 B, of 848.3 B traded. - A board filter: the source's foreign net covers every board whatever the
filter, so
foreign_net_valueisnull, and so is a side'ssell_value.
IPOT abbreviates figures
of a million and more to one decimal (1.6 M); those arrive as plain numbers
at that precision and the summary carries rounded: true. Nothing is netted
per broker or estimated. A range without trades has empty lists and a warning.
Over a range that starts before the source's latest session and contains it,
the source's foreign net counts that session twice (its value, lots and lists
are right). When a range reached the last two trading days at the time it
was read, foreign_net_value is therefore the sum of the last two sessions
with trades, each read on its own, and of the rest of the range. That is
usually three more requests (four when the range reaches a session still
trading), up to six when the latest sessions had no trades (single sessions
are read back from the end, at most five, until two had trades), and more
only when a part of the range cached while it was current has to be split
again; most of them are cached. If those cannot be read it is null with a
warning.
IPOT publishes the top 10 buyers and the top 10 sellers and no count, so its
buyer_count and seller_count are null and its lists incomplete, unless a
side has fewer than ten rows: then the list is every broker on that side and
the count is its length. A page with trades but no lists yet (a session still
trading) is incomplete. Under auto without a working session a warning says
so when a list reached ten rows.
From Stockbit (/marketdetectors/ in gross mode, see
stockbit.md) every broker that bought and every
broker that sold is listed and counted; Stockbit ranks by value and idx orders
each side by lots, so rank means the same as IPOT's (ties keep Stockbit's
order). Figures are exact (rounded: false); odd lots count as fractions of a
lot. total_value, total_lots, buy_value and sell_value are sums over the
full lists, null when a list is cut, and average_price is total_value
over the shares (total_lots × 100), computed by idx. For all investors
foreign_net_value comes from a second read of foreign investors over the same
range and board, their buying less their selling (with a board filter it is
that board's); for one investor side it is that side's buying less its selling
(foreign) or the opposite (domestic). Stockbit is not read for domestic
investors or the cash and negotiated boards yet: auto takes IPOT there.
Stockbit's classes (Asing, Lokal, Pemerintah) can differ from IPOT's.
With -o csv the rows are one broker per side: symbol, from, to, investor, board, side (buy|sell), rank, code, name, kind, lots, value, average_price, source, side_count, side_complete (the last three: the summary's source, and
that side's buyer_count or seller_count and completeness).
idx.bandar.v1 (bandar detect)
Array, one reading per requested ticker, derived from the IPOT broker summary
(see idx.broker.v1) and the price; the rules are in src/analysis/bandar.rs.
symbol, name, period (1d | 1w | 1m | 3m, or custom for
explicit dates), from, to, investor, board, top (N behind the
headline: 1, 3, 5 or 10), signal (big_accumulation | accumulation |
neutral | distribution | big_distribution: top-N net at +15 %, +5 %,
-5 %, -15 % of value), phase (quiet_accumulation | markup |
quiet_distribution | markdown | neutral, null without a price move),
net_value (IDR), net_lots and net_pct (of value) of the top-N tier,
price (last price of the final session: the quote's when the range ends on
the quote's session, otherwise the close), price_change_pct (from the close
before from), bandar_average (volume-weighted average of the top-N
buyers) and bandar_gain_pct (price against it), total_value,
total_lots, foreign_net_value, foreign_net_pct, tiers[] (one per N in
1, 3, 5, 10: top, buy_value, sell_value, net_value, buy_lots,
sell_lots, net_lots, net_pct, buy_share_pct, sell_share_pct,
buy_average, sell_average, signal), buyers[] and sellers[] (the
top N, shaped as in idx.broker.v1), daily (null unless --daily:
days[] of date, total_value, net_value, net_pct, signal,
cumulative_net_value, foreign_net_value, then accumulation_days,
distribution_days, neutral_days, streak (+3 = three accumulation
sessions in a row, -2 = two distribution sessions) and
cumulative_net_value), reasons[] and rounded. A range without trades
reads neutral with a single reason. foreign_net_value over several
sessions is rebuilt from single sessions as described for idx.broker.v1;
bandar screen reads its ranges the same way, while bandar broker, which
shows no foreign net, reads only the range pages. With an
investor filter the brokers are that side's, while total_value,
total_lots and every percentage stay those of the stock's value traded,
read from the all-investor summary of the same range and board (one more
request per range, and per session with --daily).
idx.bandar.screen.v1 (bandar screen)
An object: period, from, to, top, preset (or null), filters
(every clause, the preset's first), sort, descending, scanned (tickers
considered: the most traded by value in the latest session, or the ones
given), checked (those with trades), total_matches and rows[]: symbol,
name, signal, phase, net_pct, net_value, net_lots, price,
price_change_pct, bandar_average, bandar_gain_pct, buy_share_pct,
total_value, foreign_net_value, foreign_net_pct, top_buyers[] and
top_sellers[] (broker codes), rounded. Filter keys: acc (net_pct),
net, lots, fnet, fpct, gain, chg, turnover, conc
(buy_share_pct), price, signal=, phase=. With -o csv the rows are
the screen rows.
idx.bandar.broker.v1 (bandar broker)
An object: code, name (bundled member list), period, from, to,
scanned, checked (scanned stocks with trades), failed, bought_value
and sold_value (sums of value over every row, before --limit), then
bought[] (stocks where the broker is a top-10 buyer) and sold[] (top-10
seller), largest value first: symbol, name, rank on that side, lots,
value, average_price, share_pct (of all value traded in the stock),
opposite_value (the broker's value on the other side when it is also top 10
there, else null) and net_value (buy minus sell, in both lists, when both
sides are visible). Only the top 10 per side are published, so a broker below
tenth place in a stock is absent; nothing is estimated for it.
The web terminal answers bandar screen and bandar broker from the Worker's
scan of the last session instead of scanning live: from 16:45 WIB after each
close it waits for IPOT to publish the session's broker lists, then reads the
100 most traded stocks. The shapes are the same. When the newest trading
session is not the one shown, warnings say which one is shown and why (the
newer scan is waiting for IPOT, running, ended without IPOT publishing the
session or answering, read too few stocks, or starts at 16:45 WIB) and, from
two sessions behind, how many sessions behind it is. They also count the shown
session's stocks without a broker summary or not published by IPOT and say
when the scan reads those again or, when that would be at or after the next
session's 16:45 WIB, that they count as failed or not published then. They
also say when its stocks were picked from the quote board of another session
(a scan that started after the next open). Each stock counts once in
scanned. fetched_at is when the shown session's scan last stored a stock.
Before it stored one, it is when the session's record was filed, or when the
scan started while it has no record yet. For a session the Worker's earlier
code scanned to the end, it is when that scan ended; for one it kept no record
of, 16:45 WIB of the session, the earliest its scan could have read it.
idx.actions.v1 (stocks actions)
An object, from IPOT: symbol, name, since (the source's filter;
upcoming events are included), actions[] newest cum date first (kind =
cash_dividend | stock_dividend | stock_split | reverse_split | bonus
| rights_issue | warrant | other, label as published, amount (IDR
per share: the dividend, or a rights issue's exercise price; null where the
source shows 0), ratio_old and ratio_new (old : new, null for cash
dividends), cum_date, ex_date, recording_date, distribution_date,
end_date (end of a rights trading period, or the last step of a split or
bonus), status, details), meetings[] (RUPS: date, time, status,
venue, agenda) and warrants_rights[] (code, kind, exercise_price,
shares, listing_date, maturity_date, status, details).
idx.foreign.v1 (stocks foreign)
An object, from idx.co.id's daily stock summary: symbol, name, from,
to, days[] oldest first (date, close, change, volume, value,
foreign_buy, foreign_sell, foreign_net in shares, and
foreign_net_value_at_close) and totals (sessions, foreign_buy,
foreign_sell, foreign_net, foreign_net_value_at_close,
net_buy_sessions, net_sell_sessions, streak: +3 = three net-buy
sessions in a row, -2 = two net-sell sessions, 0 when the latest was flat).
IDX publishes foreign trading in shares only; foreign_net_value_at_close is
net shares × close, a rupiah scale for comparing stocks, not the value
foreigners traded at. volume and value exclude non-regular (negotiated)
trades, as the source reports them. --days N (MCP days) gives the latest
N sessions; --from D [--to D] (MCP from, to) the sessions of a range
among the latest 250. --source ipot (MCP source: "ipot") answers with
idx.foreign.value.v1 instead.
idx.foreign.value.v1 (stocks foreign --source ipot)
An object, from Indo Premier's broker summary (idx schema foreign-value):
symbol, name, from and to (first and last session shown), days[]
oldest first, one per session the stock traded: date, value (traded by
all investors on all boards, IDR), lots, average_price,
foreign_buy_value (as published), foreign_sell_value
(foreign_buy_value - foreign_net_value, computed by idx),
foreign_net_value (as published) and foreign_share_pct ((buy + sell) / (2 × value) × 100, computed by idx); totals (sessions, value,
foreign_buy_value, foreign_sell_value, foreign_net_value,
foreign_share_pct over all the sessions, net_buy_sessions,
net_sell_sessions, streak as in idx.foreign.v1) and rounded. Every
figure is in rupiah across all boards, negotiated trades included, so it is
not comparable field by field with idx.foreign.v1's regular-board shares;
the two share no field names. Each session costs two single-session requests
(all investors and foreign investors; a range's own foreign net is not used,
see idx.broker.v1), cached, at most 60 sessions. A day without trades is
skipped; a traded day on which no foreigner traded has zeros. The source
publishes a session after the close: while today trades it is left out with
a warning, and so is any day whose page shows trades without broker lists
(not published yet; such a page is cached for minutes, not the week). A
range with no published session with trades is not found (exit 4), and says
so when a session was left out for that reason; when a day failed, that
failure is the error instead, since the day may have traded. --days N with
fewer sessions with trades in the days read gives those, with a warning. It
rounds figures of a million and more to one decimal (1.2 T is ±4 %), and
foreign_sell_value carries the error of both its terms.
idx.market.foreign.v1 (market foreign)
An object for one session: date, stocks, the market sums foreign_buy,
foreign_sell, foreign_net (shares) and foreign_net_value_at_close,
sort (value | shares), and net_buy[] / net_sell[] (largest first):
symbol, name, close, change, change_pct, volume, value,
foreign_buy, foreign_sell, foreign_net, foreign_net_value_at_close.
idx.market.foreign.value.v1 (market foreign --source ipot)
The same question from Indo Premier, in rupiah, in a shape of its own: never
mixed with idx.market.foreign.v1, whose figures are idx.co.id's shares.
An object for one session: date, scanned (the stocks read: the 100 most
traded by value on the latest quotes, the bandar screen's scan, not every
stock), checked (those with a broker summary that had trades), market,
net_buy[] / net_sell[] (largest first) and rounded. Each row:
symbol, name, price, change_pct (from the previous session's close),
value (traded on all boards, IDR), foreign_net_value (the broker
summary's F. NVal, all boards, as published) and foreign_net_pct (of
value, computed). market is IPOT's market statistics for the session:
value (its TOTAL: regular, negotiated and cash), foreign_buy_value,
foreign_sell_value and foreign_net_value, as published; IPOT does not say
which boards its foreign figures cover. provider is ipot.
The session is the latest one IPOT has published broker summaries for (the
bandar screen's 1d); a --date before it is a usage error and a later one
not_found, and --sort shares (idx.co.id's net shares) a usage error. The
market statistics page carries no date and is dated as IPOT's broker ranking
is (see idx.market.brokers.v1 below), with its own copy of BBCA's
statistics page: when it cannot be dated, or is another session's than the
stocks', market is null and a warning says why, so on a trading day from
08:58 to 19:00 WIB it is null unless a copy read outside those hours can
answer. Figures of a million and more are abbreviated, to one decimal on
the broker summaries and two on the market statistics (rounded: true).
The web terminal's market foreign (/api/market/foreign) answers with
this shape from the nightly bandar scan's 1d rows in D1 (the session's
100 most traded stocks) and IPOT's market statistics, read and dated as its
broker ranking is there. idx.co.id refuses the site's Cloudflare Worker, so
source=idx and sort=shares are usage errors; the scan's warnings say
which session is shown when it is not the latest.
idx.market.brokers.v1 (market brokers)
An object for one session: date, members (with trades), total_volume,
total_value, total_frequency, sort (value | volume | frequency),
rounded and brokers[]: rank, code, name (as the source publishes
it), volume, value, frequency, value_share_pct, then buy_value,
sell_value, net_value (bought minus sold) and kind (local |
foreign | state_owned), which only IPOT publishes: null from
idx.co.id. A member's buying and selling count together, so total_value
is twice the market's value.
provider says which source answered: idx, idx.co.id's daily broker
summary (any session), or ipot, IPOT's ranking of its latest session
(--source ipot, MCP source: "ipot"). auto, the default, asks
idx.co.id first; when idx.co.id refuses (Cloudflare), cannot be reached,
fails, offline has no copy, or has not published the session yet, IPOT
answers with a warning saying why, but only if it holds the session asked
for: the --date, or with none the latest session that has ended.
Otherwise idx.co.id's answer stands, with what IPOT holds or why it could
not be read. IPOT could answer for no --date or the latest session that
has ended, outside a trading day's 08:58 to 19:00 WIB. There auto does
not wait out a Cloudflare challenge: idx.co.id is asked once, without the
30 s of retries, and IPOT answers at once (a cached copy of idx.co.id's
summary still answers first, marked stale); only if IPOT cannot stand in
after all is idx.co.id asked again with the retries. --source idx (MCP
source: "idx") always retries, and its blocked error names
--source ipot (MCP source: "ipot") where IPOT could answer. A session
idx.co.id has not published yet (its summary read on its own day has no
rows) is named whatever the source: with no --date the session before
comes with a warning, and a --date of it is not_found. IPOT takes no
date: a --date before its session,
or after today, is a usage error; a later one up to today, or a day without
a session, is not_found; and under auto idx.co.id's error stands. It
abbreviates figures of a million and more to two decimals (2.79 T, so
rounded: true) and publishes net_value with its own two decimals, so it
can differ from buy_value - sell_value. Members without trades are left
out, as idx.co.id leaves them out; a broker IPOT lists apart under another
member's code (JB, [CC] BJB Sekuritas Jawa Barat) is counted in that
member, as idx.co.id counts it, with a warning.
IPOT's ranking page carries no date: the session comes from a second page,
the Today row of BBCA's statistics, read with the ranking (within a
minute). When the two pages roll over to a new session has not been
measured, so a ranking read on a trading day from 08:58 to 19:00 WIB cannot
be dated: in those hours the source answers a provider error (a copy read
then is offline_miss when served offline, and network, naming the
failure, when served because IPOT could not be read), under auto
idx.co.id's error stands, and IPOT is not asked at all unless a copy read
outside those hours is cached. Until the rollover is measured, both pages
are assumed to have rolled over by 19:00 WIB. A date page that names an
older session than the latest that had ended when the pages were read is
then refused, unless the pair is the very one accepted for that session
before. A ranking that has not rolled over by 19:00 while its date page has
is caught only when this cache remembers the previous session's ranking,
from an earlier IPOT read: the same figures under a new date are refused.
The web terminal's brokers (/api/market/brokers) answers from IPOT
only, with this shape and the --source ipot rules and messages: idx.co.id
refuses the site's Cloudflare Worker, so source=idx is a usage error
there, and source=auto reads IPOT with a warning saying so. The Worker
keeps the two pages as one pair and replaces it only when both were read
again, so the copy that stands in for a failed read (stale: true) is
always a pair read together.
idx.indices.v1 (market indices)
date and indices[] in the source's order (IHSG first): code
(COMPOSITE is IHSG), previous, high, low, close, change,
change_pct (computed), constituents, volume, value, frequency,
market_cap.
idx.calendar.v1 (market calendar)
from, to, symbol (filter or null), categories (shown), total
(entries in the range before the category filter) and events[] oldest
first: date, code (a ticker, or an issuer such as DJPU for government
bonds), category (dividend | rights | bonus | split | warrant |
rups | public_expose | buyback | listing | bond | other),
milestone (cum, ex, recording, payment, distribution, listing,
trading_start, trading_end, plan, notice, result, material,
coupon, principal, maturity, report, or null), description as
published, location, meeting_at (RUPS date and time, WIB), expose_date,
source_type and source_step (the source's own fields) and id. Category
and milestone are classified by idx from the wording; entries the source
lists twice are shown once.
idx.holidays.v1 (market holidays)
year (or null for every year), years_covered, trading_days (weekdays
minus holidays in year), today, today_is_trading_day,
next_trading_day and holidays[] (date, weekday, name, name_en).
Bundled; provider is bundled.
idx.astro.v1 (market astro)
{
"date": "2026-11-06",
"at": "2026-11-06T16:00:00+07:00",
"bodies": [
{ "body": "sun", "longitude": 223.96, "sign": "scorpio", "degree": 13.96, "speed": 1.003, "retrograde": false },
{ "body": "mercury", "longitude": 219.92, "sign": "scorpio", "degree": 9.92, "speed": -1.22, "retrograde": true }
],
"moon": { "phase": "waning_crescent", "elongation": 325.98, "illumination": 0.087, "age_days": 26.7,
"previous_new_moon": "2026-10-10T22:50:00+07:00", "next_new_moon": "2026-11-09T14:02:00+07:00",
"next_full_moon": "2026-11-24T21:54:00+07:00" },
"mercury": { "retrograde": true, "retrograde_since": "2026-10-24T14:17:00+07:00",
"retrograde_until": "2026-11-13T22:56:00+07:00",
"next_retrograde": { "from": "2027-02-10T00:33:00+07:00", "until": "2027-03-03T19:41:00+07:00" } },
"weton": { "day": "Jumat", "day_en": "Friday", "pasaran": "Pahing", "neptu_day": 6, "neptu_pasaran": 9, "neptu": 15 },
"shio": { "animal": "horse", "animal_id": "Kuda", "element": "fire", "year_start": "2026-02-17" },
"upcoming": { "from": "2026-11-06T00:00:00+07:00", "to": "2026-12-06T00:00:00+07:00",
"events": [ { "at": "2026-11-09T14:02:00+07:00", "kind": "new_moon", "body": "moon", "sign": "scorpio" } ] },
"natal": { "chart": "launch", "date": "1983-04-01", "at": "1983-04-01T12:00:00+07:00", "bodies": [ ... ],
"transits": [ { "transit": "saturn", "aspect": "trine", "natal": "uranus", "orb": 0.03, "applying": false } ] },
"backtest": null,
"method": "Apparent geocentric positions in the tropical zodiac ...",
"disclaimer": "Traditional astrology, not a forecast or investment advice: ..."
}
- Computed on the machine (no network) for 16:00 WIB on
date, the IDX close;provideriscomputedandcachedisfalse. With--backtestthe envelope carries the^JKSEhistory's provenance instead (provideryahoo,cached,stale,fetched_at, warnings). bodieslists the Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune and Pluto in that order: apparent geocentric ecliptic longitude of date (tropical zodiac) to 2 decimals, thesignanddegreeof that rounded longitude,speedin degrees per day (3 decimals, negative while retrograde) andretrograde(alwaysfalsefor the Sun and Moon). It is the only top-level list, so-o csv market astroprints it.moon.phaseis one of eight 45° sectors of the Moon-Sun elongation centred on the exact phases (new_moon,waxing_crescent,first_quarter,waxing_gibbous,full_moon,waning_gibbous,last_quarter,waning_crescent), sofull_moonspans about 3.7 days; the exact instants are inupcoming.events.illuminationis the lit fraction 0-1 (3 decimals),age_daysthe days since the previous new moon.mercury:retrograde_sinceandretrograde_untilare the stations around the current retrograde (nullwhen direct);next_retrogradethe next period starting afterat.weton: the civil date's day and pasaran (Legi, Pahing, Pon, Wage, Kliwon) with their neptu. Javanese days start at sunset; idx uses the civil date.shio: the Chinese year holding the date (year_startis its Imlek, a China Standard Time date);nullbefore Imlek 1924 and after 2050.upcoming.events(kind:new_moon,first_quarter,full_moon,last_quarter,station_retrograde,station_direct,ingress) from 00:00 WIB ondatefor--daysdays (1-366; the web terminal takes up to 90), oldest first, rounded to the minute.signis the sign the body is in, or the one it enters. The Moon's own sign changes are not listed.natal(nullwith--natal none): IHSG's chart at 12:00 WIB on1983-04-01(launch, the default) or1982-08-10(base), dates from secondary sources.bodiesaddsapproximate(truefor the Moon only: the time of day is unknown).transitslists the aspects (conjunction,sextile,square,trine,opposition) of the Sun and Mercury to Pluto atatto the natal Sun and Mercury to Pluto within 2°, closest first;applyingmeans the orb shrinks over the next day.backtest(nullunless asked):symbol(^JKSE),period(1y,2y,5y,10y,max; the web terminal stops at10y),fromandto(session dates of the first and last return),returns,mean_pct,median_pct(4 decimals),win_rate_pct(2 decimals),tests,alpha(0.05),bonferroni_alpha(alpha / tests, 6 decimals),significant(groups below it),factors[]andmethod. Each factor (moon_phase,mercury,pasaran,sun_sign) lists itsgroups[]:group,n,mean_pct,median_pct,stdev_pct(4 decimals;nullwhennis 0,stdev_pctwhenn< 2),win_rate_pct,t_stat(Welch's t against all other sessions, 3 decimals) andp_value(two-sided, 6 decimals),nullwith fewer than two sessions on either side, andsignificant. Returns are close to close, one close per WIB date; a session that was still trading when the history was fetched (on its date, before 17:00 WIB) is left out with a warning, however much later the cached copy is read. A history coarser than daily is a provider error; fewer than 31 closes isnot_found.methodanddisclaimerare fixed text. There is no interpretive text in v1; meanings, if added, will be a new optional field.
idx.instruments.v1 (stocks list) and idx.members.v1 (market members)
idx.instruments.v1: total, board and search (filters or null) and
instruments[] (symbol, name, listing_date, shares, board).
idx.members.v1: total, search and members[] (code, name,
licenses[]). When idx.co.id cannot be reached and nothing is cached, the
bundled lists stand in (provider: bundled, a warning, and null for what
they lack).
idx.returns.v1 (stocks returns)
Array, one Performance per requested ticker: as_of, price, start,
candles_used, returns (1w 1m 3m 6m ytd 1y 3y 5y, percent, null when
the history is too short), cagr_3y_pct, cagr_5y_pct, volatility_1y_pct
(annualised stdev of daily log returns), max_drawdown_1y and
max_drawdown_all (pct, peak_date, trough_date, recovered),
best_day_1y, worst_day_1y, high_52w, low_52w, pct_from_52w_high,
pct_from_52w_low, days_since_52w_high, and benchmark (symbol, name,
returns, excess in percentage points) or null. Everything is computed
from five years of daily closes; dividends are excluded.
idx.market.v1 (market overview)
as_of, market_state, behind_session (true when even the newest quote,
as_of, is dated before the latest IDX session that has ended; same rule as
the quote field), index (a MarketRow for ^JKSE), universe,
quoted, advancing, declining, unchanged, turnover,
liquid_min_market_cap, and the lists gainers, losers, most_active,
heavyweights of MarketRow (symbol, name, price, change,
change_pct, volume, value, market_cap). The web terminal serves the
same shape plus index_tape, refreshed_at and market_open (false on
weekends and IDX holidays). Its quote, market and screen answers come from a
board a cron refreshes every minute in trading hours and every 15 minutes
outside them; a board older than that allows (10 or 35 minutes) is served
with stale: true, stale_reason: "fetch_failed" and a warning giving its
time.
idx.brief.v1 (stocks brief)
symbol, name, generated_at, key_points[] (plain-language lines), then
one section per topic, each either null or the dedicated command's object:
quote (one Quote, with behind_session; a key point names the last trade
date when it is true), fundamentals (idx.fundamentals.v1 data),
technical, returns (one Performance), dividends (ttm_total,
ttm_payments, ttm_yield_pct, consecutive_years, last,
payments_on_record), ownership (as_of, listed_pct, foreign_pct,
holders_listed, top_holders[] with the same fields as ownership ticker),
news (company_matches, sentiment,
score, headlines[]), and sections_failed[] naming the null sections,
with the reason in the envelope's warnings.
idx.ownership.*.v1 (ownership ...)
ticker (idx.ownership.holders.v1) and entity (idx.ownership.entity.v1)
list Holding rows: one holder's position in one issuer, the holder's
register lines (one per SID, one per scrip account without an SID) summed.
Since 0.8.2 each row adds accounts (lines summed; null for months imported
by idx 0.8.1 or older, which kept one line) and controller_or_affiliate
(CONTROLLER, AFFILIATE OF THE CONTROLLER or null, from the register as
of 30 Oct 2026); both reports add controller_reported (whether the release
has that column). changes rows add accounts_from, accounts_to and
controller_or_affiliate (later release, earlier one for exits). releases
rows keep rows (register lines) and add positions, other_date_rows and
controller_column (null for months imported by idx 0.8.1 or older).
Notes about the months read (imported by an old idx, rows dated otherwise)
come in the envelope's warnings. The versions stay v1: every change is an
added field, and the values now match the documented "one entity's position".
The analytics read the same positions of one release (as_of; percentages on
the 0-100 scale, rounded to 4 decimals at the end):
search(idx.ownership.search.v1):query,as_of,total_matches,matches[]withentity_key(pass it toentity --exactorgraph),holder,matched(exact,spelling,prefix,contains,words),spellings[](every spelling recorded, any month),issuersandtotal_pctinas_of(0 when absent that month),investor_type,local_foreign(largest position;nullwhen absent) andlast_seen.cross-holders(idx.ownership.cross-holders.v1): the filters applied (min_issuers,investor_type,origin),total_matchesandholders[]withissuers,total_pct(a breadth measure, not a share of anything),max_pctandpositions[](symbol,issuer_name,shares_total,pct).concentration(idx.ownership.concentration.v1):sort(hhi,top1,top3),ascending,total_matches,controller_reportedandrows[]withholders_listed,top_holder,top1_pct,top3_pct,listed_pct,undisclosed_pct,hhi(sum of the listed holders' squared stakes, 0-10,000) andcontroller_pct(nullwhen the release has no controller column).graph(idx.ownership.graph.v1):root(ticker:<SYMBOL>orholder:<entity_key>),depth,min_pct,max_nodes,truncated,issuers[]andholders[]with theirhops, thenlinks[](from,to,holder,symbol,shares_total,pct,hops).linkscomes last, so-o csvprints the links.resolve candidates(idx.ownership.resolve-candidates.v1):keepandmerge(entity_key,holder,issuers,last_seen),reasons(renamed,similar_name),renamed[](from,to,symbols) andlisted_together[](as_of,symbol).resolve merge|map|undo(idx.ownership.resolution.v1): a list ofaction,dry_run,resolution(id,kind,subject,holder,entity_key,combined[],created_at),spellings_moved,lines_moved,positions_moved,positions_combinedandreleases[].resolve list(idx.ownership.resolutions.v1) lists the storedresolutionobjects.
discover (idx.ownership.discover.v1) entries add family (above1,
above5, investor-type, classification or composition) and
importable (true for above1 and composition); origin can now also be
ksei.co.id. The envelope's provider names where the entries came from:
idx (idx.co.id), github-mirror (the mirror, when idx.co.id refused),
ksei (ksei.co.id only, as with --family composition), or idx+ksei and
github-mirror+ksei when the families were listed from both.
import (idx.ownership.import.v1) reports add family
(above1 or composition) and other_securities (composition only: rows of
other security types left out). For a composition month release.rows and
release.issuers count the stocks stored, release.entities is 0 and
positions, other_date_rows and controller_column are null. Both stay
v1 (added fields).
idx.ownership.composition.v1 (ownership composition)
KSEI's monthly "Kepemilikan Efek" archive for one stock (see
ownership.md). provider is
ksei when the call downloaded the month, local-db otherwise. With -o csv
the rows are the nine investor types, whatever the number of imported months:
symbol, as_of, code, investor_type, local_shares, foreign_shares, local_pct, foreign_pct.
{
"symbol": "BBCA", "as_of": "2026-10-30", "price": 4980.0,
"shares_listed": 123275050000, "shares_scripless": 52453691120, "scripless_pct": 42.5501,
"local_shares": 17194269088, "foreign_shares": 35259422032,
"local_pct": 32.7799, "foreign_pct": 67.2201,
"by_type": [
{"code": "ID", "investor_type": "INDIVIDUAL", "local_shares": 12030567470,
"foreign_shares": 324800480, "local_pct": 22.9356, "foreign_pct": 0.6192}
],
"history": [
{"as_of": "2026-09-30", "local_pct": 31.1106, "foreign_pct": 68.8894, "scripless_pct": 42.5501, "foreign_change_pp": null},
{"as_of": "2026-10-30", "local_pct": 32.7799, "foreign_pct": 67.2201, "scripless_pct": 42.5501, "foreign_change_pp": -1.6693}
],
"source": "https://www.ksei.co.id/storage/Download/BalanceposEfek20261030.zip"
}
shares_scripless is the local total plus the foreign total: the shares held
in KSEI accounts. Shares still held as certificates are outside the archive.
Every *_pct but scripless_pct is a percent of shares_scripless (4
decimals, null when it is 0); scripless_pct is shares_scripless over
shares_listed. by_type always lists the nine types in KSEI's order (IS,
CP, PF, IB, ID, MF, SC, FD, OT). history is every imported
month for the stock, oldest first; foreign_change_pp is the change from the
previous imported month. price is null when KSEI left it blank.
idx.ownership.snapshot-sync.v1 (ownership sync) and idx.ownership.status.v1 (ownership status)
sync (provider github-mirror): action (installed, updated,
imported or up_to_date), database, snapshot (the mirror's snapshot for
this idx's schema: file, generated_at, db_schema_version, oldest,
newest, releases, bytes; null when the mirror has none), the months
installed from it, imported from the monthly files (missing or
re-published months), kept (only in the
local database, kept without --force) and dropped (replaced with
--force), backup (the .bak path or null), and releases / newest
after the sync. The name is not idx.ownership.sync.v1 because the web
terminal's /api/ownership/status already answers with that one (the state of
its D1 loader).
status (provider local-db): database, exists, bytes,
db_schema_version, supported_schema_version, releases, oldest,
newest, installed_snapshot (from the file's snapshot_info table, null
for a database built by local imports), remote (mirror,
index_generated_at, releases, newest, snapshot; null offline, with
IDX_OWNERSHIP_MIRROR=off or when the mirror cannot be read), missing,
outdated (months held as the installed snapshot carried them while
index.json lists a re-published file; sync imports them again),
local_only and sync (up_to_date, install, update, import or null
without remote). Status never creates the database file.
idx.ownership.snapshot.v1 is the manifest CI publishes as
snapshot-v{V}.json next to the snapshot file (and the data of the hidden
ownership build-snapshot -o json); see
ownership.md.
idx.watchlist.v1 (watch add|rm|list|clear)
path and symbols[]. idx watch itself returns idx.quotes.v1 and
idx watch returns returns idx.returns.v1.
idx.profile.v1 (stocks profile)
data is one object: symbol, name, sector, industry, summary,
employees, founded_year, website, city, country, exchange,
currency, market_cap, shares_outstanding, directors[] (name,
title; populated by MSN, empty from Yahoo) and source.
idx.fundamentals.v1 (stocks fundamental)
data is one object grouped by theme. Percent fields end in _pct and are
already percentages (21.8 = 21.8 %). Ratios are plain multiples
(debt_to_equity: 0.6 = 60 % of equity).
{
"symbol": "BBCA", "name": "PT Bank Central Asia Tbk", "currency": "IDR",
"financial_currency": "IDR", "sector": "Financial Services", "industry": "Banks - Regional",
"price": 5175.0, "beta": -0.017, "week52_high": 8750.0, "week52_low": 4820.0,
"valuation": { "market_cap": 6.357e14, "enterprise_value": 6.099e14, "pe_trailing": 12.87,
"pe_forward": 10.73, "peg": 1.52, "price_to_book": 2.35, "price_to_sales": 5.82,
"ev_to_ebitda": null, "ev_to_revenue": 5.58 },
"profitability": { "roe_pct": 21.82, "roa_pct": 3.67, "gross_margin_pct": null,
"operating_margin_pct": 66.75, "net_margin_pct": 53.12 },
"health": { "debt_to_equity": null, "current_ratio": null, "quick_ratio": null,
"total_debt": 5.2e13, "total_cash": 7.8e13 },
"growth": { "revenue_growth_pct": 2.5, "earnings_growth_pct": 0.4,
"revenue_cagr_5y_pct": 8.63, "eps_growth_3y_pct": 12.21 },
"dividend": { "yield_pct": 6.27, "payout_ratio_pct": 75.42, "ex_dividend_date": "2026-08-31" },
"per_share": { "eps_ttm": 402.07, "eps_forward": 482.48, "book_value": 2201.5, "revenue": 888.2 },
"analyst": { "recommendation": "strong_buy", "target_mean": 8197.5, "target_high": 10100.0,
"target_low": 5900.0, "analysts": 24 },
"fiscal_year_end": "2025-12-31", "most_recent_quarter": "2026-06-30",
"verdict": { "label": "fair", "score": 2, "reasons": ["trailing P/E 12.9 is moderate (10 to 15)", "..."],
"inputs_available": 7 },
"sources": ["yahoo:quoteSummary", "msn:equities", "msn:quotes"]
}
autoasks Yahoo first and fills remainingnulls from MSN;sourceslists who contributed. Values are never averaged across sources.- Yahoo reports gross and EBITDA margins as
0for banks; that is published asnullbecause it means "not applicable", not 0 %. - MSN writes
±999.9999in a ratio field (forward P/E, PEG, P/B, P/S, debt/equity) where it cannot compute one, notably debt/equity when shareholders' equity is negative; that is published asnull. currencyis the price's currency.financial_currencyis the currency of the financial statements when the source names it (YahoofinancialCurrency; MSN does not, so it isnullunder--provider msn). Coal, oil and gas, and airline issuers such as ITMG, MEDC and GIAA report in USD: theirprice_to_book,price_to_sales,ev_to_ebitdaandev_to_revenuethen divide the IDR price by USD figures (ITMG P/B 14103 on 2026-11-06), andper_share.book_value,per_share.revenue,health.total_debtandhealth.total_cashare in USD. idx publishes them as reported, never converted; the verdict does not score P/B then, and the tables mark those multiples with*. Underautonone of these eight fields is filled from MSN, which names no statement currency, so each of them is Yahoo's ornull. P/E, EPS and the dividend yield are consistent.betais the source's own figure. Yahoo's does not track IHSG for IDX shares (BBCA 0.066 on 2026-11-06, against 0.95 from a year of daily closes against^JKSE);stocks riskcomputes beta against IHSG itself.verdict.labelisundervalued|fair|overvalued|insufficient_data(fewer than three rule inputs). Every rule that fired is inreasons. It is a heuristic, not advice.- A negative
per_share.book_value(or a negativeprice_to_bookordebt_to_equity) means shareholders' equity is below zero. The verdict then scores one adverse rule (-2, "negative shareholders' equity ...") and sets the P/B, ROE and debt/equity rules aside; those figures still count ininputs_available. An ROE above 15 % next to a net loss (negative net margin or EPS) is not rewarded either. A P/B that mixes currencies is neither scored nor counted, and no reason is listed for it.
Lenses: idx.growth.v1, idx.valuation.v1, idx.risk.v1 (stocks growth|valuation|risk)
Three focused readings next to the blended verdict, also returned by
stocks fundamental SYMBOL --lens growth|valuation|risk and by MCP
idx_fundamental with lens. Each data carries the inputs it read (the
source's values as reported, null when absent), the figures idx computed
from them, a reading and sources:
"reading": {
"label": "moderate", "score": 2, "inputs_available": 6,
"rules": [
{ "rule": "liquidity", "points": 1, "reason": "current ratio 0.79 is under 1" },
{ "rule": "volatility", "points": 1, "reason": "volatility 49.2% a year is 1.68x IHSG's 29.3% (over 1.5x)" }
],
"notes": []
}
(TLKM's risk reading on the recorded fixtures, two of its six rules shown.
A rule that could not run lands in notes, for example
"leverage and liquidity: not scored for a financial company" for a bank.)
rules[]lists every rule that ran, with itspoints(0 still ran and counts) and areasoncarrying the figure and the threshold;scoreis their sum andinputs_availabletheir number.notes[]names every rule that could not run and why (not reported, a financial company, statements in another currency, negative equity, fewer than 3 analysts, fewer than three fiscal years, benchmark unavailable, a price that barely moved). Fewer than three rules giveinsufficient_data.- Rule ids are stable; the thresholds are heuristics, not advice. The
valuation lens can disagree with the verdict of
stocks fundamental, which also weighs quality and growth. - A lens that needs several inputs fetches them concurrently. A missing one
is named in
warningsand the reading runs with fewer rules; the call fails only when the essential inputs all fail (growth: fundamentals and statements; risk: fundamentals and the stock's candles). It fails withnot_foundwhen the fundamentals or the stock's candles say the ticker is unknown; a statements "not found" (a listed ticker can have none) does not count, so the fundamentals' own error (a provider failure, an offline cache miss) is the one reported.
idx.valuation.v1: points favour cheapness
symbol, name, currency, financial_currency, financial_company,
negative_equity, price, pe_trailing, pe_forward, peg,
price_to_book, price_to_sales, ev_to_ebitda, ev_to_revenue (shown,
not scored), dividend_yield_pct, payout_ratio_pct, target_mean,
analysts, target_upside_pct (computed: target over price, minus one),
reading, sources. Label: cheap (score 3 or more), expensive (-3 or
less), fair, insufficient_data.
| rule | points |
|---|---|
pe_trailing | under 10: +2; 10 to 15: +1; 15 to 25: 0; over 25: -1; over 35: -2; not positive: 0 |
pe_forward | only without a trailing P/E, above 0: under 10: +1; over 25: -1; else 0 |
peg | above 0: under 1: +1; over 2: -1; else 0 |
price_to_book | under 1: +1; over 4: -1; else 0. Not scored with negative equity or mixed currencies |
price_to_sales | under 1: +1; over 5: -1; else 0. Not for a financial company or mixed currencies |
ev_to_ebitda | above 0: under 6: +1; over 15: -1; else 0. Not for a financial company or mixed currencies |
dividend_yield | 4 % or more: +1, but 0 when the payout exceeds 100 %; under 4 %: 0 |
analyst_target | 3 or more analysts: upside over +20 %: +1; under -10 %: -1; else 0 |
idx.growth.v1: points favour growth
symbol, name, statements_currency, most_recent_quarter,
revenue_growth_pct and earnings_growth_pct (latest quarter against the
same quarter a year earlier, as reported), revenue_cagr_5y_pct and
eps_growth_3y_pct (MSN's, when it contributed), eps_ttm, eps_forward,
eps_forward_growth_pct (computed, both EPS above zero), annual[] (one per
fiscal year, oldest first: end_date, revenue, net_income,
revenue_change_pct, net_income_change_pct, the changes computed against
the year before), revenue_cagr_pct and net_income_cagr_pct (computed from
the oldest to the newest fiscal year with a value, at least three such years,
both ends above zero), reading, sources. Label: growing (score 2 or
more), shrinking (-2 or less), mixed (points both ways), stable,
insufficient_data.
| rule | points |
|---|---|
revenue_growth, earnings_growth | latest quarter over +10 %: +1; below 0 %: -1; else 0 (a figure beyond ±100 % is marked "from a small or negative base") |
eps_forward_growth | forward EPS against trailing over +10 %: +1; below -10 %: -1; else 0 |
revenue_cagr | idx's CAGR over three or more fiscal years: over +10 %: +1; below 0 %: -1; else 0 |
revenue_cagr_5y | only with fewer than three fiscal years: MSN's 5-year revenue rate, as reported, same bands |
revenue_consistency | revenue grew every year: +1; in fewer than half the years: -1; else 0 (three or more fiscal years) |
net_income_cagr | idx's CAGR over three or more fiscal years: over +10 %: +1; below 0 %: -1; else 0. A profit turning into a loss: -1; a loss turning into a profit: +1 |
eps_growth_3y | only with fewer than three fiscal years: MSN's 3-year EPS growth, as reported, same bands |
idx.risk.v1: points add risk
symbol, name, currency, financial_company, negative_equity,
debt_to_equity, current_ratio, quick_ratio, net_margin_pct, eps_ttm,
price (null without daily candles for the stock), reading, sources.
price is computed by idx from a year of daily closes on the dates the stock
and IHSG both traded (Yahoo's ^JKSE bar often lags the shares by a session):
benchmark (^JKSE), from, to, observations (matched daily returns),
zero_returns (how many of them left the stock's close unchanged; null
without IHSG), beta and correlation (from simple daily returns, 3 dp,
null under 120 observations), volatility_pct and
benchmark_volatility_pct (annualised standard deviation of daily log
returns), max_drawdown (pct, peak_date, trough_date, recovered;
null when the price never fell) and benchmark_max_drawdown_pct. Without
IHSG the price rules become notes and price carries the stock's own
volatility and drawdown over its own dates.
Label: high (score 3 or more), low (-2 or less), moderate,
insufficient_data.
| rule | points |
|---|---|
negative_equity | +2 (any sector); the leverage rule is then skipped |
leverage | debt/equity over 2x: +1; under 0.5x: -1; else 0. Not for a financial company |
liquidity | current ratio under 1: +1; 1.5 or more: -1; else 0. Not for a financial company |
losses | negative net margin or EPS over the trailing twelve months: +1; else 0 |
beta | against IHSG, 120 or more daily returns: over 1.3: +1; under 0.7: -1; else 0 |
volatility | the stock's over IHSG's: over 1.5x: +1; under 0.8x: -1; else 0 |
drawdown | the stock's largest fall minus IHSG's: 15 points or more deeper: +1; 15 points or more shallower: -1; else 0 |
illiquidity | more than half of 20 or more matched daily returns are zero: +1, and beta, volatility and drawdown are not scored |
Price risk is relative to IHSG so that a market-wide fall (IHSG fell 46 %
peak to trough in the year to 2026-11-04) does not make every stock look
risky. A price that does not move would look calm on all three price rules
(beta 0, no volatility, no fall), yet a suspended share, one stuck at the
floor price or one that barely trades is hard to sell: when the close is
unchanged on more than half the matched sessions, the illiquidity rule
scores instead and the three become a note (their figures stay in price).
Below that line thin trading still biases a daily beta toward 0 and
understates volatility; zero_returns, observations and correlation
show how much they rest on.
idx.compare.v1 (stocks compare)
data is an array of idx.fundamentals.v1 objects in the order requested.
Symbols that failed are listed in warnings.
idx.financials.v1 (stocks financials)
{
"symbol": "BBCA", "currency": "IDR", "period": "annual",
"periods": [
{ "end_date": "2025-12-31", "period": "annual",
"income": { "total_revenue": 1.14e14, "gross_profit": null, "operating_income": null,
"ebitda": null, "ebit": null, "interest_income": 9.8e13, "interest_expense": 1.0e13,
"net_interest_income": 8.8e13, "pretax_income": 7.1e13, "tax_provision": 1.4e13,
"net_income": 5.75e13, "net_income_common": 5.75e13, "eps_basic": 467.0, "eps_diluted": 467.0 },
"balance": { "total_assets": 1.59e15, "total_liabilities": 1.31e15, "stockholders_equity": 2.81e14,
"cash_and_equivalents": 9.1e13, "total_debt": 2.4e12, "net_debt": null, "shares_outstanding": 1.23e11 },
"cash_flow": { "operating": 7.75e13, "investing": -3.4e13, "financing": -4.2e13,
"capital_expenditure": -2.45e12, "free_cash_flow": 7.5e13, "dividends_paid": -3.76e13 } }
],
"source": "yahoo:timeseries"
}
periodisannual(about four fiscal years),quarterly(about five quarters) orttm(trailing twelve months;balancefields arenull).periodsis oldest first. Amounts are incurrencyunits as reported; outflows are negative.- Banks have no EBITDA, gross profit or operating income series at the source,
so those are
null. - Every period carries all three statements.
--statement income,cashnarrows the table only: JSON and CSV keep the three, since a blanked statement would read as data the source does not have.
idx.earnings.v1 (stocks earnings)
data: estimates[], quarters[] (period like 3Q2025, end_date,
reported_date, eps_actual, eps_estimate, surprise_pct), yearly[] and
quarterly[] (period, revenue, earnings, profit_margin_pct), next
(date, date_is_estimate, eps_avg/eps_low/eps_high, eps_growth_pct,
revenue_avg/revenue_low/revenue_high, analysts), ex_dividend_date,
dividend_date, source, scope.
estimates[]is the analyst consensus for this and next quarter and fiscal year, in source order:period(0q,+1q,0y,+1y),period_type(quarterlyorannual),end_date,eps_avg/eps_low/eps_high,eps_year_ago,eps_growth_pct(percent, versus a year earlier),eps_analysts,revenue_avg/revenue_low/revenue_high,revenue_year_ago,revenue_growth_pct,revenue_analysts. An optional addition toidx.earnings.v1: envelopes from older versions lack it. A period with no figure and no analyst is left out, so a ticker no analyst covers has an empty list.- Yahoo pads figures it has not loaded as zeros; idx reports those as
null(a quarter'srevenue,earningsandprofit_margin_pct, a consensus amount inestimates[]ornext), never as0. Analyst counts keep their zero.profit_margin_pctis set only when both revenue and earnings are. scopeisnullunless rows were filtered out (stocks earnings --annual --quarterly --reported --estimates, MCPperiodandkind). Then it is{ "annual", "quarterly", "reported", "estimates" }, eachtruewhen those rows were kept, and an empty list or anullnextcomes from the filter, not the source. Giving neither flag of a pair keeps both.quarters[]andquarterly[]are reported quarterly rows,yearly[]reported annual rows,estimates[]follow theirperiod_type, andnextcounts as a quarterly estimate. The filter applies to JSON and CSV alike.
idx.technical.v1 (stocks technical)
Computed locally from daily candles (provider names where the candles came
from). data: as_of, period, candles_used, price,
change_pct_1d|1w|1m|3m, high_52w, low_52w,
moving_averages (sma20|50|200, ema20|50|200), rsi14,
macd (line, signal, histogram), bollinger (upper, middle,
lower, percent_b, bandwidth_pct), atr14, volume (last, avg20,
ratio), levels (support[], resistance[], nearest first),
trend (uptrend | downtrend | sideways | unknown) and
signal (label buy | sell | neutral, score, reasons[]).
Indicators that need more history than was available are null.
idx.orderbook.v1 (stockbit orderbook)
Realtime via the Stockbit provider (your own session; see
stockbit.md). provider is stockbit. data: symbol,
name, last, change, change_pct, previous_close, open, day_high,
day_low, volume (shares), value (IDR), bids[] and asks[] (each a
level {price, shares, orders}, best price first), and the session foreign
flow foreign_buy, foreign_sell, foreign_net (IDR). Stockbit flags these
three with has_foreign_bs; while it is not true (seen during trading, when
the fields still hold the previous session's figures) they are null and
warnings says why. Sizes are shares (one IDX lot is 100 shares). A level the
book does not show, or a field the source omits, is absent/null.
idx.ownership.institutions.v1 (ownership institutions)
From MSN Money's ownership data (provider is msn), live, no import.
data: symbol, name, institutions_pct, top10_institutions_pct,
funds_pct (percent of shares outstanding, as MSN reports them),
float_pct (free float, percent of shares outstanding), oldest_report,
newest_report, flow (institutions and funds, each new, buyers,
sellers, exited, unchanged, net_shares_change) and holders[]
(institutions first, then funds): kind (institution | fund), rank,
name, shares, shares_change (since that holder's previous report),
pct_outstanding, pct_outstanding_change (percentage points),
pct_portfolio (funds only, percent of the fund), report_date (each
holder's own) and signal (new | buyer | seller | exited |
unchanged, computed by idx; null when shares or shares_change is
missing). Funds overlap their managers: never add the two lists. A stock MSN
lists no holders for has empty holders and a warning. -o csv prints one
row per holder; with no holders it prints the holder header alone, so files
for several symbols share one layout. See ownership.md.
idx.stockbit.actions.v1 (stockbit actions, stockbit calendar)
From the user's own Stockbit session; provider is stockbit. data:
symbol (the stock asked for; null for the calendar), name (from the
bundled issuer list), from / to (the range idx applied; null when open),
kinds (each one request), total (rows Stockbit returned for these kinds
before the range), and actions[], newest first for a stock and oldest first
for the calendar. Each action: symbol (a structured warrant's own code for
those), name (IPOs, tender offers), kind (dividend, stock_dividend,
bonus, rights, warrant, split, reverse_split, rups,
public_expose, ipo, tender_offer), date (the cum date for
entitlements, the ex date without one, else the event's first day),
cum_date, ex_date, recording_date (for a RUPS, the date that decides who
may attend), distribution_date (payment; a tender offer's settlement),
dividend_per_share and currency (ISO code), ratio_old / ratio_new (as
published), factor (Stockbit's price adjustment factor), price (IDR per
share: rights or warrant exercise, tender offer, final IPO price),
trading_start / trading_end (rights and warrants), exercise_start /
exercise_end (warrants), offer_start / offer_end (IPO offering, tender
offer), listing_date, shares and shares_pct (offered or sought),
time (RUPS or expose time as published, WIB), venue, note, id
(Stockbit's row id). Placeholders (empty strings, zeros, 0001-01-01) are
null. A kind that fails is a warnings entry.
idx.stockbit.movements.v1 (stockbit movements)
From the user's own Stockbit session; provider is stockbit. data: the
filters asked for (symbol, from, to, holder_id, action as buy,
sell, transfer, cross or corporate_action, source as idx or
ksei; null when not given), pages_read, more (rows remain past the
pages read, or a later page failed, which warnings names), and
movements[], newest first: date, symbol, holder, holder_id (the
source's id for the holder, the same on all of that holder's rows), roles
(director, commissioner, or another role lowercased), foreign (true,
false, null), action (the source's label, lowercased: buy, sell,
transfer, cross, right_issue, private_placement, tender_offer, ...),
source (idx insider disclosure, ksei holder at 5 % and above),
shares_from, shares_to, shares_change (exact, signed), pct_from,
pct_to, pct_change (percent of the company as the source rounds it),
price and value (IDR, as filed; null when the source only estimates),
estimated_price and estimated_value (the source's estimate when no price
was filed; null otherwise), purpose (as filed), broker and
broker_group (seen on KSEI rows).
stockbit status (idx schema stockbit-status)
A bare object, not an envelope, and never a token value (the MCP tool
idx_stockbit_status returns the same): logged_in (true when the stored
session can work now: its refresh token has not expired, or its access token
has not; each token's own exp decides, one without an exp counts as
unexpired and one that is not a JWT as unusable), renewable (true when idx
can renew the access token because the refresh token has not expired; while
logged_in is true and renewable is false, the session stops working
when the access token expires), expired (true when a session is stored but
its tokens have expired), reason (why the session cannot work or cannot be
renewed, and that idx stockbit login renews it; null while it works and
renews), access and refresh (each present, expires_at in Unix seconds
and expires_in_secs, negative once expired; both null when the token is
absent, not a JWT or without an exp), and session_file (the absolute
path). Nothing here asks Stockbit: a token it revoked, or another tool spent,
still counts until its exp, or until this process sees Stockbit refuse it (an
idx mcp server remembers a refresh token Stockbit refused: renewable turns
false, logged_in follows the access token alone and reason says so).
sign_in is MCP only: the reply of idx_stockbit_login, and
in idx_stockbit_status the latest page sign-in that tool started in the same
server; it is absent otherwise (the CLI never sets it). Its fields: state (waiting, signed_in, failed or
already_signed_in), method (page, browser or none), url and port
(the page on 127.0.0.1 while waiting), ssh_forward (an ssh -N -L command
when the server is reached over SSH), expires_in_secs (until the page stops
listening), already_open (an earlier page was still open and is returned),
warning (the latest note, such as why Stockbit refused a pasted session) and
error (why it failed).
Errors
Written to stderr, exit code non-zero. With -o json, -o jsonl or -o auto
when piped (from the flag, IDX_OUTPUT or the config file, the flag first) an
error is one JSON line; otherwise it is error: ... text, with a hint: line
where one helps:
{"error":{"code":"not_found","message":"not found: no quotes for ZZZZ"},"exit_code":4}
| exit | code | when |
|---|---|---|
| 1 | internal, io | unexpected failure |
| 2 | usage | bad flags or symbol |
| 3 | network, provider, blocked | network failure or the source returned an error; blocked: the source's bot protection (Cloudflare) refused the request with a challenge or a block page. For idx.co.id's data a challenged request is retried for 30 s first (market brokers under auto, where IPOT could answer, asks idx.co.id once and lets IPOT stand in, and retries only if IPOT cannot), and a cached copy, when there is one, is served marked stale instead of the error. A challenge usually passes within a minute: retry then, and from another network if it keeps refusing. A block page can last longer: retry later, or from another network if it persists. Data centres, VPNs and CI runners are refused more often. The message names any other source for the same question (--source ipot for a stock's foreign flow and for the latest session's foreign net of the most traded stocks, market brokers --source ipot for the member ranking of IPOT's latest session when that is the session asked for); for the ownership register, the GitHub mirror |
| 4 | not_found | symbol or record does not exist |
| 5 | offline_miss | --offline and nothing cached |
| 6 | config, unsupported | bad config file or env value; or the selected provider cannot serve this command (--provider auto can) |
Compatibility promise
- Adding an optional field to a payload does not bump the
schemaversion. - Renaming, removing or changing the type/unit of a field does, and the old version is kept for at least one minor release behind a flag.
- Numbers are plain JSON numbers (no strings), timestamps are RFC 3339.