How to Get Financial Statements for ADRs and Foreign Issuers via API
You add TSMC or Toyota to your model and the fundamentals come back empty. The data exists — it just lives in 20-F and 6-K filings that your feed never learned to read.
In this article, you will learn:
- Why a foreign issuer financials API built only on US-GAAP XBRL misses 20-F and 6-K filings entirely
- What breaks when you try to parse a 20-F yourself — IFRS tags, mixed XBRL history, and a non-XBRL interim
- How cadence, currency and ADR ratios silently distort foreign comparisons
- How to read any ADR or foreign issuer by ticker in one normalized schema — with a free API
You build a screen, a peer set or a model, and it works beautifully. Then you add a name like TSMC, ASML or Toyota, and the fundamentals come back empty, stale, or suspiciously thin. Maybe you get an annual revenue number but no quarters. Maybe you get nothing at all.
At this point it is very tempting to conclude the data is simply unavailable — or that these companies just report less than US names do. I believed that for longer than I would like to admit. Neither is true. They report in full, every year and every interim period. They just report it in filings your data source never learned to read.
This is worth understanding rather than working around, because the moment you understand where a foreign issuer’s numbers live, you also understand why every shortcut for getting them fails in the same 2 places. So before any code, let me show you what a foreign filing actually is.
Foreign issuers file 20-F and 6-K, not 10-K and 10-Q
A foreign private issuer that lists in the US does not use the domestic filing set. The Securities Exchange Act carves out a separate regime, and the 2 documents that matter are:
- The 20-F is the annual report — the foreign equivalent of the 10-K. It carries the audited financial statements, the operating and financial review, and risk factors, but it is organized under a different item structure. Financial statements sit under Item 18 (or, for older or lighter filers, Item 17), not Item 8. Risk factors live at Item 3.D, and the MD&A equivalent is Item 5. If you built section-extraction logic around 10-K item numbers, none of it lines up.
- The 6-K carries interim results and any other material update — loosely the equivalent of a 10-Q — but with 2 twists. It is furnished, not filed, which is a legal distinction that in practice means it is a loose envelope rather than a structured report. And it has no XBRL at all: its financials are HTML tables, the same shape as an 8-K earnings exhibit, just with more currencies and more layout variety.
So a foreign issuer’s numbers live in exactly the places a US-GAAP-XBRL pipeline is not looking. The annual is tagged in a different taxonomy, and the interim is not tagged at all. A feed that only ingests 10-K and 10-Q XBRL will quietly have no interim data for these companies, and depending on how it handles the annual, possibly no annual data either. That is why they show up empty — not absence of data, but absence of a reader.
What actually breaks when you go to EDGAR yourself
So the natural next move is to skip the feed entirely and go to the source. You open EDGAR, pull up TSMC, and within about 5 minutes you meet the friction that turns this from an afternoon into a project.
The retrieval itself is rate-limited and fussy. EDGAR enforces a hard ceiling of 10 requests per second per IP, shared across everything behind that address. Breach it and you get a 403 and roughly a 10-minute block; retrying the 403 extends the block. Every request also needs a real User-Agent naming a contact — the library default is rejected on the first call. This is the classic trap where a script runs fine on your laptop and 403s the moment it shares an IP with CI or a serverless function. You can build around it with a rate limiter and a cache, but you are now maintaining infrastructure before you have parsed a single number.
Finding the right document is its own step. A 20-F filing is an SGML container with many attachments and arbitrary filenames — you cannot construct the path to the financial-statement exhibit, you have to fetch and parse the filing index to discover it. And a company files many 6-Ks a year; most are press releases, AGM notices or minor updates, and only some carry the interim statements. There is no item structure on a 6-K telling you which is which, so routing to the one that actually holds the numbers is a classification problem before it is a parsing problem.
Then the annual surprises you with its history. Foreign private issuers were only required to submit XBRL for the 20-F under the IFRS taxonomy for fiscal years ending on or after late 2017. Pull a decade of history and the recent years may carry inline XBRL while the older ones carry none — so even the annual is a mix of a tagged-data problem and an HTML-table problem, in the same series, for the same company.
The IFRS taxonomy is a different language, not a dialect
Say you get past retrieval and you are holding a 20-F with XBRL in it. Good — and you still cannot reuse your domestic concept map. The 20-F is tagged under ifrs-full, not us-gaap, and I want to be clear that these are genuinely different vocabularies rather than the same one with different spellings.
Some of the mismatches are cosmetic — IFRS Revenue versus us-gaap Revenues or RevenueFromContractWithCustomerExcludingAssessedTax. But many are structural. IFRS presents a statement of profit or loss that need not surface a “gross profit” subtotal the way a US income statement does; it often classifies expenses by nature (depreciation, employee benefits) rather than by function (cost of sales, SG&A). “Finance costs” and “finance income” are presented differently from a US interest line. The bottom line is ProfitLoss, not NetIncomeLoss. If your normalization only knows the us-gaap names, an IFRS filer’s revenue does not land on the wrong row — it lands nowhere.
On top of that, both taxonomies are moving targets. The FASB revises us-gaap annually and the IFRS Foundation revises ifrs-full annually, so a concept map is not written once; it is maintained on 2 release treadmills at the same time. And issuers extend both taxonomies with custom tags for line items that do not fit a standard concept, which you have to recognize and map by hand. Open-source XBRL processors like Arelle will faithfully parse the instance document into raw facts for you, which retires the mechanical part — but they hand you the filer’s tags verbatim and stop there. The mapping from thousands of us-gaap and ifrs-full concepts to one comparable line-item set, kept correct as both taxonomies change, is the part that is actually hard, and it is exactly the part a parser library leaves on your desk.
The 6-K is a grab-bag with no structure
The interim is worse, because there is no XBRL to fall back on. A 6-K’s financials are HTML tables, and parsing them positionally means handling every real-world messiness at once:
- Merged and multi-row headers that a naive table reader flattens into nonsense.
- Foreign currency symbols and codes mixed into cells, so the number and its unit are tangled together.
- Parenthesized negatives, and sometimes a trailing minus, and sometimes a dedicated column for the sign.
- European number formats where the comma is the decimal separator and the period groups thousands — the exact inverse of the US convention, silently producing values off by orders of magnitude if you assume US formatting.
- Scale stated once in a header (“in millions of NT$”) and never repeated, so a parser that misses the header reads raw digits.
Tools like pandas.read_html will get you a DataFrame out of a clean table, and for a tidy 6-K that is a fine start. But the interim layout is not stable across issuers, and it drifts within an issuer over time as they redesign their reporting, so a positional parser is never “done” — it is a surface you keep patching every time a company changes its template. This is the same non-XBRL parsing problem as an 8-K earnings exhibit, covered in its own article, with the currency dimension turned up.
Cadence and currency will surprise you even after parsing works
2 more assumptions you carried over from the domestic world break quietly on foreign names. And these 2 are nastier than the parsing problems, because they break your analysis rather than your code — nothing errors, the chart renders, and the conclusion is wrong.
Interim cadence is not 4 quarters everywhere. Many foreign issuers — European names especially — report on a semi-annual rhythm: full statements at the half and the full year, with lighter updates in between, following home-market convention. If your model assumes 4 evenly spaced quarters, a semi-annual filer produces gaps that look like missing data, and any trailing-twelve-month or growth calculation built on a 4-quarter grid comes out wrong.
The reporting currency is usually not dollars. TSMC reports in New Taiwan dollars, ASML in euros, Toyota in yen. Some foreign issuers report in US dollars anyway, and a few change reporting currency over their history, so you cannot infer the currency from the country. Drop a euro-denominated revenue next to a dollar one without converting and every cross-company comparison is silently wrong.
ADR ratios distort per-share figures. An ADR may represent several ordinary shares or a fraction of one. The 20-F reports earnings per ordinary share, not per ADR, so any per-share number needs adjusting by the ADR ratio before it means anything to a US holder. This is the kind of error that passes every reconciliation check — the number is internally consistent, it is just answering a different question than you think.
Why the free route stalls twice
Step back now and you can see the real shape of it. Covering foreign issuers means solving 2 separate builds on top of your domestic parser — and I want to stress that neither one is a config flag you can switch on.
- A second concept map, for IFRS. You need to recognize
ifrs-fullconcepts and custom extensions and map them onto the same standard line items as us-gaap, keeping it current as both taxonomies revise every year. - A non-XBRL interim parser, for the 6-K. You need positional parsing that survives merged headers, foreign currency symbols, parenthesized negatives, European number formats and per-issuer layout drift — and you need it to keep working as issuers change their templates.
Most feeds do neither, because either one is a project and the 2 together roughly double the parser you already did not want to maintain. That is why foreign coverage is where fundamentals data quietly ends — a gap explained more fully in why most fundamentals APIs miss foreign filers.
Read a foreign issuer by ticker, in one schema
With both builds already paid for on the ingestion side, the payoff is that a foreign issuer reads exactly like a domestic one. The Business Quant Financial Statements API ingests the 20-F, 40-F and 6-K, maps IFRS and US-GAAP tags into the same standard line items, parses the non-XBRL interims, and returns a foreign issuer in the shape a domestic one uses — identified by its US-listed ticker:
import requests
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://data.businessquant.com"
def statement(ticker, view="IS", frequency="Annual", period="10y"):
url = (f"{BASE_URL}/statements"
f"?ticker={ticker}&statement={view}"
f"&frequency={frequency}&period={period}&api_key={API_KEY}")
return requests.get(url).json()
for t in ("TSM", "ASML", "TM"): # TSMC, ASML, Toyota
doc = statement(t, "IS", "Annual", "5y")
md = doc["metadata"]
print(t.ljust(6), "template:", md["template"], "currency:", md["currency"])
The metadata.currency field tells you the reporting currency, which for foreign issuers is frequently not US dollars — New Taiwan dollars, euros, yen. The line items come back under the same standard slugs (revenue, operating-income, net-income), so a foreign name slots into a comparison keyed on slugs without special-casing, regardless of whether the underlying filing was tagged in ifrs-full or us-gaap.
Annual from the 20-F, interim from the 6-K
Frequency selection works the same as it does for domestic filers, and it maps onto the foreign filing set: the annual comes from the 20-F, and the interim quarters come from the non-XBRL 6-K, both normalized into one series. The distinction matters because the interim is the piece most feeds are missing entirely — it is the 6-K that carries no XBRL, so a pipeline that never learned to parse HTML tables simply has no quarterly data for these issuers. Here you get both frequencies in the same shape:
annual = statement("TSM", "IS", "Annual", "10y") # from the 20-F
interim = statement("TSM", "IS", "Quarter", "5y") # from the 6-K, parsed from HTML
def latest_revenue(doc):
for category in doc["data"].values():
for section in category["sections"].values():
if section["metadata"]["slug"] == "revenue" and section["values"]:
v = section["values"][0]
return v["normalizedDate"], v["reportedValue"]["fmt"]
print("annual :", latest_revenue(annual))
print("interim:", latest_revenue(interim))
Because the interim is parsed from the 6-K rather than waiting on a US-style quarterly XBRL filing that foreign issuers do not produce, you get interim coverage for these names at all — which is the part that is usually simply absent. Where an issuer reports semi-annually, the series reflects that cadence honestly rather than inventing quarters that were never filed.
See the raw foreign filing when you need it
When you want the source document behind a normalized number, the SEC suite reaches foreign filings too. The 20-F is one of the forms the Section Extraction API supports, so rather than hardcode an item key, list the sections the filing actually contains and pick the financials by key — the filing itself tells you whether the statements sit under Item18 or Item17:
def latest_20f(ticker):
url = (f"{BASE_URL}/secfilings"
f"?ticker={ticker}&formtype=20-F&page_size=1&api_key={API_KEY}")
return requests.get(url).json()["data"][0]["accession"]
def item_keys(accession):
url = f"{BASE_URL}/secfilings/{accession}/items?api_key={API_KEY}"
return [i["item_key"] for i in requests.get(url).json()["data"]]
def section(accession, item_key): # pass the key back verbatim
url = (f"{BASE_URL}/secfilings/{accession}/content"
f"?item_key={item_key}&api_key={API_KEY}")
return requests.get(url).text
acc = latest_20f("TSM")
keys = item_keys(acc) # e.g. ['Item3D', 'Item5', 'Item18', ...]
fin = "Item18" if "Item18" in keys else "Item17"
financials_html = section(acc, fin) # clean HTML, inline XBRL stripped
Item keys are case-sensitive — Item18, not item18 — so pass back whatever the /items call returns without rewriting it. The 6-K is not a supported section-extraction form, so for interim source you fetch the exhibit directly through the Documents and Exhibits API by document_type, which returns it as clean HTML with inline XBRL already stripped.
Convert currency yourself when you compare across borders
So you can pull the normalized numbers, and you can reach back into the raw filing when you need to. The last piece is making those numbers comparable across issuers that report in different currencies.
The API returns each issuer in its native reporting currency, faithfully. I want to explain why, because it occasionally reads as an inconvenience rather than a feature: the alternative is a feed silently pre-converting at some rate, on some date, that it never discloses to you. If you are placing a euro-reporting company next to a dollar-reporting one, convert at your chosen rate and date in your own code; the response gives you the currency and the period date to do it correctly:
def to_usd(doc, fx): # fx: currency code -> rate to USD
cur = doc["metadata"]["currency"]
rate = fx.get(cur, 1.0)
out = {}
for category in doc["data"].values():
for section in category["sections"].values():
for v in section["values"]:
raw = v["reportedValue"]["raw"]
out.setdefault(section["metadata"]["slug"], {})[v["normalizedDate"]] = (
raw * rate if isinstance(raw, (int, float)) else raw)
return out
Edge cases to keep in mind
The code above covers the main path — pull by ticker, read the currency, convert when comparing. A few things bite if you do not know about them in advance.
Use the US-listed ticker. Identify ADRs by their US ticker (TSM, BABA, NVO). The endpoint keys on ticker, and you do not need a CIK.
Watch the currency, always. Read metadata.currency before doing arithmetic across companies. A foreign issuer’s raw values are in its home currency, not dollars, and the currency cannot be inferred from the country.
Per-share figures are per ordinary share. If you need a per-ADR number, adjust reported EPS by the ADR ratio; the statement reports per the underlying ordinary share.
Canadian filers are a special case. Canadian companies cross-listed in the US often file a 40-F under the Multi-Jurisdictional Disclosure System rather than a 20-F; that path has its own article.
Interim completeness varies by issuer. Some foreign issuers report full statements semi-annually and lighter updates in between, reflecting home-market convention. The series reflects what was filed, not a fixed 4-quarter grid.
Start Building
So the gap in your data was never mysterious, and it was never about these companies disclosing less. Foreign issuers are missing from most feeds for a completely concrete reason: their numbers sit in 20-F, 40-F and 6-K filings, tagged under IFRS or not tagged at all — and covering them properly means a second concept map running on 2 taxonomy treadmills, plus a non-XBRL parser that never stops drifting.
Once somebody has paid for both of those on the ingestion side, an ADR reads by ticker in exactly the same normalized schema as any domestic name. Just keep 2 things in your head: the reporting currency, and the reporting cadence.
- Financial Statements API — 20-F, 40-F and 6-K normalized, by ticker, with currency.
- Section Extraction API — the 20-F financial-statement section as clean HTML by item key.
- Peers API — build a cross-border peer set to compare on standard slugs.
Get an API key, pull TSMC and Toyota by ticker, and close the foreign-coverage gap in your model.
Frequently asked questions
Which filings do foreign issuers use instead of the 10-K and 10-Q?
A foreign private issuer files a 20-F annual report, often XBRL-tagged under the IFRS taxonomy, and 6-K interim updates, which are furnished rather than filed, carry no XBRL, and present financials as HTML tables. Canadian cross-listed issuers frequently file a 40-F instead of a 20-F.
Why are foreign companies missing from many fundamentals APIs?
Feeds built only on US-GAAP XBRL do not read the IFRS-tagged 20-F or the non-XBRL 6-K, so foreign issuers have thin annual data and often no interim data at all. Covering them requires a second concept map for IFRS, maintained on the IFRS and US-GAAP taxonomy release cycles, plus a non-XBRL parser for the 6-K.
Where are the financial statements inside a 20-F?
They sit under Item 18 for most filers, or Item 17 for older or lighter presentations, not under Item 8 as in a 10-K. Risk factors are at Item 3.D and the MD&A equivalent is Item 5, so 10-K item numbers do not carry over. List the sections a filing actually contains before assuming a key.
How do I query a foreign issuer or ADR?
By its US-listed ticker, exactly like a domestic company. The endpoint returns the same normalized income statement, balance sheet and cash flow, with the reporting currency in metadata.currency, and does not require a CIK.
What currency are the numbers in?
The issuer native reporting currency, given in metadata.currency, which for foreign filers is often not US dollars and cannot be inferred from the country. Read that field before comparing across companies, and convert at your chosen rate and date using the period date in the response.
Is the Financial Statements API free to use?
Yes. Sign up for a free API key and foreign issuers read by ticker in the same schema as domestic filers, with annual data from the 20-F and interim data from the 6-K.