API reference · /v1

Small, versioned, changed only by adding.

The surface a customer's own system calls. Keys belong to an organization, carry scopes, and are metered on two things at once: the calls they make and the rows those calls return. Every number the API holds you to is in the response headers.

The machine-readable document

GET /openapi.json on the API host, no key needed. It is an OpenAPI 3.1 document of exactly the routes below - the /v1 paths and the shapes they return, and nothing else - generated from the code that answers your calls, so it cannot describe a version we are not running. Point a client generator at it.

From an AI assistant

There is an MCP server for this API, so an assistant can answer questions out of your account with your own key and your own allowance: thirteen read-only tools over provider search, one provider by NPI, the specialty, procedure and drug lookups, facilities, health systems, the published lists, and what your plan has left. It runs on your machine, it is configured in one block, and it is metered exactly as the routes below are - one call, and one row for every record a response names.

It deliberately cannot reveal a provider's contact details, never returns a home address or a cell phone, cannot start an export or an append, and fetches one page of at most fifty records per call rather than walking pages.

Configuring it is one block in your MCP client: run npi-mark-mcp, with your key in NPI_MARK_API_KEY and this API's host in NPI_MARK_API_BASE. Both are on your API keys page. The server's own README - setup, example questions, the full list of tools and what each one costs - ships beside it as integrations/mcp/README.md.

Authentication

Authorization: Bearer npi_live_<prefix>_<secret>

Keys are issued in your account settings and shown once. A key belongs to an organization, not a person, and carries scopes that can never exceed the permissions of whoever created it. Every failed authentication is the same 401 with the same message.

Scopes, each granted only to a key created by somebody who already holds the matching permission: providers.read for search, lookup, facilities, lists, systems and the A-Z; contacts.reveal to open one provider's contact details; providers.export to start an export; append.run to submit a file to append; jobs.read to follow a job and download what it produced; usage.read for the plan and the month. Home address and cell phone are not available through the API: on the Contact plan they are revealed in the app, one provider at a time, and an API reveal lists them as restricted.

What a call is checked against, in order

CheckRefusalMeaning
Subscription is live402 subscription_inactiveCancelled or suspended. Nothing was read.
Pace, calls a minute per key429 rate_limitedToo fast. Retry-After says when to come back.
Volume, calls a month per organization409 quota_exceededOut of plan. The message carries both numbers and says which window it was: the month, or a paid plan's daily cap, which clears at midnight UTC.
Volume, rows a month per organization409 quota_exceededThe page asked for is bigger than the rows left. Ask for a page that fits and it is served whole; a page is never shortened to make it fit.
Scope403 forbiddenThe key lacks the scope. Keys cannot be widened; issue a new one.
Home address and cell phonerestrictedNever returned to a key. Listed in restricted on a reveal, so an absence is never mistaken for nothing on file.

Pace and volume are deliberately different answers. A 429 means retry in a minute; a 409 means today will not work no matter how long you wait.

Headers on every successful call

HeaderMeaning
X-RateLimit-Limit, -Remaining, -ResetCalls a minute allowed for this key, what is left, and when the minute clears
X-Quota-Limit, -Used, -RemainingThe month, against the plan
X-Quota-PeriodFirst day of the period the quota counts
X-Quota-Rows-Limit, -Used, -RemainingThe month's rows, against the plan — the same allowance an export spends. As they stood when the request arrived, before this call's own rows were charged.

A plan with no limit sends no header rather than a header you cannot trust.

Endpoints

There are 30 of them, and this is all of them. Version 1 changes only by adding.

