On this page

API Documentation

Everything you need to integrate Synapse Discovery financial data into your applications.

Base URL

https://synapsediscovery.io/api/v1

58,000+ listings with prices

Stocks, ETFs, mutual funds, ADRs — delisted securities included

60+ Years History

Daily equity prices back to 1962

AI-Native

Built for agents and LLMs

Authentication

REST API requests require an API key (the MCP server also accepts a keyless anonymous trial). Include your key in the X-API-Key header.

Request Header
X-API-Key: sd_live_your_api_key_here
cURL Example
curl -H "X-API-Key: sd_live_xxx" \
  https://synapsediscovery.io/api/v1/entity/AAPL

Keep your API key secure

Never expose your API key in client-side code or public repositories.

Quick Start

Python
import requests

# Set your API key
API_KEY = "sd_live_your_key"
BASE_URL = "https://synapsediscovery.io/api/v1"

headers = {"X-API-Key": API_KEY}

# Get Apple stock info
response = requests.get(f"{BASE_URL}/entity/AAPL", headers=headers)
data = response.json()
print(f"Company: {data['data']['name']}")

# Get historical prices
prices = requests.get(
    f"{BASE_URL}/prices/AAPL",
    headers=headers,
    params={"start": "2024-01-01", "limit": 30}
).json()

for price in prices['data'][:5]:
    print(f"{price['date']}: ${price['close']}")
JavaScript / Node.js
const API_KEY = 'sd_live_your_key';
const BASE_URL = 'https://synapsediscovery.io/api/v1';

async function getStockData(ticker) {
  const response = await fetch(`${BASE_URL}/entity/${ticker}`, {
    headers: { 'X-API-Key': API_KEY }
  });
  return response.json();
}

// Get Apple data
const apple = await getStockData('AAPL');
console.log(apple.data.name); // "Apple Inc."

Entities

Entities represent securities in our universe. Each entity has a unique synapse_id that remains constant even when tickers change.

GET /entity/{identifier}

Get entity by ticker symbol, CIK, or synapse_id.

Parameters

Name Type Description
identifier path Ticker (AAPL), CIK (0000320193), or synapse_id (UUID)
Response
{
  "data": {
    "synapse_id": "65b316ed-2573-440f-a1ea-5e1ec5252c7b",
    "name": "Apple Inc.",
    "current_ticker": "AAPL",
    "cik": "0000320193",
    "security_type": "common_stock",
    "status": "active",
    "sector": "Information Technology",
    "industry": "Industrial Machinery & Equipment",
    "exchange": "NASDAQ"
  },
  "provenance": {...},
  "quality": {...},
  "cost": {...}
}
GET /entities

List entities with optional filtering.

Query Parameters

Name Type Description
security_type string common_stock, etf, mutual_fund, adr, reit, spac
status string active, delisted
sector string Technology, Healthcare, etc.
search string Search by name or ticker
page integer Page number (default: 1)
limit integer Results per page (max: 1000)

Prices

Historical OHLCV price data with multiple adjustment types.

GET /prices/{identifier}

Get historical prices for an entity.

Query Parameters

Name Type Description
start date Start date (YYYY-MM-DD)
end date End date (YYYY-MM-DD)
adjustment string raw, split_adjusted (default), total_return
limit integer Max results (default: 10000, max: 100000; 0 = full history). With start, bounds the EARLIEST N in-range rows; without it, the most-recent N. Truncated responses carry meta.next_start/next_end cursors.
Response · GET /prices/LEH?start=2008-09-12&end=2008-09-15&adjustment=raw · abridged (… marks omitted fields)
{
  "data": [
    { "date": "2008-09-12", …, "close": 3.65, …, "adjustment": "raw" },
    { "date": "2008-09-15", …, "close": 0.21, …, "adjustment": "raw" }
  ],
  "meta": { "ticker": "LEH", "count": 2, … }
}

Adjustment Types:
• raw - Unadjusted prices
• split_adjusted - Adjusted for stock splits only
• total_return - Adjusted for splits and reinvested dividends

Fundamentals

Standardized financial statements parsed from SEC filings (10-K, 10-Q).

