SimpleNPI API

The SimpleNPI API files Type 1 NPI applications with CMS on an applicant's behalf, as a registered surrogate, and reports the result. A partner creates an application. SimpleNPI collects what CMS requires directly from the applicant on a hosted page, files it, and returns the issued NPI by webhook or on request.

Design preview, not yet live.This reference describes the v1 contract as designed on Tue Oct 27, 2026. Base URLs, keys and the sandbox are not provisioned. Field names may change before launch. The data boundaries in Data handling will not.
You do not have to send applicant data.An application can be created with nothing but your own reference. Every applicant field in this API is optional prefill, and the hosted page collects whatever you leave out. See Data handling for exactly what is sent, collected, returned and kept.

Request flow

1 · PartnerPOST /applications

Your reference, optional prefill, return URL.

2 · SimpleNPIRegistry lookup

An exact match on the public registry ends the application as npi_exists. Nothing is filed twice.

3 · ApplicantHosted page

Enters DOB, SSN, birth state and any missing fields. E-signs the surrogate authorization.

4 · SimpleNPIFiles with NPPES

Status moves to filing, then issued or pended.

5 · PartnerWebhook or GET

Receives the application id and status. Fetches the NPI with GET /applications/{id}.

Environments

EnvironmentBase URLKeys
Productionhttps://api.simplenpi.com/v1live_…
Sandboxhttps://sandbox.api.simplenpi.com/v1test_…

All requests and responses are JSON over HTTPS (TLS 1.2 or later). Timestamps are RFC 3339 in UTC. Identifiers are prefixed strings: app_, evt_, whk_.

Authentication

Send your API key as a bearer token on every request. Keys are issued per partner per environment and carry scopes. A request to an endpoint outside the key's scopes returns 403 forbidden.

curl https://api.simplenpi.com/v1/applications/app_8f2k… \
  -H "Authorization: Bearer live_sk_…"
ScopeGrants
lookupPOST /lookup, GET /verify/{npi}, GET /registry/{npi} at the keyed rate limit
applications:writeCreate, cancel, resend link, purge
applications:readRetrieve and list applications
webhooksManage webhook endpoints and replay events
  • Key rotation: two keys can be live at once. Create the new key, deploy it, then revoke the old one from the dashboard.
  • Lookup without a key: POST /lookup and GET /verify/{npi} also accept unauthenticated calls at the per-IP limit. Everything else requires a key.
  • Keys are secrets. Keep them server-side. The hosted link uses a separate signing secret, so no API key is ever placed in a browser.

Errors

Errors use conventional HTTP status codes and a single JSON shape. code is stable and safe to branch on. message is for logs and may change. field is present on validation errors. Every response carries X-Request-Id; include it when contacting support.

{
  "error": {
    "code": "invalid_request",
    "message": "mobile must be E.164",
    "field": "applicant.mobile"
  }
}
StatusCodeMeaning
400invalid_requestMalformed body or a field failed validation. field names it.
401unauthorizedMissing, revoked or wrong-environment key.
403forbiddenKey lacks the scope for this endpoint.
404not_foundNo object with that id for this partner.
409duplicatepartner_ref already exists. The body includes the existing id.
409invalid_stateAction not allowed in the current status, for example cancelling an issued application.
422idempotency_mismatchSame Idempotency-Key reused with a different request body.
429rate_limitedLimit exceeded. Honor Retry-After.
500internal_errorUnexpected failure. Safe to retry with the same idempotency key.
503registry_unavailableThe CMS registry is down. Lookups fail; application creation still succeeds and the lookup runs when the registry returns.

Versioning

The major version is in the path (/v1). Within a major version, changes are additive only. Your integration should tolerate these without a code change:

  • New fields on any object, new optional request parameters, new event types.
  • New values for status, pend_reason and error.code. Treat unknown values as "in progress" or "unknown error", not as failures.
  • Changes to message text and to field order.

Breaking changes ship as a new major version. The previous version stays available for at least 12 months after the new one is announced, with the end date published in the changelog at GET /v1/changelog and sent to the partner's technical contact.