CallScopeWhat it answers
GET /v1/providersproviders.readSearch providers and organizations, with a true total.
GET /v1/providers/{npi}providers.readOne provider, the same row search returns.
POST /v1/providers/{npi}/revealcontacts.revealOpen one provider's contact details. Costs a call and a reveal.
GET /v1/providers/{npi}/leadershipproviders.readAn organization's leadership contacts counted by role. No names.
GET /v1/organizations/{npi}/profileproviders.readLinked providers by specialty, EHR, facilities, locations, system, similar organizations.
GET /v1/facilitiesproviders.readCertified facilities still in the program, by state, town, type, name or NPI.
GET /v1/facilities/{ccn}providers.readOne facility, with leadership counted by role.
GET /v1/facilities/{ccn}/profileproviders.readProviders linked to it, its EHR, sibling locations, system and closest peers by size.
GET /v1/facilities/placesproviders.readWhere facilities of one type are, with counts, by state or by town.
GET /v1/facilities/indexproviders.readEvery active facility in CCN order, a few fields each. What a sitemap is built from.
GET /v1/leadership/countsproviders.readLeadership contacts by role, facility family and state, above the publication floor.
GET /v1/systemsproviders.readHealth systems and chains with five or more active facilities, largest first.
GET /v1/systems/{slug}providers.readOne system and its member facilities.
GET /v1/health-systemsproviders.readHealth systems, biggest first, with hospitals, beds and member facilities counted.
GET /v1/health-systems/{slug}providers.readOne health system: where it is, how big it is, and its members grouped by kind.
GET /v1/health-systems/{slug}/membersproviders.readOne kind of a system's members, paged, for a system with hundreds of them.
GET /v1/listsproviders.readThe published list catalogue: specialties and facility types with national counts.
GET /v1/lists/taxonomies/{slug}providers.readOne specialty list, nationally or in one state, with counts by state.
GET /v1/lists/facility-types/{slug}providers.readOne facility-type list, nationally or in one state.
GET /v1/directory/{kind}providers.readOne node of the A-Z of active providers or organizations.
GET /v1/taxonomiesproviders.readEvery specialty in the reference list, with how many providers hold it, biggest first.
GET /v1/taxonomies/classesproviders.readThe specialty headings with the providers under each. The short list for a dropdown.
POST /v1/exportsproviders.exportExport a provider search to CSV as a job, checked against the month's row allowance.
POST /v1/appendappend.runUpload a file of NPIs or names and get the provider fields you asked for added.
GET /v1/jobsjobs.readEvery export and append this key started, newest first.
GET /v1/jobs/{job_id}jobs.readWhere one job got to, and the counts worth reading before sending the file anywhere.
GET /v1/jobs/{job_id}/downloadjobs.readStream the file the job produced. Recorded against the key that fetched it.
GET /v1/usageusage.readThe plan and this month's spend, read from the same numbers a refusal quotes.
POST /v1/privacy/requestsprivacy.submitA consumer's opt-out, deletion or access request. Our own form's scope, not a key's.
POST /v1/privacy/gpcprivacy.submitA Global Privacy Control opt-out for a visitor the site can identify. Same scope.

GET /v1/providers

Scope providers.read. Filters: q (NPI, person or organization name), npi, first_name, last_name, organization_name, state (repeatable), city, postal_code (5 digits or a 3-digit prefix), taxonomy_code, taxonomy_classification, taxonomy_specialization, credentials, type, status, has_email, employer_domain, near_zip with radius_miles, plus page and page_size up to 200. Returns a page with a true total.

curl -H "Authorization: Bearer $KEY" \
  "https://<host>/v1/providers?state=TX&taxonomy_classification=Internal%20Medicine&page_size=50"

GET /v1/providers/{npi}

Scope providers.read. One provider as the same row search returns. 404 when no such record exists; the message names the usual cause, a wrong check digit.

Contact fields appear only for a key that clears both halves of the gate. For every other key the row does not carry them.

What each field on the row means, what it is for, and how complete it is, is on the field reference - one entry per field, with the export column header and the append field name beside the API name.

GET /v1/facilities and GET /v1/facilities/{ccn}

Scope providers.read. Certified facilities still operating. Filters: state, city (exact, with a state), facility_type (hospital, nursing_home, home_health, hospice, dialysis, other), name (prefix), npi, plus page and page_size up to 200. Each row carries type, category, name, address, county, main phone, ownership, chain, beds, star ratings, emergency services, the organisation NPI and when it was certified. One facility adds its leadership contacts counted by role - never names.

GET /v1/facilities/places counts one type by state, by town in a state, or by every town; GET /v1/providers/{npi}/leadership counts an organisation's leadership contacts by role.

GET /v1/leadership/counts counts leadership contacts by role (CEO, CFO, administrator, director of nursing, board member, owner and more), facility type and state, nationally or by state. Counts only; a combination with fewer than ten people, or spread over fewer than three facilities, is left out rather than reported.

GET /v1/directory/{providers|organizations}

Scope providers.read. The A-Z as a tree of name prefixes: prefix=SMITH J returns either the names under it (up to 1,000, paged) or the longer prefixes under it with counts. Active records only.

GET /v1/lists, /systems and the profiles

Scope providers.read. /v1/lists is the published catalogue - which specialties and facility types have enough behind them to be a list, with national counts and the states that have a page - and /v1/lists/taxonomies/{slug} and /v1/lists/facility-types/{slug} are one of them, nationally or in one state. A list below its floor is a 404 rather than a short list. /v1/systems and /v1/systems/{slug} are the health systems and chains with five or more active facilities. /v1/organizations/{npi}/profile and /v1/facilities/{ccn}/profile add what a profile page shows around a record: linked providers, the EHR, sibling locations, the system, and the closest comparable organizations.

GET /v1/health-systems

Scope providers.read. Health systems as their own records rather than as a field on a facility: name, headquarters, ownership word, bed total, and counts of hospitals, member facilities and providers. /v1/health-systems/{slug} adds the members grouped by kind - hospitals, nursing homes, home health and hospice, other certified facilities - each group capped at 100 with its real total beside it, and /v1/health-systems/{slug}/members pages the rest of one kind, so a system with 900 nursing homes is a sequence of requests rather than one enormous one. A member tied to a system only by a name match is not served: an inference printed as a fact is worse than a gap.

Two counts, and they are not the same number: counts.hospitals counts hospitals and counts.facilities counts every member kind. The provider count is counted as the call runs, with the suppression list applied, so it agrees with what a provider search for the same system returns.