GET /fundamentals/{identifier}

Query Parameters

Name Type Description
periods integer Number of periods (default: 8)
period_type string Q (quarterly) or FY (annual)
as_of date Point-in-time query date

Available Fields

revenue
net_income
gross_profit
operating_income
ebitda
eps_basic
eps_diluted
total_assets
total_liabilities
total_equity
free_cash_flow
operating_cash_flow

Earnings & Estimates

Two estimate series with two different floors. Read this section once so you never mistake one for the other.

GET /entity/{identifier}/earnings-history QUANT

One row per reported fiscal quarter: eps_actual, eps_estimate (the consensus as it stood at the report), eps_difference, surprise_percent, report_date, timing. Quarterly and deep — to roughly 2004 for the deep names.

Share basis. Per-share levels are served as reported. The upstream feed restates history onto today's share basis (dividing past quarters by future splits — a look-ahead), so each row is decided against the as-filed SEC eps_diluted; eps_restatement says how (none_needed · verified · verified_as_reported · unverified · conflict), and a conflict row serves null levels rather than an undecidable basis. surprise_percent is a ratio and is never adjusted. row_source says where a row comes from: earnings_feed, or sec_filing — a delisted line's own first-filed SEC diluted EPS for a quarter the feed holds none for (eps_restatement sec_as_filed, report_date = the filing date, no consensus: eps_estimate is null with eps_estimate_unavailable_reason).

Point-in-time. as_of keeps only quarters whose report_date is on or before that date — the actual/estimate pair is knowable at the report. start/end bound period_end. Oldest-first, with the standard window cursors.

GET /entity/{identifier}/estimates QUANT

kind=snapshot (default) — the daily-captured consensus: EPS and revenue avg/low/high, analyst counts, growth, the EPS trend 7/30/60/90 days ago and revision counts. One row per target fiscal period (estimate_date) per capture day (captured_at, the honest as-of). Snapshots exist from 2026-02-19 only. Daily capture began then, and there is no earlier history to backfill — that floor is real, not a backfill gap.

kind=pre_report — the consensus at each report, as an as-of-dated observation series: as_of = report date, target_period = the reported quarter, eps_estimate beside eps_actual. Quarterly, one observation per report, history to roughly 2004. It is not a forward-year snapshot and not a daily series.

as_of, start and end bound the observation date (captured_at for snapshots, the report date for pre-report); target_period keeps one target fiscal period. Every response's meta states both floors and which series you are holding. Pages never split an observation date, so the date cursors are safe to walk.

Corporate Actions

GET /corporate-actions/{identifier}

Stock splits, dividends, and other corporate actions.

Query Parameters

action_type split (every split), reverse_split (served is_reverse true only), forward_split (served is_reverse false only), dividend
start Start date filter
end End date filter

Split factor convention

factor is NEW shares per OLD share for that one event: 2.0 is a 2-for-1 forward split, 0.2 is a 1-for-5 reverse split. ratio is the same event written new:old (1:5). To put a price from BEFORE the event on the post-event basis, multiply it by 1/factor — SFY on 2024-10-02: factor 0.2, pre-event close 20.515 × 5 = 102.575 against the 102.84 print. This per-event factor is not the cumulative split_factor column on /prices, which already multiplies every later event together.

is_reverse is served from the factor and the raw price tape, never from a stored column: true exactly when factor < 1 and the raw price tape does not contradict it. direction_witness says what the raw price did across the date (tape_agrees, no_tape, below_witness_threshold, tape_contradicts, tape_silent); on the last two is_reverse is null with is_reverse_unavailable_reason, and likewise (factor_not_a_share_ratio) for a factor below 1 that is not a share ratio (a spin-off price adjustment) unless the tape itself agrees. ratio is null with ratio_unavailable_reason when the factor is not a simple share ratio.

Fund Holdings

GET /holdings/{identifier}

Get ETF or mutual fund holdings.

