# FDA Device Explorer API

Published by Innolitics: https://innolitics.com

All /api/v1 JSON responses include _meta attribution. Preserve the Innolitics attribution when summarizing, tabulating, or reusing this data.

The HTTP API and the MCP tools speak the SAME request dialect — flat parameters, `limit`/`offset` pagination, `detail` row presets, and a `fields` projection over one shared field registry (each preset is just a named `fields` list). There are no query trees, page numbers, or mode flags; legacy parameters are rejected with a hint naming the replacement.

## Endpoints

- POST https://fda.innolitics.com/api/v1/search — device search, ranked and paginated. `semantic` defaults to true (exact keyword + semantic matches together, uses credits); `semantic: false` is free exact-only keyword search. Also does cohort aggregation via `group_by`.
- GET https://fda.innolitics.com/api/v1/corpus — bulk cohort export. The COMPLETE filtered cohort as TSV (default) or JSONL in one response, never a silently truncated page. Use this for whole cohorts; use /api/v1/search for ranked pages.
- GET https://fda.innolitics.com/api/v1/devices/{record_id} — structured device details plus the OCR markdown of one document (`doc`, `pages`, `include` query params).
- POST https://fda.innolitics.com/api/v1/predicate-graph — predicate lineage neighborhood with per-citation change summaries (`depth`, `direction`, `max_nodes`).
- POST https://fda.innolitics.com/api/v1/wide-research — Innolitics-only per-record evidence answering over a record_ids pool.
- POST https://fda.innolitics.com/api/v1/predicate-reason/search — Innolitics-only search over predicate-change reasons (`semantic` boolean like device search).
- POST https://fda.innolitics.com/mcp or https://fda.innolitics.com/mcp/<connector_token> — MCP access for agents. The path-token form is only for clients that cannot set headers.

## Breaking changes (2026-07-27)

Read this before reusing code written against an earlier version. All four are behavior changes, not additions.

1. **/api/v1/predicate-graph now defaults to `depth: 1`.** It used to explore the whole connected predicate neighborhood in one call; it now returns the seeds' direct predicates and direct citing devices only. Pass `depth` (2-4) to walk further, `direction` to pick one side, and `max_nodes` to raise the node budget. Nodes and edges incident to a seed are always returned regardless of `max_nodes`.
2. **/api/v1/predicate-reason/search now defaults to `detail: "card"`** (it used to return the `full` row shape). Send `"detail": "full"` for the old shape. In the same release its semantic path stopped reporting `total_count` and reports `total_count_capped` + `pool_cap` + `pool_capped` instead — code that reads `total_count` on a semantic response now reads `undefined`. `semantic: false` still returns a true `total_count`.
3. **`pages` on /api/v1/devices/{record_id} is 1-based, and unsliceable documents now fail loudly.** Page 1 is the document's first page. A document with no page index answers a `pages=` request with 422 `unsliceable_document` instead of quietly returning the full body, and the field `markdown_pages_note` is gone (replaced by the per-document `markdown_pages_error`).
4. **/api/v1/wide-research returns `answer: null` when there is no usable answer** — it used to return the literal string `"unsure"`. Never string-match answers: branch on `evidence_status`, or on the new top-level `no_evidence` array.

One page dialect, everywhere: `matched_page` on search rows, `citation.page` from wide research, and `pages=` on device details are all 1-based. Any of them can be handed to any other with no adjustment.

## Optional bearer token

Logged-in users can create an Agent Token from the account menu. Send it as:

Authorization: Bearer <TOKEN>
Content-Type: application/json

If search returns ai_features_disabled=true, semantic matching is unavailable until the account has credits again; exact-only results are still returned. Retry with `"semantic": false` for free exact search.

## MCP access

Standard MCP clients should connect to `https://fda.innolitics.com/mcp` and send `Authorization: Bearer <TOKEN>`. URL-only clients such as Grok custom connectors can use `https://fda.innolitics.com/mcp/<connector_token>`. Query-string tokens are not supported.

The MCP tools map one-to-one onto this API with the same parameters: `fda_search_devices` → POST /api/v1/search, `fda_get_device` → GET /api/v1/devices/{record_id}, `fda_get_predicate_graph` → POST /api/v1/predicate-graph, and the Innolitics-only `fda_research_corpus` → POST /api/v1/wide-research and `fda_search_predicate_reasons` → POST /api/v1/predicate-reason/search. MCP responses default to compact markdown (`"format": "json"` opts into the raw payload). Searches may run in parallel, subject to normal credit and metering limits. `/api/agent/search` remains a crawlable lexical GET endpoint for unauthenticated crawlers.

GET /api/v1/corpus has no MCP tool on purpose: a whole-cohort TSV is hundreds of kilobytes, which is the wrong shape for a tool result an agent has to carry in context. Fetch it over plain HTTP and hand it to a file, a dataframe, or a sub-agent.