Idempotency

Every POST accepts an Idempotency-Key header (any string up to 255 characters, a UUID is typical). A retry with the same key within 24 hours returns the original response, including the original status code, and performs no second action. The same key with a different body returns 422 idempotency_mismatch.

partner_ref is a second, permanent guard on application creation: it is unique per partner, and a later POST /applications with the same value returns 409 duplicate with the existing id. Use your own applicant or onboarding id as partner_ref so a retried or re-run onboarding step can never open two applications for one person.

POST /v1/applications
Authorization: Bearer live_sk_…
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
Content-Type: application/json

Rate limits

Endpoint groupLimitWindow
Lookup and verify, no key30 requests per IP1 minute
Lookup and verify, with key300 requests per key1 minute
Applications, create600 per key24 hours, rolling
All other authenticated calls600 per key1 minute

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds). A 429 includes Retry-After in seconds. Back off exponentially with jitter. Limits can be raised per partner on request. Polling GET /applications/{id} is allowed but webhooks are the intended path to status changes.

Data handling

This section is the contract most partners review first. It states, per data element, who supplies it, whether it ever leaves your systems, what comes back and how long SimpleNPI keeps it. The same terms appear in the data processing addendum.

Principles

  • Nothing about the applicant is required from you. POST /applications needs only partner_ref. Any applicant field you include is optional prefill; omit a field and the applicant enters it on the hosted page.
  • Prefill is field-level opt-in. Send a name and nothing else, or a name and an email, or nothing. There is no "send the whole profile" mode.
  • SSN, date of birth and birth state are never accepted by the API, never returned by it, and never included in a webhook. They exist only between the applicant's browser and SimpleNPI.
  • Webhooks are thin by default. A webhook carries the application id, your reference, the new status and a timestamp. Nothing about the person. You fetch details over your authenticated connection when you want them.
  • Returned data is registry data. The NPI, name as enumerated, taxonomy and practice address come back exactly as CMS publishes them in the public NPI Registry. SimpleNPI returns no other applicant data to the partner.
  • Single purpose. Applicant data is used to file the application, support it with the CMS Enumerator, and report status to you. It is not used for marketing to the applicant, not shared with any party other than CMS, and not sold.
  • Purge on request. DELETE /applications/{id}/applicant-data removes everything except the records CMS requires a surrogate to keep.

Data map

Data elementFrom partnerFrom applicantReturned to partnerRetained by SimpleNPI
Partner reference, return URLRequired—YesLife of the record
Legal nameOptional prefillYes, if not prefilledAs enumerated in the registryLife of the record
Email, mobileOptional prefillYes, if not prefilledNo30 days after terminal status, then deleted
Profession, state, license numberOptional prefillYes, if not prefilledTaxonomy and license as in the registryLife of the record
Mailing and practice addressOptional prefillYes, if not prefilledPractice address as in the registryLife of the record
Date of birth, birth stateNot acceptedYesNeverDeleted at terminal status
SSNNot acceptedYesNeverEncrypted at rest (KMS); deleted at terminal status
Signed surrogate authorization—E-signed on hosted pagePDF by URL on request7 years, as CMS requires of surrogates
NPI, registry record——YesLife of the record (public data)
Audit log (who did what, when)——On request7 years, contains no SSN or DOB
Lookup queriesName, state, license—Match resultNot stored. Only a rate-limit counter per key or IP.

"Terminal status" means issued, npi_exists, failed, cancelled or expired. "Life of the record" means until you purge it or the partner agreement ends, whichever comes first.

Security

  • Transport: TLS 1.2 or later everywhere. HSTS on all hosts. Webhooks are delivered to HTTPS endpoints only.
  • Storage: AWS, single US region. Datastore encrypted at rest; SSN in a separately keyed KMS-encrypted column with access logged.
  • Filing worker: runs in an isolated environment that holds the NPPES surrogate session. It has no inbound network access and reads one application at a time.
  • Hosted page: the applicant link is a signed, single-use token that expires after 14 days. The page requires a one-time code sent to the applicant's mobile before SSN entry.
  • Classification: applicant PII, no PHI. Covered by a data processing addendum; a BAA is not required. Subprocessors (cloud, SMS, email) are listed in the DPA.
  • Disclosure: every applicant-facing page and every human-readable response states that the NPI is free from CMS at nppes.cms.hhs.gov and that SimpleNPI is a private filing service.