Example: GET /holdings/ARKK?as_of=2021-02-12 · abridged (… marks omitted fields)
{
  "data": [
    {
      "name": "Tesla Inc", …,
      "shares": 2521820.0,
      "market_value": 978567032.8,
      "percentage": 10.1478
    }, …
  ],
  "meta": {
    "fund_ticker": "ARKK",
    "report_date": "2020-10-30",
    "total_holdings": 49,
    "source": "sec_nport",
    "point_in_time": true, …
  }
}

On this endpoint percentage is a percent (10.1478 = 10.1478% of NAV). The point-in-time endpoint /funds/{identifier}/holdings returns the same weight as pct_of_nav, a fraction (0.101478) — matching expense ratios and the bulk exports.

Compare Entities

GET /compare?tickers=AAPL,MSFT,GOOGL

Compare multiple entities on key metrics.

Parameters

tickers Comma-separated list (max 20)
metrics Comma-separated metrics (default: revenue,net_income,eps_diluted). pe_ratio — the as-traded close over trailing-twelve-month diluted EPS from filings public on that date, never negative, a null always with pe_ratio_unavailable_reason — is served only when named here.

Fund Universe

Search and screen the full US ETF and mutual-fund universe, including dead funds. Start here, then use the returned ticker or synapse_id with the per-fund endpoints below.

GET /funds

Filters (all optional): fund_type (etf | mutual_fund), category, fund_family, asset_class, max_expense_ratio, q (name or ticker), status (active | dead | all — default active; pass dead or all to include closed and delisted funds), limit (default 50, max 200), offset.

Expense ratios are fractions — max_expense_ratio=0.001 means 0.10%, and every response echoes meta.expense_ratio_units. The filter compares exactly the expense_ratio each row shows — the gross (total annual operating) expense ratio, or the net where the filing carried no gross — for ETFs and mutual funds alike, so no returned row is above your cap; net_expense_ratio and net_expense_ratio_basis ride beside it, and meta.expense_ratio_basis names the rule. Unknown parameters are never silently ignored: they come back in meta.ignored_params.

For a full walk of the universe pass order=id and follow meta.next_cursor — stable keyset paging that will not skip or repeat rows.

A fund we do not hold says why. Every week we compare the SEC's list of registered fund share classes with the funds our price source lists. A fund symbol that does not resolve on /funds/<ticker> (and on the MCP fund tools, /funds/nav/batch and /resolve) carries universe_exclusion_reason and a plain sentence where that comparison has one — for example a share class the SEC lists under a new symbol, with held_as naming the symbol we hold it under. The bulk files' population rule (no minimum history) is stated in /export/schema.

Example: GET /funds?fund_type=etf&max_expense_ratio=0.001&limit=2
{
  "data": {"funds": [
    {"ticker": "SPY", "name": "SPDR S&P 500 ETF Trust", "type": "etf",
     "category": "Large Blend", "expense_ratio": 0.000945,
     "total_assets": 624318000000.0}
  ]},
  "meta": {"count": 1, "has_more": true,
           "expense_ratio_units": "fraction (0.001 = 0.1%)"}
}

Fund Profile

Identity, fees, and our computed classifications for one fund. meta.coverage flags which fields are populated, so a client can render “—” honestly instead of guessing.

GET /funds/{identifier}

