# Financials Normalizer (`roseapps/financials-normalizer`) Actor

Standardises company financial statements from official filings (SEC XBRL and Companies House UK), with growth metrics, earnings-quality flags, and revenue-by-segment breakdowns -- every number linked back to its source filing.

- **URL**: https://apify.com/roseapps/financials-normalizer.md
- **Developed by:** [Elliot Rose](https://apify.com/roseapps) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 us company-period results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Financials Normalizer

Standardises company financial statements from **official filings** — SEC XBRL (US) and Companies House iXBRL (UK) — into one consistent schema of ~30 line items and 8 ratios per company-period, plus growth metrics, plain-language earnings-quality flags, and (opt-in) revenue-by-segment breakdowns. Every number links back to the exact filing (accession, form, filed date, and a direct URL) it came from.

Give it tickers or CIKs (US) or company numbers (UK); it resolves them, pulls the filer's full fact history, and normalises the mess of inconsistent tagging (`Revenues` vs `SalesRevenueNet` vs `RevenueFromContractWithCustomerExcludingAssessedTax`; UK `TurnoverRevenue` vs `Turnover`) into one shape you can chart or diff across companies — US or UK, side by side.

### Why this is different from a cheap fundamentals API

**It's point-in-time correct, not just "latest number."** Every US row separates `standardized` (the latest-filed value for that period, restatements included) from `original` (what was *first* reported, before any restatement) — and this is the one thing most low-cost fundamentals APIs get wrong or don't offer at all. Feed it a `pointInTime` date and it reproduces exactly what was knowable as of that date: later restatements become invisible, periods not yet filed come back `null`. That's the difference between a backtest with lookahead bias baked in and one without. If you're building a screener, backtester, or anything that scores historical decisions against financial data, this is the feature that matters most here.

Getting this genuinely right took real work: SEC's own `fy` field on an XBRL fact reflects which *filing* it was extracted from, not which real fiscal year the period belongs to (every 10-K reports ~3 years of comparative figures, and all of them get tagged with the current filing's year). Naively trusting it — which is what most quick XBRL parsers do — silently merges different fiscal years together. This Actor buckets by the fact's real calendar period instead, verified against real multi-year filing histories across several large-cap filers.

### Input