Lookup

Lookup and verify wrap the public CMS NPI Registry (refreshed by CMS daily) with SimpleNPI's matching rules. They accept only data the registry already publishes. Queries are not stored. Both endpoints allow cross-origin requests from any browser origin, so they can be called from a web form directly.

POST/lookupNo key required
Finds existing NPIs for a person. Called automatically on POST /applications. Call it directly to check before you start an onboarding step.
Request
{
  "first": "Jordan",
  "last": "Rivera",
  "state": "UT",
  "license_number": "1234567-4701"  // optional
}
Response 200
{
  "matches": [{
    "npi": "1234567893",
    "type": "individual",
    "name": "Jordan Rivera",
    "state": "UT",
    "taxonomy": "225700000X",
    "status": "active"
  }],
  "confidence": "exact",
  "registry": { "source": "nppes", "refreshed": "daily" },
  "disclosure": "The NPI is free from CMS at nppes.cms.hhs.gov. SimpleNPI is a private filing service."
}
FieldTypeNotes
first, lastrequiredstringLegal name. Hyphens, apostrophes and suffixes are normalized. Omit both when searching by organization.
organizationoptionalstringBusiness search in place of first/last. 2 or more characters; a trailing * makes a prefix match. Matches legal and doing-business-as names.
staterequiredstringTwo-letter license or practice state.
license_numberoptionalstringRaises a match to exact when it matches the registry's license field. Individuals only.
matches[].typeenumindividual or organization. Organization matches carry the legal name and dba when one is on file.
confidenceenumexact (same name, state and discipline, or a license match), possible, none.
registryobjectOn every lookup and verify response: source and refreshed: "daily", so callers know how current the data is.
GET/verify/{npi}No key required
Returns the current registry record for an NPI in a normalized shape, with validation checks. Responses are cached for 15 minutes.
Response 200
{
  "npi": "1234567893",
  "status": "active",
  "enumeration_type": "NPI-1",
  "name": "Jordan Rivera",
  "taxonomy": [{ "code": "225700000X", "primary": true,
                 "state": "UT", "license": "1234567-4701" }],
  "practice_address": { … },
  "enumeration_date": "2026-10-27",
  "checks": { "taxonomy_matches_profession": true }
}
Notes

A deactivated NPI returns status: "deactivated" with deactivation_date. An NPI not in the registry returns 404 not_found. GET /registry/{npi} returns the unmodified CMS record for partners that store it.

Applications

An application is one NPI filing for one person. It is created by you, completed by the applicant, filed by SimpleNPI, and ends in a terminal status.

The application object

FieldTypeDescription
idstringUnique id, prefix app_.
partner_refstringYour id for this applicant. Unique per partner.
statusenumSee lifecycle below.
npistring, nullableTen digits. Set when status is issued or npi_exists.
issued_attimestamp, nullableWhen CMS enumerated the NPI.
registry_recordobject, nullableSame shape as GET /verify/{npi}. Public registry data only.
authorization_pdf_urlstring, nullableSigned URL to the e-signed surrogate authorization. Expires in 1 hour; request the object again for a fresh one.
applicant_linkstring, nullableHosted page URL while awaiting_applicant. Single use, signed.
link_expires_attimestamp, nullable14 days after creation or last resend.
pend_reasonenum, nullableSet while pended: name_mismatch, ssn_verification, address_review, enumerator_request, other.
etatimestamp, nullableEstimated resolution while pended.
eventsarrayStatus history: [{at, type}].
created_at, updated_attimestamp

The object never contains the applicant's email, mobile, date of birth, SSN or anything else entered on the hosted page.

Lifecycle