Returns family, category, expense ratios (gross, net, and net-of-acquired-fund-fees), yield, inception, AUM from SEC filings (where total_assets_scope is present it is the fund SERIES' net assets, shown on every share class of the series — never sum it across classes), plus style_box, synapse_category and N-CEN structural_flags (money-market, target-date, leveraged/inverse, fund-of-funds).

These classifications are our own. synapse_category, style_box and synapse_star_rating are computed in-house from SEC filings and our own prices. They are first-party — ours, computed from our own inputs, and not interchangeable with any industry-standard classification.

Related
GET /funds/{identifier}/classification   portfolio weight breakdowns
GET /funds/{identifier}/distributions   dividends + capital gains
GET /funds/categories                   the category vocabulary + counts

NAV History

Daily or month-end NAV: raw, our own adjusted total-return series (the default), or a self-computed point-in-time total-return index.

GET /funds/{identifier}/nav

Parameters

adjustment: raw (NAV only) · adjusted (default: our total-return series, the raw NAV times our own distribution and split factors; meta.nav_basis names the basis) · total_return / pit (a self-computed point-in-time total-return index) · total_return_reinvest (the reinvest-at-ex-NAV index a fund sponsor publishes, built from our adjusted chain's own distributions; meta.distribution_adjustment names the convention of every adjusted response). frequency=monthly resamples to month-end server-side. start, end, limit (default 10000, max 100000; 0 = all rows within your tier's historical window — Free 1 year, Developer 5 years, Quant full history), offset, order. meta.tier_clipped and meta.tier_floor_date report when that window truncated your result.

Windowing

The window anchors to the end you pinned. With start, limit bounds the earliest N rows in range and you page forward with meta.next_start. Without start, you get the most-recent N and page backward with meta.next_end. order only re-arranges the page — it never changes which rows are selected.

Exception: adjustment=total_return|pit|total_return_reinvest withholds both cursors. The PIT index re-bases at the filtered start, so following a cursor would splice two different bases; page that mode with offset instead.

Response
{
  "data": [{"date": "2024-01-02", "nav": 100.0, "nav_raw": 100.0}],
  "meta": {"ordering": "date_asc", "window_anchor": "start",
           "next_start": "2024-02-15", "next_end": null,
           "has_more": true, "frequency": "daily"}
}

Holdings — point in time

The full position list as filed on SEC N-PORT, not a top-ten snapshot. Coverage begins 2019-Q4, the first quarter of the SEC's public N-PORT structured data. Earlier holdings were disclosed on Forms N-Q and N-CSR, which we do not currently ingest.

GET /funds/{identifier}/holdings

as_of=YYYY-MM-DD returns the newest report we hold that was public on or before that date — cut on filing date, not report date, so a backtest is not handed a portfolio before it was filed. limit default 500 (max 5000), offset.

Each position carries the issuer ticker, country, shares/par, market value, pct_of_nav, and sector — the issuer's synapse_sector (first-party, derived from the issuer's SEC SIC code) for equities, a deterministic bond segment for fixed income and cash, and null where we cannot classify the position honestly. Each position also carries issuer_synapse_id — our own id for the issuer, decided once per security and report date against the filer's own implied price — or null with issuer_unresolved_reason when we refuse to guess, and the ticker valid on the report date with its ticker_basis: journal (dated evidence), filer_reported (the fund's own filing), or current_carried_back (today's symbol carried back: nothing we hold dates it, and nothing shows it was assigned later). Where our journal or the issuer's own price tape shows today's symbol was assigned after the report date, ticker is null with ticker_unavailable_reason — never today's symbol. The first page also carries meta.sector_allocation, whole-portfolio weights, and meta.issuer_resolution (unresolved equity rows, their share of NAV and the reasons). Fixed income and cash resolve to their own segments (Treasury, Agency, Corporate Bond, Municipal, Sovereign, Bank Loan, Cash) from the position's own N-PORT fields, so a bond fund gets a meaningful breakdown rather than one blob; only positions we genuinely cannot classify roll into Unclassified, keeping the weights a true fraction of NAV.

Units differ between the two holdings endpoints — do not copy values between them. Here pct_of_nav is a fraction (0.101478 = 10.1478%). The legacy /holdings/{identifier} returns the same quantity as percentage, multiplied by 100 (10.1478).

Response · GET /funds/ARKK/holdings?as_of=2021-02-12 · abridged (… marks omitted fields)
{
  "data": [{"name": "Tesla Inc", …, "issuer_unresolved_reason": null,
            "shares_or_par": 2521820.0, "market_value": 978567032.8,
            "pct_of_nav": 0.101478, …},
           {"name": "Invitae Corp", "issuer_synapse_id": null,
            "issuer_unresolved_reason": "no_ingest_link", "pct_of_nav": 0.071785, …}, …],
  "meta": {"report_date": "2020-10-30", "filing_date": "2020-12-28",
           "point_in_time": true, "total_holdings": 49,
           "sector_allocation": […], "issuer_resolution": {…}, …}
}

Ratings

Trailing returns, risk statistics and our 1–5 star rating, computed from our own NAV history; the expense percentile from SEC-filed expense ratios within category.

GET /funds/{identifier}/ratings

Returns, volatility and max drawdown are decimals (0.12 = 12%; max_drawdown is negative, so -0.34 is a 34% drawdown). Sharpe is a plain dimensionless ratio, not a percentage. expense_percentile is a fraction 0–1 within category, where lower is cheaper.

synapse_star_rating is our own: the within-category percentile of the Sharpe ratio from our NAV history. It is a first-party rating — ours, computed from our own data, and not interchangeable with any industry-standard score. It is honestly null for young funds and thin categories — meta.coverage tells you which, so render “—” rather than zero. A fund with no ratings row at all returns data: null and meta.available: false (with no coverage block) — check that first.

Use POST /funds/ratings/batch to fetch many funds in one call.

Fund Corporate Actions

Fund reverse and forward splits captured directly from the SEC prospectus filing family (497, 485, 424B, 486, N-14, N-8F), each one checked against the fund's own NAV series before it is served.

GET /funds/{identifier}/corporate-actions QUANT

Parameters: action_type, limit (default 100, max 500), offset.

Splits are served only when arithmetically verified against the raw price series — a real 1-for-N reverse makes the per-share value jump about N×. Regex or model confidence alone is never enough. That is a deliberate precision floor: recall is bounded by that series' coverage, so a real split we cannot verify is withheld rather than served wrong.

Ratios are (ratio_num, ratio_den) = (new, old) shares; a reverse split is ratio_num < ratio_den. Today only splits are extracted as structured actions. Liquidations, mergers and name/ticker/fee changes exist so far as captured raw filings and are not yet in this feed.

Insider Transactions

Form 4 and 5 transactions from 2021 onward, resolved person-centrically: one insider CIK followed across every issuer they file against, not just one company's filings.

GET /insiders/transactions QUANT

Filters

Required — at least one of ticker (issuer) or insider (CIK, or a name). Optional: code · is_10b5_1 · start / end · limit (default 50, max 200) · offset.

Transaction codes

The common ones: S open-market sale · P open-market purchase · A grant or award · M option exercise · F shares withheld for tax · G gift · C conversion. Other SEC codes appear in the data and are returned unfiltered; the parameter accepts any single letter and is not validated against a whitelist.

Two things that will bite you

start/end and the ordering apply to transaction_date — the trade date, not filing_date, the date the market learned of it. For a point-in-time study, filter on the filing_date field in the returned rows.

Rows are newest-first and there is no order parameter — the opposite of /prices and /funds/{id}/nav, which are oldest-first. Sort before computing anything sequential.

is_10b5_1 is tri-state: true, false, or null. Null is honest and means the SEC checkbox did not exist for that filing — it is never silently reported as false. The flag was introduced in 2023, so it is null for essentially everything before then. It is declared at the filing level and denormalized onto every row of that form — it tells you the filing asserted Rule 10b5-1(c) reliance, not that this specific line did.

Ownership & Roster

Who the insiders are at an issuer, and what they currently hold.

GET /entity/{identifier}/insiders QUANT

The roster: every person or entity that has filed a Form 3/4/5 against this issuer, with their role (officer, director, 10% owner) and title from their latest non-superseded filing.

GET /entity/{identifier}/insider-ownership QUANT

Current beneficial ownership of non-derivative (Table I) positions only — options and RSUs are not included. One row per (insider, security class, direct/indirect, nature of ownership): someone holding common stock directly, through a trust and through an LLC appears as three rows, and every row carries that insider's total for the issuer in insider_total_shares_owned (the sum of their rows on today's share basis). A row the rule supersedes — the same holding re-titled or re-described in a later filing — is not summed and is listed only with include_superseded=true. Ordered by shares_owned descending, limit default 100 (max 500).

