---
title: "Portfolio holdings for a specific BDC"
method: GET
path: "/api/bdc/holdings"
tags: ["Private Credit"]
---

# Portfolio holdings for a specific BDC

`GET /api/bdc/holdings`

## Request

- Base URL: `https://www.oxfordledge.com` (the document declares no server; this is the origin it was published from)
- URL: `https://www.oxfordledge.com/api/bdc/holdings`
- Auth: none declared

Holdings for a specific BDC.

NO PAYWALL. The 3-row free teaser + `locked` / `upgrade` envelope this
docstring described until 2026-09-22 was REMOVED on 2026-06-12 (OWNER QA
+ SF-MONETIZATION-V3, which forbids paywalling EDGAR data); the prose
outlived it by three months and is corrected here rather than quietly
rewritten. `tests/test_routes_bdc_contract.py::
test_bdc_holdings_emits_locked_signal_for_non_plus` is the live pin and
it guards the REMOVAL -- the handler must not truncate or emit lock
metadata. `BDCHoldingsResponse` still declares `locked` / `lockedTotal` /
`upgrade` as Optional, and the client still reads them, as a DEFENSIVE
path against an older deploy that does truncate; nothing here writes them.

PAGING (additive, 2026-09-22, OWNER ruling on the external MCP review's
oversize-payload row: make the payload FIT rather than truncating it).
`limit` / `offset` page `holdings` only; a call declaring NEITHER is
byte-for-byte the pre-paging wire, so an installed reader sees nothing
new. Out-of-range values are REFUSED by FastAPI (422), never clamped --
the same semantic the MCP seam applies from the schema bounds. The slice
is `data.bdc_query.page_holding_rows`, twin of the `search_bdc_borrower`
pager.

## Query parameters

- `ticker` string
- `limit` integer, nullable: Max `holdings` rows to return. Omit for every row (the store's 5000-row backstop). Pages the row list only -- every filing-level total stays computed over the whole book.
- `offset` integer, nullable: Rows to skip before the page (default 0). Past the end returns an empty page with `page.total` intact.

## Response `200`

Successful Response

- BDCHoldingsResponse: `GET /api/bdc/holdings` response. F4 schema-tightening + correction (2026-04-30): `bdcTicker` and `holdings` REQUIRED. Previously declared `ticker` (snake_case) but handler returns `bdcTicker` (camelCase) plus a richer set of stat fields (filingDate, filingType, periodEnd, totalHoldings, totalParAmount, totalFairValue, weightedAvgPrice, byLienPosition, topIndustries, portfolioStructure). Schema rewritten to match handler's actual wire format. F9 (2026-04-30 PM): added `data_source` + `fetched_at` for freshness disclosure. The audit's persona Priya specifically flagged that BDC SOI data is filed quarterly, so on any given day the current view could be 0–90 days stale. Pre-F9 the page didn't display the as-of date — undermined the differentiated- data value prop. `filing_date` was already in the schema; F9 adds the source attribution + the request-time fetched_at so the frontend can render "Source: SEC EDGAR · Filed YYYY-MM-DD · Fetched HH:MM ET" above the panel. NO PAYWALL (corrected 2026-09-22, annotated not rewritten): this docstring claimed a "3-row teaser + `locked: true` overlay + `upgrade` metadata" until today, three months after that truncation was REMOVED (2026-06-12 OWNER QA + SF-MONETIZATION-V3). `locked` / `lockedTotal` / `upgrade` stay declared below, Optional and never written, as the DEFENSIVE shape the client reads against an older deploy. The 500 fallback is `{bdcTicker, holdings: [], error}` (ErrorResponse). PAGING (2026-09-22): `page` + `completeness` ride ONLY when the caller declared `limit` / `offset`, so both are Optional and an undeclared call is unchanged. They page `holdings` alone; every filing-level figure above stays computed over the store's whole population. Frontend BDC holdings panel reads `data.bdcTicker` for the header + iterates `data.holdings.forEach(...)` for the holdings table.
  - `bdcTicker` string, required
  - `holdings` BDCHolding[], required
    - `borrower` string, nullable
    - `industry` string, nullable
    - `investment_type` string, nullable
    - `fair_value` number, nullable
    - `cost` number, nullable
    - `rate` string, nullable
    - `maturity` string, nullable
  - `bdcName` string, nullable
  - `dataSource` string, nullable
  - `fetchedAt` string, nullable
  - `filingDate` string, nullable
  - `filingType` string, nullable
  - `periodEnd` string, nullable
  - `totalHoldings` integer, nullable: REGISTRY count of parsed positions -- the population totalFairValue / byLienPosition / portfolioStructure cover. NOT len(holdings): the served list drops non-identity borrower cells (see holdingsReturned / nonBorrowerRowsExcluded).
  - `holdingsReturned` integer, nullable: len(holdings) as served: totalHoldings minus the rows the non-identity borrower filter dropped.
  - `nonBorrowerRowsExcluded` integer, nullable: Rows read from the store but NOT served because their borrower cell carries no issuer identity (SOI section labels, bare legal suffixes, security-description text). Their fair value is still inside totalFairValue and the portfolio percentages.
  - `excludedRowsFairValue` number, nullable: Sum of fairValue over the excluded rows: sum(holdings[].fairValue) + this == parsedRowSumFairValue (or totalFairValue on a parsed-rows basis).
  - `totalParAmount` number, nullable
  - `totalFairValue` number, nullable
  - `weightedAvgPrice` number, nullable
  - `byLienPosition` object, nullable
  - `topIndustries` object[], nullable
  - `portfolioStructure` object, nullable
  - `locked` boolean, nullable
  - `locked_total` integer, nullable
  - `upgrade` InsiderUpgrade
    - `tier` string, nullable
    - `tier_name` string, nullable
    - `price` string, nullable
    - `url` string, nullable
    - `message` string, nullable
  - `page` BDCHoldingsPage: The declared-page block on `GET /api/bdc/holdings`. Present ONLY when the caller declared `limit` or `offset`; an undeclared call is byte-for-byte the pre-paging wire. `total` is the SERVED row count before the slice (it equals `holdingsReturned`) -- the store's number, not the page's, so `returned < total` is how a reader knows it holds a window. Same key set and meaning as the `search_bdc_borrower` page block, so a reader who has learned one BDC page block has learned both.
    - `limit` integer, required
    - `offset` integer, required
    - `returned` integer, required
    - `total` integer, required
    - `hasMore` boolean, required
  - `completeness` BDCHoldingsCompleteness: The MCP-vocabulary completeness block that rides beside `page`. Supplied by the producer rather than left to a dispatcher default: with the total KNOWN, a default that compares returned rows against `limit` would call a full page "at_cap_total_unknown".
    - `returned` integer, required
    - `limit` integer, required
    - `total_available` integer, required
    - `complete` boolean, required
    - `completeness_basis` string, required
    - `rows_key` string, required

## Other responses

- `400`: Bad Request
- `422`: Validation Error
- `500`: Internal Server Error

---

[API](https://skmtc.dev/oxfordledge/apis/oxford-ledge-api.md) · [All operations](https://skmtc.dev/oxfordledge/apis/oxford-ledge-api/llms.txt) · [OpenAPI document](https://skmtc.dev/oxfordledge/apis/oxford-ledge-api/revisions/e832a1260f0b?raw)