| Field | Type | Notes |
|---|---|---|
| `tickers` | string\[] | e.g. `["AAPL", "MSFT"]` |
| `ciks` | string\[] | with or without leading zeros / `CIK` prefix |
| `ukCompanyNumbers` | string\[] | UK companies via Companies House — see UK support below |
| `statements` | enum\[] | `IS`, `BS`, `CF`; default all three |
| `periods` | enum | `annual` (default), `quarterly`, `ttm` |
| `years` | integer | fiscal years of history, default 5, max 20 |
| `pointInTime` | date string | only consider filings filed on/before this date — see "point-in-time correct" above |
| `includeRawTags` | boolean | add a `raw_facts_sample` field per row |
| `includeSegments` | boolean | add a `segments` field per US row with a revenue-by-product/geography breakdown, when the filer reports one. Costs 1-2 extra requests per filing — off by default. |
| `secUserAgent` | string | **required for US** — `"Your Company Name contact@example.com"`, per SEC's fair access policy |
| `companiesHouseApiKey` | string (secret) | **required for UK** — get one free at [developer.company-information.service.gov.uk](https://developer.company-information.service.gov.uk/) |
| `maxConcurrency` | integer | companies processed in parallel, default 3 |

At least one of `tickers`, `ciks`, `ukCompanyNumbers` is required.

### Output

One dataset row per company-period (US) or per company (UK — Companies House gives one "latest accounts" snapshot, not a queryable multi-year history the way SEC does). Example (US, `periods: "annual"`):

```json
{
  "cik": "0001234567",
  "ticker": "ACME",
  "entity_name": "ACME CORP",
  "market": "us",
  "fy": 2022,
  "fp": "FY",
  "period_start": "2022-01-01",
  "period_end": "2022-12-31",
  "form": "10-K",
  "filed_at": "2023-02-15",
  "accn": "0001234567-23-000010",
  "filing_url": "https://www.sec.gov/Archives/edgar/data/1234567/000123456723000010/",
  "standardized": {
    "revenue": 1050000,
    "cost_of_revenue": 600000,
    "gross_profit": 450000,
    "operating_income": 350000,
    "net_income": 300000,
    "eps_basic": 3.0,
    "eps_diluted": 2.9,
    "total_assets": 5500000,
    "current_assets": 2200000,
    "cash": 1100000,
    "total_liabilities": 2300000,
    "current_liabilities": 850000,
    "long_term_debt": 1400000,
    "equity": 3200000,
    "operating_cash_flow": 400000,
    "capex": 120000,
    "free_cash_flow": 280000,
    "shares_outstanding": 105000
  },
  "original": {
    "revenue": 1000000
  },
  "ratios": {
    "gross_margin": 0.4286,
    "operating_margin": 0.3333,
    "net_margin": 0.2857,
    "current_ratio": 2.5882,
    "debt_to_equity": 0.4375,
    "roe": 0.09375,
    "roa": 0.05455,
    "fcf_margin": 0.2667
  },
  "growth": {
    "revenue_yoy": 0.3125,
    "net_income_yoy": 0.2,
    "revenue_cagr_2y": 0.2749
  },
  "red_flags": [
    {
      "flag": "restated",
      "severity": "info",
      "detail": "This period's figures were revised in a later filing: revenue. `standardized` shows the latest-filed value; `original` shows what was first reported."
    }
  ],
  "raw_tag_map": {
    "revenue": "Revenues",
    "cost_of_revenue": "CostOfRevenue",
    "total_liabilities": "Liabilities"
  },
  "missing_fields": ["interest_expense", "r_and_d", "sga", "..."],
  "confidence": 1.0,
  "confidence_note": "All requested line items were found for this period.",
  "derived_items": ["gross_profit", "free_cash_flow"],
  "derived": false,
  "segments": []
}
```

Notes on the shape:

- **`standardized`** always uses the value from the **latest-filed accession** ("restated view"). **`original`** carries only items where the earliest-filed accession reported a *different* value — i.e. it only appears for items that were actually restated. In the example above, revenue was originally reported as 1,000,000 and later restated to 1,050,000 in a subsequent 10-K's comparative column.
- **`growth`** — YoY (annual and quarterly), QoQ (quarterly), and CAGR (annual, spanning the actual window returned) for revenue, gross profit, operating income, net income, and free cash flow. `TTM` rows get a real TTM-vs-year-ago-TTM YoY when 8 quarters of history are available. The first row in any series has nothing to compare against (`growth: {}`); a gap in fiscal-year coverage is detected and skipped rather than silently computing a multi-year change and calling it YoY.
- **`red_flags`** — plain rule-based earnings-quality checks, no AI/LLM involved: restatement detection (now reliable — see the point-in-time note above), receivables growing much faster than revenue (possible aggressive revenue recognition), profit not converting to operating cash flow, and sharp leverage (debt/equity) increases. Every flag names the exact numbers behind it and is worth a second look, not a verdict. An empty list means "checked, nothing found" — it's always present, never a missing key.
- **`confidence_note`** — a plain-language explanation of the `confidence` score: whether missing items are a real data gap (core financials absent) or just secondary items this filer doesn't separately disclose (common — most filers don't break out goodwill from intangibles). Purely algorithmic, no AI involved, same as everything else in this Actor.
- **`segments`** (only present when `includeSegments: true`) — revenue by product/segment/geography, parsed from the filing's own disaggregation note when it reports one. `[]` is an honest, valid result for single-segment filers, not a failure. Labels (`member_label`) are best-effort formatting of the raw XBRL member name; the `member`/`axis` fields are the authoritative, unambiguous values.
- **`raw_tag_map`** shows which XBRL tag was actually used for each item that has a value, so you can audit exactly where a number came from.
- **`missing_fields`** lists standardised items with no value for this period (no matching tag, or excluded by `pointInTime`).
- **`derived_items`** lists items computed from others rather than read directly from a tag (e.g. `gross_profit = revenue - cost_of_revenue` when no `GrossProfit` tag exists; `free_cash_flow = operating_cash_flow - capex`, always derived; `total_liabilities = total_assets - equity` when `Liabilities` isn't tagged).
- **`fp: "Q4"`** rows (in `periods: "quarterly"`) and **`fp: "TTM"`** rows are computed, never filed directly — flow items are `derived: true`; instant (balance-sheet) items are simply the fiscal-year-end (or latest-quarter-end) values, since a balance sheet has no "Q4-only" figure.
- **`pointInTime`** reproduces what was knowable as of a given date: only filings `filed <= pointInTime` are considered, so later restatements are invisible and periods not yet filed come back with `null` values and `confidence: 0`.

A company that resolves but has no XBRL facts on file produces one row: `{"cik": ..., "identifier": ..., "market": "us", "error": "no XBRL facts on file for this filer (pre-2009 filer or non-reporting entity)"}` — every failure path returns a specific, honest reason, never a silent blank.

#### UK output shape

UK rows use the same `standardized`/`ratios`/`confidence`/`confidence_note` shape (so a UK and a US row are directly comparable field-by-field), but have `market: "uk"`, `company_number` instead of `cik`/`ticker`, no `fy`/`fp` (Companies House gives one latest-accounts snapshot per call, not a queryable history), and an `extra.employees` field UK filers commonly disclose that US filers don't. `growth`/`red_flags` are empty (`{}`/`[]`) for UK rows today — no historical series to compare against yet.

**Be aware of a real, structural UK data limitation** (documented, not hidden): unlike SEC filings, UK company accounts aren't uniformly structured data. Micro-entity and abbreviated accounts are *legally allowed* to omit the profit & loss account entirely, so `revenue`/`net_income` will genuinely be unavailable for many small UK private companies — `confidence_note` explains this per row. Some older/small filings are plain PDF scans with no machine-readable data at all — that produces a clear error row (`"...no machine-readable (iXBRL) version available..."`), never a silent gap. This module targets the common FRS-102/UK-GAAP small-company taxonomy; full IFRS support for large UK-listed groups is out of scope for now.

### Pricing events

| Event | Price | When |
|---|---|---|
| `company-period-us` | $0.01 | each standardised US (SEC) company-period row pushed |
| `company-period-uk` | $0.02 | each standardised UK (Companies House) company-period row pushed |
| `no-data` | $0.002 | a requested company/CIK/UK number resolved but produced no rows |

### MCP tools

This Actor is designed to back two MCP tools for downstream agents:

- **`get_financials(ticker, period)`** — run with `tickers: [ticker]`, `periods: period` (`annual`/`quarterly`/`ttm`), return the standardised rows.
- **`compare(tickers, metric)`** — run with `tickers`, then project `standardized.<metric>` (or `ratios.<metric>`, or `growth.<metric>_yoy`) across companies and periods for a side-by-side comparison.

### Provenance promise

Every number in `standardized` and `original` traces back to a specific filing: the row's `accn` + `form` + `filed_at` identify the exact filing, `filing_url` links straight to it, and `raw_tag_map` names the exact tag (US). Nothing is estimated from outside sources — items with no matching tag are `null` and listed in `missing_fields`, not guessed. The only computed values are the ones in `derived_items` (simple, documented arithmetic), `growth` (documented formulas), and `red_flags` (plain rule-based checks) — never third-party data, never an LLM guess.

### Limitations

- **Banks, insurers, and other regulated-industry filers** commonly use different XBRL tags for analogous concepts (e.g. `InterestAndDividendIncomeOperating` instead of `Revenues`, `Deposits`-heavy balance sheets) that this actor's `TAG_MAP` does not specifically target — expect lower `confidence` and more `missing_fields` for these filers.
- **Pre-2009 filers / periods** have no XBRL data on `data.sec.gov` at all (XBRL mandates phased in 2009–2011); these return a `no-data` row.
- **UK micro-entity/abbreviated accounts** can legally omit the P\&L entirely, and some filings are PDF-only with no machine-readable data — see "UK output shape" above. Full IFRS-taxonomy support for large UK-listed groups is not yet built.
- **`segments`** only covers revenue-by-product/geography (the common ASC 606 disaggregation note); segment-level profit/assets (a different note) aren't extracted. Table layouts vary by filer — a genuinely different shape than the common case produces `[]`, not a guess.
- Fiscal-quarter `Q4` and `TTM` figures for flow items are **derived by subtraction**, not filed directly by the company; they inherit any inconsistency in the underlying quarterly/annual tags (e.g. a mid-year accounting change can make a derived Q4 look odd).
- `confidence` is the share of core line items present after derivation — it is a completeness signal, not an accuracy guarantee. `confidence_note` explains what's actually missing and why.
- `red_flags` are heuristic signals worth a human second look, not findings — thresholds are tuned to avoid noise (e.g. a leverage-ratio flag requires both a meaningful relative *and* absolute change, so a near-zero-debt company moving from "almost none" to "still almost none" doesn't trigger a false alarm) but are not a substitute for real diligence.

# Actor input Schema

## `tickers` (type: `array`):

US-listed stock tickers, e.g. AAPL, MSFT. Resolved to CIKs via SEC's company\_tickers.json.

## `ciks` (type: `array`):

SEC Central Index Keys, with or without leading zeros or a 'CIK' prefix (e.g. 320193, 0000320193, CIK0000320193). Use when you know the CIK directly, or for filers without a ticker.

## `ukCompanyNumbers` (type: `array`):

Companies House company numbers, e.g. 04007092. Requires companiesHouseApiKey. Fetches and standardises the company's latest available accounts -- see the README for what to expect from micro-entity/PDF-only filings.

## `statements` (type: `array`):

Which statements to include standardised line items for. Defaults to all three.

## `periods` (type: `string`):

Annual (fiscal-year) rows, quarterly rows (with Q4 derived when not separately reported), or a single trailing-twelve-months row.

## `years` (type: `integer`):

How many fiscal years back to include (also bounds the quarters/TTM window). Max 20.

## `pointInTime` (type: `string`):

ISO date (YYYY-MM-DD). If set, only filings filed on or before this date are considered -- reproduces what was known as of that date, excluding later restatements.

## `includeRawTags` (type: `boolean`):

When true, adds a raw\_facts\_sample field to each row with a sample of the underlying XBRL facts (tag, value, accession) behind the standardised numbers.

## `includeSegments` (type: `boolean`):

When true, adds a `segments` field to each US row with a revenue-by-product/geography breakdown parsed from the filing's own disclosure note, when the filer reports one (single-segment companies and some filers won't have one -- an empty list is a valid, honest result). Costs 1-2 extra requests per filing, so it's off by default to keep a normal run fast.

## `secUserAgent` (type: `string`):

Required by SEC's fair access policy for all requests to sec.gov / data.sec.gov. Format: "Your Company Name contact@example.com". Replace the prefilled value with your own -- SEC asks that this identify the actual requester, not a shared default.

## `companiesHouseApiKey` (type: `string`):

Not used yet -- reserved for the phase-2 Companies House module (src/uk.py).

## `maxConcurrency` (type: `integer`):

Maximum number of companies processed in parallel.

## Actor input object example

```json
{
  "tickers": [
    "AAPL"
  ],
  "ciks": [],
  "ukCompanyNumbers": [],
  "statements": [
    "IS",
    "BS",
    "CF"
  ],
  "periods": "annual",
  "years": 5,
  "pointInTime": "",
  "includeRawTags": false,
  "includeSegments": false,
  "secUserAgent": "RoseApps FinancialsNormalizer support@eliterose.digital",
  "maxConcurrency": 3
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "tickers": [
        "AAPL"
    ],
    "secUserAgent": "RoseApps FinancialsNormalizer support@eliterose.digital"
};

// Run the Actor and wait for it to finish
const run = await client.actor("roseapps/financials-normalizer").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "tickers": ["AAPL"],
    "secUserAgent": "RoseApps FinancialsNormalizer support@eliterose.digital",
}

# Run the Actor and wait for it to finish
run = client.actor("roseapps/financials-normalizer").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "tickers": [
    "AAPL"
  ],
  "secUserAgent": "RoseApps FinancialsNormalizer support@eliterose.digital"
}' |
apify call roseapps/financials-normalizer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,roseapps/financials-normalizer"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/i5ognErtwib4pYn9R/builds/cwJWcvGMcBCjsxgeg/openapi.json