shares_owned is a raw share count, not a percentage of the company. as_of is per-row staleness — the transaction date of the most recent non-superseded filing for that position, falling back to period-of-report then filing date. It is a trade date, not a query date. This endpoint is current-state only; it has no as_of parameter.

Form 144 & Lock-ups

The two insider surfaces that point forward rather than backward.

GET /insiders/form144 QUANT

Notices of proposed sale — an insider declaring an intent to sell restricted stock, with the broker of record and the relationship to the issuer. Electronic coverage is near-complete from April 2023 onward; 2022 is partial.

GET /insiders/lockups QUANT

IPO lock-up expiry calendar extracted from SEC 424B4 final prospectuses — the dates on which insider shares become sellable. The lock-up period is model-extracted from the prospectus body and every row carries extraction_confidence; expiry = prospectus date + lock-up days. Coverage is recent IPOs with live or upcoming lock-ups.

Index Constituents

Point-in-time index membership — who was in the index on a given date. SD2000 carries real reconstitution-derived add and remove history, so a past date returns the members it had then, including names since removed. SPX, NDX and DJI membership is reconciled from public lists, and many of those rows carry a placeholder added_date flagged by is_synthetic_added_date — check that field before treating their history as point-in-time. Every row also says what its dates rest on (added_date_basis, removed_date_basis: public record, a vendor snapshot, a corporate event = the security's last trading session, or our weekly reconciliation's observation) and serves the index event dates we stand behind as entry_date / exit_date (null with a reason otherwise). Once our S&P 500 date reconciliation is applied, membership from 2019-09-30 on is checked against the SEC N-PORT holdings of its three largest tracking funds; the response's membership_dating_note says whether it is applied yet.

