# Thaler API > Financial data from SEC filings, linked to its sources. Read-only JSON over HTTPS. Free during the beta. Base URL: https://api.thaler.sh/v1. Send an API key on every request: `Authorization: Bearer thaler_…`. Keys are made at https://thaler.sh/developers/keys. ## Docs - [Overview](https://thaler.sh/developers): The base URL, a first request, and what the API covers. - [API keys](https://thaler.sh/developers/keys): Your keys and their use this month. - [Limits](https://thaler.sh/developers/limits): Requests a minute, a month and at a time, and the RateLimit headers. - [Errors](https://thaler.sh/developers/errors): Every error code and what to do about it. - [SDKs](https://thaler.sh/developers/sdks): Official clients for Python, TypeScript, Rust and Go. - [Sources and revisions](https://thaler.sh/developers/guides/sources): The filing and XBRL fact behind a value, and every value each filing reported. - [Point-in-time data](https://thaler.sh/developers/guides/as-of): Values as they were reported on a past date. - [Reference](https://thaler.sh/developers/reference): Every endpoint, its parameters and its response. - [Changelog](https://thaler.sh/developers/changelog): Changes to the API. - [OpenAPI](https://thaler.sh/developers/openapi.json): The API’s OpenAPI 3.1 document. - [llms.txt](https://thaler.sh/developers/llms.txt): These docs as plain text. - [Terms](https://thaler.sh/terms#the-api): What you may do with the API and its data. ## What the API covers - Companies: Search by name or ticker, and profiles. (/securities?query=, /securities/{ticker}/profile) - Metrics: Reported and calculated values, current or as of a past date. (/securities/{ticker}/metrics, /metrics) - Sources: The XBRL fact behind a value, and every value each filing reported. (/securities/{ticker}/metrics/{key}/lineage, /securities/{ticker}/metrics/{key}/revisions, /securities/{ticker}/raw-concepts) - Filings: By company or by day, and each filing’s documents. (/securities/{ticker}/filings, /filings/day, /filings/{accession}) - Insider trades: Open-market purchases and sales, Forms 4 and 5. (/insider-activity, /securities/{ticker}/insider-activity) - Holders: Form 13F positions, against the previous quarter. (/securities/{ticker}/holders, /holders, /holders/{cik}) - Segments: Revenue and operating income by segment, geography and product. (/securities/{ticker}/segments) - Prices: IEX’s last sale each trading day, closes of record, and market cap. (/securities/{ticker}/prices) - Screener: Every covered company, filtered and sorted by 55 figures. (/screen) - Release: The data release being served, and its checks. (/release) ## First request ```shell curl https://api.thaler.sh/v1/securities/AAPL/metrics \ --get -d period=annual -d keys=revenue -d limit=1 \ -H "Authorization: Bearer $THALER_API_KEY" ``` ## Limits The API is free during the beta. Each account has three limits, shared by all of its keys. | Limit | Value | Resets | | --- | --- | --- | | Requests a month | 20,000 | On the first of the month, 00:00 UTC | | Requests a minute | 60 | At the start of each minute (UTC) | | Requests at a time | 4 | When a request finishes | ## What counts A request counts when it gets an answer: 200s, 304s, and errors such as 400 and 404. These don’t count: - requests refused with 429 because a limit was reached; - requests that fail on Thaler’s side (500, 503, 504); - requests without a valid key (401), which don’t reach an account. ## The RateLimit headers Every response to a request with a valid key carries two headers from the IETF RateLimit fields draft. `RateLimit-Policy` gives the limits; `RateLimit` gives what remains of each and the seconds until it resets. ```http Response headers ratelimit-policy: "minute";q=60;w=60, "month";q=20000, "concurrent";q=4;qu="concurrent-requests" ratelimit: "minute";r=59;t=42, "month";r=19873;t=536400, "concurrent";r=3 ``` In `RateLimit-Policy`, `q` is the limit and `w` the window in seconds. In `RateLimit`, `r` is what remains and `t` the seconds until the count resets. ## When you reach a limit The request is refused with a 429 and isn’t counted. The body is a problem of the type the RateLimit draft defines for an exceeded quota, and `violated-policies` names the limits reached. `Retry-After` gives the seconds to wait. ```http A request over the minute’s limit HTTP/2 429 content-type: application/problem+json retry-after: 23 ratelimit: "minute";r=0;t=23, "month";r=19811;t=536400, "concurrent";r=4 { "code": "rate_limited", "detail": "This account has reached its limit of 60 requests this minute. Try again in 23 seconds.", "request_id": "req_9d0e44b1a6f35c2e8b17", "status": 429, "title": "Request cannot be satisfied as assigned quota has been exceeded", "type": "https://iana.org/assignments/http-problem-types#quota-exceeded", "violated-policies": ["minute"] } ``` For the month’s limit, `Retry-After` is the time until the first of next month. The minute’s 60 are counted per calendar minute, so a burst across the turn of a minute can make 120 in two seconds, but not more. ## Unchanged responses Every response carries an `ETag`. Send it back in `If-None-Match` and, if nothing has changed, the answer is a 304 with no body. A 304 counts as a request. Data changes when a new release is published, and filings, insider trades, holders, segments and prices also change between releases. ## Rows per request List endpoints return a page of rows, usually 25. Ask for more with `limit`, up to each endpoint’s maximum (500 on most), and page through longer lists with `offset` where an endpoint takes it. The [reference](/developers/reference) gives each endpoint’s parameters and limits. ## During the beta The limits may change during the beta. Changes are listed in the [changelog](/developers/changelog). ## Errors Errors are JSON problem details (RFC 9457) with the content type `application/problem+json`. `code` says what went wrong, `detail` says what to do, and `type` links to the code’s entry on this page. ```json A missing key { "code": "missing_key", "detail": "Send your API key in the Authorization header: Authorization: Bearer thaler_…. Create a key at https://thaler.sh/developers/keys", "request_id": "req_4242a7fab93842e5a45d", "status": 401, "title": "API key required", "type": "https://thaler.sh/developers/errors#missing_key" } ``` Every response has a request ID: in `request_id` here, in `meta.request_id` otherwise, and in the `X-Request-Id` header. An `X-Request-Id` you send (up to 64 letters, digits, `-`, `_` and `.`) is used instead. ## Codes ### bad_request {#bad_request} **400.** A parameter is missing, unknown or out of range, such as `limit=0` or a Screener column that doesn’t exist. `detail` names it. The request counts. ### missing_key {#missing_key} **401.** The request has no `Authorization: Bearer` header. Send your key as `Authorization: Bearer thaler_…`. ### invalid_key {#invalid_key} **401.** The key isn’t a Thaler API key. Keys are 45 characters: `thaler_` followed by 38 letters and digits, the last 6 of which are a checksum, so a key with a typo or a missing character is refused. ### revoked_key {#revoked_key} **401.** The key was revoked, by you or because it was found in public on GitHub. Create a new key at [API keys](/developers/keys). ### expired_key {#expired_key} **401.** The key has passed the expiry date set when it was made. Create a new key. ### not_found {#not_found} **404.** There is no such endpoint, company, filing, holder or day. On the routes under `/v1/securities/{ticker}`, a 404 means Thaler doesn’t cover a company with that ticker; search with `/v1/securities?query=`. The request counts. ### method_not_allowed {#method_not_allowed} **405.** The API only answers `GET`. ### rate_limited {#rate_limited} **429.** A limit was reached. The problem’s `type` is `https://iana.org/assignments/http-problem-types#quota-exceeded`, and `violated-policies` names the limits: `minute`, `month` or `concurrent`. Wait for `Retry-After`. The request doesn’t count. See [Limits](/developers/limits). ### internal_error {#internal_error} **500.** Thaler failed to answer. The request doesn’t count. If it happens again, write to support@thaler.sh with the request ID. ### unavailable {#unavailable} **503.** Thaler is busy or not ready. Try again shortly. The request doesn’t count. ### timeout {#timeout} **504.** The request took too long. Try again, or ask for fewer rows. The request doesn’t count. ## Retrying Retry a 429 after `Retry-After`, and a 500, 503 or 504 after a wait that grows with each attempt. A 400, 401 or 404 fails the same way again. ## SDKs Official clients for Python, TypeScript, Rust and Go. | Language | Package | Install | Requires | | --- | --- | --- | --- | | Python | `thaler` on [PyPI](https://pypi.org/project/thaler/) | `pip install thaler` | Python 3.10 or later | | TypeScript | `@thaler-sh/api` on [npm](https://www.npmjs.com/package/@thaler-sh/api) | `npm install @thaler-sh/api` | Node 20 or later, Bun, Deno, or an edge runtime | | Rust | `thaler` on [crates.io](https://crates.io/crates/thaler) | `cargo add thaler` | Rust 1.85 or later, with tokio | | Go | `github.com/thaler-sh/thaler-go` on [pkg.go.dev](https://pkg.go.dev/github.com/thaler-sh/thaler-go) | `go get github.com/thaler-sh/thaler-go` | Go 1.23 or later | A client reads its key from `THALER_API_KEY`, or takes one when it is made. Keys are secrets: use a client on a server, not in a browser or an app you ship. ## A first request ::: tabs ```python Python import thaler client = thaler.Thaler() # reads THALER_API_KEY rows = client.metrics("AAPL", period="annual", keys=["revenue"], limit=1) row = rows.data[0] print(row.value, row.read_from and row.read_from.accession_number) # 416161000000 0000320193-25-000079 ``` ```ts TypeScript import { Thaler } from "@thaler-sh/api" const client = new Thaler() // reads THALER_API_KEY const { data } = await client.metrics("AAPL", { period: "annual", keys: ["revenue"], limit: 1, }) console.log(data[0].value, data[0].read_from?.accession_number) // 416161000000 0000320193-25-000079 ``` ```rust Rust use thaler::{Client, MetricPeriod}; let client = Client::from_env()?; // reads THALER_API_KEY let rows = client .metrics("AAPL") .period(MetricPeriod::Annual) .keys(["revenue"]) .limit(1) .send() .await?; let row = &rows.data[0]; let source = row.read_from.as_ref().map_or("", |s| s.accession_number.as_str()); println!("{} {source}", row.value); // 416161000000 0000320193-25-000079 ``` ```go Go client, err := thaler.New() // reads THALER_API_KEY if err != nil { log.Fatal(err) } rows, err := client.Metrics(ctx, "AAPL", &thaler.MetricsParams{ Period: thaler.MetricPeriodAnnual, Keys: []string{"revenue"}, Limit: 1, }) if err != nil { log.Fatal(err) } row := rows.Data[0] source := "" if row.ReadFrom != nil { source = row.ReadFrom.AccessionNumber } fmt.Println(row.Value, source) // 416161000000 0000320193-25-000079 ``` ::: Every answer carries the request ID, the `ETag` and the account’s limits beside its data. Each package’s README lists every call, the Screener’s filters, point-in-time reads and its errors. ## Limits and retries A client keeps at most four requests in flight. When the minute’s sixty are spent, it waits for the reset before the next request. A 429 is retried after its `Retry-After`; one longer than a minute, the month’s limit, is returned as an error. A 500, 502, 503 or 504, or a connection that failed, is retried twice after a growing pause. A 400, 401 or 404 is returned as it stands. ## Types and errors Figures are decimals with every digit as filed, days are dates, and each model has the field names the API uses. An answer that is not as documented is an error that names the field and the request ID. An error the API answers with carries its `code`, `detail` and `request_id`, and its class says whether the request was refused, unauthenticated, not found, rate limited, or failed on Thaler’s side. Every request carries a `User-Agent` that names the client and its version. Your [keys page](/developers/keys) shows usage by client. ## Versions The clients are `0.x` while the API is in beta. A change to the API that adds a field or an endpoint is a minor version of each client. The [changelog](/developers/changelog) announces anything that would break existing use at least 30 days ahead. Any HTTP client works without an SDK: the [overview](/developers) shows a first request with curl, and the [reference](/developers/reference) every endpoint. ## Sources and revisions A metric value names the filing it came from. You can follow it to the XBRL fact behind it, and see every value each filing reported for it. ## The filing behind a value Each row from `/v1/securities/{ticker}/metrics` has `read_from`: the accession number, form and filing date of the filing the value is read from, and its place in the value’s revisions. A calculated value names the most recent of the filings its inputs come from. ```json read_from on Apple’s fiscal 2025 revenue { "accession_number": "0000320193-25-000079", "filed_at": "2025-10-31", "form": "10-K", "kind": "first", "revision_rank": 1 } ``` The accession number identifies the filing on EDGAR, and `/v1/filings/{accession}` returns the filing’s details and the list of its documents. ## The fact behind a value `/v1/securities/{ticker}/metrics/{metric_key}/lineage` returns the XBRL fact a value was read from: its taxonomy and concept (for example `us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax`), the value and unit as filed, the period, and the filing. Pass `metric_value_id` from a metrics row to get the lineage of that value alone. ```shell Request curl "https://api.thaler.sh/v1/securities/AAPL/metrics/revenue/lineage?limit=1" \ -H "Authorization: Bearer $THALER_API_KEY" ``` ## Every value each filing reported Companies report most values more than once: a quarter’s revenue appears in its 10-Q and again, as a comparison, in later filings. `/v1/securities/{ticker}/metrics/{metric_key}/revisions` returns one row per value and filing, in filing order, with what each filing reported. | kind | Meaning | | --- | --- | | first | The earliest filing to report the value | | same | The filing reported the previous value again | | revised | The filing reported a different value; `delta` is the change | | re_expressed | A per-share or share-count value adjusted for stock splits between the two filings; `split_ratio` gives new shares per old | `standing` marks the filing the current value is read from. For Kraft Heinz’s fiscal 2017 net income, `?fiscal_year=2017&fiscal_period=FY` returns three rows: 10,999,000,000 first, then 10,941,000,000 revised with a `delta` of −58,000,000, then the same again in the 10-K filed in February 2020, which is standing. Next: [Point-in-time data](/developers/guides/as-of). ## Point-in-time data Request values as they were reported on a past date. Each value then comes from filings made on or before that date, so a backtest only uses data that was available at the time. Companies revise reported values: a 10-K filed in June can change a value reported in February’s 10-K. Thaler keeps the value from every filing, so it can answer as of any date. Add `as_of=YYYY-MM-DD` to a metrics or lineage request. Each value comes from the latest filing on or before that date. Values first reported after the date are left out. Calculated values, such as margins, are recalculated from the inputs reported by that date, or left out. ## Example: one value on three dates Kraft Heinz’s net income for fiscal 2017. On March 1, 2019, the company had not yet filed its 2018 annual report. That report was filed on June 7, 2019, and restated 2017 net income lower. | As of | Net income, FY 2017 | Source filing | Kind | | --- | --- | --- | --- | | March 1, 2019 | 10,999,000,000 | 10-K filed February 16, 2018 (0001637459-18-000015) | first | | July 1, 2019 | 10,941,000,000 | 10-K filed June 7, 2019 (0001637459-19-000049) | revised | | Today | 10,941,000,000 | 10-K filed February 14, 2020 (0001637459-20-000027) | same | ```shell Request curl https://api.thaler.sh/v1/securities/KHC/metrics \ --get -d period=annual -d keys=net_income -d limit=1 \ -d as_of=2019-03-01 \ -H "Authorization: Bearer $THALER_API_KEY" ``` ```json Response, abridged { "data": [ { "end_date": "2017-12-30", "fiscal_period": "FY", "fiscal_year": 2017, "metric_key": "net_income", "read_from": { "accession_number": "0001637459-18-000015", "filed_at": "2018-02-16", "form": "10-K", "kind": "first", "revision_rank": 1 }, "revised": false, "revision_count": 0, "ticker": "KHC", "unit": "USD", "value": "10999000000" } ], "meta": { "as_of": "2019-03-01", "schema": "thaler.data.v1" } } ``` As of March 1, 2019, the value had not been revised: `revision_count` is 0, and `read_from` is the first 10-K to report it. The same request with `as_of=2019-07-01` returns 10,941,000,000 from the June 2019 10-K, with `kind` `revised`. ## Notes - Dates are EDGAR filing dates, not the dates Thaler processed the filings. - Results use Thaler’s current processing of past filings. If a metric’s definition changes, results for past dates can change too. `meta.release` records which release you read. - Values without a revision history yet are left out of as-of requests. - `as_of` works on metrics and lineage. Filings, insider trades, holders, segments and prices always return current data. Next: [Sources and revisions](/developers/guides/sources). ## Changelog Changes to the API. During the beta, changes that break existing use are announced here at least 30 days ahead, except when a change is needed for security or legal reasons. ## 1.0.0-beta.2 The OpenAPI document now matches what the API sends. No request changed, and no documented field of a response. - Every figure is a `Decimal` or `NullableDecimal`: a string of digits with a sign and a fraction when there is one, never a number, so a client can parse it without losing digits. Prices, holders, segments and the filings of a day now say so as the metrics did. - A value that can be null says so the OpenAPI 3.1 way, with `null` in its type. The 3.0 keyword the document used before is ignored by tools that read 3.1. - `keys`, `forms`, `items` and `tickers` are arrays sent comma-separated (`style: form, explode: false`), as they always were on the wire. - `value_kind` on a metric value is `reported` or `derived`, as it is in the catalog. - A filing’s details and documents, a day’s counts by form and by day, and IEX’s attribution have named schemas. - Endpoints are tagged by resource: securities, metrics, filings, insiders, holders, segments, prices, screen, releases. - Filings no longer include a `reading` object, and lineage rows no longer include two review fields. Neither was in the document, so a client generated from it never read them. A client generated from the previous document keeps working; generate it again to pick up the types. ## 1.0.0-beta.1 The first beta. - Nineteen read-only endpoints: companies, metrics with point-in-time reads, lineage, revisions, raw concepts, filings, insider trades, institutional holders, segments, prices, the Screener and the current release. - API keys, up to ten per account, with optional expiry. - Limits of 60 requests a minute, 20,000 a month and 4 at a time per account, reported in the IETF RateLimit headers. - Errors as RFC 9457 problem details. ## Endpoints ### Companies - GET /securities: Search active securities by ticker prefix or entity name. Parameters: query (required), limit - GET /securities/{ticker}/profile: Security profile. Parameters: ticker (required) ### Metrics - GET /securities/{ticker}/metrics: Canonical metric values for a security. Parameters: ticker (required), period, keys, collapse, limit, as_of - GET /metrics: Metric catalog: canonical figures plus screen-surface ratios with coverage counts - GET /securities/{ticker}/metrics/{metric_key}/lineage: Metric lineage back to source facts. Parameters: ticker (required), metric_key (required), metric_value_id, limit, as_of - GET /securities/{ticker}/metrics/{metric_key}/revisions: Every filing's statement of a metric's figures: what each read when first reported and every time since. Parameters: ticker (required), metric_key (required), metric_value_id, fiscal_year, fiscal_period, limit - GET /securities/{ticker}/raw-concepts: Raw XBRL concepts observed for a security. Parameters: ticker (required), limit ### Filings - GET /securities/{ticker}/filings: Filing timeline for a security. Parameters: ticker (required), forms, items, limit - GET /filings/day: One day of the covered universe's filings. Parameters: date - GET /filings/{accession}: A filing by accession. Parameters: accession (required) ### Insider trades - GET /insider-activity: Reported insider purchases and sales across covered companies. Parameters: tickers, since, kind, limit, offset - GET /securities/{ticker}/insider-activity: Reported insider purchases and sales for a security. Parameters: ticker (required), since, kind, limit, offset ### Institutional holders - GET /securities/{ticker}/holders: Who holds a company, from Form 13F. Parameters: ticker (required), limit, offset - GET /holders: Managers by name, or the largest. Parameters: query, limit - GET /holders/{cik}: What a manager holds, from its latest Form 13F. Parameters: cik (required), limit, offset ### Segments - GET /securities/{ticker}/segments: Figures by segment, geography and product. Parameters: ticker (required), period ### Prices - GET /securities/{ticker}/prices: Prices, from IEX and the record. Parameters: ticker (required), range, from, to ### Screener - GET /screen: Screen the company universe by reported figures. Parameters: where, sort, dir, limit, offset, columns ### Release - GET /release: The current data release