Slugs are permanent. A system folded into another still resolves and answers a 308 with merged_into naming the target, following the chain to its end; the members route 404s for one, so nothing serves a merged system's content at two addresses. This is a different family from /v1/systems, which is the chain summary the profiles reference - both use the same slug for the same system, so neither URL changes hands.

POST /v1/providers/{npi}/reveal

Scope contacts.reveal. Opens one provider's contact details - the same function the app calls, so the two cannot drift apart. One provider a month per organization, and the second time in the month is free. It costs two allowances on purpose: one API call, because a request was answered, and one reveal, because contact data was handed over. Polling it for a provider you already opened spends calls and no reveals. It spends no rows: charging one disclosure twice under two names would make both numbers meaningless.

POST /v1/exports, POST /v1/append and the jobs

An export takes the filters GET /v1/providers takes, as a JSON object, validated against the same code - a mistake is a 422 now rather than a job that fails an hour later. An append takes a CSV, TSV or XLSX, matches on NPI (and on first and last name for rows without one), and returns your file with its columns and row order intact and only the fields you asked for added. Both are checked against the month's row allowance before anything is queued, and both refuse the home-address and cell-phone fields for every key.

Which columns an export can write and which fields an append can fill - and which fields are on one surface and not the other - are on the field reference, entry by entry, with the same list machine-readable.

A file can carry the published Medicare summary as six columns: the three highest-volume procedures and the three highest-volume drugs, each with its volume, one cell each, and four sizes. Ask for them by name in columns (top_procedures, procedure_services, procedure_codes, top_drugs, prescribing_claims, prescribing_drugs) or as the claims preset, and by the same names in an append's fields. Values inside a cell are separated by ; and each carries its volume in brackets; every one of those headers ends with the data year. They widen a row and never add one, so the file costs the same rows with them as without. A provider with nothing published reads No published volume rather than an empty cell, and about one clinician in six has anything published - the individual lines, with the published averages, stay on their own per-provider call.

Scope jobs.read then follows it: GET /v1/jobs lists the jobs this key started, GET /v1/jobs/{job_id} carries the counts worth reading before you send a file anywhere - rows, suppressed rows, matched and unmatched - and GET /v1/jobs/{job_id}/download streams the file. A job that is not this key's is a 404, an unfinished one a 409, and a file past its retention a 410 that says so rather than an empty download.

GET /v1/taxonomies and /v1/usage

Build a specialty picker from /v1/taxonomies and /v1/taxonomies/classes rather than from a pasted list: the reference list gains codes, and the counts are the same suppression-aware counts search returns, so the number beside a specialty is the number you will get. /v1/usage (scope usage.read) reads the plan and the month's spend from the same entitlements every refusal quotes. A limit is null for unlimited and 0 for nothing at all - read enforced rather than testing for null.

POST /v1/privacy/requests and /v1/privacy/gpc

Scope privacy.submit, which is not issued with a customer key: these two are what our own removal form calls when a member of the public asks to be opted out, deleted or told what we hold, and what records a browser's "do not sell" signal for a visitor we can name. They are in the table above because it is the whole of version 1, and a gap in a list that promises to be complete is the one thing a reader cannot check.

They sit outside the billing gate and are metered by nothing. A person's request not to be sold, or to be deleted, does not depend on whether an invoice was paid, and a privacy request that bounced off a payment refusal is a regulator's finding rather than a billing event. Both answer 202 with a reference whatever we hold, because telling a stranger "we have no record of you" is itself an answer about somebody.

Metering

A call the API answers adds one to the month's calls, and the records it returns are charged against the month's rows — the same allowance an export spends, because a row read through a key is the same row. Both are written in the same transaction as the read and before the answer leaves, so nothing is handed over unmetered and nothing is metered that was not handed over.

A row, on this surface, is a record about a provider, an organization, a facility or a health system that the response names. What that works out to:

What you asked forRows
A page of resultsOne per record returned. Fifty for a page of fifty, twelve for a page of twelve. page_size is checked against the rows you have left before anything is read, so a page bigger than the remainder is a 409 rather than a short page.
One record by NPI, CCN or slugOne.
A record with others named beside it — a profile, a systemOne per record named, the subject included. The counts beside them, none.
A result that found nothingNone. Nothing was handed over.
Counts and index pages that name no record — specialties, places, leadership counts, the published lists, your own jobsNone.
A 404, or a 308 from a folded system slugNone, and one call. The lookup happened, which is why the call is metered; no record was named, which is why there are no rows.
A refusal — 402, 403, 409, 429None, and no call either. Being told no costs nothing.

Nothing is charged twice. Starting an export or an append costs one call and no rows; its rows are charged when the job writes them, and downloading the file it produced — again, as often as you like — costs neither. A reveal spends a call and a reveal and no rows. The converse holds too: a work email arriving in an ordinary row is metered as a row and never touches the reveal allowance.

Not here yet

  • Suppression uploads. Send them to us and they are applied for everyone.
  • Consumer audiences. Counting and exporting them is an app screen and a contract, not a key.

Keys are issued from account settings. Create an account or see plans.