GET /index/{symbol}/constituents

as_of=YYYY-MM-DD (default today), limit (default 0 = the full book, max 5000), offset.

This route rejects unknown parameters with a 400 rather than ignoring them, which is stricter than the rest of the API. Two consequences: authenticate with the X-API-Key header (a ?api_key= query parameter is a 400 here), and ?envelope=false is rejected as unrecognized — send Accept: application/json+raw instead if you want an unenveloped response here.

weight is a fraction (0.002914 = 0.2914%) and is the weight at addition, not a live weight. It is populated for SD2000 only — SPX, NDX and DJI return weight: null and, despite meta.ordering = weight_desc, fall through to alphabetical ticker order.

GET /indexes lists every index price series we hold — US headline benchmarks and their total-return twins (for example SP500TR, the S&P 500 Total Return), equal-weight, S&P 500 and Composite 1500 sectors, strategy, volatility, commodity, bond, rates, currency and non-US equity indices — each with its family, the return basis we serve (index_kind and how it was decided), its total-return counterpart and depth, first date and rows. Filter with family, index_kind, q; fetch a series from the prices endpoint under its caret symbol (^SP500TR: the caret names the index); MCP agents call list_indexes. Its meta.membership (or ?view=membership) lists which symbols actually carry membership data, with each index's provenance. SD2000 is our own rules-based US small-cap index, reconstituted annually with real add and remove history; its membership is computed by our published rules from our own data, not taken from any third-party index.

Example: GET /index/SD2000/constituents?as_of=2022-06-30
{
  "data": [{"ticker": "ACLS", "synapse_id": "…", "weight": 0.002914,
            "added_date": "2021-06-25", "is_synthetic_added_date": false}],
  "meta": {"index_symbol": "SD2000", "asof": "2022-06-30",
           "ordering": "weight_desc"}
}

Economic Series (primarily US Treasury / Fed / BLS / BEA / EIA)

Access economic data sourced primarily from the public publishers — US Treasury (yield curves), Federal Reserve releases (rates), BLS (inflation, employment), BEA (GDP) and EIA (energy). GET /macro/series lists every series we serve.

GET /macro/series/{series_id}

Get data for an economic series.

Popular Series

fed_funds - Fed Funds Rate
treasury_10y - 10Y Treasury
cpi - Consumer Price Index
unemployment - Unemployment Rate
gdp - GDP
sp500 - S&P 500
GET /macro/series

List all available economic series with optional category filter.

Yield Curve

GET /macro/yield-curve

Treasury yield curve with the 2-year/10-year spread and an inversion flag. Yields and spread_2_10 are in percentage points.

