# AkkruData API & MCP — User Guide

This document describes the data, metrics, ownership, corporate events and screening HTTP APIs: request and response fields, HTTP status codes, credit usage, and rate limits. It is intended for application integrators.

**Base URL:** `https://api.akkrudata.ai/api/v1`  

Endpoint paths in this guide are appended to that prefix (for example, `POST …/data/filings` means `POST https://api.akkrudata.ai/api/v1/data/filings`).

---

## Table of Contents

| Section | Link |
|---------|------|
| **1. Conventions** | [→](#1-conventions) |
| └ Authentication | [→](#11-authentication) |
| └ Success and error payloads | [→](#12-success-and-error-payloads) |
| └ Credits | [→](#13-credits) |
| └ Rate limiting | [→](#14-rate-limiting) |
| └ Examples Disclaimer | [→](#15-examples-disclaimer) |
| └ Filing caveats | [→](#16-filing-caveats) |
| └ Workflow budgets | [→](#17-workflow-budgets) |
| └ History truncation notice | [→](#18-history-truncation-notice) |
| └ Coverage scope notice | [→](#19-coverage-scope-notice) |
| └ Industry classification (Pro+) | [→](#110-industry-classification-pro) |
| └ Company symbols and CIKs | [→](#111-company-symbols-and-ciks) |
| **2. Credit summary** | [→](#2-credit-summary) |
| **3. Rate limits** | [→](#3-rate-limits) |
| **4. Data API** | [→](#4-data-api) |
| └ `POST /data/filings` | [→](#41-post-datafilings) |
| └ `POST /data/company` | [→](#42-post-datacompany) |
| └ `GET /data/company/list` | [→](#43-get-datacompanylist) |
| └ `POST /data/line-items` | [→](#44-post-dataline-items) |
| └ `POST /data/facts` | [→](#45-post-datafacts) |
| └ `POST /data/statement` | [→](#46-post-datastatement) |
| └ `POST /data/filing-statements` | [→](#47-post-datafiling-statements) |
| └ Statement endpoints (`/data/income-statement`, `/comprehensive-income`, `/balance-sheet`, `/cash-flow-statement`, `/equity-statement`) | [→](#48-statement-endpoints-datastatement) |
| └ `POST /data/facts/dimensional-breakdown` | [→](#49-post-datafactsdimensional-breakdown) |
| └ `POST /data/line-items/dimensional-breakdown` | [→](#410-post-dataline-itemsdimensional-breakdown) |
| └ `POST /data/compare/line-items` | [→](#411-post-datacompareline-items) |
| └ `POST /data/compare/facts` | [→](#412-post-datacomparefacts) |
| └ `GET …/excel` | [→](#413-get-datafilingsfiling_idexcel) |
| **5. Metrics API** | [→](#5-metrics-api) |
| └ Response conventions | [→](#response-conventions) |
| └ Supported metric catalog | [→](#50-supported-metric-catalog) |
| └ `POST /metric/filings` | [→](#51-post-metricfilings) |
| └ `POST /metric/all` | [→](#52-post-metricall) |
| └ `POST /metric/all/filing-date` | [→](#53-post-metricallfiling-date) |
| └ `POST /metric/items` | [→](#54-post-metricitems) |
| └ `POST /metric/historical` | [→](#55-post-metrichistorical) |
| └ Category endpoints (`/metric/profitability`, `/growth`, `/financial-health`, `/efficiency`, `/cash-flow`, `/valuation`, `/valuation/filing-date`) | [→](#56-category-endpoints-metriccategory) |
| **6. Screener API** | [→](#6-screener-api) |
| └ `POST /screener/metrics` | [→](#61-post-screenermetrics) |
| └ `GET /screener/metrics/filters` | [→](#62-get-screenermetricsfilters) |
| └ Filter DSL (`/screener/metrics`) | [→](#63-filter-dsl-screenermetrics) |
| └ Credit formula (`/screener/metrics`) | [→](#64-credit-formula-screenermetrics) |
| └ `POST /screener/ownership` | [→](#65-post-screenerownership) |
| └ Filters (`/screener/ownership`) | [→](#66-filters-screenerownership) |
| └ Credit formula (`/screener/ownership`) | [→](#67-credit-formula-screenerownership) |
| └ `POST /screener/initial-holdings` | [→](#68-post-screenerinitial-holdings) |
| └ `POST /screener/institutional-holdings` | [→](#69-post-screenerinstitutional-holdings) |
| └ Credit formula (`/screener/institutional-holdings`) | [→](#610-credit-formula-screenerinstitutional-holdings) |
| └ `POST /screener/events` | [→](#611-post-screenerevents) |
| └ Credit formula (`/screener/events`) | [→](#612-credit-formula-screenerevents) |
| **7. Ownership API** | [→](#7-ownership-api) |
| └ Transaction object | [→](#transaction-object) |
| └ `POST /ownership/transactions` | [→](#71-post-ownershiptransactions) |
| └ `POST /ownership/stats` | [→](#72-post-ownershipstats) |
| └ `POST /ownership/transaction-ids` | [→](#73-post-ownershiptransaction-ids) |
| └ `GET /ownership/tickers` | [→](#74-get-ownershiptickers) |
| └ `POST /ownership/names` | [→](#75-post-ownershipnames) |
| └ `GET /ownership/transaction-types` | [→](#76-get-ownershiptransaction-types) |
| └ Holding object | [→](#holding-object) |
| └ `POST /ownership/initial-holdings` | [→](#77-post-ownershipinitial-holdings) |
| └ `POST /ownership/initial-holdings/stats` | [→](#78-post-ownershipinitial-holdingsstats) |
| └ `POST /ownership/initial-holding-ids` | [→](#79-post-ownershipinitial-holding-ids) |
| └ Snapshot object | [→](#snapshot-object) |
| └ Institutional holding object | [→](#institutional-holding-object) |
| └ `POST /ownership/institutional-holdings/manager-history` | [→](#710-post-ownershipinstitutional-holdingsmanager-history) |
| └ `POST /ownership/institutional-holdings/portfolio` | [→](#711-post-ownershipinstitutional-holdingsportfolio) |
| └ `POST /ownership/institutional-holdings/security` | [→](#712-post-ownershipinstitutional-holdingssecurity) |
| **8. Corporate Events API** | [→](#8-corporate-events-api) |
| └ Event filing object | [→](#event-filing-object) |
| └ Event section object | [→](#event-section-object) |
| └ Event warnings | [→](#event-warnings) |
| └ `GET /events/types` | [→](#81-get-eventstypes) |
| └ `POST /events/filings` | [→](#82-post-eventsfilings) |
| └ `POST /events/filing` | [→](#83-post-eventsfiling) |
| **9. API key, account, and plan errors** | [→](#9-api-key-account-and-plan-errors) |
| **10. Request limits (validation)** | [→](#10-request-limits-validation) |
| **11. MCP access** | [→](#11-mcp-access) |
| └ Connect via OAuth | [→](#111-connect-via-oauth-recommended-for-ai-assistants) |
| └ Connect via X-API-Key | [→](#112-connect-via-x-api-key-programmatic-use) |
| └ Tool catalog | [→](#113-tool-catalog) |

---

## 1. Conventions

### 1.1 Authentication

All requests to **`/data/*`**, **`/metric/*`**, **`/screener/*`**, **`/ownership/*`**, and **`/events/*`** require:

| Header | Value |
|--------|--------|
| `X-API-Key` | Your API key (create and manage keys in the product dashboard after sign-in). |

For `POST` requests with a JSON body, also send:

| Header | Value |
|--------|--------|
| `Content-Type` | `application/json` |

### 1.2 Success and error payloads

Structured errors return JSON of the form:

```json
{
  "detail": {
    "error_code": "SOME_CODE",
    "message": "Human-readable explanation."
  }
}
```

**Validation errors** (`error_code: "VALIDATION_ERROR"`) additionally include a `fields` array describing each rejected input. Each entry has `type` (a stable code such as `missing`, `extra_forbidden`, `int_parsing`, `string_too_long`), `loc` (path to the field), `msg` (human text), and `input` (the value that failed). Use **`detail.message`** for a single-line summary and **`detail.fields`** if you need per-field structured detail.

Authentication failures use the same object form: `401 AUTH_API_KEY_MISSING` (no `X-API-Key` or bearer token), `403 AUTH_API_KEY_INVALID` (unknown or inactive key), `403 AUTH_USER_INACTIVE`, `403 AUTH_EMAIL_NOT_VERIFIED`. Every 4xx and 5xx response uses the object form above.

**Plan-tier responses.** A request may be rejected because the caller's plan does not include the requested endpoint or option. These return **`403`** with one of `PLAN_TIER_INSUFFICIENT`, `PLAN_TIER_INSUFFICIENT_HISTORY`, `PLAN_TIER_INSUFFICIENT_FORMULA_OVERRIDE`, or `PLAN_TIER_INSUFFICIENT_COVERAGE`. The `detail` body also carries `required_tier` (the lowest plan that would accept the request) and `current_tier` (the caller's current plan). See **[Section 9](#9-api-key-account-and-plan-errors)**.

### 1.3 Credits

Credits are deducted from the account tied to your API key. Amounts and timing are listed under each endpoint and in **[Section 2](#2-credit-summary)**. **`402 Payment Required`** indicates insufficient credits for that operation.

### 1.4 Rate limiting

When limits are exceeded, the API returns **`429 Too Many Requests`** with `detail.error_code` **`RATE_LIMIT_EXCEEDED`**. Limits apply per account across all API keys on that account.

### 1.5 Examples Disclaimer

> **Note:** All request and response JSON examples in this documentation are provided for reference purposes only. They may be abbreviated or truncated for readability, and the exact structure or available fields are subject to change without prior notice.

### 1.6 Filing caveats

A filing may carry a free-text disclaimer about its extraction completeness — for example, when a portion of the original filing was deliberately excluded from the extracted facts. When present, the disclaimer surfaces in two places on every endpoint that returns filing information, facts, or statements:

- A **`caveat`** field on the filing information itself (top-level for single-filing responses; per-row in `results[]` for compare endpoints; per-item in `filings[]` for the listing endpoint). Either `null` / absent (no caveat), or a string.
- A **`_warnings`** entry of the form `Filing <TICKER> <FORM> FY<YEAR>[ <QUARTER>] has caveat: <text>`. Compare endpoints aggregate per-filing caveats into a single deduplicated `_warnings` list.

The caveat is informational and does **not** affect billing or response status.

### 1.7 Workflow budgets

Indicative credit ranges for common workflows, so you can size a plan before you start. Numbers assume the L1 tier on common line-item names (`revenue`, `net income`, `operating income`, etc.). Rarer or ambiguous names land at L2–L3 and cost more; see the tier list in [§2](#2-credit-summary).

| Workflow | Endpoint pattern | Typical credits |
|---|---|---|
| Discover one company (identity + coverage map) | `POST /data/company` | **8** |
| List all filings for one company-year | `POST /data/filings` | **10** |
| Pull all three primary statements (income / balance / cash flow) for one filing | `POST /data/income-statement` + `/balance-sheet` + `/cash-flow-statement` | **84** (28 × 3) |
| Pull a few specific line items from one filing (3 items × L1) | `POST /data/line-items` | **60** (3 × 20) |
| Five-year trend on one metric for one company | `POST /data/line-items` × 5 filings, 1 item, L1 | **100** (5 × 20) |
| Peer comparison: 5 companies × 1 metric × 1 year (L1) | `POST /data/compare/line-items` | **100** (1 × 20 × 5) |
| All key metrics snapshot for one company (1 call) | `POST /metric/all` | **148** |
| Full dimensional breakdown of revenue for one filing (~50 slice facts) | `POST /data/line-items/dimensional-breakdown` | **~120** (20 + 100) |
| Download one filing's Excel workbook | `GET /data/filings/{id}/excel` | **300** |

Reference: a **Starter** plan (20,000 credits / month) covers about **330** three-item line-item queries, **240** three-statement pulls, or **135** `/metric/all` snapshots per month — pick the mix that fits your workflow.

### 1.8 History truncation notice

List, range, and discovery endpoints attach a generic `_warnings` entry when results would be larger on a higher plan. Affected endpoints: `POST /data/filings`, `POST /data/company` (the `coverage` map), `POST /metric/filings`, `POST /metric/historical`, `POST /screener/metrics`, and `POST /ownership/transactions`, `POST /ownership/stats`, `POST /ownership/names`, `POST /screener/ownership`, `POST /ownership/initial-holdings`, `POST /ownership/initial-holdings/stats`, `POST /screener/initial-holdings` (on `filing_date`). For `/screener/metrics` the `lookback` is additionally auto-clamped to your plan's window so you are only billed for the portion you can access. Single-filing endpoints (`/data/facts`, `/data/statement`, `/data/line-items`, etc.) instead return **`403 PLAN_TIER_INSUFFICIENT_HISTORY`** when the requested filing is out of window. [`POST /ownership/transaction-ids`](#73-post-ownershiptransaction-ids) lists out-of-window ids in `restricted_ids` and returns that 403 only when every id found is out of scope.

### 1.9 Coverage scope notice

Corporate event company coverage is the same across eligible paid plans.

Each plan covers a subset of companies. Single-company endpoints return **`403 PLAN_TIER_INSUFFICIENT_COVERAGE`** with `detail.required_tier` when the requested company is outside your plan's scope. `/ownership/*` endpoints do this whenever the request is anchored on a `ticker`. List, discovery, `/screener/metrics`, `/screener/ownership`, `/screener/initial-holdings`, and `/ownership/*` requests not anchored on a `ticker` are silently filtered to your plan's scope and attach a generic `_warnings` entry when companies are excluded. [`POST /ownership/transaction-ids`](#73-post-ownershiptransaction-ids) and [`POST /ownership/initial-holding-ids`](#79-post-ownershipinitial-holding-ids) report out-of-scope ids in `restricted_ids` instead.

### 1.10 Industry classification (Pro+)

Pro-plan and higher responses attach an industry `classification` block per company on `GET /data/company/list`, `POST /data/compare/line-items`, `POST /data/compare/facts`, and `POST /screener/metrics` results. Sub-Pro callers receive the same responses without it; attaching it costs no extra credits.

```json
"classification": {
  "sectors": ["Technology"],
  "source": { "scheme": "US_SIC", "code": "3571", "url": "https://www.sec.gov/search-filings/standard-industrial-classification-sic-code-list" }
}
```

| Field | Meaning |
|---|---|
| `sectors` | One or two of the 12 canonical buckets — primary first, plus an optional secondary when a code genuinely spans two. |
| `source.scheme` | `US_SIC` (US, SEC SIC), `KR_KSIC` (Korea, KSIC), `JP_TSE33` (Japan, TSE 33-sector), or `EU_ICB` (Europe, ICB). |
| `source.code` | The company's official industry code as filed. |
| `source.url` | Official page to look that code up. |

The 12 canonical buckets: **Technology, Healthcare, Finance, Consumer Discretionary, Consumer Staples, Communications, Industrials, Energy, Utilities, Materials, Real Estate, Other**. `POST /screener/metrics` ([§6.1](#61-post-screenermetrics)) and `POST /screener/ownership` ([§6.5](#65-post-screenerownership)) both accept a `sectors` filter over these.

### 1.11 Company symbols and CIKs

A `ticker` may be any symbol the company has traded under; a `cik` may be any CIK it has filed under. Responses name the company by its current symbol, and grouping and counting are per company. When the input is not the company's current symbol or CIK, or the company is delisted, `_warnings` says so.

---

## 2. Credit summary

Credit charges for **`/data`**, **`/metric`**, **`/screener`**, **`/ownership`**, and **`/events`** are as follows.

| Endpoint | Credits |
|----------|---------|
| `POST /data/filings` | **10** per successful debit. Debited **even if** the returned `filings` list is empty. |
| `POST /data/company` | **8** per successful debit. Debited **even if** `DATA_COMPANY_NOT_FOUND` is returned. |
| `GET /data/company/list` | **175** per successful debit. |
| `POST /data/line-items` | Sum over requested line items: tier **L0=10, L1=20, L2=30, L3=50** per line item. If your balance is less than the full sum, you may be charged up to your remaining balance. |
| `POST /data/facts` | **ceil(n / 5) × 10** where **n** is the number of **deduplicated** fact ids. |
| `POST /data/statement` (role_label mode) | Tiered **28 / 48**: **28** per debit, debited **even if** the filing is missing or nothing matches; the extra **20** of the higher (48-credit) tier may be capped at your remaining balance. |
| `POST /data/statement` (statement_type mode) | **28** flat per successful debit. Debited **even if** the filing is missing or has no block of this statement_type. |
| `POST /data/filing-statements` | **10** per successful debit. Debited **even if** the filing is missing. |
| `POST /data/income-statement` | **28** per successful debit. Debited **even if** the filing is missing or has no such statement. |
| `POST /data/comprehensive-income` | **28** per successful debit. Debited **even if** the filing is missing or has no such statement. |
| `POST /data/balance-sheet` | **28** per successful debit. Debited **even if** the filing is missing or has no such statement. |
| `POST /data/cash-flow-statement` | **28** per successful debit. Debited **even if** the filing is missing or has no such statement. |
| `POST /data/equity-statement` | **28** per successful debit. Debited **even if** the filing is missing or has no such statement. |
| `POST /data/facts/dimensional-breakdown` | **ceil((1 + N) / 5) × 10** where **N** is the number of returned slice facts. **10 credits** are debited on `DATA_FACT_NOT_FOUND`. |
| `POST /data/line-items/dimensional-breakdown` | Tier credit for `line_item` (**L0=10 … L3=50**) + **ceil(N_total / 5) × 10** for returned slice facts. Partial billing applies. |
| `POST /data/compare/line-items` | For each line item, **(tier credits) × (number of filings in the request)**. Same partial charging as **`POST /data/line-items`** when your balance is below the full price. |
| `POST /data/compare/facts` | **ceil(total_deduplicated_ids / 5) × 10** across **all** filing rows. |
| `GET /data/filings/{filing_id}/excel` | **300** when the download is issued. |
| `POST /metric/filings` | **10** |
| `POST /metric/all` | **148** |
| `POST /metric/all/filing-date` | **148** |
| `POST /metric/items` | **148** |
| `POST /metric/profitability` | **148** |
| `POST /metric/growth` | **148** |
| `POST /metric/financial-health` | **148** |
| `POST /metric/efficiency` | **148** |
| `POST /metric/cash-flow` | **148** |
| `POST /metric/valuation` | **148** |
| `POST /metric/valuation/filing-date` | **148** |
| `POST /metric/historical` | **55** base fee, plus **148 × (number of filing rows returned on the current page)**. Default page size is 3 filing rows max. |
| `POST /screener/metrics` | Dynamic per request: **`max(65, 425 − tickerDiscount − lookbackDiscount − quarterlyDiscount + lookbackSurcharge + filterSurcharge + temporalSurcharge + includeSurcharge)`**. Range **89–1825**. See [§6.4](#64-credit-formula-screenermetrics) for the formula. Debited **even if** results are empty. |
| `GET /screener/metrics/filters` | **10** per call. |
| `POST /ownership/transactions` | **30** per page. Debited **even if** `OWNERSHIP_COMPANY_NOT_FOUND` is returned. |
| `POST /ownership/stats` | **40** base + **ceil(n / 1000) × 5**, where **n** is the number of participating transactions. The base is debited **even if** `OWNERSHIP_COMPANY_NOT_FOUND` is returned. |
| `POST /ownership/transaction-ids` | **ceil(n / 5) × 10** where **n** is the number of **deduplicated** ids (max 200). |
| `POST /screener/ownership` | Dynamic per request: **`max(65, 200 − tickerDiscount − shortWindowDiscount + fullHistorySurcharge + 40 × conditions)`**. Range **65–920**. See [§6.7](#67-credit-formula-screenerownership) for the formula. Debited **even if** results are empty. |
| `GET /ownership/tickers` | **175** per successful debit. |
| `POST /ownership/names` | **30** per successful debit. Debited **even if** `OWNERSHIP_COMPANY_NOT_FOUND` is returned. |
| `GET /ownership/transaction-types` | **0** — static catalog, no charge. |
| `POST /ownership/initial-holdings` | **30** per page. Debited **even if** `OWNERSHIP_COMPANY_NOT_FOUND` is returned. |
| `POST /ownership/initial-holdings/stats` | **40** base + **ceil(n / 1000) × 5**, where **n** is the number of participating holdings. The base is debited **even if** `OWNERSHIP_COMPANY_NOT_FOUND` is returned. |
| `POST /ownership/initial-holding-ids` | **ceil(n / 5) × 10** where **n** is the number of **deduplicated** ids (max 200). |
| `POST /screener/initial-holdings` | Dynamic per request, same formula as `POST /screener/ownership` ([§6.7](#67-credit-formula-screenerownership)). Range **65–920**. Debited **even if** results are empty. |
| `POST /ownership/institutional-holdings/manager-history` | **30** per successful debit. Debited **even if** `OWNERSHIP_MANAGER_NOT_FOUND` is returned. |
| `POST /ownership/institutional-holdings/portfolio` | **30** per page. Debited **even if** the manager or the quarter is not found. |
| `POST /ownership/institutional-holdings/security` | **30** per page (flat mode); **40** flat with `group_by_quarter=true`. Debited **even if** `OWNERSHIP_SECURITY_NOT_FOUND` is returned. |
| `POST /screener/institutional-holdings` | Dynamic per request: **`max(65, 200 + 40 × conditions − narrowWindowDiscount + noWindowSurcharge)`**. Range **160–720**. See [§6.10](#610-credit-formula-screenerinstitutional-holdings) for the formula. Debited **even if** results are empty. |
| `GET /events/types` | **0**; Starter or higher required. |
| `POST /events/filings` | **10** per page, including empty results and missing companies. |
| `POST /events/filing` | **30**, with or without selected Items or original text; post-charge resource errors remain charged. |
| `POST /screener/events` | **65–280** per page; see [§6.12](#612-credit-formula-screenerevents). |
| `GET /billing/balance` | **0** — free balance check. Returns `{"credits_balance": <int>}` for the API key's account. Rate-limited at 30/minute. |

**Metrics resolution tiers (for `/data/line-items` and `/data/compare/line-items`):** each requested label is classified into **L0–L3**, which sets that line item’s credit cost. The chosen tier is echoed on each result as **`_resolution_level`** (`"L0"` through `"L3"`). The response may include **`_correction`** and **`_warnings`** when resolution is non-trivial.

---

## 3. Rate limits

Per-account limits (requests per minute, per route):

| Method | Path | Limit |
|--------|------|-------|
| POST | `/data/filings` | 120 |
| POST | `/data/company` | 120 |
| GET | `/data/company/list` | 120 |
| POST | `/data/line-items` | 100 |
| POST | `/data/facts` | 100 |
| POST | `/data/statement` | 120 |
| POST | `/data/filing-statements` | 120 |
| POST | `/data/income-statement` | 120 |
| POST | `/data/comprehensive-income` | 120 |
| POST | `/data/balance-sheet` | 120 |
| POST | `/data/cash-flow-statement` | 120 |
| POST | `/data/equity-statement` | 120 |
| POST | `/data/facts/dimensional-breakdown` | 60 |
| POST | `/data/line-items/dimensional-breakdown` | 60 |
| POST | `/data/compare/line-items` | 60 |
| POST | `/data/compare/facts` | 60 |
| GET | `/data/filings/{filing_id}/excel` | 20 |
| POST | `/metric/filings` | 100 |
| POST | `/metric/all` | 60 |
| POST | `/metric/all/filing-date` | 60 |
| POST | `/metric/items` | 60 |
| POST | `/metric/profitability` | 60 |
| POST | `/metric/growth` | 60 |
| POST | `/metric/financial-health` | 60 |
| POST | `/metric/efficiency` | 60 |
| POST | `/metric/cash-flow` | 60 |
| POST | `/metric/valuation` | 60 |
| POST | `/metric/valuation/filing-date` | 60 |
| POST | `/metric/historical` | 60 |
| POST | `/screener/metrics` | 10 |
| GET | `/screener/metrics/filters` | 60 |
| POST | `/screener/ownership` | 10 |
| POST | `/ownership/transactions` | 120 |
| POST | `/ownership/stats` | 120 |
| POST | `/ownership/transaction-ids` | 120 |
| GET | `/ownership/tickers` | 120 |
| POST | `/ownership/names` | 120 |
| GET | `/ownership/transaction-types` | 60 |
| POST | `/ownership/initial-holdings` | 120 |
| POST | `/ownership/initial-holdings/stats` | 120 |
| POST | `/ownership/initial-holding-ids` | 120 |
| POST | `/screener/initial-holdings` | 10 |
| POST | `/ownership/institutional-holdings/manager-history` | 120 |
| POST | `/ownership/institutional-holdings/portfolio` | 120 |
| POST | `/ownership/institutional-holdings/security` | 120 |
| POST | `/screener/institutional-holdings` | 10 |
| GET | `/events/types` | 60 |
| POST | `/events/filings` | 120 |
| POST | `/events/filing` | 60 |
| POST | `/screener/events` | 10 |

---

## 4. Data API

> **Filing caveats apply to every endpoint in this section.** When a filing carries a free-text disclaimer, it is echoed as a `caveat` field on the filing information and mirrored to the response's `_warnings` list. See [§1.6 Filing caveats](#16-filing-caveats) for the exact shape; per-endpoint sections below only call it out where the placement differs (lists, compare).

---

### 4.1 `POST /data/filings`

**Purpose:** List filings available for one company and one fiscal year, optionally filtered by form type and quarter.

**Request:**

| Field | Type | Required | Notes |
|-------|------|----------|--------|
| `ticker` | string | Yes | 1–32 characters; matched case-insensitively. |
| `fiscal_year` | integer | Yes | 1990–2100. |
| `form_type` | string | No | Max 32 characters. US forms: `10-K`, `10-Q`. Foreign-issuer annual reports: `20-F` (US-listed foreign companies), `40-F` (Canadian filers). Korean (DART), Japanese (EDINET), European (ESEF) and China A-share filings use the same values — `10-K` for the annual report, `10-Q` for the quarterly report. If omitted, all forms for that ticker and year are considered. |
| `quarter` | string | No | Max 4 characters (e.g. `Q1`). Use when you need a single quarterly filing among several. |

**Response:**

| Field | Meaning |
|-------|---------|
| `credits_used` | Always **10** when successful. |
| `fiscal_year` | Echo of request. |
| `form_type` | Present only if you sent `form_type`. |
| `quarter` | Present only if you sent `quarter`. |
| `company` | Company profile (`ticker`, `cik`, `legal_name`) or `null` if unknown. |
| `filings` | Array of filings: `id`, `ticker`, `registrant_cik`, `registrant_name` (the filing's registrant; `null` when absent), `form_type`, `fiscal_year`, `quarter`, `total_facts`, `total_blocks`, `has_excel`, `caveat` (see [§1.6](#16-filing-caveats)). |

Optional **`amendment_context`** may appear on a filing item when restatement or amendment information is available (English summary and optional references).

**Errors:**

| HTTP | `error_code` | When |
|------|----------------|------|
| 402 | `CREDITS_INSUFFICIENT` | Not enough credits. |
| 422 | — | Validation (body rules). |

**Example Request:**

```json
{
  "ticker": "AAPL",
  "fiscal_year": 2024,
  "form_type": "10-K"
}
```

**Example MCP tool call:**

```python
list_filings(ticker="AAPL", fiscal_year=2024, form_type="10-K")
```

**Example Response:**

```json
{
  "credits_used": 10,
  "fiscal_year": 2024,
  "company": {
    "ticker": "AAPL",
    "cik": "0000320193",
    "legal_name": "Apple Inc."
  },
  "filings": [
    {
      "id": 12345,
      "ticker": "AAPL",
      "registrant_cik": "0000320193",
      "registrant_name": "Apple Inc.",
      "form_type": "10-K",
      "fiscal_year": 2024,
      "quarter": "FY",
      "total_facts": 1500,
      "total_blocks": 12,
      "has_excel": true
    }
  ]
}
```

---

### 4.2 `POST /data/company`

**Purpose:** Resolve one company by **`ticker`**, **`cik`**, **`dart_corp_code`**, **`edinet_code`**, **`lei`**, or **`uscc`** and return its identity plus a coverage summary — every `form_type × fiscal_year × quarter` we hold for that issuer. Use this as the entry point when you only know a company name/ticker and need to discover what filings exist.

**Request:**

| Field | Type | Required | Notes |
|-------|------|----------|--------|
| `ticker` | string | Conditional | Exactly one of `ticker`, `cik`, `dart_corp_code`, `edinet_code`, `lei`, or `uscc` must be provided. 1–32 characters; case-insensitive. European tickers are the composite form `<ticker>_<country>` (e.g. `VIRI_F`); China A-share tickers are `<6-digit code>_CN` (e.g. `600519_CN`). |
| `cik` | string | Conditional | Numeric string; zero-padded to 10 digits on the server side. SEC-listed companies only. |
| `dart_corp_code` | string | Conditional | Numeric string; zero-padded to 8 digits on the server side. Korean DART-listed companies only. |
| `edinet_code` | string | Conditional | EDINET issuer code (`E` + 5 digits). Japanese EDINET-listed companies only. |
| `lei` | string | Conditional | 20-character ISO 17442 LEI; case-insensitive. European ESEF-listed companies only. |
| `uscc` | string | Conditional | 18-character Unified Social Credit Code; case-insensitive. China A-share companies only. |

**Response:**

| Field | Meaning |
|-------|---------|
| `ticker`, `cik`, `dart_corp_code`, `edinet_code`, `lei`, `uscc`, `legal_name` | Company identity. Only the applicable identifier is present — `cik` for SEC-listed companies, `dart_corp_code` for Korean companies, `edinet_code` for Japanese companies, `lei` for European companies, `uscc` (Unified Social Credit Code, 18 chars) for China A-share companies; the others are omitted. `legal_name` may be absent for rare records. |
| `jurisdiction` | `"US"` for SEC-listed companies; `"KR"` for Korean DART-listed companies; `"JP"` for Japanese EDINET-listed companies; `"EU"` for European ESEF-listed companies; `"CN"` for China A-share companies. |
| `sec_filings_url` | EDGAR public browse URL for the company, derived from CIK. Omitted for companies with no CIK (e.g. Korean and Japanese companies). |
| `sec_filings_urls` | SEC-listed companies only: one entry per CIK the company has filed under, current first — `{cik, is_current, url}`. |
| `tickers` | Every symbol the company has traded under — `{ticker, is_current, valid_from, valid_to}`; `valid_from` / `valid_to` only when set. |
| `dart_filings_url` | DART public browse URL for the company, derived from `dart_corp_code`. Omitted for companies with no DART corp code (e.g. US and Japanese companies). |
| `edinet_filings_url` | EDINET public browse URL for the company, derived from `edinet_code`. Omitted for companies with no EDINET code (e.g. US and Korean companies). |
| `esef_filings_url` | filings.xbrl.org public browse URL for the company, derived from `lei`. Omitted for companies with no LEI. |
| `cninfo_filings_url` | cninfo public browse URL for the company, derived from the 6-digit stock code. Omitted for non-A-share companies. |
| `filings_count` | Total filings we hold for this company across all forms and years. |
| `coverage` | Object keyed by filing-type name. **US**: SEC form names (`"10-K"`, `"10-Q"`, `"20-F"`, `"40-F"`). **KR** / **JP** / **EU** / **CN**: jurisdiction-neutral labels (`"annual"`, `"quarterly"`). Each value has `fiscal_years` (sorted list), `earliest`, `latest`, `fiscal_year_quarters` (year → list of quarter labels: `"FY"`, `"Q1"`–`"Q4"`), and `normalized_form` — a uniform identifier (`"annual"` / `"quarterly"`) you can use to iterate across jurisdictions without checking the key. |
| `credits_used` | Always **8** when successful. |

**Charging:** A `DATA_COMPANY_NOT_FOUND` response is still charged 8 credits.

**Errors:**

| HTTP | `error_code` | When |
|------|----------------|------|
| 404 | `DATA_COMPANY_NOT_FOUND` | No company on file for the given identifier. **Still debits 8 credits.** |
| 403 | `PLAN_TIER_INSUFFICIENT_COVERAGE` | The requested company is outside your plan's coverage scope. |
| 402 | `CREDITS_INSUFFICIENT` | Not enough credits. |
| 422 | — | Validation (zero or more than one of `ticker`/`cik`/`dart_corp_code`/`edinet_code`/`lei`/`uscc` supplied; non-numeric `cik` or `dart_corp_code`; `lei` not 20 letters or digits; `uscc` not 18 letters or digits). |

**Example Request:**

```json
{
  "ticker": "AAPL"
}
```

**Example MCP tool call:**

```python
lookup_company(ticker="AAPL")
```

**Example Response:**

```json
{
  "ticker": "AAPL",
  "jurisdiction": "US",
  "cik": "0000320193",
  "legal_name": "Apple Inc.",
  "sec_filings_url": "https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=0000320193&type=&dateb=&owner=include&count=40",
  "tickers": [
    {"ticker": "AAPL", "is_current": true}
  ],
  "filings_count": 12,
  "coverage": {
    "10-K": {
      "fiscal_years": [2020, 2021, 2022, 2023, 2024],
      "earliest": 2020,
      "latest": 2024,
      "fiscal_year_quarters": {
        "2020": ["FY"],
        "2021": ["FY"],
        "2022": ["FY"],
        "2023": ["FY"],
        "2024": ["FY"]
      },
      "normalized_form": "annual"
    },
    "10-Q": {
      "fiscal_years": [2023, 2024],
      "earliest": 2023,
      "latest": 2024,
      "fiscal_year_quarters": {
        "2023": ["Q1", "Q2", "Q3"],
        "2024": ["Q1", "Q2", "Q3"]
      },
      "normalized_form": "quarterly"
    }
  },
  "credits_used": 8,
  "sec_filings_urls": [
    {"cik": "0000320193", "is_current": true, "url": "https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=0000320193&type=&dateb=&owner=include&count=40"}
  ]
}
```

---

### 4.3 `GET /data/company/list`

**Purpose:** Discovery — return every company covered. Cache the result locally and refresh at most **once per day**.

**Request:**

| Query | Type | Default | Notes |
|-------|------|---------|--------|
| `jurisdiction` | string | (omitted) | Optional. `"US"`, `"KR"`, `"JP"`, `"EU"`, `"CN"`, or `"all"`. When omitted, returns everything your plan includes. |

**Response:**

| Field | Meaning |
|-------|---------|
| `companies` | One object per company, sorted by ticker, each with `ticker`, `jurisdiction`, and the one identifier that applies — `cik` (SEC-listed), `dart_corp_code` (Korean), `edinet_code` (Japanese), `lei` (European), or `uscc` (China A-share); the others are omitted. All are omitted for the rare row whose identifier is not yet on file. |
| `companies[].classification` | Industry classification block — **Pro+ only**, omitted otherwise. See [§1.10](#110-industry-classification-pro). |
| `credits_used` | Always **175** when successful. |

**Errors:**

| HTTP | `error_code` | When |
|------|----------------|------|
| 403 | `PLAN_TIER_INSUFFICIENT_COVERAGE` | The requested `jurisdiction` is outside your plan's coverage scope. |
| 402 | `CREDITS_INSUFFICIENT` | Not enough credits. |
| 400 | `JURISDICTION_INVALID` | `jurisdiction` is not one of `US`, `KR`, `JP`, `EU`, `CN`, `all`. |
| 422 | `VALIDATION_ERROR` | A query parameter other than `jurisdiction` was supplied. Not charged. |

**Example Request:**

```
GET /api/v1/data/company/list
X-API-Key: <your-api-key>
```

**Example MCP tool call:**

```python
list_companies()
```

**Example Response:**

```json
{
  "companies": [
    {"ticker": "AAPL",   "jurisdiction": "US", "cik": "0000320193",
     "classification": {"sectors": ["Technology"], "source": {"scheme": "US_SIC", "code": "3571", "url": "https://www.sec.gov/search-filings/standard-industrial-classification-sic-code-list"}}},
    {"ticker": "MSFT",   "jurisdiction": "US", "cik": "0000789019"},
    {"ticker": "005930", "jurisdiction": "KR", "dart_corp_code": "00126380"},
    {"ticker": "1332",   "jurisdiction": "JP", "edinet_code": "E00014"},
    {"ticker": "VIRI_F", "jurisdiction": "EU", "lei": "969500FCVQ5SLAAUJV59"},
    {"ticker": "600519_CN", "jurisdiction": "CN", "uscc": "9152000071430580XT"}
  ],
  "credits_used": 175
}
```

---

### 4.4 `POST /data/line-items`

**Purpose:** Return structured facts for **one** filing by **line-item name** (e.g. `revenue`, `net income`, `operating expenses`). Names are resolved through a tiered L0–L3 path; per-line-item credit pricing.

**Filing selection:** supply **`filing_id`**, or supply **`ticker`**, **`fiscal_year`**, **`form_type`** (default **`10-K`**), and optional **`quarter`**. If **`filing_id`** is present with identity fields, **`filing_id`** wins.

**Request:**

| Field | Type | Required | Notes |
|-------|------|----------|--------|
| `filing_id` | integer | Conditional | 1–10⁹. If set, ticker/year fields are optional. |
| `ticker` | string | Conditional | Required if `filing_id` omitted. 1–32 characters. |
| `form_type` | string | No | Default `10-K`, max 32. |
| `fiscal_year` | integer | Conditional | Required if `filing_id` omitted. 1990–2100. |
| `quarter` | string | No | Max 4 characters. |
| `line_items` | array of string | Yes | Non-empty; max **50** entries; each entry max **256** characters. |
| `light_weight_mode` | boolean | No | Default `false`. When `true`, each `results[*].dimensional_breakdowns` is set to `null`; saves context when you already know exactly what you need. A notice is appended to `_warnings`. Charged identically. |

**Response:** `ticker`, `form_type`, `fiscal_year`, `quarter`, `source_url_prefix`, `results`, `credits_used`, `credits_by_line_item` (credits charged per requested line item), optional `_correction` / `_warnings`, optional `amendment_context`. Each entry in `results` carries `metric`, `concept`, `value`, `raw_value`, `unit`, `period`, `dimensions`, `dimensional_breakdowns`, `restated` / `prior` (same meaning as on **[`POST /data/facts`](#45-post-datafacts)**; `null` when the fact was not touched by an amendment), **`_resolution_level`** (`"L0"`, `"L1"`, `"L2"`, or `"L3"`), and **one of**:

- **`fact_id`** (string) + **`source_locator`** (string) — the number appears at exactly one position in the source filing.
- **`fact_ids`** (list of `{fact_id, source_locator}` objects) — the same logical fact (matching concept, value, period, dimensions, and breakdowns) is reported at multiple positions in the source filing (e.g. income statement + equity statement + cash flow). All positions are listed equal-rank.

The two fields are mutually exclusive; each result row carries exactly one of them.

When one line-item name matches more than one concept, `results` come pre-ordered by Akkru smart ranking, and the multi-tag `_warnings` entry lists the matched concepts in that same order. The order is a recommendation only — review all returned results and judge which one you need.

Results are consolidated totals only. A value reported on a dimension (a single product line, segment, or region) is never returned by name, even when a printed statement row carries that name. Two ways to read slice values: `POST /data/facts/dimensional-breakdown` / `POST /data/line-items/dimensional-breakdown` starting from the total's `fact_id`, or `POST /data/statement` to read the full statement table.

`is_consolidated_total` is a string: `"true"` (the source filing explicitly tags the value as a consolidated total), `"equivalent"` (the source filing does not explicitly tag the value as a consolidated total; the value is presented as an equivalent consolidated total; see `_warnings`), or `"false"`.

`revenue` follows each filing's own reported top line. For financial-sector companies (banks, brokers, insurers) that top line is a net figure (e.g. net interest income plus net fee income, or insurance revenue), so it is not directly comparable to the gross sales reported by non-financial companies.

Some filings tag per-share items (dividends per share, share counts) only per share class or dividend type; rows carrying such classifications do not resolve by name. They remain readable in the source statements and notes via `POST /data/filing-statements` + `POST /data/statement`.

**Credits:** tiered L0=10 … L3=50 per line item, partial-credit billing. Zero-balance → **HTTP 402**.

**Errors:** `DATA_FILING_NOT_FOUND`, `DATA_FILING_QUARTER_REQUIRED`, `CREDITS_ZERO_BALANCE`.

**Example Request:**

```json
{
  "filing_id": 12345,
  "line_items": ["revenue", "net income"],
  "light_weight_mode": true
}
```

**Example MCP tool call:**

```python
query_line_items(filing_id=12345, line_items=["revenue", "net income"], light_weight_mode=True)
```

**Example response excerpt** (one single-position row + one multi-position row):

```json
{
  "results": [
    {
      "metric": "depreciation",
      "concept": "us-gaap_Depreciation",
      "fact_id": "f-665",
      "source_locator": "aapl-20250927.htm#fact-identifier-780",
      "value": 8000000000.0,
      "raw_value": "8.0",
      "unit": "usd",
      "period": "2024-09-29 ~ 2025-09-27",
      "is_consolidated_total": "true",
      "_resolution_level": "L1"
    },
    {
      "metric": "revenue",
      "concept": "us-gaap_RevenueFromContractWithCustomerExcludingAssessedTax",
      "fact_ids": [
        { "fact_id": "f-78",   "source_locator": "aapl-20250927.htm#fact-identifier-95"  },
        { "fact_id": "f-392",  "source_locator": "aapl-20250927.htm#fact-identifier-113" },
        { "fact_id": "f-1021", "source_locator": "aapl-20250927.htm#fact-identifier-121" },
        { "fact_id": "f-1113", "source_locator": "aapl-20250927.htm#fact-identifier-143" }
      ],
      "value": 416161000000.0,
      "raw_value": "416,161",
      "unit": "usd",
      "period": "2024-09-29 ~ 2025-09-27",
      "is_consolidated_total": "true",
      "_resolution_level": "L1"
    }
  ],
  "credits_used": 40
}
```

---

### 4.5 `POST /data/facts`

**Purpose:** Return structured facts for **one** filing by **`fact_id`**. For line-item-name queries (e.g. `revenue`, `net income`) use **[`POST /data/line-items`](#44-post-dataline-items)** instead.

**Filing selection:** supply **`filing_id`**, or supply **`ticker`**, **`fiscal_year`**, **`form_type`** (default **`10-K`**), and optional **`quarter`**. If **`filing_id`** is present with identity fields, **`filing_id`** wins.

**Request:**

| Field | Type | Required | Notes |
|-------|------|----------|--------|
| `filing_id` | integer | Conditional | 1–10⁹. If set, ticker/year fields are optional. |
| `ticker` | string | Conditional | Required if `filing_id` omitted. 1–32 characters. |
| `form_type` | string | No | Default `10-K`, max 32. |
| `fiscal_year` | integer | Conditional | Required if `filing_id` omitted. 1990–2100. |
| `quarter` | string | No | Max 4 characters. |
| `fact_ids` | array of string | Yes | Non-empty; max **500** strings; each max **128** characters. |
| `light_weight_mode` | boolean | No | Default `false`. Leaner response — see *Light-weight mode* below; saves context when you already know exactly what you need. Charged identically. |

**Response:**

| Field | Meaning |
|-------|---------|
| `ticker`, `form_type`, `fiscal_year`, `quarter` | Filing identity. |
| `source_url_prefix` | **Source-document URL base** for this filing on its regulator's platform (SEC viewer for US filings; DART, EDINET, filings.xbrl.org, cninfo for KR / JP / EU / CN). May be `null`. The API does **not** return full source URLs; callers concatenate `source_url_prefix` with each fact's `source_locator`. |
| `results` | Array of facts (each includes `metric`, `concept`, `fact_id`, optional `source_locator`, values, units, period fields, plus `dimensions`, `dimensional_breakdowns` and `is_consolidated_total` — same meaning as in [§4.4](#44-post-dataline-items)). `source_locator` may be `null` or a fragment / relative path (amendments may use a `../`-relative locator); callers concatenate it with `source_url_prefix` as-is. **Amendment-affected facts** also carry `restated: true` plus a `prior` object: for a value-**changed** fact `prior` holds the superseded value (`raw_value` / `calculated_value` / `unit` / `decimals` / `fact_id` / `source_locator`); for a fact **added** by the amendment `prior` is the sentinel `{ "added_by_amendment": "This fact was not present in the original filing; it was added by an amendment." }`. Both fields are always present; on original/unaffected facts they are `null`. |
| `credits_used` | `ceil(deduped_fact_id_count / 5) × 10`. |
| `amendment_context` | Optional; same idea as under filings when applicable. |
| `_warnings` | Optional strings (duplicate ids deduplicated, missing ids not found in the filing, plus the *Light-weight mode* notice when that flag is on). |

**Light-weight mode** (`light_weight_mode=true`): each `results[*].dimensional_breakdowns` is set to `null`. A notice is appended to `_warnings`.

**Errors:**

| HTTP | `error_code` | When |
|------|----------------|------|
| 400 | `DATA_FILING_QUARTER_REQUIRED` | Several filings match without a quarter; message lists available quarters. |
| 404 | `DATA_FILING_NOT_FOUND` | No filing for the given id or identity. |
| 402 | `CREDITS_INSUFFICIENT` | Not enough credits. |
| 422 | — | Validation (empty fact_ids, limits, missing filing selector). |

**Example Request:**

```json
{
  "filing_id": 12345,
  "fact_ids": ["ix_1", "ix_2"]
}
```

**Example MCP tool call:**

```python
get_filing_facts(filing_id=12345, fact_ids=["ix_1", "ix_2"])
```

---

### 4.6 `POST /data/statement`

**Purpose:** Return one or more **financial statement block(s)** (JSON) for a filing. Two mutually exclusive lookup modes:

- **`role_label` mode** — the filer's original XBRL role string (varies per filer; e.g. `"CONSOLIDATED STATEMENTS OF OPERATIONS"` for Apple). Returns the one matched block.
- **`statement_type` mode** — the canonical statement name (e.g. `"Income Statement"`, `"Balance Sheet"`, `"Comprehensive Income"`, `"Stockholders Equity"`, `"Cash Flow Statement"`). Call **[`POST /data/filing-statements`](#47-post-datafiling-statements)** for the valid values in a specific filing. Case-insensitive; trailing `(N)` suffix is ignored. Returns every matching block; a block that combines several statements matches each of its names. Available for SEC filings only; use `role_label` mode for other jurisdictions.

For the 5 main statements prefer the dedicated [statement endpoints](#48-statement-endpoints-datastatement) (`/data/income-statement` etc.) — same response shape, with the target spelled in the URL.

**Filing selection:** same pattern as **`POST /data/facts`** (`filing_id` **or** `ticker` + `fiscal_year` + `form_type` + optional `quarter`).

**Request:**

| Field | Type | Required | Notes |
|-------|------|----------|--------|
| `filing_id` | integer | Conditional | Same rules as facts. |
| `ticker` | string | Conditional | |
| `form_type` | string | No | Default `10-K`. |
| `fiscal_year` | integer | Conditional | |
| `quarter` | string | No | |
| `role_label` | string | **XOR `statement_type`** | 1–4000 characters; original filer XBRL role string. |
| `statement_type` | string | **XOR `role_label`** | 1–4000 characters; canonical statement-type label. |
| `light_weight_mode` | boolean | No | Default `false`. Leaner response — see *Light-weight mode* below; saves context when you already know exactly what you need. Charged identically. |

**Credits:**

- `role_label` mode: tiered **28 / 48**. **28** is debited before the filing is resolved (balance below 28 → **HTTP 402**); the extra **20** of the higher (48-credit) tier is debited after matching, up to your remaining balance.
- `statement_type` mode: **28** flat, debited before the filing is resolved (balance below 28 → **HTTP 402**).

**Response:**

| Field | Meaning |
|-------|---------|
| `ticker`, `form_type`, `fiscal_year`, `quarter` | Filing identity. |
| `matches` | Array of match objects (length 1 for role_label mode; length 1+ for statement_type mode). Each has `block_index`, `role_label`, `statement_type` (array of names; more than one when the block combines several statements), `matched_via` (describes how it was selected), and `block` (the full statement block content). |
| `credits_used` | Number of credits deducted. |
| `amendment_context` | Optional; restatement or amendment information. |
| `_correction` | Optional; present on the 48-credit tier. |
| `_warnings` | Optional; includes the *Light-weight mode* notice when that flag is on. |

**Light-weight mode** (`light_weight_mode=true`) — applied to every match's `block`:

- `block.child_components` → replaced by a placeholder string
- In every `block.parent_xbrl_table.mapped_xbrl_facts[*]`:
  - `xbrl_fact`: `concept_ns`, `concept_local`, `item_type`, `decimals`, `sign_attr`, `sign_from_xbrl`, `format`
  - `html_locator`: `xlsx_cell`

A notice is appended to `_warnings`.

**Errors:**

| HTTP | `error_code` | When |
|------|----------------|------|
| 400 | `DATA_FILING_QUARTER_REQUIRED` | Ambiguous quarterly filings (charged). |
| 404 | `DATA_FILING_NOT_FOUND` | Filing not found. **Still charged 28 credits.** |
| 404 | `DATA_STATEMENT_ROLE_LABEL_UNRESOLVED` | role_label mode: no block matched; charged for the match tier reached. |
| 404 | `DATA_STATEMENT_NOT_AVAILABLE_IN_FILING` | statement_type mode: no block has this statement_type (charged). |
| 400 | `DATA_STATEMENT_TYPE_UNSUPPORTED_FOR_KR` | statement_type mode: Korean (DART) filings not yet supported — coming soon (charged). |
| 400 | `DATA_STATEMENT_TYPE_UNSUPPORTED_FOR_JP` | statement_type mode: Japanese (EDINET) filings not yet supported — coming soon (charged). |
| 400 | `DATA_STATEMENT_TYPE_UNSUPPORTED_FOR_EU` | statement_type mode: European (ESEF) filings not yet supported — coming soon (charged). |
| 400 | `DATA_STATEMENT_TYPE_UNSUPPORTED_FOR_CN` | statement_type mode: not available for China A-share filings; use `role_label` mode (charged). |
| 404 | `DATA_STATEMENT_BLOCK_JSON_MISSING` | Label matched but the block content is unavailable (**charged**). |
| 402 | `CREDITS_INSUFFICIENT` | Balance below 28. |
| 422 | — | Validation (e.g. both `role_label` and `statement_type` given, or neither). |

**Example Request (role_label mode):**

```json
{
  "ticker": "AAPL",
  "form_type": "10-K",
  "fiscal_year": 2024,
  "role_label": "CONSOLIDATED STATEMENTS OF OPERATIONS"
}
```

**Example Request (statement_type mode):**

```json
{
  "ticker": "AAPL",
  "form_type": "10-K",
  "fiscal_year": 2024,
  "statement_type": "Income Statement"
}
```

**Example MCP tool call:**

```python
get_filing_statement(ticker="AAPL", form_type="10-K", fiscal_year=2024,
                    statement_type="Income Statement")
```

**Example Response:**

```json
{
  "ticker": "AAPL",
  "form_type": "10-K",
  "fiscal_year": 2024,
  "quarter": "FY",
  "matches": [
    {
      "block_index": 1,
      "role_label": "CONSOLIDATED STATEMENTS OF OPERATIONS",
      "statement_type": ["Income Statement"],
      "matched_via": {
        "source": "statement_type",
        "query": "Income Statement",
        "matched_statement_type": "Income Statement"
      },
      "block": { "elements": [ "... truncated ..." ] }
    }
  ],
  "credits_used": 28
}
```

---

### 4.7 `POST /data/filing-statements`

**Purpose:** Discovery — list every statement block in a filing with its `block_index`, `statement_type` (array of canonical statement-type labels; more than one when the block combines several statements; `null` for non-financial blocks such as the cover page), and `role_label` (filer-specific XBRL string). Use this to find out exactly which `statement_type` / `role_label` values a specific filing exposes before calling **[`POST /data/statement`](#46-post-datastatement)** or the convenience endpoints.

**Filing selection:** standard (`filing_id` **or** `ticker` + `fiscal_year` + `form_type` + optional `quarter`).

**Request:**

| Field | Type | Required | Notes |
|-------|------|----------|--------|
| `filing_id` | integer | Conditional | |
| `ticker` | string | Conditional | |
| `form_type` | string | No | Default `10-K`. |
| `fiscal_year` | integer | Conditional | |
| `quarter` | string | No | |

**Response:**

| Field | Meaning |
|-------|---------|
| `ticker`, `form_type`, `fiscal_year`, `quarter` | Filing identity. |
| `statements` | Array of `{block_index, statement_type, role_label}`, ordered by `block_index`. |
| `credits_used` | Always **10** when successful. |

**Errors:**

| HTTP | `error_code` | When |
|------|----------------|------|
| 400 | `DATA_FILING_QUARTER_REQUIRED` | Ambiguous quarterly filings. |
| 404 | `DATA_FILING_NOT_FOUND` | Filing not found. **Still charged 10 credits** (this matches `/data/filings` and `/data/company` — discovery endpoints charge to cover the lookup attempt). |
| 402 | `CREDITS_INSUFFICIENT` | Insufficient credits. |
| 422 | — | Validation. |

**Example Request:**

```json
{
  "ticker": "AAPL",
  "form_type": "10-K",
  "fiscal_year": 2024
}
```

**Example MCP tool call:**

```python
list_filing_statements(ticker="AAPL", form_type="10-K", fiscal_year=2024)
```

**Example Response:**

```json
{
  "ticker": "AAPL",
  "form_type": "10-K",
  "fiscal_year": 2024,
  "quarter": "FY",
  "statements": [
    { "block_index": 0, "statement_type": null,                    "role_label": "Cover Page" },
    { "block_index": 1, "statement_type": ["Income Statement"],    "role_label": "CONSOLIDATED STATEMENTS OF OPERATIONS" },
    { "block_index": 2, "statement_type": ["Comprehensive Income"],"role_label": "CONSOLIDATED STATEMENTS OF COMPREHENSIVE INCOME" },
    { "block_index": 3, "statement_type": ["Balance Sheet"],       "role_label": "CONSOLIDATED BALANCE SHEETS" },
    { "block_index": 4, "statement_type": ["Stockholders Equity"], "role_label": "CONSOLIDATED STATEMENTS OF SHAREHOLDERS' EQUITY" },
    { "block_index": 5, "statement_type": ["Cash Flow Statement"], "role_label": "CONSOLIDATED STATEMENTS OF CASH FLOWS" },
    { "block_index": 6, "statement_type": ["Earnings Per Share"],"role_label": "Earnings Per Share - Computation of Basic and Diluted..." }
  ],
  "credits_used": 10
}
```

---

### 4.8 Statement endpoints (`/data/<statement>`)

Five endpoints that fetch one specific statement directly by URL. Each is equivalent to [`POST /data/statement`](#46-post-datastatement) with the matching `statement_type` — fixed by the URL path, so `role_label` and `statement_type` are not accepted in the body. Flat **28 credits** per successful debit, debited before the filing is resolved (a missing filing or statement is still charged); rate limit **120/minute**. Available for SEC filings only.

| Endpoint | Statement returned |
|---|---|
| `POST /data/income-statement` | Income Statement |
| `POST /data/comprehensive-income` | Statement of Comprehensive Income |
| `POST /data/balance-sheet` | Balance Sheet |
| `POST /data/cash-flow-statement` | Cash Flow Statement |
| `POST /data/equity-statement` | Statement of Stockholders' / Shareholders' Equity |

**Request body:** same fields as [`POST /data/statement`](#46-post-datastatement) **minus** `role_label` and `statement_type`.

| Field | Type | Required | Notes |
|-------|------|----------|--------|
| `filing_id` | integer | Conditional | |
| `ticker` | string | Conditional | |
| `form_type` | string | No | Default `10-K`. |
| `fiscal_year` | integer | Conditional | |
| `quarter` | string | No | |
| `light_weight_mode` | boolean | No | Default `false`. Saves context when you already know exactly what you need. |

**Response:** identical to [`POST /data/statement`](#46-post-datastatement) (`matches: [...]`). `matched_via.source` is always `"statement_type"`.

**Errors:** identical to [`POST /data/statement`](#46-post-datastatement) (`statement_type` mode).

**Example request:**

```json
{ "ticker": "AAPL", "form_type": "10-K", "fiscal_year": 2024 }
```

**Example MCP tool calls:**

```python
get_income_statement(ticker="AAPL", form_type="10-K", fiscal_year=2024, light_weight_mode=True)
get_comprehensive_income(ticker="AAPL", form_type="10-K", fiscal_year=2024)
get_balance_sheet(ticker="AAPL", form_type="10-K", fiscal_year=2024)
get_cash_flow_statement(ticker="AAPL", form_type="10-K", fiscal_year=2024)
get_equity_statement(ticker="AAPL", form_type="10-K", fiscal_year=2024)
```

---

### 4.9 `POST /data/facts/dimensional-breakdown`

**Purpose:** Return one consolidated fact plus every slice fact it breaks down into. Slice facts are grouped by the set of XBRL axes their dimensions sit on.

**Filing selection:** standard (`filing_id` **or** `ticker` + `fiscal_year` + `form_type` + optional `quarter`).

**Request:**

| Field | Type | Required | Notes |
|-------|------|----------|--------|
| `filing_id` | integer | Conditional | |
| `ticker` | string | Conditional | |
| `form_type` | string | No | Default `10-K`. |
| `fiscal_year` | integer | Conditional | |
| `quarter` | string | No | |
| `fact_id` | string | **Yes** | The `fact_id` of a consolidated fact. |
| `recursive` | boolean | No | Default `true`. When `false`, only the immediate children are returned. |

**Response:**

| Field | Meaning |
|-------|---------|
| `ticker`, `form_type`, `fiscal_year`, `quarter`, `source_url_prefix` | Filing identity. |
| `root` | The fact identified by `fact_id`: `concept`, `value`, `raw_value`, `unit`, `period`, `dimensions`, `is_consolidated_total`, `fact_id`, `source_locator`. The `metric`, `fact_ids`, `restated`, `prior`, and `dimensional_breakdowns` fields of **[`POST /data/facts`](#45-post-datafacts)** are not returned here. |
| `groups` | Array of `{axes, facts}` entries. `axes` is the sorted list of XBRL axis keys shared by every fact in `facts`; `facts` is the list of slice facts. Sorted by descending fact count, ties broken lexicographically by `axes`. |
| `groups[].facts[].groups` | Present only when `recursive=true`. Same shape, nested per level. |
| `_warnings` | Optional. |
| `credits_used` | `ceil((1 + N) / 5) × 10`, where `N` is the number of returned slice facts. |

**Member granularity within a group:** A single axis may carry both summary members and their detail components. Summary members overlap their detail components, so summing all facts in one group can double-count.

**Credits:** `ceil((1 + N) / 5) × 10`. **10 credits are charged** on `DATA_FACT_NOT_FOUND`. A root with no breakdowns returns 200 with `groups: []` and is charged 10 credits.

**Errors:**

| HTTP | `error_code` | When |
|------|----------------|------|
| 400 | `DATA_FILING_QUARTER_REQUIRED` | Ambiguous quarterly filings. |
| 404 | `DATA_FILING_NOT_FOUND` | Filing not found. |
| 404 | `DATA_FACT_NOT_FOUND` | `fact_id` is not present in this filing. **Still charged 10 credits.** |
| 402 | `CREDITS_INSUFFICIENT` | Not enough credits. |
| 422 | — | Validation. |

**Example Request:**

```json
{
  "ticker": "AAPL",
  "form_type": "10-K",
  "fiscal_year": 2025,
  "fact_id": "f-78"
}
```

**Example MCP tool call:**

```python
get_dimensional_breakdown_by_fact_id(
    ticker="AAPL", form_type="10-K", fiscal_year=2025, fact_id="f-78"
)
```

**Example Response (abridged):**

```json
{
  "ticker": "AAPL", "form_type": "10-K",
  "fiscal_year": 2025, "quarter": "FY",
  "source_url_prefix": "...",
  "root": {
    "concept": "us-gaap_RevenueFromContractWithCustomerExcludingAssessedTax",
    "fact_id": "f-78",
    "value": 416161000000.0,
    "raw_value": "416,161",
    "unit": "usd",
    "period": "2024-09-29 ~ 2025-09-27",
    "is_consolidated_total": "true",
    "dimensions": []
  },
  "groups": [
    {
      "axes": ["srt_ProductOrServiceAxis"],
      "facts": [
        {"concept": "us-gaap_RevenueFromContractWithCustomerExcludingAssessedTax",
         "fact_id": "f-72", "value": 307003000000.0,
         "dimensions": [{"axis": "srt_ProductOrServiceAxis","member": "us-gaap_ProductMember"}],
         "groups": []},
        {"fact_id": "f-377", "value": 209586000000.0,
         "dimensions": [{"axis": "srt_ProductOrServiceAxis","member": "aapl_IPhoneMember"}],
         "groups": []}
      ]
    },
    {
      "axes": ["srt_ConsolidationItemsAxis","us-gaap_StatementBusinessSegmentsAxis"],
      "facts": [
        {"fact_id": "f-1016", "value": 178353000000.0,
         "dimensions": [
           {"axis":"srt_ConsolidationItemsAxis","member":"us-gaap_OperatingSegmentsMember"},
           {"axis":"us-gaap_StatementBusinessSegmentsAxis","member":"aapl_AmericasSegmentMember"}
         ],
         "groups": []}
      ]
    },
    {
      "axes": ["srt_StatementGeographicalAxis"],
      "facts": [
        {"fact_id": "f-1104", "value": 151790000000.0,
         "dimensions": [{"axis":"srt_StatementGeographicalAxis","member":"country_US"}],
         "groups": []}
      ]
    }
  ],
  "credits_used": 28
}
```

---

### 4.10 `POST /data/line-items/dimensional-breakdown`

**Purpose:** Resolve a line-item name (e.g. `revenue`, `net income`) and return the dimensional breakdown of every consolidated fact that matches. Each match is one root with its axis-grouped slice facts. When the name matches more than one concept, matches come pre-ordered by Akkru smart ranking (same ordering as `POST /data/line-items`); the order is a recommendation only — review all matches and judge which one you need.

**Filing selection:** standard (`filing_id` **or** `ticker` + `fiscal_year` + `form_type` + optional `quarter`).

**Request:**

| Field | Type | Required | Notes |
|-------|------|----------|--------|
| `filing_id` | integer | Conditional | |
| `ticker` | string | Conditional | |
| `form_type` | string | No | Default `10-K`. |
| `fiscal_year` | integer | Conditional | |
| `quarter` | string | No | |
| `line_item` | string | **Yes** | A single line-item name (e.g. `"revenue"`). |
| `recursive` | boolean | No | Default `true`. |

**Response:**

| Field | Meaning |
|-------|---------|
| `ticker`, `form_type`, `fiscal_year`, `quarter`, `source_url_prefix` | Filing identity. |
| `matches` | Array of `{root, groups, duplicate_root_fact_ids?}` entries. |
| `matches[].root` | Same shape as `root` in **[`POST /data/facts/dimensional-breakdown`](#49-post-datafactsdimensional-breakdown)**, plus `metric` (echoes the request `line_item`) and `_resolution_level` (`"L0"`–`"L3"`). |
| `matches[].groups` | Same shape and ordering as `groups` in **[`POST /data/facts/dimensional-breakdown`](#49-post-datafactsdimensional-breakdown)**. Each fact also carries `metric` and `_resolution_level`. |
| `matches[].duplicate_root_fact_ids` | Optional. Other `fact_id` values that resolved to the same logical fact as `root`. |
| `_correction` | Optional. Same shape as on **[`POST /data/line-items`](#44-post-dataline-items)**. |
| `_warnings` | Optional. |
| `credits_used` | Resolver tier credit + `ceil(N_total / 5) × 10`, where `N_total` is the slice facts returned across all roots. |

**Member granularity within a group:** Same as **[`POST /data/facts/dimensional-breakdown`](#49-post-datafactsdimensional-breakdown)** — summary members and their detail components may coexist; summing all facts in one group can double-count.

**Credits:** Tiered resolution credit (**L0=10 … L3=50**) + `ceil(N_total / 5) × 10`. Partial billing applies.

**Errors:**

| HTTP | `error_code` | When |
|------|----------------|------|
| 400 | `DATA_FILING_QUARTER_REQUIRED` | Ambiguous quarterly filings. |
| 404 | `DATA_FILING_NOT_FOUND` | Filing not found. |
| 402 | `CREDITS_ZERO_BALANCE` | Balance is zero. |
| 402 | `CREDITS_INSUFFICIENT` | Insufficient credits. |
| 422 | — | Validation. |

**Example Request:**

```json
{
  "ticker": "AAPL",
  "form_type": "10-K",
  "fiscal_year": 2025,
  "line_item": "revenue"
}
```

**Example MCP tool call:**

```python
get_dimensional_breakdown_by_line_item(
    ticker="AAPL", form_type="10-K", fiscal_year=2025, line_item="revenue"
)
```

**Example Response (abridged):**

```json
{
  "ticker": "AAPL", "form_type": "10-K",
  "fiscal_year": 2025, "quarter": "FY",
  "source_url_prefix": "...",
  "matches": [
    {
      "root": {
        "metric": "revenue",
        "concept": "us-gaap_RevenueFromContractWithCustomerExcludingAssessedTax",
        "fact_id": "f-78",
        "value": 416161000000.0,
        "period": "2024-09-29 ~ 2025-09-27",
        "is_consolidated_total": "true",
        "dimensions": [],
        "_resolution_level": "L1"
      },
      "duplicate_root_fact_ids": ["f-392","f-1021","f-1113"],
      "groups": [ /* same shape as 4.9 */ ]
    },
    {
      "root": { "metric": "revenue", "fact_id": "f-79",
                "value": 391035000000.0, "period": "2023-10-01 ~ 2024-09-28", "_resolution_level": "L1", "..." : "..." },
      "duplicate_root_fact_ids": ["f-393","f-1050","f-1114"],
      "groups": [ /* ... */ ]
    },
    {
      "root": { "metric": "revenue", "fact_id": "f-80",
                "value": 383285000000.0, "period": "2022-09-25 ~ 2023-09-30", "_resolution_level": "L1", "..." : "..." },
      "duplicate_root_fact_ids": ["f-394","f-1079","f-1115"],
      "groups": [ /* ... */ ]
    }
  ],
  "credits_used": 110
}
```

---

### 4.11 `POST /data/compare/line-items`

**Purpose:** Compare a list of **line-item names** (e.g. `revenue`, `net income`) across **up to 25 filings**. Names are resolved through the same L0–L3 tiered path as **[`POST /data/line-items`](#44-post-dataline-items)**; per-line-item × per-filing pricing applies. Results are consolidated totals only, same as [§4.4](#44-post-dataline-items).

**Request:**

| Field | Type | Required | Notes |
|-------|------|----------|--------|
| `filings` | array | Yes | 1–25 rows. Each row: `filing_id` **or** (`ticker` + `fiscal_year` + `form_type` + optional `quarter`), same resolution rules as **`POST /data/facts`**. **No** `fact_ids` field on rows. |
| `line_items` | array of string | Yes | Non-empty; max **50** strings; each max **256** chars. |
| `light_weight_mode` | boolean | No | Default `false`. Leaner response — see *Light-weight mode* below; saves context when you already know exactly what you need. Charged identically. |

**Response:**

| Field | Meaning |
|-------|---------|
| `amendment_contexts` | Array parallel to `results`; each entry is an object or **`null`**. |
| `results` | Each element: `ticker`, `form_type`, `fiscal_year`, `quarter`, **`source_url_prefix`** (filing-level SEC URL base, may be `null`), optional **`caveat`** (per-row; see [§1.6](#16-filing-caveats)), **`unresolved`** (requested line items with no consolidated-total fact in that filing), **`facts`** (array of fact objects; each fact carries either `fact_id` + `source_locator` or `fact_ids`, as in **`POST /data/line-items`**, plus **`_resolution_level`** = `"L0"` … `"L3"` indicating which tier matched that line item), and — for **Pro+** callers — a per-row **`classification`** block (see [§1.10](#110-industry-classification-pro)). |
| `credits_used` | Per credit rules in [Section 2](#2-credit-summary). |
| `credits_by_line_item` | Credits charged per requested line item, summed over the compared filings. |
| `_correction`, `_warnings` | Optional; same general meaning as **`POST /data/line-items`**. Multi-concept line items are ordered by Akkru smart ranking within each row's `facts`, same as `POST /data/line-items` (a recommendation only — review all results). A `_warnings` entry is appended when the compared filings' fiscal period end dates differ by more than 45 days. The dimensional-breakdown notice is aggregated into a single string listing affected filings + fact ids. Per-row caveats are also aggregated into top-level `_warnings` (deduplicated; see [§1.6](#16-filing-caveats)). |

**Light-weight mode** (`light_weight_mode=true`): each `results[*].facts[*].dimensional_breakdowns` is set to `null`. A notice is appended to top-level `_warnings`.

**Partial-credit behavior:** This endpoint charges per line item × per filing with partial-credit billing. If your balance is too low to cover the full request mid-run, later line items will return as **unresolved** rather than abort the whole call; check `_warnings` / `_correction` to see which were skipped, then top up and re-issue for the rest.

**Errors:**

| HTTP | `error_code` | When |
|------|----------------|------|
| 400 | `DATA_FILING_QUARTER_REQUIRED` | A row needs `quarter` to disambiguate. |
| 404 | `DATA_FILING_NOT_FOUND` | A row’s filing does not exist (message may reference row index). |
| 402 | `CREDITS_ZERO_BALANCE` | Zero balance. |
| 422 | — | Limits, missing line_items, or other validation. |

**Example Request:**

```json
{
  "filings": [
    { "ticker": "AAPL", "form_type": "10-K", "fiscal_year": 2024 },
    { "ticker": "AAPL", "form_type": "10-K", "fiscal_year": 2023 }
  ],
  "line_items": ["revenue"]
}
```

**Example MCP tool call:**

```python
compare_line_items(
    filings=[
        {"ticker": "AAPL", "form_type": "10-K", "fiscal_year": 2024},
        {"ticker": "AAPL", "form_type": "10-K", "fiscal_year": 2023},
    ],
    line_items=["revenue"],
)
```

**Example Response:**

```json
{
  "amendment_contexts": [null, null],
  "results": [
    {
      "ticker": "AAPL",
      "form_type": "10-K",
      "fiscal_year": 2024,
      "quarter": "FY",
      "source_url_prefix": "https://www.sec.gov/ix?doc=/Archives/edgar/data/.../aapl-20240928.htm",
      "facts": [
        {
          "metric": "revenue",
          "concept": "us-gaap_Revenue...",
          "fact_id": "f-1",
          "source_locator": "#fact-identifier-f-1",
          "value": 383285000000.0
        }
      ]
    },
    {
      "ticker": "AAPL",
      "form_type": "10-K",
      "fiscal_year": 2023,
      "quarter": "FY",
      "source_url_prefix": "https://www.sec.gov/ix?doc=/Archives/edgar/data/.../aapl-20230930.htm",
      "facts": [
        {
          "metric": "revenue",
          "concept": "us-gaap_Revenue...",
          "fact_id": "f-1",
          "source_locator": "#fact-identifier-f-1",
          "value": 394328000000.0
        }
      ]
    }
  ],
  "credits_used": 80
}
```

---

### 4.12 `POST /data/compare/facts`

**Purpose:** Compare XBRL **`fact_ids`** across **up to 25 filings**. Each filing row carries its own `fact_ids` list.

**Request:**

| Field | Type | Required | Notes |
|-------|------|----------|--------|
| `filings` | array | Yes | 1–25 rows. Each row: `filing_id` **or** (`ticker` + `fiscal_year` + `form_type` + optional `quarter`), **plus** a required non-empty `fact_ids` list (max **500** per row; **2500** total ids across the whole request; each id ≤ **128** chars). |
| `light_weight_mode` | boolean | No | Default `false`. Leaner response — see *Light-weight mode* below; saves context when you already know exactly what you need. Charged identically. |

**Response:** Same shape as `POST /data/compare/line-items`. A `_warnings` entry is appended when the compared filings' fiscal period end dates differ by more than 45 days.

**Light-weight mode** (`light_weight_mode=true`): each `results[*].facts[*].dimensional_breakdowns` is set to `null`. A notice is appended to top-level `_warnings`.

**Partial-credit behavior:** Full request cost is `ceil(total_deduped_fact_ids / 5) × 10` credits. The endpoint uses partial-credit billing, so if your balance can only cover part of the request you'll be charged that much and the response serves a proportional subset of the fact_ids (each 10 credits buy up to 5 ids; filings are processed in request order — once the budget runs out the remaining filings come back with `facts: []`). A `_warnings[0]` line states how many ids were served vs requested, and `credits_used` reflects what was actually spent. Zero balance → **HTTP 402**.

**Errors:**

| HTTP | `error_code` | When |
|------|----------------|------|
| 400 | `DATA_FILING_QUARTER_REQUIRED` | A row needs `quarter` to disambiguate. |
| 404 | `DATA_FILING_NOT_FOUND` | A row’s filing does not exist. |
| 402 | `CREDITS_INSUFFICIENT` | Balance is zero. |
| 422 | — | Limits, missing fact_ids, or other validation. |

**Example Request:**

```json
{
  "filings": [
    { "filing_id": 12345, "fact_ids": ["ix_1", "ix_2"] },
    { "filing_id": 12344, "fact_ids": ["ix_1"] }
  ]
}
```

**Example MCP tool call:**

```python
compare_facts(
    filings=[
        {"filing_id": 12345, "fact_ids": ["ix_1", "ix_2"]},
        {"filing_id": 12344, "fact_ids": ["ix_1"]},
    ],
)
```

---

### 4.13 `GET /data/filings/{filing_id}/excel`

**Purpose:** Obtain a **time-limited download URL** for the filing’s Excel attachment when the file is available.

**Path parameter:** `filing_id` (integer).

**Response:**

| Field | Meaning |
|-------|---------|
| `ticker`, `fiscal_year` | Filing identity. |
| `download_url` | The time-limited URL to download the Excel file. |
| `expires_in` | Expiration time of the URL in seconds (e.g., **3600**). |
| `credits_used` | Always **300** when successful. |

**Errors:**

| HTTP | `error_code` | When |
|------|----------------|------|
| 404 | `DATA_FILING_NOT_FOUND` | Unknown `filing_id`. |
| 404 | `DATA_EXCEL_NOT_AVAILABLE_FOR_FILING` | No Excel attachment on file. |
| 404 | `DATA_EXCEL_OBJECT_NOT_IN_STORAGE` | The Excel file is not available. |
| 503 | `DATA_EXCEL_PUBLIC_URL_UNAVAILABLE` | A download URL cannot be provided for this request. |
| 402 | `CREDITS_INSUFFICIENT` | Insufficient credits. |

**Example MCP tool call:**

```python
get_filing_excel(filing_id=12345)
```

**Example Response:**

```json
{
  "ticker": "AAPL",
  "fiscal_year": 2024,
  "download_url": "https://api.akkrudata.ai/...",
  "expires_in": 3600,
  "credits_used": 300
}
```

---

## 5. Metrics API

All routes accept **`X-API-Key`** as in [Section 1](#1-conventions). Endpoints in this section require the **Starter plan or higher**; Free callers receive `403 PLAN_TIER_INSUFFICIENT` (see [Section 9](#9-api-key-account-and-plan-errors)). The `formula_override` request field is additionally gated to the **Pro plan or higher** — Starter callers can use these endpoints but must omit `formula_override`.

---

### Response conventions

Every metric returned by `/metric/*` endpoints carries a `status` field and a `derivation` field. The rest of the object's contents depend on these two.

#### `status`

| Value | Meaning |
|---|---|
| `ok` | Metric was computed; `value` is set. |
| `not_applicable` | Metric does not apply to this filing. Carries a `reason` string; `value` is `null`. |
| `missing_input` | A required XBRL fact is missing. Carries a `missing_symbols` list; `value` is `null`. |
| `needs_price_data` | Price-sensitive metric called without `current_price`. `value` is `null`; pass `current_price` to upgrade to `ok`. |
| `needs_user_formula` | The filing uses a non-standard concept combination. Pass `accept_suggested_formula=true` or supply `formula_override` to compute. `value` is `null`; a `suggested_formula` field is included. |
| `needs_history` | A prior-period value is required (e.g. growth rates) but is not available from this filing's payload. `value` is `null`. |
| `divide_by_zero` | Denominator was zero. `value` is `null`. |
| `eval_error` | Formula execution failed. `value` is `null`. |

`not_applicable` takes precedence over every other status — once an NA rule fires for a metric, the response will be `not_applicable` regardless of what would otherwise apply.

#### `derivation`

| Value | Meaning |
|---|---|
| `derived` | Computed from multiple XBRL facts via the standard formula. |
| `as_reported` | Single XBRL fact taken at face value, no formula run. |
| `composite` | Built from multiple non-standard concepts (e.g. REIT CapEx = REInvestment + Maintenance). |
| `segment_sum` | No consolidated total in the filing; summed across segment members. |
| `class_sum` | Summed across asset / liability class members. |
| `company_override` | A company-specific override rule produced the value. |
| `proxy_substituted` | A proxy fact was used in place of the primary one (most often on ADR / foreign-issuer price-sensitive metrics). |
| `TTM_rollforward` | A trailing-12-month value for a quarterly filing. Comparable to annual filings of the same metric without further adjustment. |
| `user_formula_accepted` | Result of `accept_suggested_formula=true` — the returned `suggested_formula` was applied. |
| `user_formula_custom` | Result of a caller-supplied `formula_override`. |
| `user_must_compute` | Companion to `status="needs_user_formula"`. A caller-provided calculation is required. |
| `not_applicable` | Companion to `status="not_applicable"`. |

When a metric cannot be computed, `derivation` does not name a computation path — it mirrors that metric's `status`, so values such as `missing_input` and `needs_price_data` also appear here. Branch on `status` first.

#### Currency and FX

A metric object may carry these three currency-related fields:

| Field | Meaning |
|---|---|
| `unit` | Display unit. Either a currency code (`usd`, `eur`, `twd`, …) for absolute monetary metrics, or `ratio` / `percent` / `days` / `shares` etc. for non-monetary metrics. |
| `currency` | Source currency for absolute monetary metrics. Absent on ratio / percent / unitless metrics. |
| `fx_required` | `true` when the metric is denominated in a non-USD currency (caller may want to convert to USD before using the value). `false` when no conversion is needed (already USD, or unitless). |

For price-sensitive metrics on foreign-issuer filings (20-F / 40-F whose `reporting_currency` is not USD), the metric object also carries a `price_used` block:

| Subfield | Meaning |
|---|---|
| `close` | The price actually used in the formula (in the filer's reporting currency). |
| `date` | Date of `close`. |
| `source` | Identifier for the source of the price used. |
| `original_adr_price_usd` | The USD ADR price supplied (or filing-date USD price). |
| `adr_ratio` | ADR ratio (number of common shares per ADR). |
| `fx_rate_local_per_usd` | Conversion factor used for the period. |
| `reporting_ccy` | Filer's reporting currency (e.g. `EUR`, `TWD`, `CAD`). |

To convert a foreign-issuer absolute metric to USD:

```
value_usd = value / fx_rate_local_per_usd
```

For USD-reporting filings, `fx_required` is `false`, `currency` (when present) is `usd`, and `price_used` contains only `close`, `date`, `source`.

#### Light-weight mode

All `/metric/*` endpoints accept **`light_weight_mode`** (boolean, default `false`). When `true`, each returned metric object is reduced to its essential fields. The following per-metric fields are removed:

| Field | Removed in light-weight mode |
|---|---|
| `components` | ✓ |
| `fact_refs` | ✓ |
| `formula_expression` | ✓ |
| `computed_as` | ✓ |
| `caveat` | ✓ |
| `missing_symbols` | ✓ |

Fields retained: `value`, `status`, `derivation`, `unit`, `currency`, `fx_required`, `fx_rate_used`, `formula`, `price_used`, and any per-metric flags (`split_adjusted_value` etc.).

A notice is appended to `_warnings`. Credits are charged identically. Re-issue with `light_weight_mode=false` (or omit it) for the full payload.

---

### 5.0 Supported metric catalog

The API exposes **82** metrics, listed below by category. Use the **name** column verbatim wherever the API or MCP tools take a metric id (e.g. `metric_names` on `POST /metric/items`, `formula_override` keys, `metric_names` on `POST /metric/historical`). Names marked **★** are **price-sensitive** — their realtime values require `current_price` in the request body (see [§5.2](#52-post-metricall)).

#### Profitability (10)

| Name | Unit | Formula |
|------|------|---------|
| `gross_margin` | ratio | `(Revenue - CostOfRevenue) / Revenue` |
| `operating_margin` | ratio | `OperatingIncome / Revenue` |
| `net_margin` | ratio | `NetIncome / Revenue` |
| `ebitda_margin` | ratio | `(OperatingIncome + DepreciationAndAmortization) / Revenue` |
| `nopat` | reporting currency | `OperatingIncome * (1 - TaxRate)` |
| `nopat_reported_oi_proxy` | reporting currency | `OperatingIncome * (1 - TaxRate)` |
| `nopat_margin` | ratio | `NOPAT / Revenue` |
| `owner_earnings_estimate` | reporting currency | `CFO - MaintenanceCapex - SBC_CF` |
| `owner_earnings_cash_proxy` | reporting currency | `CFO - Capex - SBC_CF` |
| `owner_earnings_per_share` | reporting currency | `OE_common / Shares_avg` |

#### Returns (5)

| Name | Unit | Formula |
|------|------|---------|
| `return_on_equity` | ratio | `NetIncome / ((Equity + EquityPrior) / 2)` |
| `return_on_assets` | ratio | `NetIncome / ((Assets + AssetsPrior) / 2)` |
| `return_on_invested_capital` | ratio | `NetIncome / (((Equity + LongTermDebt + ShortTermDebt) + (EquityPrior + LongTermDebtPrior + ShortTermDebtPrior)) / 2)` |
| `return_on_tangible_equity` | ratio | `NetIncome / (((Equity - Goodwill - Intangibles) + (EquityPrior - GoodwillPrior - IntangiblesPrior)) / 2)` |
| `return_on_net_operating_assets` | ratio | `NOPAT / AvgNOA` |

#### Leverage (8)

| Name | Unit | Formula |
|------|------|---------|
| `debt_to_equity` | ratio | `(LongTermDebt + ShortTermDebt) / Equity` |
| `debt_to_assets` | ratio | `(LongTermDebt + ShortTermDebt) / Assets` |
| `debt_to_ebitda` | ratio | `(LongTermDebt + ShortTermDebt) / (OperatingIncome + DepreciationAndAmortization)` |
| `interest_coverage` | ratio | `OperatingIncome / InterestExpense` |
| `net_debt` | reporting currency | `Debt - UnrestrictedCash` |
| `net_debt_to_ebitda` | ratio | `NetDebt / OperatingEBITDA` |
| `net_financial_obligations` | reporting currency | `FO - FA` |
| `net_financial_expense_after_tax` | reporting currency | `(InterestExpenseDebt - InterestIncome) * (1 - TaxRate)` |

#### Liquidity (3)

| Name | Unit | Formula |
|------|------|---------|
| `current_ratio` | ratio | `CurrentAssets / CurrentLiabilities` |
| `quick_ratio` | ratio | `(CurrentAssets - Inventory) / CurrentLiabilities` |
| `cash_ratio` | ratio | `Cash / CurrentLiabilities` |

#### Efficiency / activity (10)

| Name | Unit | Formula |
|------|------|---------|
| `asset_turnover` | ratio | `Revenue / ((Assets + AssetsPrior) / 2)` |
| `inventory_turnover` | ratio | `CostOfRevenue / Inventory` |
| `receivables_turnover` | ratio | `Revenue / AccountsReceivable` |
| `days_inventory` | days | `PeriodDays * Inventory / CostOfRevenue` |
| `days_receivable` | days | `PeriodDays * AccountsReceivable / Revenue` |
| `days_payable` | days | `PeriodDays * AccountsPayable / CostOfRevenue` |
| `cash_conversion_cycle` | days | `days_inventory + days_receivable − days_payable` |
| `payables_turnover` | ratio | `CostOfRevenue / ((AccountsPayable + AccountsPayablePrior) / 2)` |
| `net_operating_assets` | reporting currency | `TotalAssets - TotalLiabilities + FO - FA` |
| `net_operating_asset_turnover` | ratio | `Revenue / AvgNOA` |

#### Cash flow & shareholder returns (18)

| Name | Unit | Formula |
|------|------|---------|
| `free_cash_flow` | reporting currency | `CFO - Capex` |
| `fcf_margin` | ratio | `(CFO - Capex) / Revenue` |
| `payout_ratio` | ratio | `Dividends / NetIncome` |
| `free_cash_flow_per_share` | reporting currency | `FCF / Shares_avg` |
| `free_cash_flow_after_sbc` | reporting currency | `FCF - SBC_PL` |
| `fcf_ex_sbc_addback` | reporting currency | `FCF - SBC_CF` |
| `free_cash_flow_cagr_3y` | ratio | `(FCF_t / FCF_t-3)^(1/3) - 1` |
| `free_cash_flow_cagr_5y` | ratio | `(FCF_t / FCF_t-5)^(1/5) - 1` |
| `free_cash_flow_cagr_10y` | ratio | `(FCF_t / FCF_t-10)^(1/10) - 1` |
| `free_cash_flow_per_share_cagr_3y` | ratio | `(FCF_per_share_t / FCF_per_share_t-3)^(1/3) - 1` |
| `free_cash_flow_per_share_cagr_5y` | ratio | `(FCF_per_share_t / FCF_per_share_t-5)^(1/5) - 1` |
| `free_cash_flow_per_share_cagr_10y` | ratio | `(FCF_per_share_t / FCF_per_share_t-10)^(1/10) - 1` |
| `cfo_to_net_income` | ratio | `CFO / NI` |
| `fcf_to_net_income` | ratio | `FCF / NI` |
| `cash_distributions_to_fcf` | ratio | `(CommonCashDividends + PreferredCashDividends + NCICashDistributions) / FCF` |
| `common_dividend_burden_on_group_fcf` | ratio | `CommonCashDividends / FCF` |
| `sbc_to_revenue` | ratio | `SBC_PL / Revenue` |
| `sbc_to_fcf` | ratio | `SBC_PL / FCF` |

#### Valuation — price-sensitive (13) ★

These require `current_price` in the request body to compute their realtime values; without it the `realtime` block returns the standard *missing-price* hint instead.

> **Tip:** populate `current_price` from any realtime quote source (e.g. Finnhub, Alpha Vantage, yfinance).

| Name | Unit | Formula |
|------|------|---------|
| `market_cap` ★ | reporting currency | `SharesOutstanding * Price` |
| `enterprise_value` ★ | reporting currency | `MarketCap + LongTermDebt + ShortTermDebt - Cash` |
| `pe_ratio` ★ | ratio | `Price / EPS` |
| `price_to_book` ★ | ratio | `MarketCap / Equity` |
| `price_to_sales` ★ | ratio | `MarketCap / Revenue` |
| `price_to_cash_flow` ★ | ratio | `MarketCap / CFO` |
| `ev_to_ebitda` ★ | ratio | `EnterpriseValue / (OperatingIncome + DepreciationAndAmortization)` |
| `ev_to_revenue` ★ | ratio | `EnterpriseValue / Revenue` |
| `ev_to_fcf` ★ | ratio | `EnterpriseValue / (CFO - Capex)` |
| `fcf_yield` ★ | ratio | `(CFO - Capex) / MarketCap` |
| `dividend_yield` ★ | ratio | `DividendsPerShare / Price` |
| `buyback_yield` ★ | ratio | `StockRepurchases / MarketCap` |
| `owner_earnings_yield` ★ | ratio | `OE_common / MarketCap` |

#### Growth (15)

Year-over-year (yoy) growth compares the current period to the same period one year earlier; quarter-over-quarter (qoq) compares the current quarter to the immediately preceding quarter.

| Name | Unit | Formula |
|------|------|---------|
| `revenue_growth_yoy` | ratio | `(Revenue_t - Revenue_{t-4Q}) / Revenue_{t-4Q}` |
| `revenue_growth_qoq` | ratio | `(Revenue_t - Revenue_{t-1Q}) / Revenue_{t-1Q}` |
| `revenue_growth_qoq_single_quarter` | ratio | `(Revenue_Qn_single - Revenue_Q(n-1)_single) / Revenue_Q(n-1)_single` |
| `eps_growth_yoy` | ratio | `(EPS_t - EPS_{t-4Q}) / EPS_{t-4Q}` |
| `net_income_growth_yoy` | ratio | `(NI_t - NI_{t-4Q}) / NI_{t-4Q}` |
| `fcf_growth_yoy` | ratio | `(FCF_t - FCF_{t-4Q}) / FCF_{t-4Q}` |
| `revenue_cagr_3y` | ratio | `(Revenue_t / Revenue_t-3)^(1/3) - 1` |
| `revenue_cagr_5y` | ratio | `(Revenue_t / Revenue_t-5)^(1/5) - 1` |
| `revenue_cagr_10y` | ratio | `(Revenue_t / Revenue_t-10)^(1/10) - 1` |
| `diluted_eps_cagr_3y` | ratio | `(DilutedEPS_t / DilutedEPS_t-3)^(1/3) - 1` |
| `diluted_eps_cagr_5y` | ratio | `(DilutedEPS_t / DilutedEPS_t-5)^(1/5) - 1` |
| `diluted_eps_cagr_10y` | ratio | `(DilutedEPS_t / DilutedEPS_t-10)^(1/10) - 1` |
| `shares_outstanding_cagr_3y` | ratio | `(Shares_end_t / Shares_end_t-3)^(1/3) - 1` |
| `shares_outstanding_cagr_5y` | ratio | `(Shares_end_t / Shares_end_t-5)^(1/5) - 1` |
| `shares_outstanding_cagr_10y` | ratio | `(Shares_end_t / Shares_end_t-10)^(1/10) - 1` |

Notes:
- A given filing may not have every metric — values requiring symbols absent from that filing simply do not appear in the response. The catalog above lists everything the API can return, not what every filing contains.
- Non-USD reporters (20-F / 40-F filings whose `reporting_currency` is e.g. `brl`, `eur`) report `free_cash_flow` and `market_cap` / `enterprise_value` in their local currency; the per-metric `unit` field carries the actual currency.

---

### 5.1 `POST /metric/filings`

**Purpose:** List filings for a ticker that have **metrics available**, returning one row per filing with identifiers you use as **`metric_group_id`** on other metric routes.

**Request:**

| Field | Type | Required | Notes |
|-------|------|----------|--------|
| `ticker` | string | Yes | 1–32 characters. |
| `fiscal_year` | integer | Conditional | 1990–2100. You must supply **`fiscal_year`** **or** **`as_of_date`** (`YYYY-MM-DD`). |
| `form_types` | array of string | No | Max **8** entries; each max **32** characters. |
| `quarter` | string | No | Max **4** characters. |
| `as_of_date` | string | No | `YYYY-MM-DD`: exclude filings filed after this date. |

**Response:**

| Field | Meaning |
|-------|---------|
| `credits_used` | Always **10** when successful. |
| `fiscal_year` | Echo of request. |
| `as_of_date` | Echo of request. |
| `company` | Company profile (`ticker`, `cik`, `legal_name`) or `null`. |
| `metric_groups` | Array of objects; each includes `metric_group_id`, filing identity fields, dates, reporting currency, and optional amendment flags. |
| `form_types`, `quarter` | Present only if you sent them in the request. |
| `_warnings` | Optional; present when dates exclude all rows. |

**Errors:**

| HTTP | `error_code` | When |
|------|----------------|------|
| 400 | `METRICS_UNSUPPORTED_FOR_KR` | Korean (DART) metrics not yet supported — coming soon (not charged). |
| 400 | `METRICS_UNSUPPORTED_FOR_JP` | Japanese (EDINET) metrics not yet supported — coming soon (not charged). |
| 400 | `METRICS_UNSUPPORTED_FOR_EU` | European (ESEF) metrics not yet supported — coming soon (not charged). |
| 400 | `METRICS_UNSUPPORTED_FOR_CN` | Computed metrics are not available for China A-share filings (not charged). |
| 402 | `CREDITS_INSUFFICIENT` | Insufficient credits. |
| 422 | — | Validation (e.g. neither `fiscal_year` nor `as_of_date`). |

**Example Request:**

```json
{
  "ticker": "AAPL",
  "fiscal_year": 2024
}
```

**Example MCP tool call:**

```python
list_metric_snapshots(ticker="AAPL", fiscal_year=2024)
```

**Example Response:**

```json
{
  "credits_used": 10,
  "fiscal_year": 2024,
  "as_of_date": null,
  "company": {
    "ticker": "AAPL",
    "cik": "0000320193",
    "legal_name": "Apple Inc."
  },
  "metric_groups": [
    {
      "metric_group_id": 9876,
      "ticker": "AAPL",
      "form_type": "10-K",
      "fiscal_year": 2024,
      "quarter": "FY",
      "period_end": "2024-09-28",
      "filing_date": "2024-11-01",
      "reporting_currency": "USD"
    }
  ]
}
```

---

### 5.2 `POST /metric/all`

**Purpose:** Return the **full metrics object** for one **`metric_group_id`**, including realtime price-sensitive metrics when the caller supplies `current_price`, and optional **formula** overrides.

**Request:**

| Field | Type | Default | Notes |
|-------|------|---------|--------|
| `metric_group_id` | integer | — | **Required.** 1–10⁹. From **`POST /metric/filings`**. |
| `as_of_date` | string | `null` | `YYYY-MM-DD`: exclude this filing if its filing date is after this date (amendments are pre-merged into the primary filing — no version switching). |
| `accept_suggested_formula` | boolean | `false` | When `true`, applies suggested formula hints included with the filing’s metrics (if any). |
| `formula_override` | object | `null` | Per-metric overrides. **Keys are metric names** (e.g. `"operating_margin"`). Values are objects containing `"concepts"` (array of strings, **at most 50** entries per metric) and `"operators"` (array of strings, e.g. `["+", "-"]`, length must be exactly `len(concepts) - 1`). A lone top-level `{ "concepts": ... }` object is **invalid**. |
| `current_price` | number | `null` | USD price used to compute realtime price-sensitive metrics (`market_cap`, `pe_ratio`, `enterprise_value`, `price_to_book`, `price_to_sales`, `price_to_cash_flow`, `ev_to_ebitda`, `ev_to_revenue`, `ev_to_fcf`, `fcf_yield`, `dividend_yield`, `buyback_yield`, `owner_earnings_yield`). Must be `> 0`, `≤ 10,000,000`, with at most **4 decimal places**. Pass the USD price as-is for all filings, including foreign-issuer ADRs. Invalid values return **HTTP 422** without charging credits. When omitted, the `realtime` block is replaced by `{ "status": "missing", "hint": "..." }` and the same hint appears in `_warnings`. |
| `current_fx_rate` | number | `null` | Optional fallback FX rate (units of the filing's `reporting_currency` per 1 USD), used **only** when an up-to-date conversion rate is temporarily unavailable for foreign-issuer ADR filings. Normally not required. Must be `> 0`, `≤ 1,000,000`, with at most **6 decimal places**. Invalid values return **HTTP 422** without charging credits. |
| `light_weight_mode` | boolean | `false` | When `true`, strips per-metric audit fields from every entry. A notice is appended to `_warnings`. Charged identically. See [Light-weight mode](#light-weight-mode). |

**Response:**

| Field | Meaning |
|-------|---------|
| `credits_used` | Always **148** when successful. |
| `metric_group_id` | The metric group ID actually used (may be `null` if excluded by `as_of_date`). |
| `valuation_context` | Echo of flags and formula inputs. |
| `ticker`, `form_type`, `fiscal_year`, `quarter`, `cik`, `source_accession`, `primary_document`, `source_url_prefix` | Filing identity and SEC base URL. `source_url_prefix` may be `null`; combine it with each fact’s `source_locator` (see below) to build the SEC viewer URL. |
| `segments` | Array of segment data. |
| `price_insensitive_metrics` | Object containing metrics that do not depend on stock price. Within each metric, nested `fact_refs` / `components` entries that expose `fact_id` also expose **`source_locator`** (may be `null`). |
| `price_sensitive_metrics` | Object containing metrics that depend on stock price. Always includes both a `realtime` sub-object (computed from the request's `current_price`; when realtime cannot be computed it becomes `{ "status": "missing" \| "missing_fx_fallback", "hint": "..." }` instead) and a `filing_date_price` + `historical` pair (the same metrics computed from the filing-date stock price). For foreign-issuer ADR filings each computed metric's `price_used` block carries the ADR ratio, the filer's reporting currency, and the FX rate used — see [Response conventions](#response-conventions). Same `source_locator` convention on nested fact references where `fact_id` is present. If you only need the filing-date computation, call **[`POST /metric/all/filing-date`](#53-post-metricallfiling-date)** instead. |
| `price_context` | Present whenever the response would include a `realtime` block. `status` is `"ok"` (price applied), `"missing"` (no `current_price` supplied), or `"missing_fx_fallback"` (foreign-issuer filing and an up-to-date conversion rate is temporarily unavailable). When `ok`, also includes `price`, `price_date`, `source: "user_supplied"`, and `fx_fallback_used: true` if the caller-supplied `current_fx_rate` was used because an up-to-date rate was unavailable. `mode` is always present; on the two non-`ok` statuses a `message` states what to send. |
| `ttm_composite`, `dei_corrected` | Optional root fields, present if they exist in the payload. |
| `amended` | Present and `true` **only** when this filing had a later amendment merged in. The returned metric values are already the amended figures. |
| `corporate_actions` | Optional root field, present when the ticker has corporate action events in the lookback window. Contains `events_in_lookback` and `events_after_period_end` (relative to this filing's period end); each event carries `ex_date`, `ratio`, `type` (`forward` / `reverse`), `event_type` (e.g. `stock_split`, `stock_dividend`), `affects_total_shares`, `affects_holder_share_count`, and `relevant_to_metrics`. Also carries `adjustment_factor_to_today` (factor converting this filing's per-share basis to the current basis), `adjustment_factor_anchor` (the date the factor is anchored on), and `adjustment_factor_anchor_basis` (how the anchor is chosen; `period_end`). When `adjustment_factor_to_today` is not `1`, three per-share metrics — `pe_ratio`, `dividend_yield`, `eps_growth_yoy` — additionally carry `split_adjusted_value`, `split_adjustment_factor`, and `comparable_to_post_split_basis`, so values from this filing can be compared on the current per-share basis; when it is `1` the filing is already on that basis and those three fields are absent. Total values (revenue, market_cap, etc.) are unaffected. Tickers with no events in the window omit the field entirely. |
| `reporting_currency` | The currency the filer reports in (e.g. `usd`, `brl`, `eur`). US filers (10-K / 10-Q) are always `usd`; foreign filers (20-F / 40-F) may be any local currency. Each metric's actual unit is on its own `unit` field; non-USD absolute metrics also carry `currency` and `fx_required: true`. See [Response conventions](#response-conventions). |
| `_warnings` | Optional; warnings about amendments or formulas. |

**Errors:**

| HTTP | `error_code` | When |
|------|----------------|------|
| 404 | `METRIC_GROUP_NOT_FOUND` | Unknown `metric_group_id`. |
| 404 | `METRIC_SUMMARY_NOT_FOUND` | Filing has no metrics summary. |
| 404 | `METRIC_SUMMARY_OBJECT_MISSING` | The metrics summary could not be retrieved. |
| 500 | `METRIC_SUMMARY_INVALID_FORMAT` | The metrics summary data is invalid. |
| 402 | `CREDITS_INSUFFICIENT` | Insufficient credits. |
| 409 | `METRIC_FORMULA_OVERRIDE_INVALID` | Invalid `formula_override`. |
| 422 | `METRIC_FORMULA_APPLY_FAILED` | Formula application failed. |
| 422 | `VALIDATION_ERROR` | `current_price` is non-numeric, non-finite, `≤ 0`, `> 10,000,000`, or has more than 4 decimal places; **or** `current_fx_rate` is non-numeric, non-finite, `≤ 0`, `> 1,000,000`, or has more than 6 decimal places. |

**Example Request:**

```json
{
  "metric_group_id": 12345,
  "current_price": 225.0,
  "formula_override": {
    "operating_margin": {
      "concepts": ["us-gaap_GrossProfit", "us-gaap_SellingGeneralAndAdministrativeExpense"],
      "operators": ["-"]
    }
  }
}
```

**Example MCP tool call:**

```python
get_metrics_bundle(
    metric_group_id=12345,
    current_price=225.0,
    formula_override={
        "operating_margin": {
            "concepts": ["us-gaap_GrossProfit", "us-gaap_SellingGeneralAndAdministrativeExpense"],
            "operators": ["-"],
        }
    },
)
```

**Example Response:**

```json
{
  "credits_used": 148,
  "metric_group_id": 12345,
  "valuation_context": {    "as_of_date": null,
    "accept_suggested_formula": false,
    "formula_override": null
  },
  "ticker": "AAPL",
  "form_type": "10-K",
  "fiscal_year": 2024,
  "quarter": "FY",
  "cik": "0000320193",
  "source_accession": "0000320193-24-000123",
  "primary_document": "aapl-20240928.htm",
  "source_url_prefix": "https://www.sec.gov/ix?doc=/Archives/edgar/data/.../aapl-20240928.htm",
  "segments": [],
  "price_insensitive_metrics": {
    "revenue": { "value": 383285000000.0, "unit": "USD" }
  },
  "price_sensitive_metrics": {
    "realtime": {
      "market_cap": { "value": 3404250000000.0, "unit": "USD" }
    }
  },
  "price_context": {
    "mode": "user_supplied",
    "status": "ok",
    "price": 225.0,
    "price_date": "2026-05-18",
    "source": "user_supplied"
  },
  "_warnings": []
}
```

**Example Response (filing affected by a stock split):**

```json
{
  "ticker": "NVDA",
  "fiscal_year": 2025,
  "quarter": "Q1",
  "corporate_actions": {
    "events_after_period_end": [
      { "ex_date": "2024-06-10", "ratio": 10.0, "type": "forward", "event_type": "stock_split",
        "affects_total_shares": true, "affects_holder_share_count": true, "relevant_to_metrics": true }
    ],
    "events_in_lookback": [
      { "ex_date": "2021-07-20", "ratio": 4.0,  "type": "forward", "event_type": "stock_split",
        "affects_total_shares": true, "affects_holder_share_count": true, "relevant_to_metrics": true },
      { "ex_date": "2024-06-10", "ratio": 10.0, "type": "forward", "event_type": "stock_split",
        "affects_total_shares": true, "affects_holder_share_count": true, "relevant_to_metrics": true }
    ],
    "adjustment_factor_to_today": 10.0,
    "adjustment_factor_anchor": "2024-04-28",
    "adjustment_factor_anchor_basis": "period_end"
  },
  "price_sensitive_metrics": {
    "realtime": {
      "pe_ratio": {
        "value": 24.25,
        "split_adjusted_value": 2.43,
        "split_adjustment_factor": 10.0,
        "comparable_to_post_split_basis": false
      }
    }
  }
}
```

---

### 5.3 `POST /metric/all/filing-date`

**Purpose:** Same metrics bundle as **[`POST /metric/all`](#52-post-metricall)**, but returns **only** the filing-date price block — no realtime computation. Use this when you don't need today's market price (analyzing historical filings, batch loads, or any case where current stock price is irrelevant).

**Request:** Same body fields as **`POST /metric/all`** *minus* `current_price` and `current_fx_rate`. See the note below.

| Field | Type | Default | Notes |
|-------|------|---------|--------|
| `metric_group_id` | integer | — | **Required.** 1–10⁹. From **`POST /metric/filings`**. |
| `as_of_date` | string | `null` | Same as **`POST /metric/all`**. |
| `accept_suggested_formula` | boolean | `false` | Same as **`POST /metric/all`**. |
| `formula_override` | object | `null` | Same as **`POST /metric/all`**. |
| `light_weight_mode` | boolean | `false` | Same as **`POST /metric/all`**. See [Light-weight mode](#light-weight-mode). |

**Note on realtime-only fields:** Sending `current_price` or `current_fx_rate` returns **`422 VALIDATION_ERROR`** (`type: extra_forbidden`) without charging credits. If you need realtime price-sensitive metrics, call **[`POST /metric/all`](#52-post-metricall)** instead.

**Response:** Same shape as **`POST /metric/all`** except `price_sensitive_metrics` contains only `filing_date_price` + `historical` (no `realtime`); `price_context` is not present.

**Credits:** Same flat **148** as **`POST /metric/all`**.

**Errors:** Same as **`POST /metric/all`**.

**Example Request:**

```json
{
  "metric_group_id": 12345
}
```

**Example MCP tool call:**

```python
get_metrics_bundle_filing_date(metric_group_id=12345)
```

---

### 5.4 `POST /metric/items`

**Purpose:** Same sourcing as **`POST /metric/all`**, but returns **only** the metric names you list.

**Request:** Same as **`POST /metric/all`**, plus:

| Field | Type | Required | Notes |
|-------|------|----------|--------|
| `metric_names` | array of string | Yes | 1–100 names; each max **256** characters. Each name must be a **supported metric name** **and** appear in that filing’s metrics (otherwise **400**). |
| `light_weight_mode` | boolean | No | Same as **`POST /metric/all`**. See [Light-weight mode](#light-weight-mode). |

**Credits:** **148** per successful debit.

**Response:**

| Field | Meaning |
|-------|---------|
| `credits_used` | Always **148** when successful. |
| `metric_group_id` | The metric group ID actually used. |
| `valuation_context` | Echo of flags, formula inputs, and requested `metric_names`. |
| `ticker`, `form_type`, `fiscal_year`, `quarter`, `cik`, `source_accession`, `primary_document`, `source_url_prefix` | Filing identity and SEC base URL. `source_url_prefix` may be `null`; combine it with each fact’s `source_locator` (see below) to build the SEC viewer URL. |
| `price_insensitive_metrics` | Object containing the requested price-insensitive metrics. Nested `fact_refs` / `components` entries with `fact_id` also expose **`source_locator`** (may be `null`). |
| `price_sensitive_metrics` | Object containing the requested price-sensitive metrics. Its `realtime` sub-object follows the same convention as [`POST /metric/all`](#52-post-metricall): metric rows when `current_price` is supplied, `{ "status": "missing", "hint": "..." }` otherwise. Same `source_locator` convention on nested fact references where `fact_id` is present. |
| `price_context` | Present whenever the response would include a `realtime` block (same shape as in [`POST /metric/all`](#52-post-metricall)). |
| `segments` | Array of segment data. |
| `ttm_composite`, `dei_corrected` | Optional root fields, present if they exist in the payload. |
| `corporate_actions` | Optional root field; same shape and semantics as in [`POST /metric/all`](#52-post-metricall). The same per-metric split variants appear on `pe_ratio` / `dividend_yield` / `eps_growth_yoy` when you request them. |
| `_warnings` | Optional; warnings about amendments or formulas. |

**Errors:** Same classes as **`POST /metric/all`**, plus:

| HTTP | `error_code` | When |
|------|----------------|------|
| 400 | `METRIC_INVALID_METRIC_NAME` | Unknown name or name not in this filing’s payload. |
| 402 | `CREDITS_INSUFFICIENT` | Insufficient credits. |

**Example Request:**

```json
{
  "metric_group_id": 12345,
  "metric_names": ["revenue", "net_income"]
}
```

**Example MCP tool call:**

```python
get_metrics_subset(metric_group_id=12345, metric_names=["revenue", "net_income"])
```

**Example Response:**

```json
{
  "credits_used": 148,
  "metric_group_id": 12345,
  "metric_group_id": 12345,
  "valuation_context": {    "as_of_date": null,
    "accept_suggested_formula": false,
    "formula_override": null,
    "metric_names": ["revenue", "net_income"]
  },
  "ticker": "AAPL",
  "form_type": "10-K",
  "fiscal_year": 2024,
  "quarter": "FY",
  "cik": "0000320193",
  "source_accession": "0000320193-24-000123",
  "primary_document": "aapl-20240928.htm",
  "source_url_prefix": "https://www.sec.gov/ix?doc=/Archives/edgar/data/.../aapl-20240928.htm",
  "price_insensitive_metrics": {
    "revenue": { "value": 383285000000.0, "unit": "USD" },
    "net_income": { "value": 93736000000.0, "unit": "USD" }
  },
  "price_sensitive_metrics": {}
}
```

---

### 5.5 `POST /metric/historical`

**Purpose:** Return metrics for **many periods** (quarterly or annual) for one ticker, **paginated**. Price-sensitive values use **filing-date figures only** (no live market refresh).

**Request:**

| Field | Type | Default | Notes |
|-------|------|---------|--------|
| `ticker` | string | — | **Required.** 1–32 characters. |
| `period_type` | string | `quarterly` | Either **`quarterly`** or **`annual`**. `annual` covers `10-K` plus the foreign-issuer annual reports `20-F` and `40-F`; `quarterly` covers `10-Q` only (foreign issuers have no quarterly counterpart). |
| `start_period_start` | string | `null` | Optional filter: period start **ISO date** (`YYYY-MM-DD`). |
| `end_period_end` | string | `null` | Optional filter: same date rules. |
| `metric_names` | array | `null` | If omitted or null, **all** metrics returned for each period; if set, only those names (must be supported and present in **each** returned period—otherwise **400**). Max **100** names. |
| `as_of_date` | string | `null` | Same meaning as **`/metric/all`**. |
| `accept_suggested_formula` | boolean | `false` | |
| `formula_override` | object | `null` | Same structure rules as **`/metric/all`** (including **≤ 50** `concepts` per metric). |
| `sort` | string | `period_end_asc` | **`period_end_asc`** or **`period_end_desc`**. |
| `page` | integer | `1` | Page index, 1–10⁶. |
| `page_size` | integer | `3` | 1–**5** (maximum page size). |
| `light_weight_mode` | boolean | `false` | Same as **`POST /metric/all`** but applied to every metric object inside every `periods[*]`. See [Light-weight mode](#light-weight-mode). |

**Credits:** **55** (base) **+ 148 × (number of rows in `periods` for this response)**. If the base debit succeeds but the per-row debit fails, you receive **`402`** with an explanation; the base **55** credits are spent.

**Response:**

| Field | Meaning |
|-------|---------|
| `page_info` | Object with pagination details (`current_page`, `total_pages`, `page_size`, `total_items`, `next_page_hint`). |
| `credits_used` | Number of credits deducted for this request. |
| `ticker` | The company's current symbol. |
| `query_context` | Echo of the request parameters used for the query. |
| `periods` | Array of period objects. Each includes filing metadata (including **`source_url_prefix`** per period, may be `null`), `price_insensitive_metrics`, `price_sensitive_metrics` (as static maps), and optional root meta fields. Within each metric object, `fact_refs` / `components` entries that expose `fact_id` also expose **`source_locator`** (may be `null`). Each period may carry its own **`corporate_actions`** field, an **`amended`** flag (`true` when that period's filing had a merged amendment), and per-metric split variants as described in [`POST /metric/all`](#52-post-metricall); when comparing a per-share metric across periods that straddle a split, use `split_adjusted_value` instead of raw `value`. |
| `_warnings` | Optional; warnings about amendments or formulas. |

**Errors:**

| HTTP | `error_code` | When |
|------|----------------|------|
| 400 | `METRIC_INVALID_METRIC_NAME` | Invalid or missing names in a period when `metric_names` is set. |
| 400 | `METRICS_UNSUPPORTED_FOR_KR` | Korean (DART) metrics not yet supported — coming soon (not charged). |
| 400 | `METRICS_UNSUPPORTED_FOR_JP` | Japanese (EDINET) metrics not yet supported — coming soon (not charged). |
| 400 | `METRICS_UNSUPPORTED_FOR_EU` | European (ESEF) metrics not yet supported — coming soon (not charged). |
| 400 | `METRICS_UNSUPPORTED_FOR_CN` | Computed metrics are not available for China A-share filings (not charged). |
| 402 | `CREDITS_INSUFFICIENT` | Insufficient credits for base or per-row charges. |
| 409 | `METRIC_FORMULA_OVERRIDE_INVALID` | Invalid `formula_override`. |
| 422 | `METRIC_FORMULA_APPLY_FAILED` | Formula application failed; or validation (`period_type`, `sort`, limits). |

**Example Request:**

```json
{
  "ticker": "AAPL",
  "period_type": "quarterly",
  "page": 1,
  "page_size": 2
}
```

**Example MCP tool call:**

```python
get_metrics_timeseries(ticker="AAPL", period_type="quarterly", page=1, page_size=2)
```

**Example Response:**

```json
{
  "page_info": {
    "current_page": 1,
    "total_pages": 5,
    "page_size": 2,
    "total_items": 10,
    "next_page_hint": "Use the same request body with \"page\": 2."
  },
  "credits_used": 351,
  "ticker": "AAPL",
  "query_context": {
    "period_type": "quarterly",
    "start_period_start": null,
    "end_period_end": null,
    "as_of_date": null,    "accept_suggested_formula": false,
    "formula_override": null,
    "metric_names": null,
    "sort": "period_end_asc"
  },
  "periods": [
    {
      "metric_group_id": 9876,
      "period_start": "2024-07-01",
      "period_end": "2024-09-28",
      "filing_date": "2024-11-01",
      "ticker": "AAPL",
      "form_type": "10-K",
      "fiscal_year": 2024,
      "quarter": "Q4",
      "cik": "0000320193",
      "source_accession": "0000320193-24-000123",
      "primary_document": "aapl-20240928.htm",
      "source_url_prefix": "https://www.sec.gov/ix?doc=/Archives/edgar/data/.../aapl-20240928.htm",
      "segments": [],
      "price_insensitive_metrics": {
        "revenue": { "value": 94930000000.0, "unit": "USD" }
      },
      "price_sensitive_metrics": {
        "filing_date_price": 222.91,
        "historical": {
          "market_cap": { "value": 3300000000000.0, "unit": "USD" }
        }
      }
    }
  ]
}
```

---

### 5.6 Category endpoints (`/metric/<category>`)

Seven endpoints returning curated metric sets — margins, growth, leverage, efficiency, cash-flow, valuation. Use these when you want a coherent slice of metrics without enumerating names. Flat **148 credits**, rate limit **60/minute**.

| Endpoint | Metrics returned |
|---|---|
| `POST /metric/profitability` | `gross_margin`, `operating_margin`, `net_margin`, `ebitda_margin`, `return_on_equity`, `return_on_assets`, `return_on_invested_capital`, `return_on_tangible_equity`, `fcf_margin`, `nopat`, `nopat_reported_oi_proxy`, `nopat_margin`, `return_on_net_operating_assets`, `owner_earnings_estimate`, `owner_earnings_cash_proxy`, `owner_earnings_per_share` (16) |
| `POST /metric/growth` | `revenue_growth_yoy`, `revenue_growth_qoq`, `revenue_growth_qoq_single_quarter`, `eps_growth_yoy`, `net_income_growth_yoy`, `fcf_growth_yoy`, `revenue_cagr_3y`, `revenue_cagr_5y`, `revenue_cagr_10y`, `diluted_eps_cagr_3y`, `diluted_eps_cagr_5y`, `diluted_eps_cagr_10y`, `shares_outstanding_cagr_3y`, `shares_outstanding_cagr_5y`, `shares_outstanding_cagr_10y` (15) |
| `POST /metric/financial-health` | `debt_to_equity`, `debt_to_assets`, `debt_to_ebitda`, `interest_coverage`, `current_ratio`, `quick_ratio`, `cash_ratio`, `net_debt`, `net_debt_to_ebitda`, `net_financial_obligations`, `net_financial_expense_after_tax` (11) |
| `POST /metric/efficiency` | `asset_turnover`, `inventory_turnover`, `receivables_turnover`, `days_inventory`, `days_receivable`, `days_payable`, `cash_conversion_cycle`, `payables_turnover`, `net_operating_assets`, `net_operating_asset_turnover` (10) |
| `POST /metric/cash-flow` | `free_cash_flow`, `fcf_margin`, `payout_ratio`, `free_cash_flow_per_share`, `free_cash_flow_after_sbc`, `fcf_ex_sbc_addback`, `free_cash_flow_cagr_3y`, `free_cash_flow_cagr_5y`, `free_cash_flow_cagr_10y`, `free_cash_flow_per_share_cagr_3y`, `free_cash_flow_per_share_cagr_5y`, `free_cash_flow_per_share_cagr_10y`, `cfo_to_net_income`, `fcf_to_net_income`, `cash_distributions_to_fcf`, `common_dividend_burden_on_group_fcf`, `sbc_to_revenue`, `sbc_to_fcf` (18 — `fcf_margin` also in `/metric/profitability`) |
| `POST /metric/valuation` | All 13 price-sensitive ratios: `market_cap`, `enterprise_value`, `pe_ratio`, `price_to_book`, `price_to_sales`, `price_to_cash_flow`, `ev_to_ebitda`, `ev_to_revenue`, `ev_to_fcf`, `fcf_yield`, `dividend_yield`, `buyback_yield`, `owner_earnings_yield`. Accepts `current_price` for realtime; returns both `realtime` and `filing_date_price` blocks. |
| `POST /metric/valuation/filing-date` | Same 13 valuation ratios as above but **only** the filing-date price block — `current_price` and `current_fx_rate` are rejected with `422 VALIDATION_ERROR`. |

**Request body** (5 simple categories + `/metric/valuation`): `metric_group_id` required (1–10⁹). Optional: `accept_suggested_formula`, `formula_override`, `as_of_date`, `current_price`, `current_fx_rate`, `light_weight_mode` — same semantics as on [`POST /metric/all`](#52-post-metricall). `metric_names` is **not** accepted (the category fixes the metric set; sending it returns `422 VALIDATION_ERROR`).

**Request body** (`/metric/valuation/filing-date`): same as above minus `current_price` / `current_fx_rate` — see [`POST /metric/all/filing-date`](#53-post-metricallfiling-date). `light_weight_mode` is accepted on this endpoint as well.

**Response shape:** `ticker`, `form_type`, `fiscal_year`, `quarter`, `cik`, `source_accession`, `primary_document`, `metric_group_id`, `source_url_prefix`, `valuation_context`, `segments`, `price_insensitive_metrics`, `price_sensitive_metrics`, `credits_used`, plus the conditional `price_context` and the optional root fields `ttm_composite`, `dei_corrected`, `corporate_actions` and `_warnings`. For `/metric/valuation` and `/metric/valuation/filing-date` the `price_insensitive_metrics` block is always `{}` (all 13 valuation ratios are price-sensitive).

**Errors:** `METRIC_GROUP_NOT_FOUND`, `CREDITS_INSUFFICIENT`, standard `422 VALIDATION_ERROR`.

**Example MCP tool call:**

```python
get_profitability_metrics(metric_group_id=46)
```

---

## 6. Screener API

[§6.11](#611-post-screenerevents) screens corporate event filings and returns matching section summaries.

Query the universe of US-listed filers by composing metric conditions ("ROE > 20% for the last 5 years AND PE < 20"). Supports OR/NOT, time-window modifiers (`for_latest` / `for_consecutive` / `for_at_least`), 1–120 period lookback, USD-normalized values for ADR foreign filers, and paging up to 500 cumulative results.

[§6.5](#65-post-screenerownership) screens insider trades rather than metrics; its rows use the transaction object defined in [§7](#7-ownership-api). [§6.9](#69-post-screenerinstitutional-holdings) screens institutional 13F filings; its rows use the [snapshot object](#snapshot-object), also defined in [§7](#7-ownership-api).

> **Plan-tier gate:** Every endpoint in this section requires the **Pro plan or higher**; Free / Starter callers receive `403 PLAN_TIER_INSUFFICIENT` (see [Section 9](#9-api-key-account-and-plan-errors)). The MCP wrappers (`search_stocks`, `list_screener_filters`, `search_insider_trades`) forward the same REST endpoints and surface the same 403 envelope.

> **Pricing note:** Filters that touch price-sensitive metrics (`market_cap`, `enterprise_value`, all 13 valuation ratios) use the price observed on each filer's filing date; these dates differ across filers. For same-day cross-ticker comparison, call [`POST /metric/all`](#52-post-metricall) per ticker with a common `current_price`.

### 6.1 `POST /screener/metrics`

**Purpose:** Return filings that match a DSL filter tree, grouped by company (default) or flattened.

**Request body** (`APIRequest`, `extra="forbid"`):

| Field | Type | Default | Notes |
|---|---|---|---|
| `as_of_date` | ISO date | today (UTC) | Must be ≤ today. Filings with `filing_date > as_of_date` are excluded (avoids backtest leakage). |
| `lookback` | int 1–120 | 1 | Most recent N filings per company within scope. |
| `include_quarterly` | bool | `false` | `false` → annual only (10-K / 20-F / 40-F). `true` adds 10-Q. |
| `tickers` | string[] | omitted | Whitelist, max 100. Case-insensitive (normalized to upper). Empty or omitted = full universe. |
| `exclude_tickers` | string[] | `[]` | Blacklist, max 100. |
| `usd_only` | bool | `false` | When `true`, exclude foreign-currency reporters. |
| `sectors` | string[] | omitted | Restrict the universe to these canonical sector buckets (the 12 in [§1.10](#110-industry-classification-pro)). Omitted = no sector restriction. |
| `where.filter` | DSL tree | (required) | See [§6.3](#63-filter-dsl-screenermetrics). Leaf count 1–10, nesting depth ≤ 4. |
| `where.sort_by` | string | see [§6.3](#63-filter-dsl-screenermetrics) | Metric name, `"ticker"`, `"period_end"`, `"filing_date"`, or `"match_count"`. |
| `where.sort_order` | `"asc"` \| `"desc"` | `"asc"` | |
| `include_metrics_using_filing_date_price` | bool | `true` | `false` rejects price-sensitive metrics in `filter` / `sort_by` / `include_metrics`. |
| `exclude_derivations` | string[] | `[]` | Subset of `derived`, `as_reported`, `composite`, `class_sum`, `segment_sum`, `company_override`, `proxy_substituted`, `TTM_rollforward`. |
| `include_metrics` | string[] | `[]` | Extra metric names (≤ 10) to include in the response beyond filter-referenced ones. |
| `group_by_ticker` | bool | `true` | `true` → grouped by company with nested `filings[]`. `false` → flat per-filing rows. |
| `page` | int ≥ 1 | 1 | 1-indexed. |
| `page_size` | int 1–100 | 50 | Counts companies (when grouped) or filings (when flat). |

**Pagination cap:** `page × page_size ≤ 500` (validation 422). Use larger `page_size` rather than more pages. Tighten filters to look beyond the first 500 results.

**Response (`group_by_ticker=true`, default):**

```json
{
  "page": 1, "page_size": 50,
  "has_more": true,
  "next_page": 2,
  "as_of_date_applied": "2024-12-31",
  "lookback_applied": 5,
  "include_quarterly_applied": false,
  "results": [
    {
      "ticker": "AAPL",
      "match_count": 5,
      "filings": [
        {
          "metric_id": 46,
          "form_type": "10-K",
          "fiscal_year": 2024, "quarter": "FY",
          "period_end": "2024-09-28",
          "filing_date": "2024-11-01",
          "reporting_currency": "usd",
          "metrics": {
            "return_on_equity": {"value": 1.5774, "derivation": "derived", "unit": "ratio"},
            "pe_ratio": {"value": 33.21, "derivation": "derived", "unit": "ratio", "price_date": "2024-11-01"},
            "market_cap": {"value": 3.37e12, "derivation": "as_reported", "unit": "usd", "price_date": "2024-11-01"}
          }
        }
        /* T-1, T-2, … up to lookback filings within company */
      ]
    }
  ],
  "credits_used": 242
}
```

**Response (`group_by_ticker=false`):** identical envelope, but `results[]` is a flat list of `{ticker, metric_id, form_type, fiscal_year, quarter, period_end, filing_date, reporting_currency, metrics}` — one row per matching filing.

Each `results[]` entry (grouped or flat) also carries an industry **`classification`** block — see [§1.10](#110-industry-classification-pro).

**`match_count`** is the number of filings within `lookback` that satisfy the **whole** filter tree on a per-filing basis (using each filing's own metric values).

**Foreign-currency filers (ADRs)** carry `fx_rate_used` + `currency` on FX-converted metric entries:

```json
"market_cap": {
  "value": 2.42e11,            // USD-converted from JPY
  "derivation": "as_reported",
  "unit": "usd",
  "currency": "jpy",
  "fx_rate_used": 145.2,
  "price_date": "2024-06-30"
}
```

All screener filtering, sorting, and comparison use the USD-converted value. USD reporters omit `fx_rate_used` and `currency`.

**Within-company `filings[]` ordering** is fixed at `filing_date DESC, period_end DESC` (newest first), independent of `where.sort_order`. For time-series ordering, use `group_by_ticker=false` with `sort_by="period_end"` or `"filing_date"`.

**Errors:** `422 VALIDATION_ERROR` (schema), `402 CREDITS_INSUFFICIENT`, **`504 QUERY_TIMEOUT`** when the query exceeds the 30-second server timeout (the request is charged; tighten filters and retry).

**Example MCP tool call:**

```python
search_stocks(lookback=5, include_quarterly=False,
              where={"filter": {"metric": "return_on_equity", "op": ">", "value": 0.2, "for_latest": 5}},
              page_size=10)
```

### 6.2 `GET /screener/metrics/filters`

**Purpose:** Discovery — returns the supported metric catalog, operators, temporal modifiers, form types, derivations, and one complete example request. **10 credits.**

```json
{
  "metrics": [
    {"name": "return_on_equity", "unit": "ratio", "price_sensitive": false},
    {"name": "market_cap", "unit": "usd", "price_sensitive": true}
    /* ... 82 total ... */
  ],
  "operators": ["<", "<=", ">", ">=", "between"],
  "temporal_modifiers": ["for_latest", "for_consecutive", "for_at_least"],
  "form_types": ["10-K", "10-Q", "20-F", "40-F"],
  "derivations": ["TTM_rollforward", "as_reported", "class_sum", "company_override", "composite", "derived", "proxy_substituted", "segment_sum"],
  "sectors": ["Technology", "Healthcare", "Finance", "Consumer Discretionary", "Consumer Staples", "Communications", "Industrials", "Energy", "Utilities", "Materials", "Real Estate", "Other"],
  "example_request": { /* full POST /screener/metrics body */ }
}
```

`sectors` lists the 12 canonical buckets accepted by the `POST /screener/metrics` `sectors` filter ([§1.10](#110-industry-classification-pro)).

**Example MCP tool call:**

```python
list_screener_filters()
```

### 6.3 Filter DSL (`/screener/metrics`)

#### Leaf node

```json
{"metric": "<name>", "op": "<operator>", "value": <number | [lo, hi]>,
 "for_latest": <N> | "for_consecutive": <N> | "for_at_least": <N>}
```

| Operator | Value shape | Notes |
|---|---|---|
| `>`, `>=`, `<`, `<=` | single number | |
| `between` | `[lo, hi]` (`lo ≤ hi`) | Inclusive on both ends. |

`=` / `!=` are **not** supported in v1. For equality matching use `between [v − ε, v + ε]`.

`value` must be numeric. `null` is rejected. Filings where the metric value is missing are excluded from filter comparisons (no synthetic match).

#### Temporal modifiers (mutually exclusive, at most one per leaf)

| Modifier | Meaning |
|---|---|
| *(none)* | Latest filing in `lookback` must satisfy. Equivalent to `for_latest: 1`. |
| `for_latest: N` | Most recent N filings (right-anchored) all satisfy. |
| `for_consecutive: N` | Some N adjacent filings within `lookback` all satisfy (sliding window over `filing_date`). |
| `for_at_least: N` | At least N filings within `lookback` satisfy (any positions). |

`N ≥ 1` and `N ≤ lookback`. Tickers without enough filings in `lookback` to evaluate the modifier are filtered out.

#### Composite nodes

```json
{"and": [<node>, <node>, …]}
{"or":  [<node>, <node>, …]}
{"not": <node>}
```

`where.filter` may itself be a bare leaf (no wrapper required for single-condition queries).

#### Limits

- Leaf nodes total: **1–10** (0 → rejected as "filter required"; >10 → rejected as "too complex")
- Nesting depth: **≤ 4**
- `include_metrics` length: **≤ 10**
- `tickers`, `exclude_tickers`: each **≤ 100** entries

### 6.4 Credit formula (`/screener/metrics`)

`POST /screener/metrics` charges per call (paginate ⇒ each page bills independently):

```
credits = max(65, 425
              − tickerDiscount             // ceil(200 × (100 − n_tickers) / 100); 0 (whole universe, or ≥ 100 tickers) … 198 (1 ticker)
              − lookbackDiscount           // 0 (lookback ≥ 30) … 78 (lookback = 1)
              − quarterlyDiscount          // 60 when include_quarterly=false, else 0
              + lookbackSurcharge          // 0 (lookback ≤ 30) … 240 (lookback = 120)
              + filterSurcharge            // 40 × (leaves − 1), 0 … 360
              + temporalSurcharge          // per-leaf 60 (for_latest / for_at_least) or 100 (for_consecutive), capped 600
              + includeMetricsSurcharge)   // 20 × |include_metrics \ filter_metrics|, 0 … 200
```

Examples:

| Scenario | Credits |
|---|---|
| 1 ticker, lookback=1, annual only, 1 leaf (best case) | **89** |
| Whole universe, lookback=30, quarterly+annual, 1 leaf | **425** |
| 2 tickers, lookback=5, annual, 2 leaves, 1 `for_latest`, 2 extra `include_metrics` | **242** |
| Whole universe, lookback=120, 10 leaves all `for_consecutive`, 10 extra include_metrics | **1825** (worst case) |

**Charge applies even when `results` is empty, and on `504 QUERY_TIMEOUT`**. **No charge** only on `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`, or `403 PLAN_TIER_INSUFFICIENT` (all rejected before any deduction).

`GET /screener/metrics/filters` charges **10 credits** flat.

### 6.5 `POST /screener/ownership`

**Purpose:** Full-library transaction screening. Trade-level conditions plus company-level aggregate gates; returns the matching transactions.

**Request:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `start_date`, `as_of_date` | ISO date | — | Window on `transaction_date`; `as_of_date` also excludes filings submitted after it. `as_of_date` ≤ today; `start_date` ≤ `as_of_date`. |
| `tickers`, `exclude_tickers` | string[] | omitted | ≤ 100 each. |
| `sectors` | string[] | omitted | The 12 canonical buckets ([§1.10](#110-industry-classification-pro)). |
| `trade_filters` | object | omitted | See [§6.6](#66-filters-screenerownership). |
| `aggregate_filters` | object[] | omitted | ≤ 8. Company-level gates — see [§6.6](#66-filters-screenerownership). |
| `group_by_ticker` | bool | `true` | `true` → grouped by company. `false` → flat rows. |
| `sort_by` | string | — | Grouped: `ticker`, `txn_count`. Flat: `transaction_date`, `filing_date`, `transaction_value`, `shares`, `price_per_share`, `own_pct_change`, `split_adjusted_shares`, `split_adjusted_price`. |
| `sort_order` | `"desc"` \| `"asc"` | `"desc"` | |
| `page` | int ≥ 1 | 1 | |
| `page_size` | int 1–100 | 50 | Counts transactions in both modes: with `group_by_ticker=true` a page holds that many transactions, however few companies they come from. |

**Pagination cap:** `page × page_size ≤ 500` (422).

**Response:**

| Field | Meaning |
|---|---|
| `page`, `page_size`, `has_more`, `next_page` | Pagination. |
| `as_of_date_applied`, `start_date_applied` | Window actually used. |
| `results` | `group_by_ticker=true`. Each entry: `ticker`, `issuer`, `issuer_cik`, `txn_count` (the company's full hit count, not the page's), `transactions`. |
| `transactions` | `group_by_ticker=false`. Flat rows, each additionally carrying `ticker`, `issuer`, `issuer_cik` and `company_id`. |
| `anomalies_excluded_count` | Transactions excluded by the consistency checks; `0` when `trade_filters.include_anomalies=true`. |
| `credits_used`, `_warnings` | |

Every row carries `accession_number`, `filing_date`, `source_url_prefix`, `insiders`, the [transaction object](#transaction-object), and `computed`.

**Errors:** `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`, `403 PLAN_TIER_INSUFFICIENT` below Pro, `504 QUERY_TIMEOUT` at the 30-second server timeout (charged).

**Example MCP tool call:**

```python
search_insider_trades(start_date="2026-04-01", tickers=["AAPL"],
                      trade_filters={"transaction_code": ["P", "S"]}, page_size=10)
```

### 6.6 Filters (`/screener/ownership`)

**`trade_filters`:**

| Field | Type | Notes |
|---|---|---|
| `transaction_code` | string[] | ≤ 20. Unknown codes dropped with a warning. |
| `relationship` | string[] | `is_director`, `is_officer`, `is_ten_percent_owner`, `is_other`. OR semantics. |
| `is_derivative` | bool | |
| `principal_amount_not_shares` | bool | Omit for no filter, `true` for debt-principal rows only, `false` to exclude them. |
| `include_anomalies` | bool | Default `false`. |
| `exclude_likely_merged` | bool | Default `false`. |
| `include_unresolved_amendments` | bool | Default `false`. |
| `conditions` | object[] | ≤ 8. `{field, op, value}`. `field` ∈ `transaction_value`, `price_per_share`, `shares`, `own_pct_change`, `split_adjusted_shares`, `split_adjusted_price`. |

**`aggregate_filters`:** `{metric, op, value}`, `metric` ∈ `value_acquired`, `value_disposed`, `net_value`, `shares_acquired`, `shares_disposed`, `net_shares`, `txn_count`, `txn_count_acquired`, `txn_count_disposed`, `max_txn_value_acquired`, `max_txn_value_disposed`, `distinct_insiders`.

**Operators** (both filter types): `>`, `>=`, `<`, `<=`, `between` (`[lo, hi]`, `lo ≤ hi`).

### 6.7 Credit formula (`/screener/ownership`)

`POST /screener/ownership` charges per call:

```
credits = max(65, 200
              − tickerDiscount        // 100 − min(n_tickers, 100); 0 when tickers is omitted
              − shortWindowDiscount   // 40 when the window is ≤ 90 days
              + fullHistorySurcharge  // 80 when start_date is omitted
              + 40 × conditions)      // trade conditions + aggregate filters
```

Range **65–920**. Charged even when the result set is empty, and on `504 QUERY_TIMEOUT`. **No charge** on `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`, or `403 PLAN_TIER_INSUFFICIENT` (all rejected before any deduction).

### 6.8 `POST /screener/initial-holdings`

**Purpose:** Full-library screening of initial holdings (Form 3). Holding-level conditions plus company-level aggregate gates; returns the matching holdings. Requires **Pro or higher**.

**Request:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `start_date`, `as_of_date` | ISO date | — | Window on `filing_date`. `as_of_date` ≤ today; `start_date` ≤ `as_of_date`. |
| `tickers`, `exclude_tickers` | string[] | omitted | ≤ 100 each. |
| `sectors` | string[] | omitted | The 12 canonical buckets ([§1.10](#110-industry-classification-pro)). |
| `holding_filters` | object | omitted | See below. |
| `aggregate_filters` | object[] | omitted | ≤ 8. Company-level gates — see below. |
| `group_by_ticker` | bool | `true` | `true` → grouped by company. `false` → flat rows. |
| `sort_by` | string | — | Grouped: `ticker`, `holdings_count`, `shares_owned_total`. Flat: `filing_date`, `shares_owned`, `split_adjusted_shares_owned`, `underlying_security_shares`, `conversion_or_exercise_price`. |
| `sort_order` | `"desc"` \| `"asc"` | `"desc"` | |
| `page` | int ≥ 1 | 1 | |
| `page_size` | int 1–100 | 50 | Counts holdings in both modes: with `group_by_ticker=true` a page holds that many holdings, however few companies they come from. |

**Pagination cap:** `page × page_size ≤ 500` (422).

**`holding_filters`:**

| Field | Type | Notes |
|---|---|---|
| `relationship` | string[] | `is_director`, `is_officer`, `is_ten_percent_owner`, `is_other`. OR semantics. |
| `is_derivative` | bool | |
| `direct_or_indirect` | `"D"` \| `"I"` | |
| `include_anomalies` | bool | Default `false`. |
| `exclude_likely_merged` | bool | Default `false`. |
| `include_unresolved_amendments` | bool | Default `false`. |
| `conditions` | object[] | ≤ 8. `{field, op, value}`. `field` ∈ `shares_owned`, `split_adjusted_shares_owned`, `underlying_security_shares`, `split_adjusted_underlying_security_shares`, `conversion_or_exercise_price`. |

**`aggregate_filters`:** `{metric, op, value}`, `metric` ∈ `filing_count`, `no_securities_filing_count`, `distinct_insiders`, `holdings_count`, `shares_owned_total`. `filing_count` and `no_securities_filing_count` count filings, including filings that report no holdings; `holdings_count` and `shares_owned_total` are measured on the matching holdings.

**Operators** (both filter types): `>`, `>=`, `<`, `<=`, `between` (`[lo, hi]`, `lo ≤ hi`).

**Response:**

| Field | Meaning |
|---|---|
| `page`, `page_size`, `has_more`, `next_page` | Pagination. |
| `as_of_date_applied`, `start_date_applied` | Window actually used. |
| `results` | `group_by_ticker=true`. Each entry: `ticker`, `issuer`, `issuer_cik`, `holdings_count` and `shares_owned_total` (the company's full hit values, not the page's), `holdings`. |
| `holdings` | `group_by_ticker=false`. Flat rows, each additionally carrying `ticker`, `issuer`, `issuer_cik` and `company_id`. |
| `anomalies_excluded_count` | Holdings excluded by the consistency checks; `0` when `holding_filters.include_anomalies=true`. |
| `credits_used`, `_warnings` | |

Every row carries `accession_number`, `filing_date`, `period_of_report`, `no_securities_owned`, `source_url_prefix`, `insiders`, and the [holding object](#holding-object).

**Credits:** the [§6.7](#67-credit-formula-screenerownership) formula, with `conditions` = holding conditions + aggregate filters. Range **65–920**.

**Errors:** `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`, `403 PLAN_TIER_INSUFFICIENT` below Pro, `504 QUERY_TIMEOUT` at the 30-second server timeout (charged).

**Example MCP tool call:**

```python
search_initial_holdings(tickers=["TSLA"], holding_filters={"is_derivative": False}, page_size=10)
```

### 6.9 `POST /screener/institutional-holdings`

**Purpose:** Screen SEC Form 13F institutional filings across the whole library. One row = one manager-quarter, carrying that quarter's headline numbers (the [snapshot object](#snapshot-object) defined in [§7](#7-ownership-api), plus the manager's `cik` and `name`). An empty body is the full listing, ordered by `total_positions_value` descending. Requires **Pro or higher**.

**Request:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `start_date`, `end_date` | ISO date | omitted | Bound the calendar **quarter end**: quarters whose period end falls inside the range participate. Give one date to bound one side only; omit both to scan every quarter. |
| `conditions` | object[] | omitted | ≤ 8 of `{field, op, value}` — see below. |
| `is_amended` | bool | omitted | `true` → only quarters with merged amendments; `false` → only quarters without. |
| `confidential` | bool | omitted | Filter on the confidential-treatment flag. |
| `is_combination_report` | bool | omitted | `true` → 13F COMBINATION REPORT quarters (the filer also reports holdings managed by other managers); `false` → 13F HOLDINGS REPORT quarters. |
| `sort_by` | string | `total_positions_value` | One of `total_positions_value`, `positions_count`, `derivatives_count`, `total_derivatives_notional`, `portfolio_value_qoq_pct`, `top1_security_pct`, `top10_security_pct`, `est_turnover`, `period`, `filing_date`. |
| `sort_order` | `"desc"` \| `"asc"` | `"desc"` | |
| `page` | int ≥ 1 | 1 | |
| `page_size` | int 1–100 | 50 | |

**Pagination cap:** `page × page_size ≤ 500` (422).

**`conditions`:** each is `{field, op, value}` with `op` ∈ `>`, `>=`, `<`, `<=`, `between` (`[lo, hi]`, `lo ≤ hi`, inclusive on both ends). `field` is one of `total_positions_value`, `positions_count`, `derivatives_count`, `total_derivatives_notional`, `portfolio_value_qoq_pct`, `top1_security_pct`, `top10_security_pct`, `est_turnover`, `cover_table_value`, `new_count`, `add_count`, `reduce_count`, `hold_count`, `sold_out_count` (the five counters read the quarter's `activity_counts`), or `filing_date` (values as ISO date strings).

A quarter where the compared field is null never matches. In particular, a quarter with no prior filed quarter to compare against has null `portfolio_value_qoq_pct`, `est_turnover` and `activity_counts`, so conditions on those fields never match it.

**Response:** `matches` (snapshot rows with `cik` / `name`), `total_matches`, `page`, `page_size`, `credits_used`, and — exactly when the returned rows cover more than one quarter — a `_warnings` entry saying the rows span quarters and the same manager can appear once per quarter.

**Errors:** `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`, `403 PLAN_TIER_INSUFFICIENT` below Pro (all rejected before any deduction), `504 QUERY_TIMEOUT` at the 30-second server timeout (charged).

**Example MCP tool call:**

```python
search_institutional_holdings(start_date="2026-01-01", end_date="2026-03-31",
                    conditions=[{"field": "total_positions_value", "op": ">=", "value": 100000000000}],
                    page_size=10)
```

### 6.10 Credit formula (`/screener/institutional-holdings`)

`POST /screener/institutional-holdings` charges per call (paginate ⇒ each page bills independently):

```
credits = max(65, 200
              + 40 × conditions        // numeric conditions + boolean filters actually passed
              − narrowWindowDiscount   // 40 when both dates are given and span ≤ 184 days (at most two quarters)
              + noWindowSurcharge)     // 80 when neither date is given
```

Range **160–720**.

| Scenario | Credits |
|---|---|
| Empty body (full listing) | **280** |
| One quarter (`start_date` = `end_date`), 1 condition | **200** |
| 1 condition + `is_amended`, no dates | **360** |

Charged even when `matches` is empty, and on `504 QUERY_TIMEOUT`. **No charge** on `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`, or `403 PLAN_TIER_INSUFFICIENT` (all rejected before any deduction).

---

### 6.11 `POST /screener/events`

**Purpose:** Screen corporate event filings by company, filing date and Item code. Returns matching section summaries grouped by filing. Requires **Pro or higher**.

**Request:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `tickers` | string[] | omitted | 1–100 symbols, each 1–32 characters. Case-insensitive; duplicates removed. Omitted = all available companies. |
| `start_date`, `end_date` | ISO date | omitted | Inclusive bounds on `filing_date`; `start_date` ≤ `end_date`. Either bound may be omitted. |
| `as_of_date` | ISO date | omitted | Exclude filings filed after this date: an `end_date` with no start. Must not be in the future or earlier than `start_date`. |
| `items` | string[] | omitted | 1–33 codes from [`GET /events/types`](#81-get-eventstypes); duplicates removed. Omitted = all Items. |
| `item_match` | `"any"` \| `"all"` | `"any"` | Match at least one or all requested Item codes in a filing. Applies only when `items` is supplied. |
| `include_unresolved_amendments` | bool | `false` | Include standalone amendments without an associated original filing. |
| `exclude_likely_merged` | bool | `false` | Exclude filings with `merge_status="likely"`. |
| `page` | int ≥ 1 | 1 | |
| `page_size` | int 1–10 | 5 | Filings per page. |

**Pagination cap:** `page × page_size ≤ 500` (422).

**Response:**

| Field | Meaning |
|---|---|
| `page`, `page_size`, `has_more`, `next_page` | Pagination. `next_page` is `null` on the last page. |
| `filings` | [Event filing objects](#event-filing-object), each with a `sections` array of matching [event section objects](#event-section-object). Empty results return `[]`. |
| `credits_used`, `_warnings` | Credits charged and optional [event warnings](#event-warnings). |

Results are ordered by `filing_date` descending. Each issuer and accession appears once, including when several share-class symbols match. With `items`, only matching sections are returned; the filing's `items` and `items_declared` remain complete. Original text, `prior` and `amendments` are not included.

**Credits:** **65–280 per page**, using the [§6.12](#612-credit-formula-screenerevents) formula.

**Errors:** `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`, `403 PLAN_TIER_INSUFFICIENT` below Pro, `504 EVENTS_QUERY_TIMEOUT` at the 30-second query timeout (charged).

**Example Request:**

```json
{
  "tickers": [
    "CNP"
  ],
  "start_date": "2023-01-01",
  "end_date": "2023-01-31",
  "items": [
    "5.02"
  ],
  "page": 1,
  "page_size": 5
}
```

**Example MCP tool call:**

```python
search_events(tickers=["CNP"], start_date="2023-01-01", end_date="2023-01-31", items=["5.02"], page_size=5)
```

**Example Response:**

```json
{
  "page": 1,
  "page_size": 5,
  "has_more": false,
  "next_page": null,
  "filings": [
    {
      "id": 4688,
      "ticker": "CNP",
      "issuer_cik": "0001130310",
      "accession_number": "0001130310-23-000002",
      "form_type": "8-K",
      "filing_date": "2023-01-03",
      "period_of_report": "2023-01-03",
      "items": [
        "5.02",
        "7.01",
        "9.01"
      ],
      "items_declared": [
        "5.02",
        "7.01",
        "9.01"
      ],
      "merge_status": "confirmed",
      "source_url_prefix": "https://www.sec.gov/Archives/edgar/data/1130310/000113031023000002/",
      "sections": [
        {
          "id": 10130,
          "section_id": "5.02",
          "item": "5.02",
          "title": "Departure of Directors or Certain Officers; Election of Directors; Appointment of Certain Officers; Compensatory Arrangements of Certain Officers",
          "summary": "On January 3, 2023, CenterPoint Energy, Inc. announced a new organizational structure effective immediately. Chief Executive Officer David J. Lesar continues in his role; President and Chief Operating Officer Jason P. Wells continues and will also serve as Chief Financial Officer until a successor is appointed…",
          "source_accession": "0001130310-23-000028",
          "source_filing_date": "2023-02-21",
          "restated": "modified",
          "source_locator": "../000113031023000028/cnp-20230217.htm"
        }
      ]
    }
  ],
  "credits_used": 65
}
```

### 6.12 Credit formula (`/screener/events`)

`POST /screener/events` charges per page:

```
credits = max(65, 200
              − tickerDiscount        // 100 − n distinct requested tickers; 0 when omitted
              − shortWindowDiscount   // 40 when both dates are supplied and differ by ≤ 90 days
              + fullHistorySurcharge) // 80 when start_date is omitted
```

Range **65–280**. Item codes do not add a per-condition charge.

| Request scope | Credits per page |
|---|---:|
| One ticker, both dates within 90 days | 65 |
| All companies, both dates within 90 days | 160 |
| All companies, a longer range with `start_date` | 200 |
| All companies, no `start_date` | 280 |

Charged even when `filings` is empty, and on `504 EVENTS_QUERY_TIMEOUT`. No charge on validation, authentication, endpoint-tier, insufficient-credit or rate-limit errors.

---

## 7. Ownership API

Insider trades (Form 4), initial beneficial-ownership statements (Form 3), and institutional holdings reports (Form 13F) reported to the SEC. US filings only. For the Form 4 / Form 3 endpoints, the plan coverage scope ([§1.9](#19-coverage-scope-notice)) and history window ([§1.8](#18-history-truncation-notice)) apply as they do elsewhere, on the filing's `filing_date`; the 13F endpoints are organized by the filing **manager** (institution), not by company. The endpoints in this section are available on every plan; full-library screening is [`POST /screener/ownership`](#65-post-screenerownership), [`POST /screener/initial-holdings`](#68-post-screenerinitial-holdings) and [`POST /screener/institutional-holdings`](#69-post-screenerinstitutional-holdings) in [Section 6](#6-screener-api), which require **Pro or higher**.

Credits are deducted before the query runs, so a `404` or `403` raised afterwards is still charged.

### Transaction object

Returned by `POST /ownership/transactions`, `POST /ownership/transaction-ids`, and `POST /screener/ownership`.

| Field | Meaning |
|-------|---------|
| `id` | Transaction id. Pass to `POST /ownership/transaction-ids`. |
| `is_derivative` | Derivative table row (`true`) or non-derivative (`false`). |
| `transaction_form_type` | `"4"` or `"5"`. |
| `transaction_date`, `transaction_code` | As filed. |
| `transaction_type` | Official SEC name for `transaction_code`. |
| `transaction_type_group` | One of the five SEC groupings — see [§7.6](#76-get-ownershiptransaction-types). |
| `acquired_disposed_code` | `"A"` acquired, `"D"` disposed, or `null`. Read direction from this field, not from `transaction_code`. |
| `shares`, `price_per_share`, `transaction_value`, `shares_owned_following` | As filed. `transaction_value` may be `null` or `0`. |
| `transaction_note` | Filer-side note explaining a `null` / `0` value. `null` when absent. |
| `principal_amount_not_shares` | `true` when `shares` holds a debt principal amount, not a share count. |
| `security_title`, `direct_or_indirect`, `nature_of_ownership` | As filed. |
| `footnote_refs` | Footnote ids for this transaction. Resolve against the filing's `footnotes` map. |
| `source_locator` | Combine with the filing's `source_url_prefix` to build the SEC URL. |
| `restated` | `null`, `"cancelled"`, `"modified"`, or `"added"` — set when an amendment changed this transaction. |
| `prior` | Present only when `restated` is set. The pre-amendment values. |
| `split_adjusted` | Present only when the filed values are not on the current per-share basis. See below. |
| `data_quality_flags` | Present only when at least one rule fires. See below. |

Derivative rows additionally carry `conversion_or_exercise_price`, `exercise_date`, `expiration_date`, `underlying_security_title`, `underlying_security_shares`, `underlying_security_value`, and `value_owned_following`.

#### `split_adjusted`

`shares` and `price_per_share` are in the units of the transaction's own trade date. When those differ from the current per-share basis, the block is present:

```json
"split_adjusted": {
  "shares": 4000.0,
  "price_per_share": 125.86,
  "shares_owned_following": 48000.0,
  "split_adjustment_factor": 4.0,
  "comparable_to_post_split_basis": false
}
```

Absent means the filed values are already on the current per-share basis. `split_adjustment_factor` and `comparable_to_post_split_basis` carry the same meaning and the same presence rule as on the metrics API ([§5.2](#52-post-metricall)).

#### `data_quality_flags`

Rows that fail a consistency check carry one or more of:

`future_transaction_date`, `future_deemed_execution_date`, `zero_price_open_market`, `derivative_date_order`, `negative_shares`, `negative_price`, `negative_transaction_value`, `negative_shares_owned_following`, `value_mismatch`, `manual_flags`.

Rows flagged `manual_flags` also carry a `manual_flags` array of `{code, note}` entries describing the issue.

Flagged rows are excluded from `POST /ownership/transactions`, `POST /ownership/stats` and `POST /screener/ownership` unless `include_anomalies=true` (`trade_filters.include_anomalies` on the screener). Each endpoint reports how many it withheld: the listing in `anomalies_excluded_on_page` (this page only), stats and the screener in `anomalies_excluded_count` (the whole requested range).

#### `computed`

`POST /screener/ownership` rows additionally carry a `computed` block with `shares_owned_before` and `own_pct_change`. Each entry has `value`, `status` (`ok` / `unavailable`), `unit`, `formula`, `computed_as`, and `fact_refs` pointing at the inputs. When an input is missing, `value` is `null`, `status` is `"unavailable"`, and `computed_as` states the reason.

---

### 7.1 `POST /ownership/transactions`

**Purpose:** Anchored listing of insider-trade filings and their transactions. Requires `ticker` (or `cik`) and/or `insider_cik`; unanchored queries belong to [§6.5](#65-post-screenerownership).

**Request:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `ticker` | string | — | Uppercased automatically. |
| `cik` | string | — | The company's CIK, instead of `ticker`. Digits only, ≤ 10; left-padded to 10. |
| `insider_cik` | string | — | Digits only, ≤ 10; left-padded to 10. |
| `transaction_code` | string[] | omitted | ≤ 20 entries. Unknown codes are dropped with a `_warnings` entry. |
| `transaction_form_type` | `"4"` \| `"5"` | omitted | |
| `relationship` | string[] | omitted | Any of `is_director`, `is_officer`, `is_ten_percent_owner`, `is_other`. OR semantics, evaluated per filing. |
| `include_unresolved_amendments` | bool | `false` | |
| `exclude_likely_merged` | bool | `false` | |
| `include_anomalies` | bool | `false` | |
| `page` | int ≥ 1 | 1 | |
| `page_size` | int 1–100 | 50 | Counts filings. |

At least one of `ticker` / `cik` / `insider_cik` is required, and not both `ticker` and `cik` (422 otherwise).

**Response:**

| Field | Meaning |
|---|---|
| `page`, `page_size`, `has_more`, `next_page` | Pagination. |
| `anomalies_excluded_on_page` | How many transactions **on this page** were withheld because they failed a consistency check. `0` when `include_anomalies=true`. Page-scoped: it counts only the rows hidden inside the filings this page returned, and is independent of the whole-range `anomalies_excluded_count` on [`POST /ownership/stats`](#72-post-ownershipstats). |
| `filings` | Array. Each carries `id`, `ticker`, `issuer`, `issuer_cik`, `accession_number`, `form_type`, `filing_date`, `period_of_report`, `date_of_original_submission`, `merge_status`, `remarks`, `source_url_prefix`, `insiders`, `footnotes`, `transactions`, and `amendments` (present only when amendments were merged). |
| `filings[].issuer_cik` | The issuer CIK declared by the filing itself. `null` when the filing does not provide issuer identity. |
| `filings[].insiders` | `cik`, `name`, `is_director`, `is_officer`, `is_ten_percent_owner`, `is_other`, `officer_title`, `other_text`. |
| `filings[].footnotes` | Map of footnote id → text for this filing. |
| `_warnings` | Optional. |
| `credits_used` | Always **30**. |

**Credits:** **30 per page.**

**Errors:** `404 OWNERSHIP_COMPANY_NOT_FOUND` (charged), `403 PLAN_TIER_INSUFFICIENT_COVERAGE` when the company is outside your plan's coverage scope (charged), `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`.

**Example MCP tool call:**

```python
list_insider_transactions(ticker="AAPL", transaction_code=["P", "S"], page_size=5)
```

---

### 7.2 `POST /ownership/stats`

**Purpose:** Aggregate statistics over one company and/or one insider, plus the ids of every transaction that contributed.

**Request:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `ticker` | string | — | |
| `cik` | string | — | The company's CIK, instead of `ticker`. |
| `insider_cik` | string | — | |
| `start_date`, `as_of_date` | ISO date | — | Window on `transaction_date`; `as_of_date` also excludes filings submitted after it. `as_of_date` ≤ today; `start_date` ≤ `as_of_date`. |
| `transaction_code` | string[] | omitted | ≤ 20 entries. |
| `method` | `"ad"` \| `"ps"` | `"ad"` | `ad` splits on `acquired_disposed_code`. `ps` treats P as a buy and S as a sell, and narrows the scope to those two codes. |
| `include_anomalies` | bool | `false` | |
| `relationship` | string[] | omitted | Any of `is_director`, `is_officer`, `is_ten_percent_owner`, `is_other`. OR semantics, evaluated per filing. |
| `is_derivative` | bool | omitted | |
| `exclude_likely_merged` | bool | `false` | |
| `include_unresolved_amendments` | bool | `false` | |
| `ids_page` | int ≥ 1 | 1 | Paginates `transaction_ids` only. |
| `ids_page_size` | int 1–1000 | 200 | |

At least one of `ticker` / `cik` / `insider_cik` is required, and not both `ticker` and `cik`.

**Response:** `stats`, `method_applied`, `start_date_applied`, `as_of_date_applied`, `anomalies_excluded_count`, `credits_used`, optional `corporate_actions`, optional `_warnings`, then `ids_total`, `ids_page`, `ids_has_more`, `transaction_ids`.

Key names inside `stats` follow `method`: `acquired` / `disposed` for `ad`, `bought` / `sold` for `ps`.

| `stats` field | Meaning |
|---|---|
| `value_acquired`, `value_disposed`, `net_value` | Cash value totals. Transactions with no cash value contribute 0. |
| `shares_acquired`, `shares_disposed`, `net_shares` | Share totals on the split-adjusted basis, excluding rows where `principal_amount_not_shares` is `true`. |
| `avg_price_acquired`, `avg_price_disposed` | Total value ÷ total split-adjusted shares over the transactions that have a cash value, on the same basis as `shares_*`. `null` when there are none. |
| `txn_count`, `txn_count_acquired`, `txn_count_disposed` | Counts. |
| `txn_count_with_value` | Transactions carrying a non-zero cash value. |
| `no_value_txn_count` | Transactions with no cash value. |
| `ad_code_missing_count` | Transactions counted in neither direction. |
| `principal_amount_txn_count` | Debt-principal rows excluded from `shares_*` and `avg_price_*`. |
| `distinct_insiders` | Distinct reporting owners in scope. |
| `filings_with_multiple_filers` | Joint filings in scope. |
| `max_txn_value_acquired`, `max_txn_value_disposed` | Largest single transaction per side. `null` when the side is empty. |
| `avg_value_per_txn_acquired`, `avg_value_per_txn_disposed` | Mean over transactions that have a cash value. |
| `earliest_transaction_date`, `latest_transaction_date` | Range actually covered. |
| `corporate_action_crossing` | `true` when the window spans a corporate action. |

`corporate_actions` is present only when the request is anchored on a `ticker` that has corporate actions on file: an array of `{ex_date, ratio, type, event_type}`.

`_warnings` names the count and the parameter that changes it whenever transactions were excluded, skipped, or carry no value.

**Credits:** **40 base**, plus **ceil(n / 1000) × 5** where `n` is the number of participating transactions. Both charges are recorded as one usage entry.

**Errors:** `404 OWNERSHIP_COMPANY_NOT_FOUND` (charged), `403 PLAN_TIER_INSUFFICIENT_COVERAGE` when the company is outside your plan's coverage scope (charged), `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`.

**Example MCP tool call:**

```python
get_insider_stats(ticker="AAPL", start_date="2025-01-01", method="ad")
```

---

### 7.3 `POST /ownership/transaction-ids`

**Purpose:** Fetch transactions by id — the detail companion to the `transaction_ids` returned by [§7.2](#72-post-ownershipstats).

**Request:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `transaction_ids` | int[] | — | **Required.** 1–100 positive integers. Duplicates are removed with a `_warnings` entry. |
| `page` | int ≥ 1 | 1 | |
| `page_size` | int 1–100 | 100 | Counts filings. |

**Response:** same envelope as [§7.1](#71-post-ownershiptransactions), plus `missing_ids` when some ids were not found and `restricted_ids` when some exist but are outside your plan's scope. Both are charged for.

**Credits:** **ceil(n / 5) × 10** on the deduplicated id count (max **200**).

**Errors:** `404 OWNERSHIP_TRANSACTION_NOT_FOUND` when **none** of the requested ids exist (charged; the response carries `missing_ids`). `403 PLAN_TIER_INSUFFICIENT_HISTORY` when every requested id that exists is outside your plan's scope (charged; the response carries `restricted_ids`). Partial misses and partial restrictions return 200. `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`.

**Example MCP tool call:**

```python
get_insider_transactions_by_id(transaction_ids=[123456, 123457])
```

---

### 7.4 `GET /ownership/tickers`

**Purpose:** Discovery — one row per company with insider-trade or initial-holdings coverage. Unpaginated; cache the result.

**Response:** `tickers` (array of `{ticker, jurisdiction, cik, forms}`), `credits_used`, and optional `_warnings`. `forms` lists what the ticker has on file: `"4"` insider transactions, `"3"` initial holdings.

**Credits:** **175.**

**Errors:** `402 CREDITS_INSUFFICIENT`.

**Example MCP tool call:**

```python
list_insider_tickers()
```

---

### 7.5 `POST /ownership/names`

**Purpose:** All insiders of one company, aggregated per CIK across its Form 4 and Form 3 filings.

**Request:**

| Field | Type | Notes |
|---|---|---|
| `ticker` | string | Exactly one of `ticker` or `cik`. |
| `cik` | string | The company's CIK. Digits only, ≤ 10; left-padded to 10. |
| `relationship` | string[] | Any of `ever_director`, `ever_officer`, `ever_ten_percent_owner`, `ever_other`. OR semantics. This endpoint aggregates across every filing, so the filter means "ever reported in this role"; the per-filing `is_*` form is used by [§7.2](#72-post-ownershipstats) and [§6.5](#65-post-screenerownership). |

**Response:** `names`, `credits_used`, and optional `_warnings`. Each name carries:

| Field | Meaning |
|---|---|
| `owner_cik`, `owner_name` | Spelling from this insider's most recent filing. |
| `name_changed` | `true` when earlier filings spelled the name differently. |
| `caveat` | Present only when `name_changed` is `true`. |
| `ever_director`, `ever_officer`, `ever_ten_percent_owner`, `ever_other` | `true` when any filing reported that role. |
| `latest_officer_title` | Most recent non-null officer title. `null` when never reported. |
| `officer_title_as_of` | Filing date `latest_officer_title` came from. |
| `filing_count` | Filings of this company naming this insider, Form 4 and Form 3 together. |
| `form4_filing_count`, `form3_filing_count` | The same count per form. |
| `earliest_filing_date`, `latest_filing_date` | Range. |

**Credits:** **30.**

**Errors:** `404 OWNERSHIP_COMPANY_NOT_FOUND` (charged), `403 PLAN_TIER_INSUFFICIENT_COVERAGE` when the company is outside your plan's coverage scope (charged).

**Example MCP tool call:**

```python
list_company_insiders(ticker="AAPL", relationship=["ever_officer"])
```

---

### 7.6 `GET /ownership/transaction-types`

**Purpose:** The SEC transaction-code catalog — 20 codes with their official name and grouping.

**Response:** `transaction_types` (array of `{code, name, group}`), `source_url` (the SEC form the wording is copied from), and `credits_used`.

**Credits:** **0.** No charge, no usage entry.

**Example MCP tool call:**

```python
list_insider_transaction_types()
```

---

### Holding object

Returned by `POST /ownership/initial-holdings`, `POST /ownership/initial-holding-ids`, and `POST /screener/initial-holdings`. A Form 3 reports positions held on becoming an insider, so a holding has no transaction date, code, price, or direction.

| Field | Meaning |
|-------|---------|
| `id` | Holding id. Pass to `POST /ownership/initial-holding-ids`. |
| `is_derivative` | Derivative table row (`true`) or non-derivative (`false`). |
| `security_title`, `direct_or_indirect`, `nature_of_ownership` | As filed. |
| `shares_owned` | Amount of securities beneficially owned, as filed. `null` on derivative rows (the form has no such column for them) and on cancelled rows. |
| `footnote_refs` | Footnote ids for this holding. Resolve against the filing's `footnotes` map. |
| `source_locator` | Combine with the filing's `source_url_prefix` to build the SEC URL. |
| `restated` | `null`, `"cancelled"`, `"modified"`, or `"added"` — set when an amendment changed this holding. |
| `prior` | Present only when `restated` is set. The pre-amendment values. |
| `split_adjusted` | Present only when the filed values are not on the current per-share basis. See below. |
| `data_quality_flags` | Present only when at least one rule fires. See below. |

Derivative rows additionally carry `conversion_or_exercise_price`, `exercise_date`, `expiration_date`, `underlying_security_title`, `underlying_security_shares`, and `underlying_security_value`.

#### `split_adjusted`

Same meaning and presence rule as on the [transaction object](#split_adjusted): present only when the filed values are not on the current per-share basis.

```json
"split_adjusted": {
  "shares_owned": 4000.0,
  "underlying_security_shares": null,
  "conversion_or_exercise_price": null,
  "split_adjustment_factor": 4.0,
  "comparable_to_post_split_basis": false
}
```

#### `data_quality_flags`

Rows that fail a consistency check carry one or more of:

`missing_shares_owned`, `missing_underlying_amount`, `derivative_date_order`, `negative_shares_owned`, `negative_underlying_security_shares`, `negative_underlying_security_value`, `negative_conversion_or_exercise_price`, `manual_flags`.

Rows flagged `manual_flags` also carry a `manual_flags` array of `{code, note}` entries describing the issue.

Flagged rows are excluded from `POST /ownership/initial-holdings`, `POST /ownership/initial-holdings/stats` and `POST /screener/initial-holdings` unless `include_anomalies=true` (`holding_filters.include_anomalies` on the screener). Each endpoint reports how many it withheld: the listing in `anomalies_excluded_on_page` (this page only), stats and the screener in `anomalies_excluded_count` (the whole requested range).

---

### 7.7 `POST /ownership/initial-holdings`

**Purpose:** Anchored listing of initial beneficial-ownership filings (Form 3) and their holdings. Requires `ticker` (or `cik`) and/or `insider_cik`; unanchored queries belong to [§6.8](#68-post-screenerinitial-holdings).

**Request:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `ticker` | string | — | Uppercased automatically. |
| `cik` | string | — | The company's CIK, instead of `ticker`. Digits only, ≤ 10; left-padded to 10. |
| `insider_cik` | string | — | Digits only, ≤ 10; left-padded to 10. |
| `relationship` | string[] | omitted | Any of `is_director`, `is_officer`, `is_ten_percent_owner`, `is_other`. OR semantics, evaluated per filing. |
| `no_securities_owned` | bool | omitted | `true` → only filings that report no holdings; `false` → only filings with holdings; omitted → both. |
| `include_unresolved_amendments` | bool | `false` | |
| `exclude_likely_merged` | bool | `false` | |
| `include_anomalies` | bool | `false` | |
| `page` | int ≥ 1 | 1 | |
| `page_size` | int 1–100 | 50 | Counts filings. |

At least one of `ticker` / `cik` / `insider_cik` is required, and not both `ticker` and `cik` (422 otherwise).

**Response:**

| Field | Meaning |
|---|---|
| `page`, `page_size`, `has_more`, `next_page` | Pagination. |
| `anomalies_excluded_on_page` | How many holdings **on this page** were withheld because they failed a consistency check. `0` when `include_anomalies=true`. Page-scoped, independent of the whole-range `anomalies_excluded_count` on [`POST /ownership/initial-holdings/stats`](#78-post-ownershipinitial-holdingsstats). |
| `filings` | Array. Each carries `id`, `ticker`, `issuer`, `issuer_cik`, `accession_number`, `form_type`, `filing_date`, `period_of_report`, `date_of_original_submission`, `merge_status`, `remarks`, `no_securities_owned`, `source_url_prefix`, `insiders`, `footnotes`, `holdings`, and `amendments` (present only when amendments were merged). |
| `filings[].no_securities_owned` | `true` when the filer reported holding no securities; such filings have an empty `holdings` array. |
| `filings[].insiders` | `cik`, `name`, `is_director`, `is_officer`, `is_ten_percent_owner`, `is_other`, `officer_title`, `other_text`. |
| `filings[].footnotes` | Map of footnote id → text for this filing. |
| `_warnings` | Optional. |
| `credits_used` | Always **30**. |

**Credits:** **30 per page.**

**Errors:** `404 OWNERSHIP_COMPANY_NOT_FOUND` (charged), `403 PLAN_TIER_INSUFFICIENT_COVERAGE` when the company is outside your plan's coverage scope (charged), `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`.

**Example MCP tool call:**

```python
list_initial_holdings(ticker="TSLA", page_size=5)
```

---

### 7.8 `POST /ownership/initial-holdings/stats`

**Purpose:** Aggregate statistics over the Form 3 filings of one company and/or one insider, plus the ids of every holding that contributed.

**Request:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `ticker` | string | — | |
| `cik` | string | — | The company's CIK, instead of `ticker`. |
| `insider_cik` | string | — | |
| `start_date`, `as_of_date` | ISO date | — | Window on `filing_date`. `as_of_date` ≤ today; `start_date` ≤ `as_of_date`. |
| `include_anomalies` | bool | `false` | |
| `relationship` | string[] | omitted | Any of `is_director`, `is_officer`, `is_ten_percent_owner`, `is_other`. OR semantics, evaluated per filing. |
| `is_derivative` | bool | omitted | Restricts the holding totals; filing counts are unaffected. |
| `exclude_likely_merged` | bool | `false` | |
| `include_unresolved_amendments` | bool | `false` | |
| `ids_page` | int ≥ 1 | 1 | Paginates `holding_ids` only. |
| `ids_page_size` | int 1–1000 | 200 | |

At least one of `ticker` / `cik` / `insider_cik` is required, and not both `ticker` and `cik`.

**Response:** `stats`, `start_date_applied`, `as_of_date_applied`, `anomalies_excluded_count`, `credits_used`, optional `corporate_actions`, optional `_warnings`, then `ids_total`, `ids_page`, `ids_has_more`, `holding_ids`.

| `stats` field | Meaning |
|---|---|
| `filing_count` | Form 3 filings in scope. |
| `no_securities_filing_count` | Of those, filings that report no holdings. |
| `distinct_insiders` | Distinct reporting owners in scope. |
| `filings_with_multiple_filers` | Joint filings in scope. |
| `holdings_count`, `holdings_count_non_derivative`, `holdings_count_derivative` | Holding rows in scope. |
| `shares_owned_total` | Sum of `shares_owned` over non-derivative rows, on the split-adjusted basis. |
| `underlying_shares_total` | Sum of `underlying_security_shares` over derivative rows, on the split-adjusted basis. |
| `earliest_filing_date`, `latest_filing_date` | Range actually covered. |
| `corporate_action_crossing` | `true` when the window spans a corporate action. |

`corporate_actions` is present only when the request is anchored on a `ticker` that has corporate actions on file: an array of `{ex_date, ratio, type, event_type}`.

`_warnings` names the count and the parameter that changes it whenever holdings were excluded.

**Credits:** **40 base**, plus **ceil(n / 1000) × 5** where `n` is the number of participating holdings. Both charges are recorded as one usage entry.

**Errors:** `404 OWNERSHIP_COMPANY_NOT_FOUND` (charged), `403 PLAN_TIER_INSUFFICIENT_COVERAGE` when the company is outside your plan's coverage scope (charged), `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`.

**Example MCP tool call:**

```python
get_initial_holding_stats(ticker="TSLA", start_date="2020-01-01")
```

---

### 7.9 `POST /ownership/initial-holding-ids`

**Purpose:** Fetch holdings by id — the detail companion to the `holding_ids` returned by [§7.8](#78-post-ownershipinitial-holdingsstats).

**Request:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `holding_ids` | int[] | — | **Required.** 1–100 positive integers. Duplicates are removed with a `_warnings` entry. |
| `page` | int ≥ 1 | 1 | |
| `page_size` | int 1–100 | 100 | Counts filings. |

**Response:** same envelope as [§7.7](#77-post-ownershipinitial-holdings), plus `missing_ids` when some ids were not found and `restricted_ids` when some exist but are outside your plan's scope. Both are charged for.

**Credits:** **ceil(n / 5) × 10** on the deduplicated id count (max **200**).

**Errors:** `404 OWNERSHIP_HOLDING_NOT_FOUND` when **none** of the requested ids exist (charged; the response carries `missing_ids`). `403 PLAN_TIER_INSUFFICIENT_HISTORY` when every requested id that exists is outside your plan's scope (charged; the response carries `restricted_ids`). Partial misses and partial restrictions return 200. `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`.

**Example MCP tool call:**

```python
get_initial_holdings_by_id(holding_ids=[1234, 1235])
```

---

### Snapshot object

Returned by [`POST /screener/institutional-holdings`](#69-post-screenerinstitutional-holdings) (as `matches[]`, with `cik` and `name`) and [`POST /ownership/institutional-holdings/manager-history`](#710-post-ownershipinstitutional-holdingsmanager-history) (as `quarters[]`, without them — the manager identity sits once at the top of that response). One row = one manager × one calendar quarter, after amendments are merged.

| Field | Meaning |
|-------|---------|
| `cik`, `name` | The filing manager. Screener rows only. |
| `period` | Calendar quarter end the report covers. |
| `filing_date`, `accession` | The filing this quarter's data comes from. |
| `is_combination_report` | `true` = 13F COMBINATION REPORT (the filer also reports holdings managed by other managers), `false` = 13F HOLDINGS REPORT. |
| `confidential` | The quarter carries confidential-treatment holdings. |
| `is_amended` | Amendments were merged into this quarter. The full archive is on the portfolio endpoint's quarter header. |
| `unit_multiplier` | `1`, or the whole-quarter unit-correction multiplier applied to the filed values. When it is not `1`, `unit_multiplier_reason` (`corrected_error` or `pre2023_rule`) is present; the key is omitted when null. |
| `total_positions_value` | Total value of the stock holdings, USD. |
| `total_derivatives_notional` | Total notional of the option holdings, USD. |
| `cover_table_value` | The cover-page total as filed, verbatim. |
| `positions_count`, `derivatives_count` | Live stock / option rows in the quarter. |
| `portfolio_value_qoq_pct` | Quarter-over-quarter change of `total_positions_value`, percent. |
| `top1_security_pct`, `top10_security_pct` | Portfolio share of the largest / ten largest stock holdings, percent (stock holdings only). |
| `est_turnover` | Estimated traded value versus the prior quarter, USD. |
| `activity_counts` | Map of row activity → count: `NEW`, `ADD`, `REDUCE`, `HOLD`, `SOLD_OUT`, `CUSIP_CHANGE`. |
| `sec_url` | The URL opens the filing's EDGAR index page; see the files listed there for full details. |

A quarter with **no prior filed quarter** to compare against has null `portfolio_value_qoq_pct`, `est_turnover` and `activity_counts`, which must not be read as everything being new.

Quarters marked `confidential` in the returned result are also flagged in `_warnings`.

### Institutional holding object

Returned by [`POST /ownership/institutional-holdings/portfolio`](#711-post-ownershipinstitutional-holdingsportfolio) and [`POST /ownership/institutional-holdings/security`](#712-post-ownershipinstitutional-holdingssecurity) (flat mode) as `transactions[]`. Security rows additionally carry `manager_cik` / `manager_name`. Three row kinds, told apart by `type`:

- **`positions`** — a stock holding.
- **`derivatives`** — an option holding (`put_call` says which side).
- **`exited`** — a position closed out this quarter: identity plus the closing deltas only (`d_shares`, and `d_value` or `d_notional_value_usd`).

| Field | Meaning |
|-------|---------|
| `type`, `period` | Row kind and calendar quarter end. |
| `cusip`, `ticker`, `current_ticker`, `ticker_status`, `issuer`, `issuer_norm`, `class`, `security_type` | Security identity. `ticker` is the symbol matched to the CUSIP, `current_ticker` the current one (`null` when none). `issuer` is the name as filed, `issuer_norm` the normalized name, `class` the share-class text as filed. `ticker_status` is `us_listing`, `when_issued`, `non_us_listing`, `none` (no matching listed ticker), or null. |
| `put_call` | `"Put"` / `"Call"` on option rows; null on stock rows. When the row kinds are mixed it is one of the fields telling them apart. |
| `shares_or_principal_amount` | `SH` = the row's `shares` is a share count; `PRN` = a principal amount in USD (debt securities), not a share count; null = a row cancelled by an amendment, or missing in the original filing. |
| `shares` | Share count or principal amount, per the previous field. |
| `vote_sole`, `vote_shared`, `vote_none` | Voting-authority split as filed. |
| `discretion` | Investment discretion as filed (`SOLE`, `DFND`, `OTR` and combinations). |
| `source_accessions` | The amendment filings this row's current values come from; null = the quarter's main filing. |
| `activity` | `NEW` / `ADD` / `REDUCE` / `HOLD` / `CUSIP_CHANGE` versus the prior quarter. Null when there is no prior filed quarter to compare against. Exited rows carry no activity label — the exit is the row itself. |
| `predecessor_cusip` | On `CUSIP_CHANGE` rows: the CUSIP this security was held under in the prior quarter. |
| `shares_before_cusip_change` | Shares held under the predecessor CUSIP; present only when non-null, directly after `predecessor_cusip`. |
| `d_pct` | Share change versus the prior quarter, percent (`d_shares` is the absolute change). |
| `unit_multiplier` | Row-level unit-correction multiplier (can be fractional, e.g. `0.001`); present when this row's filed value was individually corrected. |
| `restated` | `added` / `modified` / `cancelled` when an amendment changed this row; null otherwise. A cancelled row is a tombstone: its current-value fields are null and the old values sit in `prior_versions`. |
| `prior_versions` | Pre-amendment field values, newest first. |
| `submgr_breakdown` | Sub-manager split of the position: `{submgr_index, submgr_name, shares, pct_of_position}`. |
| `filer_flags` | Present only when the row is flagged: `zero_shares_with_value` (0 shares filed together with a value above 0), `implied_price_outlier` (value ÷ shares is at least 5× away from the security's closing price on the last trading day of the quarter). The filed values are returned unchanged and the row is not excluded. |

Stock rows additionally carry `value` (USD), `weight` (this holding's share of the quarter's `total_positions_value` — a percent **string** with two decimals, e.g. `"61.88%"`; null on cancelled rows), `rank` (1 = the quarter's largest stock holding), `avg_price`, and `d_value`. Option rows carry `notional_value_usd`, `underlying_price_implied`, and `d_notional_value_usd`.

**Warnings** (`_warnings`, present only when non-empty) on the portfolio / security endpoints flag:

- an amendment whose **claimed action type differs from its actual action** (portfolio only — see the `claimed_type` and `action` fields in `quarter.amendments_merged`);
- **unit-correction multipliers** in the returned result, whole-quarter or row-level — see the `unit_multiplier` fields;
- **`CUSIP_CHANGE` rows** in the returned result — see each row's `predecessor_cusip`;
- a quarter **with no prior filed quarter** in the returned result — `activity`, `d_shares`, `d_value`, `d_pct` and the quarter's `est_turnover`, `portfolio_value_qoq_pct`, `activity_counts` are null there and must not be read as everything being new;
- **flagged rows** in the returned result — see each row's `filer_flags`;
- **cancelled rows** in the returned result — see each row's `restated`.

---

### 7.10 `POST /ownership/institutional-holdings/manager-history`

**Purpose:** One manager's 13F history — one [snapshot object](#snapshot-object) per quarter filed, newest first by default.

**Request:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `cik` | string | — | 13F filer CIK, digits verbatim (not zero-padded). Provide exactly one of `cik` / `name`. |
| `name` | string | — | Manager name, filed or normalized spelling. Whitespace and case are ignored; otherwise the match is exact. |
| `sort_order` | `"desc"` \| `"asc"` | `"desc"` | By `period`. |
| `page` | int ≥ 1 | 1 | |
| `page_size` | int 1–100 | 50 | Counts quarters. |

**Response:** `manager` (`cik`, `name`, `sec_url_prefix` — the manager's EDGAR directory prefix), `quarters` (snapshot rows), `total_quarters`, `page`, `page_size`, `credits_used`.

**Credits:** **30.**

**Errors:** `404 OWNERSHIP_MANAGER_NOT_FOUND` when no single manager matches (charged) — several managers can file under the same or a similar name; query by CIK instead. `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`.

**Example MCP tool call:**

```python
get_institutional_manager_history(cik="1000097", page_size=8)
```

---

### 7.11 `POST /ownership/institutional-holdings/portfolio`

**Purpose:** One manager × one quarter — the holdings detail as [institutional holding objects](#institutional-holding-object), under a quarter header that repeats the snapshot numbers and carries the amendment archive.

**Request:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `cik` / `name` | string | — | Exactly one, as in [§7.10](#710-post-ownershipinstitutional-holdingsmanager-history). |
| `quarter` | string | — | **Required.** Calendar quarter label `YYYYQn`, e.g. `2026Q1` (Q1 = Mar 31, Q2 = Jun 30, Q3 = Sep 30, Q4 = Dec 31). |
| `type` | string | omitted | `positions` / `derivatives` / `exited`. Omitted = all three; each row carries its own `type`. The response echoes the filter as `types`. |
| `activity` | string[] | omitted | ≤ 6 of `NEW`, `ADD`, `REDUCE`, `HOLD`, `CUSIP_CHANGE`. Unknown values are dropped with a `_warnings` entry. Exited rows carry no activity label, so combined with `type="exited"` the filter matches nothing (a warning says so). |
| `restated` | string[] | omitted | ≤ 3 of `added`, `modified`, `cancelled`. |
| `cusip` | string | omitted | 9 characters; pins one security inside the quarter. Give `cusip` or `ticker`, not both. |
| `ticker` | string | omitted | Uppercased automatically; any symbol the company has traded under. |
| `sort_by` | string | omitted | `period`, `d_shares`, `ticker`, `issuer_norm`, `type` (row kind as text, alphabetical: `derivatives` < `exited` < `positions`). Omitted = the filed row order. |
| `sort_order` | `"desc"` \| `"asc"` | `"desc"` | |
| `page` | int ≥ 1 | 1 | |
| `page_size` | int 1–100 | 50 | Counts rows after filtering, all kinds together. |

**Response:** `manager` (as in [§7.10](#710-post-ownershipinstitutional-holdingsmanager-history)); `quarter` — a [snapshot object](#snapshot-object) plus `accessions` (every filing applied to the quarter, in order), `sec_locators` (accession → EDGAR index-page path; combine with the manager's `sec_url_prefix`), `amendments_merged` (see below) and `prior_versions` (superseded cover pages); then `types`, `transactions`, `total_transactions`, `page`, `page_size`, `credits_used`, and `_warnings` (see the [institutional holding object](#institutional-holding-object)).

`amendments_merged` — one entry per amendment applied to the quarter: `accession`, `filing_date`, `claimed_type` (the type the amendment declared for itself, or null), `action` (what it actually did: `REPLACE` / `MERGE_ADD` / `DUPLICATE_SUPERSEDED` / `REJECTED`), `cover_total`, `confidential`, and `positions_added` / `positions_restated` / `positions_cancelled`.

**Credits:** **30 per page.**

**Errors:** `404 OWNERSHIP_MANAGER_NOT_FOUND` (charged); `404 OWNERSHIP_MANAGER_QUARTER_NOT_FOUND` when the manager exists but no 13F data was found for that quarter (charged); `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`.

**Example MCP tool call:**

```python
get_institutional_portfolio(cik="1000097", quarter="2026Q1", type="positions", page_size=50)
```

---

### 7.12 `POST /ownership/institutional-holdings/security`

**Purpose:** One security's holder rows across managers — who holds it, quarter by quarter. Flat mode returns [institutional holding objects](#institutional-holding-object) with `manager_cik` / `manager_name` on each row; `group_by_quarter=true` switches to one aggregate row per quarter.

**Request:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `cusip` | string | — | 9 characters. Provide exactly one of `cusip` / `ticker`. |
| `ticker` | string | — | Uppercased automatically; any symbol the company has traded under. A ticker can match several CUSIPs; rows for all of them are included, `securities` lists each one, and a `_warnings` entry says how many matched. |
| `quarter` | string | omitted | `YYYYQn`. Omitted = every quarter, newest first. |
| `type` | string | omitted | As in [§7.11](#711-post-ownershipinstitutional-holdingsportfolio). |
| `activity` | string[] | omitted | As in [§7.11](#711-post-ownershipinstitutional-holdingsportfolio). |
| `group_by_quarter` | bool | `false` | `true` → one aggregate row per quarter (`period`, `holders`, `total_shares`, `total_value`, computed over live stock rows), unpaged; `type` / `activity` / `sort_by` / `page` are ignored with a warning. |
| `sort_by` | string | omitted | `period`, `d_shares`, `ticker`, `issuer_norm`, `type`, `shares`, `value`. Omitted = newest quarter first. |
| `sort_order` | `"desc"` \| `"asc"` | `"desc"` | |
| `page` | int ≥ 1 | 1 | |
| `page_size` | int 1–100 | 50 | |

**Response:** `securities` — one entry per matched CUSIP: `cusip`, `ticker`, `current_ticker`, `ticker_status`, `issuer_norm`, `security_type`, `first_seen` / `last_seen` (first / latest quarter end the security appears in), `latest_holders` (holder count at `last_seen`); then `group_by_quarter`, `credits_used`, and either the flat fields (`types`, `transactions`, `total_transactions`, `page`, `page_size`) or the grouped ones (`transactions`, `total_transactions` — no paging fields).

**Credits:** **30 per page** (flat mode); **40 flat** (`group_by_quarter=true`, unpaged).

**Errors:** `404 OWNERSHIP_SECURITY_NOT_FOUND` (charged), `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`.

**Example MCP tool call:**

```python
get_institutional_security_holders(ticker="AAPL", quarter="2026Q1", page_size=50)
```

---

## 8. Corporate Events API

Corporate event summaries, original filing text, attachments and amendment history for US companies. Endpoints in this section require **Starter or higher**. Cross-company and date-filtered searches use [`POST /screener/events`](#611-post-screenerevents).

### Event filing object

Returned by `POST /events/filings`, `POST /events/filing` and `POST /screener/events`.

| Field | Meaning |
|---|---|
| `id` | Filing identifier, used as `filing_id` on `POST /events/filing`. |
| `ticker`, `issuer_cik` | Company symbol, and the 10-digit CIK this filing was filed under. |
| `accession_number` | SEC accession number. |
| `form_type` | Filing type as reported. |
| `filing_date` | Filing date, `YYYY-MM-DD`. |
| `period_of_report` | Reported event date, `YYYY-MM-DD`. |
| `items` | Item codes represented by the filing's current sections. |
| `items_declared` | Item codes declared in the filing. |
| `merge_status` | Amendment association: `null` (none), `confirmed`, `likely`, or `unresolved` (standalone amendment). |
| `source_url_prefix` | Base URL for resolving each section's `source_locator`, including relative `../` paths. |

### Event section object

Returned in `sections` by `POST /events/filing` and `POST /screener/events`.

| Field | Meaning |
|---|---|
| `id`, `section_id` | Section identifiers; `section_id` identifies the section within its filing. |
| `item`, `title` | Item code and section title. |
| `summary` | Current event summary, including available amendments. |
| `source_accession`, `source_locator` | Source accession and document path for the summary. Resolve the path against the filing's `source_url_prefix`. |
| `source_filing_date` | Filing date of `source_accession`, `YYYY-MM-DD`. |
| `restated` | `null`, `added`, `modified` or `cancelled`. |
| `prior` | Previous summaries, when available: `{section_id, accession, filing_date, summary}` entries, newest first. Returned only by `POST /events/filing`. |

The detail response also includes `amendments` when available. Each entry contains `accession`, `source_locator`, `amendment_type`, `amendment_filing_date`, `sections_added`, `sections_restated`, `merge_key` (amendment reference label), and `amendment_reason` (text or `null`).

Attachments are returned as `exhibits` when requested. Each entry contains `exhibit`, `description`, `source_locator`, `source_accession` and `text` (`null` when the attachment has no text).

### Event warnings

`_warnings` is omitted when there are no notices. Identical messages appear once.

- Different `items` and `items_declared` sets produce a filing-level notice. List and screening responses combine the affected filing count for that page into one notice.
- Selecting `items` on the detail endpoint produces a notice if sections are omitted.
- Reading a standalone unresolved amendment produces a notice.
- Returned attachments without text produce a notice.
- History limits, the company's other symbols or CIKs ([§1.11](#111-company-symbols-and-ciks)) and the screening pagination cap may produce notices.

### 8.1 `GET /events/types`

**Purpose:** List the 33 supported Item codes, their official English titles and the official source URL.

**Request:** No parameters.

**Response:** `items` (array of `{item, title}`), `source_url`, and `credits_used`.

**Credits:** **0**.

**Errors:** `403 PLAN_TIER_INSUFFICIENT` below Starter. Authentication and rate-limit errors follow [Section 1](#1-conventions).

**Example Request:**

```http
GET /api/v1/events/types
X-API-Key: <your-api-key>
```

**Example MCP tool call:**

```python
list_event_types()
```

**Example Response:**

```json
{
  "items": [
    {
      "item": "2.02",
      "title": "Results of Operations and Financial Condition"
    },
    {
      "item": "5.02",
      "title": "Departure of Directors or Certain Officers; Election of Directors; Appointment of Certain Officers; Compensatory Arrangements of Certain Officers"
    }
  ],
  "source_url": "https://www.sec.gov/files/form8-k.pdf",
  "credits_used": 0
}
```

---

### 8.2 `POST /events/filings`

**Purpose:** List one company's event filings, newest first. The returned `id` identifies a filing for [`POST /events/filing`](#83-post-eventsfiling).

**Request:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `ticker` | string | omitted | Exactly one of `ticker` or `cik`. 1–32 characters; case-insensitive; surrounding whitespace ignored. |
| `cik` | string | omitted | Exactly one of `ticker` or `cik`. 1–10 digits; leading zeros may be omitted. |
| `page` | int ≥ 1 | 1 | |
| `page_size` | int 1–100 | 50 | |

**Response:** `page`, `page_size`, `has_more`, `next_page`, `filings` ([event filing objects](#event-filing-object)), `credits_used`, and optional `_warnings`. Empty pages return `filings: []`; the last page has `next_page: null`.

This list returns filing metadata and excludes standalone unresolved amendments. Item/date filters and the option to include those amendments are available on [`POST /screener/events`](#611-post-screenerevents).

**Credits:** **10 per page**, including empty pages.

**Errors:** `404 EVENTS_COMPANY_NOT_FOUND` (charged), `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`, `403 PLAN_TIER_INSUFFICIENT` below Starter.

**Example Request:**

```json
{
  "ticker": "CNP",
  "page": 1,
  "page_size": 1
}
```

**Example MCP tool call:**

```python
list_event_filings(ticker="CNP", page_size=1)
```

**Example Response:**

```json
{
  "page": 1,
  "page_size": 1,
  "has_more": true,
  "next_page": 2,
  "filings": [
    {
      "id": 4688,
      "ticker": "CNP",
      "issuer_cik": "0001130310",
      "accession_number": "0001130310-23-000002",
      "form_type": "8-K",
      "filing_date": "2023-01-03",
      "period_of_report": "2023-01-03",
      "items": [
        "5.02",
        "7.01",
        "9.01"
      ],
      "items_declared": [
        "5.02",
        "7.01",
        "9.01"
      ],
      "merge_status": "confirmed",
      "source_url_prefix": "https://www.sec.gov/Archives/edgar/data/1130310/000113031023000002/"
    }
  ],
  "credits_used": 10
}
```

---

### 8.3 `POST /events/filing`

**Purpose:** Read one event filing, with optional Item selection, original text and attachments.

**Request:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `filing_id` | integer | required | An `id` from `/events/filings` or `/screener/events`. 1–9223372036854775807; strings and booleans are rejected. |
| `items` | string[] | omitted | 1–33 codes from [`GET /events/types`](#81-get-eventstypes); duplicates removed. Omit or send `null` for all sections. |
| `include_original_raw_text` | bool | `false` | Include complete original filing text as `raw_text`, at no additional cost. |
| `include_exhibits` | bool | `false` | Include the filing's attachments as `exhibits`, at no additional cost. |
| `as_of_date` | ISO date | omitted | Read this filing only if it was filed on or before this date. Must not be in the future. |

**Response:** `filing` ([event filing object](#event-filing-object)) with `sections` ([event section objects](#event-section-object)), available `amendments`, and `raw_text` / `exhibits` when requested; then `credits_used` and optional `_warnings`.

`items` filters only `sections`. Filing metadata, amendment records and requested original text remain complete. Valid Item codes with no matching sections return `sections: []`.

**Credits:** **30 per call**, including requests with no matching sections.

**Errors:** `404 EVENTS_FILING_NOT_FOUND` (charged), `404 EVENTS_FILING_NOT_YET_FILED` (charged; the filing is later than `as_of_date`), `403 PLAN_TIER_INSUFFICIENT_HISTORY` (charged), `422 VALIDATION_ERROR`, `402 CREDITS_INSUFFICIENT`, `403 PLAN_TIER_INSUFFICIENT` below Starter.

**Example Request:**

```json
{
  "filing_id": 4688,
  "items": [
    "5.02"
  ],
  "include_original_raw_text": false,
  "include_exhibits": false,
  "as_of_date": null
}
```

**Example MCP tool call:**

```python
get_event_filing(filing_id=4688, items=["5.02"], include_original_raw_text=False, include_exhibits=False, as_of_date=None)
```

**Example Response:**

```json
{
  "filing": {
    "id": 4688,
    "ticker": "CNP",
    "issuer_cik": "0001130310",
    "accession_number": "0001130310-23-000002",
    "form_type": "8-K",
    "filing_date": "2023-01-03",
    "period_of_report": "2023-01-03",
    "items": [
      "5.02",
      "7.01",
      "9.01"
    ],
    "items_declared": [
      "5.02",
      "7.01",
      "9.01"
    ],
    "merge_status": "confirmed",
    "source_url_prefix": "https://www.sec.gov/Archives/edgar/data/1130310/000113031023000002/",
    "sections": [
      {
        "id": 10130,
        "section_id": "5.02",
        "item": "5.02",
        "title": "Departure of Directors or Certain Officers; Election of Directors; Appointment of Certain Officers; Compensatory Arrangements of Certain Officers",
        "summary": "On January 3, 2023, CenterPoint Energy, Inc. announced a new organizational structure effective immediately. Chief Executive Officer David J. Lesar continues in his role; President and Chief Operating Officer Jason P. Wells continues and will also serve as Chief Financial Officer until a successor is appointed…",
        "source_accession": "0001130310-23-000028",
        "source_filing_date": "2023-02-21",
        "restated": "modified",
        "source_locator": "../000113031023000028/cnp-20230217.htm"
      }
    ],
    "amendments": [
      {
        "accession": "0001130310-23-000028",
        "source_locator": "../000113031023000028/cnp-20230217.htm",
        "amendment_type": "merged",
        "amendment_filing_date": "2023-02-21",
        "sections_added": 0,
        "sections_restated": 2,
        "merge_key": "explicit_reference",
        "amendment_reason": "This Current Report on Form 8-K/A amends our Current Report on Form 8-K filed with the Securities and Exchange Commission on January 3, 2023 and dated as of January 3, 2023."
      }
    ]
  },
  "credits_used": 30,
  "_warnings": [
    "Some sections were omitted because the request included items. To retrieve all sections for this filing, omit the items field."
  ]
}
```

---

## 9. API key, account, and plan errors

| HTTP | When |
|------|------|
| 401 | `AUTH_API_KEY_MISSING` — no `X-API-Key` header and no bearer token. |
| 403 | `AUTH_API_KEY_INVALID` — unknown or inactive key. |
| 403 | `AUTH_USER_INACTIVE` — the key's account is inactive or deleted. |
| 403 | `AUTH_EMAIL_NOT_VERIFIED` — the key's account has not verified its email. |
| 403 | `AUTH_API_KEY_EXPIRED` — key past its expiry. |
| 403 | `AUTH_API_KEY_INACTIVE` — key disabled by inactivity policy. |
| 403 | `PLAN_TIER_INSUFFICIENT` — the endpoint is not available on the caller's current plan. |
| 403 | `PLAN_TIER_INSUFFICIENT_HISTORY` — the requested filing is older than what the caller's current plan covers. For corporate event filings, the body carries `filing_date` and `earliest_allowed_filing_date`; fiscal-year-based filings carry `filing_fiscal_year` and `earliest_allowed_fiscal_year`. |
| 403 | `PLAN_TIER_INSUFFICIENT_FORMULA_OVERRIDE` — the `formula_override` request field is not available on the caller's current plan. Omit `formula_override` to proceed, or upgrade. |
| 403 | `PLAN_TIER_INSUFFICIENT_COVERAGE` — the requested company or filing is outside the caller's plan's coverage scope. Body also carries `coverage_band`. |

Every `PLAN_TIER_*` response includes `required_tier` and `current_tier` strings (lowercase plan names) in the `detail` body so clients can prompt for an upgrade without parsing the message text.

---

## 10. Request limits (validation)

These caps apply at validation time (**422** when exceeded):

| Limit | Value |
|-------|--------|
| Line items per request (`/data/line-items`, `/data/compare/line-items`) | 50 |
| Fact ids per request (`/data/facts`) | 500 |
| Fact ids per row (`/data/compare/facts`) | 500 |
| Total fact ids (`/data/compare/facts`) | 2500 |
| Filings per compare request | 25 |
| Metric names (`/metric/items`, optional `/metric/historical`) | 100 |
| Concepts per metric in `formula_override` (`/metric/all`, `/metric/items`, `/metric/historical`) | 50 |
| Form types (`/metric/filings`) | 8 |
| Historical page size | 5 |
| Corporate event Item codes (`/events/filing`, `/screener/events`) | 33 |
| Corporate event screening tickers | 100 |
| Corporate event screening page size | 10 (default 5) |
| Corporate event screening cumulative results | page × page_size ≤ 500 |

---

## 11. MCP access

The data, metrics, ownership, corporate events and screening APIs are also available through **Model Context Protocol (MCP)** at:

```
https://api.akkrudata.ai/mcp
```

Authenticate via one of the two methods below. Both share your AkkruData account, credits, and rate limits. A tool call that includes an argument the tool does not declare is rejected before it runs and is not charged.

### 11.1 Connect via OAuth (recommended for AI assistants)

For AI clients with built-in MCP support (Claude Desktop, Claude Web, etc.):

1. In your AI client, add `https://api.akkrudata.ai/mcp` as a custom MCP connector.
2. Click **Connect**. Your browser will open an AkkruData sign-in page.
3. Sign in with the same credentials you use for the dashboard (email/password, Google, or GitHub).
4. Review the connecting AI client's reported identity, then click **Authorize**.
5. Done. The AI client now holds a token and can call AkkruData on your behalf. Token renewal is automatic.

You can list and disconnect any authorized AI client at any time from the dashboard's **Connected AI assistants** panel. A single account may have up to **10 simultaneous AI client connections**.

### 11.2 Connect via X-API-Key (programmatic use)

Available only on deployments where MCP OAuth is disabled.

For scripted MCP clients, local development, or any setup where you cannot run the OAuth browser flow:

```
URL:     https://api.akkrudata.ai/mcp
Header:  X-API-Key: <your-api-key>
```

The same API key used for REST works for MCP. Create and rotate keys from the dashboard.

### 11.3 Tool catalog

| MCP tool | Documentation |
|----------|---------------|
| `list_filings` | [`POST /data/filings`](#41-post-datafilings) |
| `lookup_company` | [`POST /data/company`](#42-post-datacompany) |
| `list_companies` | [`GET /data/company/list`](#43-get-datacompanylist) |
| `query_line_items` | [`POST /data/line-items`](#44-post-dataline-items) |
| `get_filing_facts` | [`POST /data/facts`](#45-post-datafacts) |
| `get_filing_statement` | [`POST /data/statement`](#46-post-datastatement) |
| `list_filing_statements` | [`POST /data/filing-statements`](#47-post-datafiling-statements) |
| `get_income_statement` | [`POST /data/income-statement`](#48-statement-endpoints-datastatement) |
| `get_comprehensive_income` | [`POST /data/comprehensive-income`](#48-statement-endpoints-datastatement) |
| `get_balance_sheet` | [`POST /data/balance-sheet`](#48-statement-endpoints-datastatement) |
| `get_cash_flow_statement` | [`POST /data/cash-flow-statement`](#48-statement-endpoints-datastatement) |
| `get_equity_statement` | [`POST /data/equity-statement`](#48-statement-endpoints-datastatement) |
| `get_dimensional_breakdown_by_fact_id` | [`POST /data/facts/dimensional-breakdown`](#49-post-datafactsdimensional-breakdown) |
| `get_dimensional_breakdown_by_line_item` | [`POST /data/line-items/dimensional-breakdown`](#410-post-dataline-itemsdimensional-breakdown) |
| `compare_line_items` | [`POST /data/compare/line-items`](#411-post-datacompareline-items) |
| `compare_facts` | [`POST /data/compare/facts`](#412-post-datacomparefacts) |
| `get_filing_excel` | [`GET /data/filings/{filing_id}/excel`](#413-get-datafilingsfiling_idexcel) |
| `list_metric_snapshots` | [`POST /metric/filings`](#51-post-metricfilings) |
| `get_metrics_bundle` | [`POST /metric/all`](#52-post-metricall) |
| `get_metrics_bundle_filing_date` | [`POST /metric/all/filing-date`](#53-post-metricallfiling-date) |
| `get_metrics_subset` | [`POST /metric/items`](#54-post-metricitems) |
| `get_profitability_metrics` | [`POST /metric/profitability`](#56-category-endpoints-metriccategory) |
| `get_growth_metrics` | [`POST /metric/growth`](#56-category-endpoints-metriccategory) |
| `get_financial_health_metrics` | [`POST /metric/financial-health`](#56-category-endpoints-metriccategory) |
| `get_efficiency_metrics` | [`POST /metric/efficiency`](#56-category-endpoints-metriccategory) |
| `get_cash_flow_metrics` | [`POST /metric/cash-flow`](#56-category-endpoints-metriccategory) |
| `get_valuation_metrics` | [`POST /metric/valuation`](#56-category-endpoints-metriccategory) |
| `get_valuation_metrics_filing_date` | [`POST /metric/valuation/filing-date`](#56-category-endpoints-metriccategory) |
| `get_metrics_timeseries` | [`POST /metric/historical`](#55-post-metrichistorical) |
| `search_stocks` | [`POST /screener/metrics`](#61-post-screenermetrics) |
| `list_screener_filters` | [`GET /screener/metrics/filters`](#62-get-screenermetricsfilters) |
| `list_insider_transactions` | [`POST /ownership/transactions`](#71-post-ownershiptransactions) |
| `get_insider_stats` | [`POST /ownership/stats`](#72-post-ownershipstats) |
| `get_insider_transactions_by_id` | [`POST /ownership/transaction-ids`](#73-post-ownershiptransaction-ids) |
| `search_insider_trades` | [`POST /screener/ownership`](#65-post-screenerownership) |
| `list_insider_tickers` | [`GET /ownership/tickers`](#74-get-ownershiptickers) |
| `list_company_insiders` | [`POST /ownership/names`](#75-post-ownershipnames) |
| `list_insider_transaction_types` | [`GET /ownership/transaction-types`](#76-get-ownershiptransaction-types) |
| `list_initial_holdings` | [`POST /ownership/initial-holdings`](#77-post-ownershipinitial-holdings) |
| `get_initial_holding_stats` | [`POST /ownership/initial-holdings/stats`](#78-post-ownershipinitial-holdingsstats) |
| `get_initial_holdings_by_id` | [`POST /ownership/initial-holding-ids`](#79-post-ownershipinitial-holding-ids) |
| `search_initial_holdings` | [`POST /screener/initial-holdings`](#68-post-screenerinitial-holdings) |
| `get_institutional_manager_history` | [`POST /ownership/institutional-holdings/manager-history`](#710-post-ownershipinstitutional-holdingsmanager-history) |
| `get_institutional_portfolio` | [`POST /ownership/institutional-holdings/portfolio`](#711-post-ownershipinstitutional-holdingsportfolio) |
| `get_institutional_security_holders` | [`POST /ownership/institutional-holdings/security`](#712-post-ownershipinstitutional-holdingssecurity) |
| `search_institutional_holdings` | [`POST /screener/institutional-holdings`](#69-post-screenerinstitutional-holdings) |
| `list_event_types` | [`GET /events/types`](#81-get-eventstypes) |
| `list_event_filings` | [`POST /events/filings`](#82-post-eventsfilings) |
| `get_event_filing` | [`POST /events/filing`](#83-post-eventsfiling) |
| `search_events` | [`POST /screener/events`](#611-post-screenerevents) |

Each linked section includes an **Example MCP tool call** with the exact tool name, arguments to pass, and the response shape to expect.

---

*AkkruData — API & MCP user guide.*