awaiting_applicant→ready→filing→issued
orfiling→pended→issued / failed
terminal without filing:npi_existscancelledexpired
  • awaiting_applicant: link created. Ends as expired if unused for 14 days.
  • ready: the applicant finished the hosted page and signed. Filing starts within one minute.
  • filing to issued: usually within 10 minutes. Submissions made overnight issue by about 6 am Eastern.
  • pended: CMS asked for review. SimpleNPI works it with the Enumerator; eta is updated as it changes. Can take days.
  • failed: CMS rejected the application and it cannot be corrected. The reason is in the last event.
POST/applicationsapplications:write
Creates an application and runs the registry lookup. Returns 201 with an applicant_link, or 201 with status: "npi_exists" and npi set when the lookup finds an exact match. Only partner_ref is required.
Request, minimal
{
  "partner_ref": "hw-48213",
  "return_url": "https://partner.com/onboarding/npi"
}
Request, with prefill
{
  "partner_ref": "hw-48213",
  "return_url": "https://partner.com/onboarding/npi",
  "applicant": {
    "first": "Jordan", "last": "Rivera",
    "email": "jordan@example.com",
    "profession": "massage_therapist",
    "state": "UT"
  },
  "options": { "notify": false }
}
Response 201
{
  "id": "app_8f2k…",
  "partner_ref": "hw-48213",
  "status": "awaiting_applicant",
  "applicant_link": "https://simplenpi.com/a/<signed>",
  "link_expires_at": "2026-11-10T17:00:00Z",
  "npi": null,
  "lookup": { "confidence": "none" },
  "created_at": "2026-10-27T17:00:00Z"
}
ParameterTypeNotes
partner_refrequiredstringYour applicant or onboarding id. Unique per partner. Duplicate returns 409 with the existing id.
return_urloptionalstringWhere the hosted page sends the applicant when done. Must be HTTPS and on a domain registered to your partner account.
applicant.first, applicant.lastoptionalstringPrefills name. Both or neither.
applicant.emailoptionalstringPrefills contact. Required only if options.notify is true and mobile is absent.
applicant.mobileoptionalstring, E.164Prefills contact. Required only if options.notify is true and email is absent.
applicant.professionoptionalenumMaps to a taxonomy code. GET /professions lists supported values.
applicant.state, applicant.license_numberoptionalstringPrefills license. Improves the lookup when present.
addresses.mailing, addresses.practiceoptionalobjectPrefills addresses. Practice defaults to mailing. The applicant can edit.
options.notifyoptionalboolean, default falseWhen true, SimpleNPI sends the link to the applicant by email or text. When false, you present applicant_link yourself and SimpleNPI contacts the applicant only for the one-time code on the hosted page.
options.skip_lookupoptionalboolean, default falseSkips the pre-check. The lookup still runs after the applicant completes the page, so nothing is filed twice.
GET/applications/{id}applications:read
Retrieves an application. This is the call to make after a webhook. Also accepts GET /applications/by-ref/{partner_ref}.
Response 200
{
  "id": "app_8f2k…",
  "partner_ref": "hw-48213",
  "status": "issued",
  "npi": "1234567893",
  "issued_at": "2026-10-27T17:41:09Z",
  "registry_record": { … },
  "authorization_pdf_url": "https://…",
  "pend_reason": null,
  "eta": null,
  "events": [
    { "at": "2026-10-27T17:00:00Z", "type": "awaiting_applicant" },
    { "at": "2026-10-27T17:33:12Z", "type": "ready" },
    { "at": "2026-10-27T17:33:40Z", "type": "filing" },
    { "at": "2026-10-27T17:41:09Z", "type": "issued" }
  ]
}
Other application endpoints
GET /applicationsList. Filters status, created_after, created_before. Cursor pagination: limit (max 100) and starting_after; response has has_more.
POST /applications/{id}/cancelAllowed while awaiting_applicant or ready. Otherwise 409 invalid_state.
POST /applications/{id}/resend-linkIssues a new signed link and extends link_expires_at. Invalidates the previous link.
DELETE /applications/{id}/applicant-dataPurges contact, address and prefill data now rather than on the retention schedule. Keeps the id, status, NPI, and the signed authorization CMS requires. Returns 204.
GET /professionsSupported profession values and their taxonomy codes.