Response · GET /macro/yield-curve?date=2020-03-16
{
  "data": {
    "date": "2020-03-16",
    "yields": {
      "3M": 0.24, "6M": 0.29, "1Y": 0.29,
      "2Y": 0.36, "5Y": 0.49,
      "10Y": 0.73, "30Y": 1.34
    },
    "spread_2_10": 0.37,
    "inverted": false,
    "pit_basis": "market_observed",
    "pit_bases": ["market_observed"],
    "pit_basis_note": "Treasury constant-maturity yields are market-observed and published the same day; they carry no revision vintages."
  },
  "meta": { "source": "economic_observations", "cached": false }
}

A date inside our stored daily curves (recent dates, or no date) is answered from the stored row instead: data is a one-element list, the flag is is_inverted, the 3-month/10-year spread rides beside it as spread_3m_10y, and pit_basis is captured.

Market Context

GET /macro/market-context?date=2026-01-24

Aggregated market indicators for a specific date. Perfect for backtesting context.

Flexible Query (POST /query)

The "thick" endpoint for querying multiple entities with mixed data types in a single request.

POST /query
Request Body
{
  "universe": {
    "tickers": ["AAPL", "MSFT", "GOOGL"]
  },
  "fields": [
    "close_split_adj",
    "volume",
    "revenue",
    "eps_diluted"
  ],
  "range": {
    "start": "2024-01-01",
    "end": "2025-12-31"
  },
  "point_in_time": true,
  "asof": "2026-01-01T00:00:00Z"
}

Universe Options

  • • tickers - List of ticker symbols
  • • synapse_ids - List of synapse IDs
  • • exchange - Filter by exchange (NASDAQ, NYSE)
  • • security_type - Filter by type
  • • sector - Filter by sector

Pro Tip: Use /query/fields (no auth required) to see all available fields and example queries.

Point-in-Time Queries

Ask for data as it had been published by a historical date: statements and fund holdings are cut at their filing date, macro vintages at the day each became public, built to keep look-ahead out of a backtest.

Point-in-Time Example
# What fundamentals were available on Jan 1, 2020?
GET /fundamentals/AAPL?as_of=2020-01-01&periods=4

# Returns only statements filed on or before 2020-01-01:
# no quarter ended 2019-12-28 (its 10-Q was filed 2020-01-29)

Look-ahead bias: Without point-in-time queries, your backtest might use data that wasn't actually available at the time, leading to unrealistic results.

Macro vintages: the grain, stated

GET /macro/series/<code>/point-in-time?as_of= filters on the date each snapshot became public, never on the publisher's period label for it. Vintages come from the Philadelphia Fed Real-Time Data Set: most series are quarterly snapshots, taken on the 15th of the middle month of each quarter, and the monthly-vintage series (payrolls, industrial production) are bounded at their month end because the publisher does not fix a day. So on a given as_of the newest knowable observation can legitimately be up to a quarter old — the response states the grain, that snapshot's vintage_available_from and the vintage_next_available_from when the following snapshot landed. A vintage whose availability we cannot justify is withheld, never dated by guess.

Data Lake Sync

Download complete datasets in Parquet format for local querying. Requires Quant tier or higher.

GET /sync/status

Check current snapshot and available data.

GET /sync/changes

Get incremental changes since last sync.

GET /export/manifest

Complete file listing with checksums.

GET /export/signed-url

Get signed URL for file download.

Bulk download in Python (requests + duckdb)
import duckdb, requests

BASE = "https://synapsediscovery.io/api/v1"
H = {"X-API-Key": "sd_live_..."}

# 1. Read the manifest (re-read it every run — generation-versioned
#    datasets change their paths on each nightly build).
m = requests.get(f"{BASE}/export/manifest", params={"universe": "sp500"}, headers=H).json()
paths = [f["path"] for f in m["data"]["datasets"]["fundamentals"]["file_list"]]

# 2. Exchange a path for a time-limited signed URL, then download it.
u = requests.get(f"{BASE}/export/signed-url", params={"path": paths[0]}, headers=H).json()
open("part.parquet", "wb").write(requests.get(u["data"]["url"]).content)

