Skip to content

Source attribution

Most financial data APIs are black boxes: a price is “the price” and you have no way to know where it came from. oneapi.finance does the opposite. Every row we serve carries a meta.source and a meta.fetched_at, and we treat that as a load-bearing part of the contract.

This page explains why source attribution exists, how to read it, and how to use it in production.

Why we do this

Three reasons.

  1. Reproducibility. If your screener flagged AAPL as a buy yesterday based on our pegRatio, you can reproduce that decision because the source of the underlying inputs is recorded. Black-box vendors cannot offer this.

  2. Defensive monitoring. When an upstream goes down or starts emitting garbage, the source field is your earliest warning. A spike in source = "stockanalysis" for a ticker that usually shows source = "yahoo_query1" tells you that yahoo failed and we fell back. You can alert on that.

  3. Honest disclosure. We scrape public sources. We do not pretend otherwise. By labeling every row, we make the trust model explicit instead of papering over it.

What meta.source looks like

Every quote, time-series response, statistics row, and FX rate carries:

"meta": {
"source": "yahoo_query1",
"fetched_at": "2026-05-04T20:14:32Z"
}

fetched_at is when our worker pulled the data from source. It is not necessarily when the data became live upstream — see delayed data for the full freshness story.

Source identifiers

The current set of source identifiers and what they cover:

meta.sourceUpstreamCovers
yahoo_query1query1.finance.yahoo.comQuotes, time-series, statistics, dividends/splits, profile. Primary for retail tickers.
yahoo_query2query2.finance.yahoo.comFailover for yahoo_query1. Same shape.
stockanalysisstockanalysis.comStatistics, fundamentals (TTM/quarterly), profile. Strong for non-US listings.
sec_edgardata.sec.govAuthoritative filings for US issuers. Income, balance, cash flow.
nbimnbim.noLimited use for sovereign-fund holdings (roadmap).
coingeckocoingecko.comCrypto only.
exchangerate_hostexchangerate.hostFX rates.
internal_cache(us)Served from our cache. The original source is stored alongside but not surfaced when we do this.

The list is stable but not closed: we add sources as we add coverage. Any addition is announced in the changelog.

Practical patterns

Alert when a primary source fails

If your application normally sees source = "yahoo_query1" for a ticker and you start seeing the fallback (stockanalysis), the upstream failed. Track the source distribution over time and alert on shifts.

from collections import Counter
def source_distribution(quotes):
return Counter(q["meta"]["source"] for q in quotes if q.get("meta"))
# Run nightly; alert if the distribution diverges from the previous week's.

Mark stale data in your UI

If you display a price to a user, surface the source and freshness:

function QuoteFreshness({ quote }) {
const fetchedAt = new Date(quote.meta.fetched_at);
const ageMin = Math.round((Date.now() - fetchedAt.getTime()) / 60_000);
return (
<span className="quote-meta">
{ageMin}m ago · {quote.meta.source}
</span>
);
}

Refuse to act on stale fundamentals

For monthly screening, you might require statistics fetched within the last 24 hours:

from datetime import datetime, timezone, timedelta
def is_fresh(stats, max_age=timedelta(hours=24)):
fetched = datetime.fromisoformat(stats["meta"]["fetched_at"].replace("Z", "+00:00"))
return datetime.now(timezone.utc) - fetched < max_age

Provenance at the field level

For statistics specifically, each field can come from a different source (for example, revenueTtm from sec_edgar and forwardPe from stockanalysis). The database stores this in a provenance JSONB column. The API does not yet expose it row-by-row. If you need field-level provenance, file a request — we will add it as a meta.provenance map without breaking the existing shape.

What’s next