Webhooks

Webhooks notify your endpoint when an application changes status. Payloads are thin by default: they identify the application and the new status and carry no applicant data. Fetch the full object with GET /applications/{id} over your authenticated connection.

Endpoints

EndpointNotes
POST /webhooksBody {url, events, payload}. url must be HTTPS. events is a list or ["*"]. payload is "thin" (default) or "full". Returns the endpoint with a secret, shown once.
GET /webhooks, DELETE /webhooks/{id}List and remove endpoints.
POST /webhooks/{id}/rotate-secretReturns a new secret. The old one keeps verifying for 24 hours.
POST /events/{event_id}/replayRe-delivers one event to all matching endpoints.

Event types

TypeSent when
application.awaiting_applicantLink created (only when options.notify is true, so you know the applicant was contacted).
application.readyApplicant completed and signed the hosted page.
application.issuedCMS enumerated the NPI.
application.pendedCMS asked for review. Also sent when pend_reason or eta change.
application.failedCMS rejected the application.
application.expiredLink unused for 14 days.
application.cancelledCancelled by you or by the applicant on the hosted page.
POSTyour endpointapplication.issued
Payload, thin (default)
{
  "id": "evt_01J…",
  "type": "application.issued",
  "created_at": "2026-10-27T17:41:09Z",
  "data": {
    "application_id": "app_8f2k…",
    "partner_ref": "hw-48213",
    "status": "issued"
  }
}
Payload, full (opt in)
{
  …,
  "data": {
    "application_id": "app_8f2k…",
    "partner_ref": "hw-48213",
    "status": "issued",
    "npi": "1234567893",
    "issued_at": "2026-10-27T17:41:09Z",
    "registry_record": { … }
  }
}
Headers
Content-Type: application/json
X-SimpleNPI-Event: application.issued
X-SimpleNPI-Delivery: dlv_01J…
X-SimpleNPI-Signature: t=1761586869,v1=<hex hmac-sha256>
Verify (Node)
const crypto = require('crypto');

function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(
    header.split(',').map(p => p.split('=')));
  if (Math.abs(Date.now()/1000 - Number(t)) > 300) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(v1));
}

Delivery rules. Respond 2xx within 10 seconds; do your work asynchronously. Anything else, or a timeout, is retried at 1 minute, 10 minutes, 1 hour, 6 hours and 24 hours, then the endpoint is marked failing and the technical contact is emailed. Deliveries are at-least-once and not guaranteed in order: dedupe on id and treat GET /applications/{id} as the source of truth for current status. Verify the signature over the raw request body before parsing JSON.

Testing

The sandbox is the same API at the sandbox base URL with test_ keys. Nothing is sent to CMS.

  • Deterministic outcomes. The sandbox filer decides from the applicant's last name entered on the hosted page: Issued issues a test NPI (1999999xxx) 30 seconds after ready; Pended pends with pend_reason: "name_mismatch" and issues after 5 minutes; Failed fails; Exists returns npi_exists on creation. Any other name issues.
  • Test SSNs. The hosted page accepts 000-xx-xxxx in the sandbox and rejects real-looking values.
  • Hosted page shortcut. POST /applications/{id}/complete (sandbox only) fills and signs the hosted page as the applicant, so an integration test needs no browser.
  • Webhooks. Register a sandbox endpoint, or use the dashboard's request log and replay button. The signing scheme is identical to production.
  • Rate limits are the same as production so a load test behaves the same way.

Downloads

Available with a sandbox key.

  • OpenAPI 3.1 at GET /v1/openapi.json, and a Postman collection generated from it.
  • Signature verification samples in Node, Python and Ruby.
  • Data processing addendum with subprocessor list, retention schedule (the table in Data handling) and breach notification terms.
  • Security questionnaire answers (SIG Lite format), W-9, and proof of insurance.
  • Status page and changelog at GET /v1/changelog.