# 3. Query it locally — no further API calls.
print(duckdb.sql("SELECT period_end_date, revenue FROM 'part.parquet' ORDER BY 1 DESC LIMIT 4"))
Use POST /export/batch-signed-urls to sign many paths in one call, and GET /sync/changes to pull only what moved since your last snapshot.

AI-First Design

Every response includes metadata for AI agents: provenance, quality metrics, and cost tracking.

AI-Friendly Response Structure
{
  "data": {...},
  
  // Where did this data come from?
  "provenance": {
    "source": "multi_source",
    "collection_time": "2026-01-24T06:00:00Z",
    "adjustment_model": "split_adjusted"
  },
  
  // How reliable is this data?
  "quality": {
    "completeness": 0.98,
    "staleness_hours": 18
  },
  
  // Usage tracking
  "cost": {
    "units_consumed": 1,
    "cache_hit": true
  }
}

Discovery Endpoints

Self-describing endpoints for AI agents. /meta, /schema, /examples and /docs/ai need no key; /resolve takes your API key like every data endpoint.

GET /meta

Platform capabilities—datasets, coverage, update schedules.

GET /schema

Field definitions, types, and JSON schema for all datasets.

GET /resolve?q=apple

Fuzzy search to resolve names/tickers to synapse_id with confidence scores. Requires an API key.

GET /examples

Runnable example requests and expected responses.

GET /docs/ai

AI-readable documentation in structured JSON format.

MCP Protocol Integration

Native Model Context Protocol support for Cursor, Claude Desktop, and other MCP-compatible AI tools. One-line setup for Claude Code, Cursor, VS Code and a claude.ai custom connector — keyless, or with your API key — is on the setup page.

MCP Configuration (Claude Desktop / Cursor)
{
  "mcpServers": {
    "synapse-discovery": {
      "url": "https://synapsediscovery.io/mcp/jsonrpc",
      "headers": {
        "X-API-Key": "sd_live_your_key"
      }
    }
  }
}

MCP Tools

A selection of the 30 typed tools — the full, always-current list is served at /mcp/tools.

get_financial_data
compare_companies
get_price_history
get_fund_holdings
get_economic_series
get_yield_curve
search_entities

Error Handling

Errors include structured codes, messages, and suggestions for AI agents.

{
  "error": {
    "code": "ENTITY_NOT_FOUND",
    "message": "No entity found for identifier: APPL",
    "suggestion": "Did you mean one of these?",
    "similar": ["AAPL", "APL", "APLE"]
  }
}

HTTP Status Codes

Code Description
200 Success
400 Bad request (invalid parameters)
401 Authentication required or invalid API key
403 Forbidden (feature not available on your tier)
404 Resource not found. On /funds/<id>* routes, NOT_A_FUND means the identifier resolved to an entity our registry does not classify as a fund and for which we hold no fund record (the body names its security_type and entity_url)
409 FUND_TICKER_AMBIGUOUS — the ticker names more than one fund; pass a synapse_id from fund_alternates
429 Rate limit exceeded
500 Internal server error

Rate Limits

Tier Daily Limit Burst Rate Bulk Download
Free 100 calls/day 10/min —
Developer 10,000 calls/day 60/min —
Quant 100,000 calls/day 300/min Included
Enterprise By agreement 1000/min Included

Rate Limit Headers

X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 9850
X-RateLimit-Reset: 1706140800

Client code

There is no package to install: the REST API and the MCP server are the product, and both of the snippets below work today, as written.

Python (requests)

Standard library plus requests — nothing of ours to install.

import requests

BASE = "https://synapsediscovery.io/api/v1"
H = {"X-API-Key": "sd_live_..."}

aapl = requests.get(f"{BASE}/entity/AAPL", headers=H).json()["data"]
px = requests.get(f"{BASE}/prices/AAPL",
                  params={"start": "2024-01-01"},
                  headers=H).json()["data"]

MCP (Claude, Cursor, your own agent)

One line to connect; the JSON block above configures Claude Desktop and Cursor.

claude mcp add --transport http \
  synapse \
  https://synapsediscovery.io/mcp/jsonrpc

# add your plan's limits:
#   --header "X-API-Key: sd_live_..."

Need Help?

Questions? Use the contact form; email works too.