Nothing here is a bug report. A limitation that would be fixed by fabricating a
value — a guessed ticker↔CNPJ join, a derived split factor, an English label
keyed on a code that means two things — stays a limitation on purpose.
Account codes are not comparable across sectors
CVM’s chart of accounts is sector-specific. The sameaccount_code carries a
different line in a bank’s filing than in an industrial company’s, and nothing in
the code itself tells you which chart you are reading. Measured against the live
API for PETR4 (industrial) and Banco do Brasil (cd_cvm 1023, bank):
Four consequences, all visible in the response:
setorandsegmentoare on everyfinancialsandcompany_financialsrow. They are there to be used as a partition key, not read as a label: partition every median, rank and percentile bysetor, and the comparison above stops being a cross-chart one. This is what makes the limitation navigable — it does not remove it.api.financialsreturns the as-filed Portugueseaccount_name. It is the only label in the payload that is correct for every chart, because it is the one the company filed. Read it, not just the code.- There is deliberately no canonical English line-item mapping. Keying one on
account_codewould mislabel one sector or the other —3.03cannot be both “gross profit” and “gross financial intermediation result” under one name. - Insurers and other regulated sectors are expected to differ too, and have not been characterised. Treat “not a bank and not an industrial” as unknown rather than as one of the two columns above.
Shipped (catalog v29).
api.income_statements returns the income statement as
one row per period with named fields, and it resolves each field from the as-filed
label rather than from the account code. chart names the layout the filing used
(industrial, bank, insurer). setor and segmento also ride on every
statement row (v28) for peer medians.Keying on the label turned out to be more complete, not just safer: net income
sits on 3.11 for two charts, on 3.09 for a third that files no 3.11, and on
3.13 for insurers. Since v36 company_financials.net_income is keyed the same
way, so the two surfaces agree.Shipped (catalog v35). api.balance_sheets and api.cash_flow_statements
follow the same design. On the balance sheet, equity sits on 2.03, 2.07 or 2.08
depending on the chart, and where one label is filed twice (current and non-current
Empréstimos e Financiamentos) the parent line’s label tells them apart. Banks file
no current/non-current split and no debt line, so those fields are null for them.
The cash flow maps only the section totals and the cash reconciliation: capex and
dividends are free text per filer, so read those lines with financials.Still not offered. A canonical English mapping for every line. The named field
set above is a bounded, measured subset; a full mapping is not, and a code-keyed one
would still mislabel a chart.Planned, and deliberately behind it. A friendlier financials output shape —
readable period labels, scaled values, year-over-year change. It is deferred
precisely because the sector separation has to land first: a convenient wide shape
built on today’s codes would harden the mistake above into an interface.net_income is keyed on the filed label, never on a code
Since catalog v36, api.company_financials resolves net_income the same way as
api.income_statements: from the filed label Lucro/Prejuízo Consolidado do Período (or Lucro ou Prejuízo Líquido Consolidado do Período), wherever the
chart puts it.
Until v35 it read
3.11 alone. That returned null for the 282 bank-B statements
(Itaú Unibanco and BTG Pactual among them) and, for insurers, returned the
continuing-operations line under the name net_income.
No code is substituted for another. For Banco do Brasil, FY2024, consolidated:
3.09 is profit before statutory profit-sharing on this chart, and matches 3.11
only because 3.10 is zero. A COALESCE(3.11, 3.09) would be wrong for the first
filer with a non-zero 3.10; the label match never reads it.
This is verified for one bank. It is not a claim about every bank, and the
non-bank charts have not been swept for the same pattern.
Not every listed company resolves from its ticker
A ticker resolves to a company only through CVM’s published FCA valores-mobiliários map, and that map covers only the companies that published it. Nothing is name-matched — a guessed ticker↔CNPJ join is exactly what this API refuses to make. So a ticker can be real, traded, and still resolve to nothing.ITUB4 does:
financials resolves nothing and returns 200 []. An empty array here means
“unresolvable”, not “this company filed nothing”.
Workaround: resolve by company name or cd_cvm with api.lookup, then pass the
id it returns. A company row carries cnpj and a tickers array; those ids read
fundamentals reliably.
A quarterly filing publishes every account twice
An ITR prints the same account under oneref_date twice — once for the three-month
span, once year-to-date — distinguished only by period_start / period_end,
surfaced as period_months. Nothing is collapsed for you, because collapsing means
choosing, and either choice is wrong for someone.
And you cannot reliably rebuild a cumulative figure by summing quarters:
- A company with a non-calendar fiscal year breaks the arithmetic — São Martinho
(
cd_cvm20516) files April–March, so its nine-month period starts 1 April, not 1 January. - A restated earlier quarter breaks it too; only the newest
versionof each statement is served, so the quarter you would subtract may no longer be the quarter that was originally filed.
Quotes are unadjusted, except one column
close, close_unit and the adjusted flag are as traded, and adjusted is false
on every quote row: a statement of fact rather than a default waiting to be flipped.
A return computed from them across a split date is wrong, by the split ratio.
The one adjusted price is quote_history’s close_adj, its
default field (and, with cash reinvested, the selectable close_total_return). It is
continuous across splits, groupings and bonus shares, anchored to the ticker’s latest
session, and a window SILO cannot stand behind is refused with the cause, never served
raw: outside shares and units, before the issuer’s corporate events are proven swept,
or at an event this version does not adjust. close_total_return is NULL with a reason
where a distribution cannot be valued. Spin-offs, mergers, capital reductions and
subscriptions are not adjusted. quotes, the typed views and panel are unchanged.
Nothing is interpolated or forward-filled either; see
Market coverage.
The B3 lending and flow group is a ratchet
Roughly 21 business days of retention at the source, no archive, and no backfill at any price. It affectsshort_interest, short_interest_by_sector, investor_flow,
lending_trades and lending_participants: a session not captured on the day is
gone, not expensive.
This is the one limitation on this page that no amount of work here can fix, and it
is documented in full — including the silently clamped HTTP 200 — under
the B3 BDI ratchet. Read it before you
plan a long lending series.
Next
Market coverage
How far back each domain goes, and the ratchet in full.
Conventions & limits
The contract: caps, null semantics, freshness, errors.
Paging without losing rows
Three ways a response gets cut, and only one tells you.