How to Get Canadian Cross-Listed Financials from 40-F Filings
You add Canadian National or Enbridge to your model and the fundamentals come back empty. The company reports every quarter, the statements are audited and public, but it files a 40-F instead of a 10-K — and your pipeline has never seen that form before.
In this article, you will learn:
- Why a 40-F financials API built on 10-K logic returns nothing for Canadian cross-listed issuers
- What the 40-F wrapper form actually is, and why its financials hide in an exhibit rather than an item
- How IFRS tagging, mixed currencies and non-XBRL interims break a US-only pipeline in 3 places at once
- How to read any Canadian cross-listed issuer by ticker in one normalized schema — with a free API
Your pipeline handles US filers. It handles the big ADRs. You are feeling good about it. Then a Canadian name lands on your list — a railroad, a pipeline, a miner — and there is simply nothing to parse.
What makes this one maddening is that the company reports every quarter, its statements are audited and public, and your code still returns an empty result that looks identical to a company with no data at all. The problem is not coverage. It is that the form this company reports in, and the way that form packages its numbers, is unlike anything a US-only pipeline was ever built to open.
Canadian issuers file a 40-F, under the MJDS
The Multi-Jurisdictional Disclosure System is a long-standing arrangement between US and Canadian securities regulators that lets eligible, seasoned Canadian companies satisfy their US reporting obligations by filing their Canadian disclosure documents, wrapped in a US cover form. For a large cross-listed Canadian issuer, that cover form is the 40-F — the annual report — not the 10-K.
If you take one idea from this page, take this one: the 40-F is a wrapper. The 40-F cover document itself is thin: a page or 2 of certifications and a list of what is attached. The substance — the audited financial statements, the management discussion and analysis, the Annual Information Form — lives in the exhibits. This is Canadian continuous-disclosure documents stapled behind a US cover, which is a genuinely different object from a 10-K where the statements live in a defined item of the primary document.
2 consequences follow immediately for anyone parsing filings, and a 3rd that most people discover only later:
- The statements are frequently prepared under IFRS, because Canada adopted IFRS for publicly accountable enterprises in 2011, so any XBRL is tagged in a different taxonomy from US-GAAP — when there is XBRL at all.
- The reporting currency is often Canadian dollars, though many resource and energy issuers report in US dollars because their commodities are priced in dollars — so you cannot assume from the flag.
- The numbers are in an exhibit, not in an item of the primary document, so the item-based extraction that works on a 10-K has nothing to grab onto.
A pipeline built to recognize 10-K and 10-Q, tagged in US-GAAP, denominated in dollars, structured by item number, misses on all 4 points at once and returns an empty result that reads as “no data.”
The 40-F hides its financials in an exhibit
Now here is the pitfall that catches even people who already know the 40-F exists — it caught me. When you fetch a 10-K, you can extract Item 8 and have the financial statements. When you fetch a 40-F and go looking for an equivalent item, there isn’t one — the cover form has no financial-statement item, because the statements were never meant to live in the cover. They are a separate exhibit, often the same audited financial statements PDF or HTML the company filed with Canadian regulators, and sometimes bundled inside the Annual Information Form.
So the DIY path here is not “extract the right item.” It is: fetch the filing, enumerate its documents, identify which exhibit is the financial statements by its normalized document type, fetch that exhibit, and only then parse it. Each of those steps is an EDGAR round trip under the 10-requests-per-second-per-IP ceiling, with a mandatory User-Agent, and the exhibit filenames are arbitrary — you cannot construct them, you have to read the manifest to learn them. And because the financials are Canadian-format documents, once you reach them you are back to the same IFRS-tagging and currency questions, plus positional HTML-table parsing for anything not tagged.
IFRS or US-GAAP, CAD or USD, never assume
Finding the exhibit is the mechanical problem. The conceptual one starts the moment you open it, because what you find inside does not look like a US filing.
Your instinct will be to hardcode “Canadian company means IFRS and Canadian dollars.” Please do not. It is wrong often enough to corrupt a dataset, and it fails silently when it does. Canada moved to IFRS in 2011, so most Canadian financials since then are IFRS — but rate-regulated utilities received transition relief and some Canadian SEC registrants report under US-GAAP, so the accounting basis genuinely varies by issuer. Currency varies the same way: a railroad may report in Canadian dollars while a large oil-and-gas producer reports in US dollars because its revenue is dollar-denominated. There is no country-level rule you can apply; each of these is a per-issuer fact you have to read from the filing.
The reason this matters so much is that a currency mistake is silent. Mixing a CAD-reporting company into a USD comparison does not throw an error — it produces a plausible-looking number that is off by the exchange rate, buried in a cross-company screen you are no longer inspecting line by line. Currency handling stops being a nicety and becomes a first-class correctness concern the moment a single Canadian name enters a comparison.
Quarterly, unlike much of the rest of the world
So the annual has its complications — a wrapper form, a different taxonomy, a currency you cannot assume. The interim, though, is where Canadian issuers actually have an advantage over most foreign filers.
Here is a piece of good news that also explains why Canadian names are worth getting right. Unlike much of Europe, Canadian securities rules require quarterly reporting, and Canadian issuers furnish those interim results into EDGAR as 6-K filings. So a 40-F filer typically does have 4 quarters a year — the coverage is there to be had, if you can read the 6-K. That is a real advantage over semi-annual foreign filers, but it comes with the same catch: the 6-K carries no XBRL, so the quarters are HTML tables you have to parse positionally, with Canadian and US currency formatting mixed across issuers.
Why bolting on 40-F support yourself is a project
So let me be plain about what adding Canadian coverage to a home-grown parser actually costs, because it is not a config flag. You need the same 2 builds that foreign coverage always demands, and then a Canadian-specific wrinkle on top:
- An IFRS concept map so the 40-F’s line items land on your standard rows, maintained as both the IFRS and US-GAAP taxonomies revise each year.
- A non-XBRL interim parser for the 6-K quarters, robust to merged headers, currency symbols and per-issuer layout drift.
- Exhibit routing that finds the financial-statement document inside a wrapper filing with no financial-statement item — a step the 10-K path never made you build.
On top of all 3 you inherit currency handling as a first-class concern. It is the general foreign-issuer build with a Canadian label and an extra routing step, and most pipelines never do it. The broader version of this pattern is in financial statements for ADRs and foreign issuers.
Read a 40-F filer by ticker
With that work done on the ingestion side, a Canadian issuer reads like any other name. The Business Quant Financial Statements API ingests the 40-F and its accompanying 6-K interims, maps IFRS and US-GAAP into the same standard line items, resolves the exhibit routing for you, and returns a Canadian issuer by its US ticker in the normal schema:
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 ("CNI", "ENB"): # Canadian National Railway, Enbridge
doc = statement(t, "IS", "Annual", "5y")
md = doc["metadata"]
print(t.ljust(5), "currency:", md["currency"], "template:", md["template"])
The metadata.currency field is not optional reading here — it tells you whether a given 40-F filer reports in CAD or USD, which decides whether you can drop the numbers straight into a dollar comparison or have to convert first. The line items themselves arrive under the same standard slugs as any US filer.
Fetch the raw 40-F when you want the source
Reading by ticker gives you the normalized numbers. Sometimes you want to see the actual filing behind them — the exhibit that carries the audited statements.
The 40-F is a wrapper around Canadian disclosure rather than a US item-structured report, so it is one of the forms the Section Extraction API does not break into item keys. That is expected, not a gap: the financials were never in an item to begin with. To see the source you list the filing’s documents and fetch the financial-statement exhibit by its normalized document_type through the Documents and Exhibits API, which returns it as clean HTML with inline XBRL stripped:
def latest_40f(ticker):
url = (f"{BASE_URL}/secfilings"
f"?ticker={ticker}&formtype=40-F&page_size=1&api_key={API_KEY}")
return requests.get(url).json()["data"][0]["accession"]
accession = latest_40f("CNI")
manifest = requests.get(
f"{BASE_URL}/secfilings/{accession}/documents?api_key={API_KEY}").json()["data"]
for d in manifest:
# filter on the normalized document_type, never on filename or description
print(d["filesno"], d["document_type"], d["filename"])
def fetch_file(accession, filename):
url = (f"{BASE_URL}/secfilings/{accession}/content"
f"?filename={filename}&api_key={API_KEY}")
return requests.get(url).text
Filter on document_type to find the statements exhibit — not on the filename, which is arbitrary, and not on the free-text description, which drifts. That is the single routing decision the wrapper form forces on you, and it is a lookup rather than a parse.
Annual from the 40-F, interim from the 6-K
Frequency works exactly as it does elsewhere, mapped onto the Canadian filing set: the annual comes from the 40-F, the interim quarters from the non-XBRL 6-K. Because Canadian issuers report quarterly rather than semi-annually, you generally get all 4 — which is the advantage Canadian names have over most other foreign filers, and the reason they are worth getting right in your model:
annual = statement("ENB", "BS", "Annual", "10y") # from the 40-F
interim = statement("ENB", "IS", "Quarter", "5y") # from the 6-K
def latest(doc, slug):
for category in doc["data"].values():
for section in category["sections"].values():
if section["metadata"]["slug"] == slug and section["values"]:
v = section["values"][0]
return v["normalizedDate"], v["reportedValue"]["fmt"]
print("total assets (annual):", latest(annual, "total-assets"))
print("revenue (interim) :", latest(interim, "revenue"))
Edge cases to keep in mind
The code above covers the main path — annual by ticker, interim by ticker, raw exhibit when you need it. A few things will bite if you do not account for them.
Currency is per issuer, not per country. I will say it again because it is the one that bites: do not assume every Canadian filer reports in CAD. Several report in USD because their revenue is dollar-priced. Always branch on metadata.currency, never on the country.
Accounting basis is per issuer too. Most Canadian financials since 2011 are IFRS, but rate-regulated utilities and some SEC registrants report under US-GAAP. Because you query by ticker and read the normalized statement keyed on slugs, this distinction does not change your code — but it is why a naive “Canadian equals IFRS” parser is fragile.
Some large Canadian companies are US domestic filers. A Canadian-headquartered company that does not use the MJDS may file a regular 10-K instead of a 40-F. Because the API sources from whatever the company actually filed, querying by ticker returns the right statement either way.
Use the US-listed ticker. Identify these issuers by their US ticker (CNI, ENB, TRP). No CIK is needed.
The general foreign-issuer story applies. Everything here is a Canadian instance of the broader pattern in financial statements for ADRs and foreign issuers.
Start Building
So that is why a Canadian name comes back empty from a US-only pipeline, and it is 4 problems wearing one coat. A wrapper form with no financial-statement item. Often IFRS, sometimes not. Often a different currency, sometimes not. Statements hidden in an exhibit, interims in non-XBRL 6-K filings.
With the 40-F and 6-K ingested, the exhibit routing resolved, and IFRS and US-GAAP normalized into one schema, a Canadian cross-listed issuer reads by ticker exactly like any other name. Just check the currency before you compare anything.
- Financial Statements API — 40-F and 6-K normalized, by ticker, with currency.
- Documents and Exhibits API — the 40-F financial-statement exhibit by document type, if you want the source.
- SEC Filings API — the underlying 40-F and 6-K filings and their manifests.
Get an API key, pull Enbridge and Canadian National by ticker, and add Canadian coverage without a second parser.
Frequently asked questions
What is a 40-F and who files it?
The 40-F is the annual report that eligible Canadian issuers file to satisfy US reporting under the Multi-Jurisdictional Disclosure System, in place of a 10-K. It is a thin cover form that wraps the company's Canadian disclosure, including audited financial statements often prepared under IFRS, which live in the exhibits rather than in an item of the cover document.
Why does a Canadian company return no data in a US-only pipeline?
Because it files a 40-F rather than a 10-K, its financial statements sit in an exhibit rather than a defined item, its statements are often tagged under IFRS rather than US-GAAP, and it may report in Canadian dollars. A pipeline expecting a 10-K, in US-GAAP, in dollars, structured by item, matches none of those and returns an empty result.
Where are the financial statements in a 40-F?
In an exhibit, not in an item of the cover form. The 40-F itself is largely certifications and a list of attachments; the audited statements are a separate document, sometimes bundled with the Annual Information Form. You find it by listing the filing documents and filtering on the normalized document_type.
What currency are 40-F financials in?
It depends on the issuer. Many Canadian filers report in Canadian dollars, but resource and energy companies often report in US dollars because their revenue is dollar-priced. Read metadata.currency for each company rather than assuming, and convert before comparing across currencies.
How do I get interim numbers for a Canadian issuer?
Query the endpoint with frequency set to Quarter. Canadian rules require quarterly reporting, so the interim updates come from the non-XBRL 6-K filings, parsed and normalized into the same series as the 40-F annual data, and you generally get all 4 quarters by ticker.
Is the Financial Statements API free to use?
Yes. Sign up for a free API key and Canadian cross-listed issuers read by ticker in the same normalized schema as domestic filers, with annual data from the 40-F and interim data from the 6-K.