# CMS Open Payments API — Physician Roll-up (Sunshine Act) (`malonestar/cms-open-payments`) Actor

CMS Open Payments (Sunshine Act) API: look up physician or manufacturer general payments by name, NPI or manufacturer, rolled up per physician × manufacturer — total USD, payment count, date range and payment-type mix. Pharma-compliance ready. Keyless, official CMS.

- **URL**: https://apify.com/malonestar/cms-open-payments.md
- **Developed by:** [Kyle Maloney](https://apify.com/malonestar) (community)
- **Categories:** Lead generation, Developer tools, Automation
- **Stats:** 2 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.40 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## CMS Open Payments API - Physician x Manufacturer Roll-up (Sunshine Act)

A **CMS Open Payments (Sunshine Act) API**: look up **physician** or
**manufacturer** general payments by name, **NPI**, profile ID or manufacturer,
and roll them up **per recipient x manufacturer** - total USD, payment count,
date range, product mix and payment-type mix. Turn millions of disclosure rows
into a compliance-ready summary. Keyless, official CMS source.

**Every row states its own completeness.** CMS returns an exact match count for
your query; this Actor reports it as `matching_rows_total` alongside
`rows_fetched`, `rows_coverage_pct` and a `totals_are_partial` flag, so a capped
pull can never be mistaken for a relationship total.

### Who it's for

- **Pharma / medtech compliance** teams auditing physician payment exposure.
- **Investigative and policy researchers** analysing industry-to-physician money.
- **Healthcare due-diligence teams and journalists** profiling a physician,
  teaching hospital or manufacturer.
- **AI agents** needing a "look up Open Payments" tool.

### What it does

Resolves the correct CMS General Payment detailed dataset from the live CMS
catalogue (program years **2019-2025** as of 2026-08-01), runs four live drift
assertions, queries the datastore, and aggregates each recipient x manufacturer
pair into one row: total dollars, payment count, first/last payment dates,
natures of payment, products and NDC codes, and CMS compliance indicators
(dispute status, third-party payment, physician ownership, delayed publication).
Optional NPPES specialty enrichment.

### Example input

```json
{
  "lastName": "smith",
  "firstName": "john",
  "programYear": "2025",
  "includeSpecialty": true,
  "maxResults": 500
}
```

Look up one physician precisely (the recommended mode):

```json
{ "npi": "1659344299" }
```

Profile a manufacturer:

```json
{ "manufacturer": "Genentech", "maxResults": 500 }
```

### Inputs

| Input | Type | Notes |
|---|---|---|
| `lastName` | string | Covered-recipient last name. **Substring** match - `Smith` also matches Goldsmith, Nesmith, Smithers. |
| `firstName` | string | Covered-recipient first name, substring match. |
| `npi` | string | Exact 10-digit NPI. The most precise lookup. |
| `profileId` | string | Exact CMS covered-recipient profile ID. |
| `manufacturer` | string | Manufacturer / GPO name, substring match. |
| `programYear` | string | e.g. `2025`. Blank = most recent published year. **A year CMS does not publish now fails the run and lists the available years** instead of silently answering about a different year. |
| `includeSpecialty` | boolean | Best-effort NPPES specialty enrichment. Reported in `nppes_status`. |
| `maxResults` | integer | Cap on raw CMS rows pulled before roll-up. CMS pages via SQL OFFSET, which slows sharply past a few thousand rows. |

At least one filter is required. An unfiltered run fails immediately - nothing is
emitted and nothing is billed.

### Output fields

One row per (recipient x manufacturer) relationship. **70 declared fields.**

#### Recipient

| Field | Description |
|---|---|
| `recipient_name` | Physician full name, or the teaching hospital name when the recipient is a hospital. |
| `recipient_npi` | 10-digit NPI, if disclosed. |
| `recipient_profile_id` | CMS covered-recipient profile ID. |
| `recipient_type` | e.g. Covered Recipient Physician, Covered Recipient Teaching Hospital. |
| `recipient_key_basis` | How the recipient was identified for grouping: `npi`, `profile_id`, `teaching_hospital_id`, `teaching_hospital_name`, `person_name`, `unidentified`. |
| `recipient_city` / `recipient_state` / `recipient_zip_code` / `recipient_country` | Primary business address. |
| `recipient_specialty` | Specialty from CMS slot 1, optionally NPPES-enriched. |
| `recipient_specialties_all` | All six CMS specialty slots, deduped. |
| `teaching_hospital_name` / `teaching_hospital_ccn` / `teaching_hospital_id` | Populated when the recipient is a teaching hospital. Verified populating input: `{"manufacturer":"Genentech","maxResults":500}`. |

#### Payer

| Field | Description |
|---|---|
| `manufacturer_name` | Manufacturer or GPO that made the payments. |
| `manufacturer_id` | CMS payer ID. |
| `manufacturer_state` / `manufacturer_country` | Payer location. |
| `submitting_manufacturer_name` | The entity that submitted the record - frequently different from the payer. |

#### Money

| Field | Description |
|---|---|
| `total_amount_usd` | Sum of published payment amounts in the relationship. |
| `payment_count` | Number of CMS **rows** aggregated. |
| `payments_included_total` | Sum of CMS `number_of_payments_included_in_total_amount` - the payments those rows represent. |
| `largest_single_payment_usd` | Largest individual payment. |
| `mean_payment_usd` | Mean across rows with a published amount. |
| `amount_rows_unparseable` | Rows where CMS published no usable amount. Excluded from the sum, never counted as $0. |
| `first_payment_date` / `last_payment_date` | Date range, YYYY-MM-DD. |
| `intensity` | Human-readable "$X across N payment(s)". |

#### Payment character

| Field | Description |
|---|---|
| `natures_of_payment` / `distinct_natures_count` | Consulting fee, food and beverage, travel, royalty, etc. |
| `forms_of_payment` | Cash or cash equivalent, in-kind items and services, etc. |
| `products` | Product names from CMS slot 1 (legacy field, unchanged). |
| `products_all` / `distinct_products_count` | Product names across all five CMS slots. |
| `product_types` | Drug, Biological, Device, Medical Supply. |
| `product_therapeutic_areas` | CMS product category / therapeutic area. |
| `product_ndc_codes` | NDC codes CMS associates with the products. |
| `covered_product_payment_count` / `noncovered_product_payment_count` | Covered vs non-covered product rows. |
| `related_product_payment_count` | Rows CMS ties to at least one covered product. |
| `contextual_information_samples` | Up to five free-text notes the manufacturer attached. |
| `record_ids_sample` | Up to 25 CMS `record_id` values so any figure can be traced to source rows. |
| `change_types` | CMS `change_type`: NEW, CHANGED, UNCHANGED. |

#### Compliance indicators

| Field | Description |
|---|---|
| `disputed_payment_count` / `any_payment_disputed` | Payments the recipient disputed with CMS. |
| `delayed_publication_count` | Rows CMS flagged for delayed publication. |
| `physician_ownership_payment_count` | Rows with the CMS physician-ownership indicator. |
| `third_party_payment_count` / `third_party_entities` | Payments routed to a third party. |
| `charity_payment_count` | Rows with the CMS charity indicator. |

#### Completeness, provenance and per-source status

| Field | Description |
|---|---|
| `matching_rows_total` | The **exact** number of CMS rows matching your query. |
| `rows_fetched` | Rows actually retrieved and aggregated. |
| `rows_coverage_pct` | `rows_fetched` as a percentage of the match count. Null when unknown - never 0 as a stand-in. |
| `totals_are_partial` | True when fewer rows were retrieved than CMS reports matching, so the money figures are **lower bounds**. |
| `result_completeness` | `complete` or `partial`. |
| `completeness_note` | Plain-language statement of what the row does and does not cover. |
| `query_stopped_by` | `exhausted`, `max_results`, `time_budget` or `page_error`. |
| `name_match_mode` | `exact_key` (NPI / profile ID) vs `substring` (name) vs `manufacturer_substring`. |
| `cms_datastore_status` | `ok`, `partial`, `partial_error`. Never blank. |
| `cms_datastore_error` | Error text when a page failed mid-run. Null on a healthy run - that is good news, not a dead column. |
| `nppes_status` | `not_requested`, `ok`, `partial`, `unavailable`. A dead NPPES is reported here, never rendered as "no specialty". |
| `dataset_identifier` / `dataset_title` | The CMS dataset that answered the query. |
| `program_year_requested` / `program_year_resolved` | What you asked for vs what was queried. |
| `program_year` | Reporting year from the CMS rows. |
| `payment_publication_date` | Latest CMS publication date across the rows - the data vintage. |
| `retrieved_at` | UTC timestamp of the query. |
| `source_system` / `source_url` | Provenance. |

### Reliability - what this Actor asserts live, on every run

An offline test cannot see a silently degraded upstream. Before any billable row
is emitted, this Actor fails the run if any of these regress. Every measured
value is logged.

1. **Column presence** - all 91 CMS columns it reads must still exist. A CMS
   rename returns HTTP 400 `Column not found.`, which previously became zero rows.
2. **Dataset size** - the national file must be in a plausible band
   (measured 2026-08-01: 2025 = 16,131,856 rows; 2024 = 15,498,687).
3. **Negative control** - an impossible NPI must match **exactly 0** rows. If CMS
   ever starts dropping unsatisfied filters, every roll-up would silently become
   a slice of the whole national file.
4. **Positive canary** - `last_name LIKE %SMITH%` must match a plausible band
   (measured 2026-08-01: 89,441).

If the CMS datastore never answers, the run **fails** with an explicit message -
it never emits "no payments found", which is not the same fact.

### Limits you should know

- CMS pages via SQL `OFFSET`, which is O(n). Deep pulls are slow; a wall-clock
  budget stops very deep paging and flags the result partial.
- The datastore rejects `limit > 500` (HTTP 400), so pages are 500 rows.
- A name search is a **substring** sweep. Use `npi` or `profileId` for a
  single-physician answer.
- These are CMS **general** payments. Research payments and ownership/investment
  interest are separate CMS files and are not included.

### Use as an MCP tool

Available to AI agents through Apify's MCP server. Field-level descriptions are
written for tool selection: an agent can read `result_completeness` and
`matching_rows_total` to decide whether to re-query with a narrower filter.

### Pricing

Pay per result: one event per emitted (recipient x manufacturer) relationship
row. A run that fails a drift assertion or never reaches CMS emits nothing and
bills nothing.

### FAQ

**Why did my NPI lookup return nothing before?** Until v1.1 the input schema
carried a server-side default of `lastName: "Smith"`, which Apify injected into
every run - so an NPI-only lookup executed as `npi = X AND last_name LIKE
%SMITH%`. That default is gone.

**Are these totals complete?** Only when `result_completeness` is `complete`.
Check `totals_are_partial` before quoting a figure.

**Is a payment here evidence of wrongdoing?** No. Open Payments is a
transparency disclosure. This is a screening and research tool, not a
determination about any individual.

### Related actors

- **CMS Part D Prescriber Delta** - year-over-year Medicare Part D drug spending.
- **License Verifier** - multi-state clinician licence and OIG-exclusion checks.
- **CMS NADAC Price Movers** - drug acquisition-cost movement.

# Actor input Schema

## `lastName` (type: `string`):

Covered-recipient (physician) last name to search for. Case-insensitive SUBSTRING match, so Smith also matches Goldsmith, Nesmith and Smithers. Combine with firstName to narrow. Leave blank to search by NPI, profile ID or manufacturer only. NOTE: this field deliberately has no schema default -- a default is injected server-side into every run, which silently AND-ed every NPI-only lookup with a last-name filter.

## `firstName` (type: `string`):

Covered-recipient (physician) first name. Case-insensitive substring match. Best used together with lastName.

## `npi` (type: `string`):

Exact 10-digit National Provider Identifier of the physician. The most precise way to look up one physician's payments.

## `profileId` (type: `string`):

Exact CMS Open Payments covered-recipient profile ID, if you already have it (an alternative precise physician key to NPI).

## `manufacturer` (type: `string`):

Name (or part of the name) of the drug/device manufacturer or GPO making the payment. Case-insensitive substring match. Use alone to profile a manufacturer, or with a physician filter to narrow to one relationship.

## `programYear` (type: `string`):

Reporting year to query, e.g. 2024. Leave blank to automatically use the most recent General Payments dataset published by CMS.

## `includeSpecialty` (type: `boolean`):

When on, look up each physician's specialty from the NPPES NPI registry when the payment record lacks one. Best-effort; never fails the run.

## `maxResults` (type: `integer`):

Upper bound on raw payment rows fetched (in pages of 500) before aggregating. NOTE: CMS paginates via SQL OFFSET, which slows sharply past ~2000 rows; a time budget stops very deep pulls and returns partial results. Narrow the filter (add firstName/NPI/manufacturer) for complete deep results.

## Actor input object example

```json
{
  "lastName": "Smith",
  "includeSpecialty": false,
  "maxResults": 500
}
```

# Actor output Schema

## `results` (type: `string`):

Physician x manufacturer payment rollups, in the default dataset.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "lastName": "Smith",
    "maxResults": 500
};

// Run the Actor and wait for it to finish
const run = await client.actor("malonestar/cms-open-payments").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "lastName": "Smith",
    "maxResults": 500,
}

# Run the Actor and wait for it to finish
run = client.actor("malonestar/cms-open-payments").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "lastName": "Smith",
  "maxResults": 500
}' |
apify call malonestar/cms-open-payments --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=malonestar/cms-open-payments",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/ead1rqx9fNsTwIeBh/builds/O4x0NGd6szFDTuubg/openapi.json