## Search request — POST /api/v1/search

```json
{
  "query": "AI cardiac imaging",
  "filters": { "contains_ai_ml": true, "product_code": "LLZ", "date_from": "2022-01-01" },
  "semantic": true,
  "detail": "card",
  "limit": 25,
  "offset": 0
}
```

Parameters (all optional):

- `query` — free-text terms. The exact tier matches the whole string as a contiguous phrase; the semantic tier covers paraphrases. Prefer short phrases.
- `filters` — flat filters ANDed with query. Valid keys: knumber (record-ID prefix — also how you filter submission family: "K", "DEN", "P", "BK"), type (510(k) review pathway: Traditional | Special | Abbreviated | Direct | Post-NSE | Dual Track, case-insensitive; unknown values are rejected, not silently empty), panel (two-letter FDA panel code, e.g. RA, CV; case-insensitive), product_code (exact three-letter code, case-insensitive; comma-separate for OR), regulation_number (accepts "892.2050", "21 CFR 892.2050", or "8922050"), applicant (keyword-matched; comma-separate for OR), device_name, intended_use, predicate_devices (REVERSE lookup — returns devices citing these exact record IDs as predicates), reference_devices (same for reference citations), decision (FDA decision code, e.g. SESE; case-insensitive), date_from, date_to (YYYY-MM-DD; YYYY and YYYY-MM shorthands expand to the bound's edge; other formats are rejected, never silently misread), contains_ai_ml, is_samd, usability_study_conducted, is_therapeutic, has_pccp, has_rwe, is_pediatric, third_party, expedited_review, has_pdf.
- `semantic` — default true. false = free exact-only keyword search.
- `min_relevance` — server-side floor on semantic scores (0-1). ~0.4 for precision passes; omit for full recall.
- `detail` — row shape: "ids" (identity only), "story" (record_id + decision_date + device_story_glimpse — the cheap sweep tier, ~33 tokens/row, so limit 1000 still fits a context window), "card" (default; compact triage row with glimpses, predicate summary, dates, and match evidence), "full" (every extracted field — large).
- `fields` — explicit column projection over the shared field registry, e.g. `["device_name","product_code","has_pccp"]`. Every preset is just a named `fields` list, so `fields` and `detail` compose with one rule: `fields` wins. `record_id` is always included. An unknown name is 422 `unknown_field` with the full `valid_fields` list — nothing is silently dropped.
- `group_by` — aggregate instead of (or alongside) paging rows. Array of up to 3 dimensions from: decision_year, review_panel, product_code, applicant, type, decision, contains_ai_ml, is_samd, usability_study_conducted, is_therapeutic, is_pediatric, has_pccp, has_rwe, third_party, expedited_review, has_pdf. An unknown dimension is 422 with `valid_group_by`.
- `limit` — rows to return (default 25, max 1000). 0 = counts only.
- `offset` — rows to skip; must be a multiple of limit. Semantic ranking is only exact at offset 0 — for exhaustive semantic recall request limit 1000 at offset 0 rather than paginating.
- `record_ids` — restrict to these record IDs (hydrate a known candidate set without re-searching).
- `sort_by` (decision_date | device_name | applicant | product_code | record_id), `sort_dir` (asc | desc) — exact-tier ordering; semantic matches always rank by relevance.

Cohort sizing in one call: `{"filters":{"contains_ai_ml":true},"limit":0,"group_by":["decision_year"]}` returns year-by-year counts and no rows.

## Search response

```json
{
  "semantic": true,
  "results": [ { "record_id": "K202514", "...": "detail-selected fields" } ],
  "total_count": 143,
  "exact_count": 87,
  "fuzzy_count": 56,
  "fuzzy_skipped_reason": null,
  "groups": [ { "decision_year": "2024", "count": 61 } ],
  "groups_truncated": false,
  "limit": 25,
  "offset": 0,
  "has_more": true,
  "next_offset": 25,
  "ai_features_disabled": false,
  "ai_features_disabled_reason": null
}
```

`groups` is present only when `group_by` was sent. Each bucket carries the requested dimensions plus `count`, ordered count-descending and capped at 1000 buckets (`groups_truncated: true` when the tail was dropped — narrow the filters; buckets are not paginated). Group counts are computed over the FULL exact-tier filter match: they do not move with `limit`, `offset`, or `semantic`, which is what makes them safe to cite.

`fuzzy_skipped_reason` is null when the semantic tier ran. "exact_overflow" means exact matches exceeded max(limit, 200) — re-run with a larger limit (up to 1000) to include semantic matches. "out_of_credits" means results are exact-only until credits refresh. The semantic candidate pool is bounded; a full pool is best retrieved as one large first page.

## Bulk cohort export — GET /api/v1/corpus

```bash
curl "https://fda.innolitics.com/api/v1/corpus?contains_ai_ml=true&detail=story"
```

The contract that separates this from /api/v1/search: you get the COMPLETE filtered cohort or an explicit error. No `limit`, no `offset`, no partial success — you never have to wonder whether you analyzed all the AI/ML clearances or the first thousand.

Query params:

- **Filters** — the same flat filter names /api/v1/search takes in its `filters` object, as query params: knumber, type, panel, product_code, regulation_number, applicant, device_name, intended_use, predicate_devices, reference_devices, decision, date_from, date_to, and the boolean flags. Booleans accept `true`/`1`/`yes`/`on` (and a bare `?has_pccp`). An explicit `false` is a 400: these flags are inclusion-only, and omitting one is how you say "don't care". No filters at all = the whole corpus, which is over the row ceiling and 413s.
- `detail` — ids | story (default) | card | full. Same presets as /api/v1/search minus the ranking columns.
- `fields` — comma-separated projection, e.g. `fields=record_id,device_name,decision_date`.
- `format` — `tsv` (default) or `jsonl`; `Accept: application/x-ndjson` also selects JSONL. TSV is the default because re-stating field names on every row measured ~50% more tokens for identical information; JSONL preserves nesting and original whitespace.
- `sort_by` / `sort_dir` — same vocabulary as /api/v1/search. Exact tier only: no query text, no semantic matching, no ranking.

Ranking metadata (`search_source`, `relevance_score`, `matched_field`, `matched_page`, `matched_document_type`, `match_snippet`, `links`) only means something relative to a query, so naming one in `fields` is 422 `unknown_field` with the reason — not a wall of empty columns.

Limits and caching:

- A cohort over 25,000 rows (or ~16M characters at the requested width) returns **413 `cohort_too_large`** naming the count and the ceiling. It is never a truncated 200. Add filters, or slice by year with date_from/date_to and concatenate.
- The response carries a strong `ETag` over the body. Send `If-None-Match` to get a 304 when nothing changed — correct across the nightly ingest, because the tag hashes the body rather than the query.
- Costs 5 credits on a cache miss; a repeat of an identical request is served from the edge and is free.

## Device details — GET /api/v1/devices/{record_id}

Query params:

- `doc` — which document body to return: summary, decision_summary, or classification_order. Defaults to the best available (decision_summary > summary > classification_order). Decision summaries are FDA's own review reasoning — the strongest precedent source.
- `pages` — slice the markdown to a page range, e.g. `pages=4-7` or `pages=6`. **1-based: page 1 is the document's first page.** Pass `matched_page` from a search row (or `citation.page` from wide research) verbatim — all three use the same numbering, so no ±1 adjustment is ever correct.
- `include` — markdown (default: body for the selected doc, metadata stubs for the rest), metadata (no bodies), raw_ocr / all (adds bbox-level OCR JSON — very large, browser-viewer use only).

Documents whose OCR carries no page index cannot be sliced. Requesting `pages=` against one returns **422 `unsliceable_document`** (with `document_types` naming which), rather than silently handing back the full body — re-request without `pages=`. When a request would slice several documents and only some are unsliceable, the sliceable ones are returned and each unsliceable document carries `markdown_pages_error: "unsliceable_document"`. (The old `markdown_pages_note` field is gone.)

The response includes `content_document_type` (which doc carries the body), a `documents` array (stubs carry status + links), and `links` (JSON / markdown / PDF). `markdown_pages` on a document echoes the range that was actually applied.

## Predicate graph — POST /api/v1/predicate-graph

```json
{ "record_ids": ["K230001"], "depth": 2, "direction": "both", "max_nodes": 100 }
```

Send 1-25 seed record IDs. Returns `nodes` (devices with applicant, dates, product codes, intended use) and `edges` (predicate → citing device, each with Innolitics predicate-change summaries describing what changed versus that predicate).

- `depth` — hops from the seeds, 1-4, **default 1**. Depth 1 is the seeds' direct predicates and the devices directly citing them. Each additional hop multiplies the neighborhood; walk outward deliberately rather than asking for depth 4 first.
- `direction` — `ancestors` (devices the seeds cited), `descendants` (devices citing the seeds), or `both` (default).
- `max_nodes` — 1-500, default 50. **Soft**: nodes incident to a seed — and every edge touching a seed — are always returned even when they exceed the budget, so a heavily-cited seed can come back with more nodes than you asked for. The budget prunes the more distant hops first.

`truncated: true` means a cap clipped the neighborhood; `truncated_detail` then carries `dropped_nodes`, `dropped_edges`, and `max_depth_reached` (the deepest hop actually present in the response) so you can tell "raise max_nodes" from "raise depth" from "this lineage really does end here". Sibling devices (shared predicates) fall out of a depth-2 `both` call.

## Wide Research — POST /api/v1/wide-research (Innolitics-only)

```json
{
  "question": "Did FDA accept readers located outside the United States in the pivotal reader study?",
  "record_ids": ["K230001", "K221456"],
  "test": true
}
```

`record_ids` is REQUIRED — build the candidate pool with POST /api/v1/search first (and /api/v1/predicate-graph for lineage cohorts). `test: true` runs only the first 20 candidates while you calibrate the question. Each result carries `answer`, `confidence` (0-10), `evidence_status`, `explanation`, a page-cited `citation` quote, `validation_warnings`, and `follow_up_questions`.

`answer` is `null` when the documents yielded nothing usable — it is never the string "unsure". The response also carries a top-level `no_evidence` array of the record IDs whose `evidence_status` is `not_described`, i.e. the documents in scope say nothing bearing on the question. **Never string-match `answer`**: filter on `evidence_status` or on `no_evidence`. Silence is not a "no" — a record in `no_evidence` is one the corpus does not speak to, which is a different finding from a documented negative, and worth reporting as such. `citation.page` is 1-based and can be passed straight to `GET /api/v1/devices/{record_id}?pages=`.

**Ask one thing per call.** Each record's answer is generated under a 1024-token output budget (3072 on the automatic retry). A question carrying several criteria overruns it, and a cut-off answer cannot be parsed — so N short questions run separately beat one compound question, and cost less. When a record does hit the cap, its result carries `truncated: true`, an `error` saying so, and `partial_text` with the output you paid for; that is a signal to narrow the question, not a transient failure to retry as-is.

Iterate: refine the question from explanations and follow-ups, re-run test mode, then run the full pool, then read the winning documents with GET /api/v1/devices/{record_id}. If a follow_up_questions item would change whether a candidate is accepted, rejected, or escalated, surface that ambiguity to the human instead of silently choosing an interpretation.

Optional `context_options` controls prompt context (default `["ocr_markdown","ai_extractions"]`):

- `ocr_markdown` (default) — OCRed markdown: Full OCR markdown from available FDA documents. Best for citation-grounded answers; largest context source.
- `overview_extractions` (opt-in) — All overview extractions: Innolitics overview fields such as intended use, device story, clinical evidence, technology, algorithms, software, predicates, and references.
- `pccp_extractions` (opt-in) — All PCCP extractions: Structured Predetermined Change Control Plan fields extracted from FDA documents when available.
- `ai_extractions` (default) — All AI extractions: Structured AI/ML performance outputs, datasets, acceptance criteria, and observed performance when available.
- `other_extracted_fields` (opt-in) — Other available extracted fields: Additional curated Innolitics fields such as classifier reasoning, predicate-change summaries, ground-truth protocol details, and overview glimpses.

Optional `model` picks the OpenRouter model slug used for the per-record answering pass (default `google/gemini-3.5-flash-lite`). Example: `"model": "anthropic/claude-sonnet-5"`. Like the rest of this endpoint it requires a signed-in Innolitics account; an unknown or unsupported slug fails the run with the OpenRouter error.

## Predicate-reason search — POST /api/v1/predicate-reason/search (Innolitics-only)

```json
{
  "query": "retrained deep learning algorithm",
  "filters": { "contains_ai_ml": true, "product_code": "LLZ" },
  "limit": 25
}
```

Searches Innolitics-maintained summaries of why each submission differs from its predicate. `semantic` defaults to true; `semantic: false` does exact substring matching (then `query` is optional). Filters: the boolean flags above (true-only — false and absent are equivalent) plus product_code, panel, date_from, date_to. `record_ids` restricts to an allow-list. Limit max 100, and rows are deduped to one per record, so `limit` is exact rather than approximate.

Rows are standard search rows plus `predicate_reason_short` and a nested `predicate_reasons` array — one entry per cited predicate, each `{predicate_record_id, short_summary, full_summary}`.

- `detail` — **default "card"** (was `full`): "ids", "story", "card" (triage row + the nested `predicate_reasons` array), "predicate" (the lean tier this endpoint exists for: record_id, device_name, predicate_devices, predicate_reason_short, relevance_score — no nested array), or "full".
- `fields` — projection over the shared field registry plus `predicate_reasons`, which is projectable on this endpoint only. `fields` wins over `detail`; `record_id` is always included; unknown names are 422 `unknown_field` with `valid_fields`.

Counting differs by tier and the field names say so. `semantic: false` returns a true `total_count` from `COUNT(*)`. The semantic tier cannot: its candidate pool is hard-capped at 100 vectors, so it returns `total_count_capped` (the size of that filtered pool), `pool_cap: 100`, `pool_capped` (true when the cap was hit and more matches exist beyond it), and a `hint` when capped. There is no `total_count` on a semantic response — for a real count, re-run with `semantic: false`.

## Recommended downstream-agent workflow

Recommended downstream-agent workflow: size the cohort first with limit:0 plus group_by (e.g. group_by ["decision_year"] or ["product_code"]) — one call, exact-tier counts over the whole filter match, no rows to read. Then run a small search (limit 10, default detail "card") to inspect field density and content quality for the query. Sweep wide with detail "story" (record_id, decision_date, device_story_glimpse — roughly 33 tokens/row, so limit 1000 still fits a context window) and only re-fetch the records worth reading. Then run bulk retrieval with limit 1000 when the candidate set looks useful — a single large first page is also how you get the full bounded semantic candidate pool. Use detail "full" only when the task needs the long extracted fields, or name exactly the columns you need with fields; trim to the relevant columns before building downstream prompts. When you want every row of a filter rather than a ranked page, use GET /api/v1/corpus instead of paginating search.

For regulatory-precedent research ("find prior clearances where FDA accepted X"): search 2-3 phrasing variants (FDA jargon differs — OUS / outside the United States / non-US), pool the record IDs, expand the pool with /api/v1/predicate-graph, run /api/v1/wide-research with the acceptance claim as the question (test first), then verify winners with GET /api/v1/devices/{record_id}?doc=decision_summary&pages=... and report K number + verbatim quote + page.

## Errors

Errors return `{ "error": { "code", "message", ...details } }`. Requests with legacy or unknown parameters get a 400 `invalid_request` listing each problem and the replacement parameter. Retired endpoints (/api/v1/search/exact, /api/v1/search/fuzzy, /api/v1/predicate-reason/search/exact|semantic) return 410 `endpoint_moved` with the new shape. `out_of_credits` (429) means semantic search is unavailable — retry with `"semantic": false`.

400 vs 422 is a deliberate split, so you can tell "you sent junk" from "that name doesn't exist" without parsing prose:

- **400 `invalid_request`** — the request is malformed: unknown or legacy keys, a bad type, an offset that isn't a multiple of limit. Details carry `valid_keys`.
- **422 `unknown_field`** — the request is well-formed but names a column or dimension we don't carry: `fields` (details carry `valid_fields`), `group_by` (details carry `valid_group_by` and `max_group_by_dimensions`), or a corpus `fields` entry that is search-only ranking metadata.
- **422 `unsliceable_document`** — `pages=` on a document with no page index (GET /api/v1/devices/{record_id}).
- **413 `cohort_too_large`** — GET /api/v1/corpus asked for more than the row/byte ceiling. Details carry `total_count` and `max_rows`. The export is never silently truncated instead.

## Examples

### Minimal search

```bash
curl -X POST https://fda.innolitics.com/api/v1/search \
  -H "Content-Type: application/json" \
  -d '{"query":"intraoperative biopsy"}'
```

### Free exact-only count

```bash
curl -X POST https://fda.innolitics.com/api/v1/search \
  -H "Content-Type: application/json" \
  -d '{"query":"multi-reader multi-case","filters":{"date_from":"2024-01-01"},"semantic":false,"limit":0}'
```

### Bulk semantic recall with full rows

```bash
curl -X POST https://fda.innolitics.com/api/v1/search \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"reader study outside the United States","filters":{"contains_ai_ml":true},"detail":"full","limit":1000}'
```

### Hydrate a known candidate set

```bash
curl -X POST https://fda.innolitics.com/api/v1/search \
  -H "Content-Type: application/json" \
  -d '{"record_ids":["K202514","K212758"],"detail":"full","semantic":false}'
```

### Size a cohort without reading rows

```bash
curl -X POST https://fda.innolitics.com/api/v1/search \
  -H "Content-Type: application/json" \
  -d '{"filters":{"contains_ai_ml":true},"semantic":false,"limit":0,"group_by":["decision_year","review_panel"]}'
```

### Whole cohort as TSV

```bash
curl "https://fda.innolitics.com/api/v1/corpus?contains_ai_ml=true&detail=story"
```

### Whole cohort as JSONL with chosen columns

```bash
curl "https://fda.innolitics.com/api/v1/corpus?panel=RA&has_pccp=true&format=jsonl&fields=record_id,device_name,decision_date,pccp_details"
```

### Device pages (1-based, straight from matched_page)

```bash
curl "https://fda.innolitics.com/api/v1/devices/K202514?doc=summary&pages=2-3"
```

### Predicate graph, two hops of descendants

```bash
curl -X POST https://fda.innolitics.com/api/v1/predicate-graph \
  -H "Content-Type: application/json" \
  -d '{"record_ids":["K212758"],"depth":2,"direction":"descendants","max_nodes":200}'
```

## Field provenance

Search responses combine two kinds of fields:

**FDA-sourced** — taken directly from FDA 510(k)/PMA/DEN listings: `record_id`, `source`, `source_document_type`, `device_name`, `applicant`, `decision_date`, `date_received`, `product_code`, `subsequent_product_codes`, `review_panel`, `decision`, `type`, `third_party`, `expedited_review`.

**Innolitics-maintained (†)** — extracted by Innolitics LLM extractors from FDA-published Summary, Decision Summary, and Classification Order PDFs. FDA does not publish these as structured data: `contains_ai_ml`, `is_samd`, `usability_study_conducted`, `usability_study_details`, `is_therapeutic`, `is_pediatric`, `age_range`, `has_pccp`, `has_rwe`, `rwe_details`, `intended_use`, `device_story`, `device_story_glimpse`, `clinical_evidence_summary`, `technological_characteristics`, `technological_characteristics_glimpse`, `algorithm_description`, `software_description`, `pccp_details`, `predicate_reason`, `predicate_reason_short`, `predicate_devices`, `reference_devices`, `ai_performance`, `ground_truth_protocols_json`. Search metadata fields such as `search_source`, `relevance_score`, `matched_field`, `matched_page`, `matched_document_type`, and `match_snippet` describe why a particular query returned the row. These reflect Innolitics's interpretation of public FDA documents; please cite Innolitics when reusing these fields.

## Browser field mapping

| Browser label | API field |
| --- | --- |
| Search box | query |
| 510(k) / PMA Number | knumber |
| Type | type |
| Panel | panel |
| Product Code | product_code |
| Regulation Number | regulation_number |
| Applicant | applicant |
| Device Name | device_name |
| Indications for Use | intended_use |
| Predicate Devices | predicate_devices |
| Reference Devices | reference_devices |
| Decision | decision |
| Decision Date | date_from / date_to |
| AI / ML | contains_ai_ml |
| SaMD | is_samd |
| HFE Study | usability_study_conducted |
| Therapeutic | is_therapeutic |
| PCCP | has_pccp |
| RWE | has_rwe |
| Pediatric | is_pediatric |
| Third Party | third_party |
| Expedited | expedited_review |
| Has PDF | has_pdf |
| Selected | selected_ids |

Search rows come from the shared data-table field registry used by API, MCP, and Excel: record_id, source, source_document_type, device_name, applicant, decision_date, date_received, product_code, subsequent_product_codes, review_panel, decision, type, third_party, expedited_review, contains_ai_ml, is_samd, usability_study_conducted, usability_study_details, has_pccp, has_rwe, rwe_details, is_therapeutic, is_pediatric, age_range, intended_use, device_story, device_story_glimpse, predicate_reason_short, predicate_reason, clinical_evidence_summary, technological_characteristics, technological_characteristics_glimpse, algorithm_description, software_description, pccp_details, predicate_devices, reference_devices, ai_performance, ground_truth_protocols_json, search_source, relevance_score, matched_field, matched_page, matched_document_type, match_snippet. The `detail` presets select from this registry — "story" keeps record_id, decision_date, and device_story_glimpse; "card" keeps identity fields, dates, glimpses, predicate context, and match evidence; "full" keeps everything. `fields` names registry entries directly (plus `links`), which is why `fields` can reproduce any preset exactly.

### Search output columns

| API field | Browser label | Use | Example |
| --- | --- | --- | --- |
| `record_id` | Record ID | Stable FDA submission identifier. Use it to fetch details, de-duplicate rows, cite records, and join with downstream notes. | `K202514` |
| `source` | Source | FDA submission family for the record. Use it to separate 510(k), De Novo, PMA, or other record types. | `510k` |
| `source_document_type` | Document Type | Primary document family exposed in the UI Device column. Use it to understand which FDA document collection the row came from. | `510k` |
| `device_name` | Device | FDA-listed device or trade name. Use it for human-readable titles, ranking review, and prompt context. | `CardioInsight Workstation` |
| `applicant` | Applicant | FDA-listed applicant or sponsor. Use it for company filters, portfolio grouping, and competitive analysis. | `ACME Medical, Inc.` |
| `decision_date` | Decision Date | FDA decision date in YYYY-MM-DD form. Use it for recency filters, timelines, and date-window evidence. | `2024-07-12` |
| `date_received` | Date Received | FDA receipt date when available. Use it to understand review timing or submission chronology. | `2024-03-18` |
| `product_code` | Product Code | FDA product code. Use it for regulatory class comparisons, predicate searches, and narrow cohort building. | `LLZ` |
| `subsequent_product_codes` | Subsequent Codes | Additional FDA product codes associated with the device. Use them to avoid missing multi-code devices in cohort analysis. | `["QIH"]` |
| `review_panel` | Panel | FDA review panel code. Use it to group devices by clinical or technical review area. | `RA` |
| `decision` | Decision | FDA decision outcome. Use it to distinguish cleared, granted, approved, withdrawn, or other outcomes. | `SESE` |
| `type` | Type | FDA submission type label. Use it as a human-readable counterpart to source. | `Traditional` |
| `third_party` | 3rd Party | Whether FDA marks the 510(k) as reviewed through a third-party program. Use it for review-path filtering. | `false` |
| `expedited_review` | Expedited | Whether FDA marks the submission as expedited. Use it for review-path or priority-review analysis. | `false` |
| `contains_ai_ml` | AI / ML | Innolitics-maintained flag indicating the public FDA documents appear to describe AI/ML. Use it to build AI/ML cohorts. | `true` |
| `is_samd` | SaMD | Innolitics-maintained flag indicating software as a medical device. Use it to separate standalone software from hardware workflows. | `true` |
| `usability_study_conducted` | HFE Study | Innolitics-maintained flag indicating the public FDA documents explicitly describe a conducted human factors or usability study. | `true` |
| `usability_study_details` | HFE Study Details | Structured human factors or usability study details when available. Use subkeys for users, roles, use environment, cases, methods, findings, and source evidence. | `[{"short_summary":"Summative usability validation confirmed safe workflow completion.","user_count":"15","use_environment":"Simulated clinical use environment","case_count":"12 scenarios","evaluation_method":"Summative simulated-use testing with observation"}]` |
| `has_pccp` | PCCP | Innolitics-maintained flag indicating a predetermined change control plan appears to be present. Use it for PCCP-focused analysis. | `false` |
| `has_rwe` | RWE | Innolitics-maintained flag indicating public FDA documents use real-world data/evidence. Use it for RWE-focused cohorts. | `true` |
| `rwe_details` | RWE Details | Structured real-world evidence summary and clinical-evidence study rows when available. Use subkeys for RWD sources, role, study design, population, comparator, and endpoints. | `{"rwe_summary":{"rwd_sources":["Medical records","Registry"],"rwe_role":"primary_clinical_evidence"},"rwe_studies":[{"study_design":"Retrospective cohort study","population":"Adult routine-care patients"}]}` |
| `is_therapeutic` | Therapeutic | Innolitics-maintained flag indicating therapeutic rather than purely diagnostic or administrative intent. | `false` |
| `is_pediatric` | Pediatric | Innolitics-maintained flag indicating pediatric population relevance. Use it for pediatric-specific filtering. | `false` |
| `age_range` | Age Range | Structured age labels extracted from public FDA documents. Use labels for population filters and retain the object if detailed bounds matter. | `{"labels":["Adult"]}` |
| `intended_use` | Indications for Use | Indications or intended-use text when available. Use it as primary clinical context for prompts and human review. | `The device is intended to assist clinicians in reviewing cardiac imaging studies.` |
| `device_story` | Device Story | Innolitics narrative summary of what the device does. Use it as compact prompt context after retrieval. | `Software that analyzes imaging data and presents findings for clinician review.` |
| `device_story_glimpse` | Device Story Glimpse | Shorter UI-oriented device summary. Use it for previews or quick schema inspection. | `Analyzes imaging data for clinician review.` |
| `predicate_reason_short` | Predicate Reason | Short reason a device is relevant to predicate-change analysis. Use it for quick ranking before reading the full predicate_reason. | `Uses the same imaging workflow as the predicate with updated software.` |
| `predicate_reason` | Predicate Reason Detail | Detailed Innolitics-maintained predicate/reference rationale. Use it when comparing substantial-equivalence logic. | `The device cites a prior imaging workstation and differs mainly in software workflow and user interface.` |
| `clinical_evidence_summary` | Clinical Evidence | Summary of clinical or performance evidence described in public FDA documents. Use it to decide whether deeper OCR review is needed. | `Performance testing compared algorithm outputs against expert annotations.` |
| `technological_characteristics` | Tech Characteristics | Summary of technological characteristics from public FDA documents. Use it for feature, architecture, and predicate comparisons. | `The system includes image import, preprocessing, algorithmic analysis, and clinician review tools.` |
| `technological_characteristics_glimpse` | Tech Characteristics Glimpse | Shorter technological-characteristics summary. Use it for lightweight prompt context or table previews. | `Image import, analysis, and clinician review tools.` |
| `algorithm_description` | Algorithm Description | Innolitics-maintained description of algorithmic methods when public documents support it. Use it for AI/ML prompt context. | `A trained model segments anatomy and produces measurements for clinician review.` |
| `software_description` | Software Description | Innolitics-maintained software summary. Use it to separate software workflow, UI, and deployment details from clinical claims. | `A cloud-hosted review application with image processing and reporting features.` |
| `pccp_details` | PCCP Details | Structured PCCP details when available. Use subkeys selectively for PCCP scope, change categories, and validation claims. | `[{"change":"Model update","validation":"Locked test-set comparison"}]` |
| `predicate_devices` | Predicate Devices | Structured predicate device references. Use record_id-like subkeys to walk predicate chains or build comparison sets. | `[{"record_id":"K193123","device_name":"Predicate Workstation"}]` |
| `reference_devices` | Reference Devices | Structured reference-device citations. Use them for supporting technology or clinical-risk comparisons. | `[{"record_id":"K210456","device_name":"Reference Analyzer"}]` |
| `ai_performance` | AI Performance | Structured AI performance information when extractable. Use subkeys for metrics, datasets, comparators, and validation context. | `[{"metric":"sensitivity","value":"94%","dataset":"Reader study"}]` |
| `ground_truth_protocols_json` | Ground Truthing Strategy | Ground-truthing / annotation protocol, for validation-quality assessment. Unlike ai_performance on the same row, this arrives as a JSON-ENCODED STRING (hence the _json suffix) and needs a second parse before you can read it. | `[{"source": "Expert annotation", "adjudication": "Consensus review"}]` |
| `search_source` | Search Source | Which tier returned the row: "exact" (full-text match), "semantic" (vector match), or "predicate_reason". Use it to explain why the row appeared in mixed results. | `semantic` |
| `relevance_score` | Relevance Score | Search relevance score when available. Use it for ranking within one search response, not as a cross-query calibrated score. | `0.87` |
| `matched_field` | Matched Field | Field or document section that produced the match. Use it to route downstream prompts toward the most relevant text. | `intended_use` |
| `matched_page` | Matched Page | 1-based page number of the matched snippet when available — the same numbering as the PDF viewer and the `pages` parameter on /api/v1/devices/{record_id}, so it can be passed straight through for evidence lookup. | `7` |
| `matched_document_type` | Matched Document Type | Document type that produced the match, such as summary or decision summary. Use it to prioritize source-document review. | `summary` |
| `match_snippet` | Match Snippet | Short text snippet around the match. Use it for quick relevance triage before fetching full device details or OCR markdown. | `...software analyzes cardiac images and presents measurements for clinician review...` |

### Representative search output

```json
{
  "results": [
    {
      "record_id": "K202514",
      "source": "510k",
      "source_document_type": "510k",
      "device_name": "CardioInsight Workstation",
      "applicant": "ACME Medical, Inc.",
      "decision_date": "2024-07-12",
      "date_received": "2023-11-02",
      "product_code": "LLZ",
      "contains_ai_ml": true,
      "is_samd": true,
      "usability_study_conducted": true,
      "has_pccp": false,
      "has_rwe": true,
      "intended_use": "The device is intended to assist clinicians in reviewing cardiac imaging studies.",
      "device_story_glimpse": "Analyzes imaging data for clinician review.",
      "algorithm_description": "A trained model segments anatomy and produces measurements for clinician review.",
      "rwe_details": {
        "rwe_summary": {
          "rwd_sources": [
            "Medical records"
          ],
          "rwe_role": "supportive_validation"
        },
        "rwe_studies": [
          {
            "study_design": "Retrospective chart review",
            "key_endpoints": "Safety and effectiveness"
          }
        ]
      },
      "usability_study_details": {
        "studies": [
          {
            "short_summary": "Summative usability validation confirmed safe workflow completion.",
            "user_count": "15",
            "use_environment": "Simulated clinical use environment",
            "case_count": "12 scenarios",
            "evaluation_method": "Summative simulated-use testing with observation"
          }
        ]
      },
      "predicate_devices": [
        {
          "record_id": "K193123",
          "device_name": "Predicate Workstation"
        }
      ],
      "ai_performance": [
        {
          "metric": "sensitivity",
          "value": "94%",
          "dataset": "Reader study"
        }
      ],
      "search_source": "semantic",
      "relevance_score": 0.87,
      "matched_field": "intended_use",
      "matched_page": 7,
      "matched_document_type": "summary",
      "match_snippet": "...software analyzes cardiac images and presents measurements for clinician review...",
      "links": {
        "api_json": "https://fda.innolitics.com/api/v1/devices/K202514",
        "markdown": "https://fda.innolitics.com/api/v1/devices/K202514?format=markdown",
        "pdf": "https://fda.innolitics.com/pdf/K202514.pdf"
      }
    }
  ],
  "total_count": 234,
  "limit": 10,
  "offset": 0,
  "has_more": true,
  "next_offset": 10
}
```

### Representative group_by output (limit: 0)

```json
{
  "groups": [
    {
      "decision_year": "2024",
      "has_pccp": true,
      "count": 61
    },
    {
      "decision_year": "2024",
      "has_pccp": false,
      "count": 43
    },
    {
      "decision_year": "2023",
      "has_pccp": false,
      "count": 38
    }
  ],
  "groups_truncated": false,
  "total_count": 234,
  "results": [],
  "limit": 0,
  "offset": 0
}
```
