Developer reference

Provider intelligence, as a REST API.

RESTful JSON API for programmatic access to dental provider data, territory triggers, market intelligence, license monitoring, and practice scoring. 273,623 providers across all 50 states, 41 enriched.

Authentication

Include your API key in the Authorization header of every request.

Authorization: Bearer ps_live_your_api_key_here

API keys are generated in Settings → API Keys in your dashboard. Keys start with ps_live_. The full key is shown only once at creation, so store it securely.

Dashboard users can also access the API via session cookies (automatic when logged in). API key authentication is required for programmatic access.

No API key? Two alternatives

Rate Limits

Rate limits are applied per API key using a sliding window algorithm. Exceeding the limit returns HTTP 429 with rate limit headers.

API Starter

60 req/min

$99/mo

API Pro

500 req/min

$299/mo

Team (Dashboard)

200 req/min

$499/mo

Enterprise

1,000 req/min

$1,999+/mo

Response Headers

X-RateLimit-Limit: 100 # Max requests per window X-RateLimit-Remaining: 87 # Requests remaining X-RateLimit-Reset: 1712000000 # Window reset timestamp (ms)

API Scopes

API keys are issued with specific scopes that control which endpoints they can access. Attempting to access an endpoint without the required scope returns HTTP 403.

readread

Search providers, retrieve profiles, alerts, market intelligence data. Included in all API keys.

Endpoints: All GET endpoints except exports

exportexport

Bulk data export (CSV and JSON). Required for /api/providers/export and /api/scoring/export.

Endpoints: Export endpoints

writewrite

Create/update saved searches, profile, preferences. Required for POST/PATCH/DELETE on user data.

Endpoints: Saved searches, profile, preferences, onboarding

Plan tier gating (separate from scopes)

A small number of endpoints require a specific plan tier beyond the scope check. These are flagged with a Team+ badge in the endpoint list below. Rep plan keys receive a 403 when hitting these endpoints with an explanatory error message. The Team+ tier covers Team, Enterprise, and any API plan that bundles Team-level data access.

403 Error Example

{ "data": null, "error": "Insufficient scope. Required: \"export\". Your key has: [\"read\"]." }

Response Convention

Every read endpoint returns a JSON envelope with a stable shape. Success responses always include both data and error keys. Error responses use the same envelope with data: null. Never check !response.data alone — an empty list is a valid success.

Success

{ "data": [...] | { ... }, "error": null, "meta": { ... } // paginated endpoints }

Error

{ "data": null, "error": "Human-readable message" }

Two endpoints add an extra wrapper field beyond the standard envelope: /api/demographics echoes the requested metric param, and /api/whitespace echoes the resolved state. Both still include data and error for consistent client handling.

The /api/preferences endpoints (account settings) and write endpoints that return {success: true} use simpler shapes appropriate to user-settings + mutation domains. Both are documented per-endpoint below.

Endpoints

Providers

GET/api/providersread

Search and filter dental providers. Supports full-text search, specialty filters, geography, license status, DSO affiliation, Medicare billing history, practice size, and pagination (offset or cursor-based).

ParameterTypeDescription
qstringFull-text search across name, NPI, city, ZIP, organization
statestringState abbreviation. Comma-separated for multi-state (e.g. TX,CA,FL)
citystringCity name (partial match)
zipstringZIP code prefix match
specialtystringDental specialty (e.g. general-dentistry, orthodontics)
sole_propbooleanFilter sole proprietors. true or false
independentbooleanFilter independent practices (no parent organization)
is_dsobooleanFilter DSO-affiliated providers. true or false
has_cmsbooleanFilter providers with CMS Medicare Part B billing history. Approximately 1.76% of dental providers nationally (mostly oral surgery and maxillofacial).
practicestringPartial match on inferred practice name (the co-location-derived practice attribution).
min_practice_sizeintegerMinimum number of co-located providers at the same practice address (e.g. 5 = group practices only).
license_statusstringLicense status (Active, Expired, Suspended, Cancelled, Deceased, Retired, Vol Surrender). Comma-separated for multiple.
exp_monthsintegerLicense expiring within the next N months (forward-looking).
expired_withinintegerLicense expired within the last N months (backward-looking; useful for recently-inactive providers).
disciplinebooleanHas disciplinary action on record. Bifurcated match: rows are returned when has_disciplinary_action OR has_historical_discipline is true. Same semantics on /api/providers/map, /api/providers/export, /api/scoring, and /api/scoring/export.
enriched_onlybooleanOnly providers with state board license enrichment
anesthesia_levelintegerMinimum anesthesia permit level (1-4)
entity_typestringNPI entity type: 1 (Individual) or 2 (Organization)
parent_orgstringParent organization name (partial match)
grad_beforeintegerGraduation year <= value (TX-specific retirement filter; legacy — prefer retirement_risk for state-agnostic behavior).
retirement_riskbooleanState-agnostic retirement filter. true matches providers approaching retirement via any of three signals: graduation_year (TX) <= ~26 years before estimated age 60, age_range bucket 60-110 (FL), or license_issue_date (CO) more than 34 years ago. Recommended over grad_before since it covers every enriched state.
excludedbooleanOIG LEIE federal exclusion filter. true = only excluded providers (compliance-screening workflow). false = hide excluded. Unset = include with a danger badge (platform default — no silent data removal).
has_emailbooleanProvider email coverage filter. Useful for outreach segmentation where the state board publishes email (e.g. FL MQA). true = only rows with non-null provider_email. false = rows without.
sedation_permitstringSedation permit filter. "__any__" = providers holding any sedation permit (sedation_permits IS NOT NULL). Any other value matches rows whose sedation_permits text[] array contains that exact permit string (e.g. "Moderate Sedation", "General-Deep Sedation", or state-specific codes like "081" / "082" for CA).
include_suppressedbooleanDefault-off feedback suppression opt-out. By default, providers users have flagged as already-acquired or closed are excluded from results. Set to true to override and include them — useful for verifying that feedback was applied or for compliance audits.
radius_zipstringCenter ZIP for radius search (use with radius_miles)
radius_milesstringRadius in miles from radius_zip
sortstringSort column (default: last_name). Options: last_name, city, state, specialty, license_expiration_date, graduation_year
orderstringSort direction: asc (default) or desc
pageintegerPage number (default: 1). Ignored when using cursor.
per_pageintegerResults per page. Dashboard: 1-100, API keys: 1-1000 (default: 25)
cursorstringNPI of last item for cursor-based pagination. Forces order by NPI ascending. Use for bulk data iteration: stable under concurrent writes.

Returns { data, error, meta: { total, page, per_page, total_pages, next_cursor? } }

GET/api/providers/:npiread

Retrieve a complete provider profile by NPI number. Includes co-located providers at the same address and any resolved practice-level entities.

ParameterTypeDescription
npi*string10-digit National Provider Identifier (path parameter)

Returns { data: { ...provider, co_located: [...], practice_entities: [...] }, error }

GET/api/providers/mapread

Map-rendering endpoint: returns geocoded provider points within a bounding box, with enough fields to render clusters and popups. Capped at 30,000 results per request.

ParameterTypeDescription
bounds*stringBounding box as CSV: south,west,north,east (e.g. 25.5,-106.5,36.5,-93.5 for Texas)
statestringOptional state filter
specialtystringSpecialty filter
sole_propbooleanSole proprietors only
license_statusstringLicense status filter
enriched_onlybooleanOnly providers with state board enrichment
is_dsobooleanDSO-flagged filter
has_cmsbooleanMedicare billers only
disciplinebooleanBifurcated discipline filter (active OR historical) — same semantics as /api/providers.
grad_beforeintegerRetirement-age filter (TX-specific; FL/CO retirement signals not supported on the map endpoint — use /api/providers if you need them)
independentbooleanIndependent practices only
excludedbooleanOIG LEIE federal exclusion filter. true = only excluded, false = hide. Map markers color-code excluded rows when this is unset (platform default).
include_suppressedbooleanDefault-off feedback suppression opt-out. Set to true to include providers users have flagged as already-acquired or closed.

Returns { data: [{ npi, last_name, first_name, specialty, latitude, longitude, license_status, license_expiration_date, has_disciplinary_action, is_federally_excluded, co_located_count, inferred_practice_name, graduation_year, date_of_birth, is_dso, dso_detection, dso_signals, is_sole_proprietor, organization_name, entity_type_code, ... }], error }

Export

GET/api/providers/exportexport

Bulk export provider data as CSV or JSON. Every filter parameter on /api/providers is also accepted here, including excluded, has_email, sedation_permit, retirement_risk, and include_suppressed. JSON format supports cursor-based pagination for iterating through large datasets.

Trial behavior: Trial users are capped at 25 rows per export. Paid tiers get up to 5,000 rows (10,000 for API keys).

ParameterTypeDescription
formatstringResponse format: csv (default) or json
cursorstringNPI-based cursor for JSON pagination. Returns next_cursor in response.
per_pageintegerResults per page for JSON format (default: 1000, max: 10000 for API keys)
...Every /api/providers filter parameter is supported (q, state, city, zip, specialty, sole_prop, independent, is_dso, has_cms, practice, min_practice_size, license_status, exp_months, expired_within, discipline, enriched_only, anesthesia_level, entity_type, parent_org, grad_before, retirement_risk, excluded, has_email, sedation_permit, include_suppressed, radius_zip, radius_miles).

CSV: file download. JSON: { data, error, meta: { total, count, per_page, next_cursor } }

GET/api/scoring/exportexport

Export practice scores as CSV or JSON. Includes 6-factor scoring algorithm results. Every filter parameter on /api/providers is also accepted here.

Trial behavior: Trial users are capped at 25 rows per export. Paid tiers capped at 5,000 rows (10,000 for API keys). No cursor pagination on this endpoint — use /api/providers/export with sort=score if you need to iterate beyond the cap.

ParameterTypeDescription
formatstringResponse format: csv (default) or json
min_scoreintegerMinimum practice score (0-100)
...Every /api/providers filter parameter is supported (q, state, city, zip, specialty, sole_prop, independent, is_dso, has_cms, practice, min_practice_size, license_status, exp_months, expired_within, discipline, enriched_only, anesthesia_level, entity_type, parent_org, grad_before, retirement_risk, excluded, has_email, sedation_permit, include_suppressed, radius_zip, radius_miles).

CSV: file download. JSON: { data: [{ ...provider, score, score_factors }], error, meta: { total, count } }. JSON has no next_cursor — pagination is offset-only here.

Scoring

GET/api/scoringread

Practice scoring. Returns individual providers ranked by a six-factor algorithm: solo/independent status (25%), retirement risk (20%), practice vintage (15%), practice size (15%), license status (15%), and discipline history (10%). Each response row also includes a confidence score and (when applicable) Medicare Part B aggregates. Reps use the score to prioritize call lists; DSO biz-dev teams use the same score for acquisition target ranking.

ParameterTypeDescription
min_scoreintegerMinimum practice score threshold (0-100)
sortstringSort field. Options: score (default), last_name, city, enumeration_date, license_expiration_date, co_located_count, inferred_practice_name, cms_paid (CMS lifetime paid). Note: score and cms_paid sort modes paginate in memory after fetching up to a 30,000-row state cohort; use /api/providers (sort=last_name) for cursor pagination beyond that ceiling. cursor param is silently ignored on this endpoint.
orderstringSort direction: asc or desc (default: desc for score, asc for name)
...Every /api/providers filter + pagination parameter is supported (q, state, city, zip, specialty, sole_prop, independent, is_dso, has_cms, practice, min_practice_size, license_status, exp_months, discipline, enriched_only, anesthesia_level, parent_org, grad_before, retirement_risk, excluded, has_email, sedation_permit, include_suppressed, radius_zip, radius_miles, page, per_page).

Returns { data: [{ ...provider, score, score_factors, confidence, cms_medicare? }], error, meta }

Market Intelligence

GET/api/whitespaceread

Market whitespace analysis. Returns geographic areas scored by population-to-provider ratio, HPSA designation, and demographic factors. Supports state-wide or geo-scoped (ZIP+radius, city) queries.

ParameterTypeDescription
statestringState abbreviation (default: TX)
zipstringCenter ZIP for radius search (use with radius)
radiusintegerRadius in miles from zip (max 200)
citystringCity centroid for a 25-mile radius search (fallback when zip/radius aren't provided)
limitintegerMax results (default: 2000, max: 2000). ZIPs with provider_count <= 1 are filtered out before pagination — fully unserved ZIPs do not appear here; use /api/demographics for those.

Returns { data: [{ zip, provider_count, population, whitespace_score, ... }], error }

GET/api/reimbursementread

Medicaid fee schedule rates by CDT code and geography.

ParameterTypeDescription
statestringState abbreviation (default: TX)
codestringCDT code filter (e.g. D0120)
localitystringMedicare locality filter (e.g. "TEXAS HEALTH STEPS - DENTAL")
limitintegerMax results (default: 20, max: 2000)
GET/api/demographicsread

ZIP-level demographics from Census ACS data. Supports state-wide or geo-scoped queries. Returns 400 if `metric` is not one of the listed enum values.

ParameterTypeDescription
statestringState abbreviation
metricstringSort metric: total_population, median_household_income, median_age, pct_under_18, pct_65_plus
zipstringCenter ZIP for radius search (use with radius)
radiusintegerRadius in miles from zip
citystringCity centroid for a 25-mile radius search
limitintegerMax results (default: 200, max: 500)

Returns { data: [{ zip, ...demographic_fields }], error, metric }. The extra `metric` field echoes the requested sort metric so callers running parallel queries with different metrics can disambiguate responses without tracking request URLs.

GET/api/npdbread

Aggregate NPDB malpractice payment and adverse action trends by state and year. Anonymized, no individual provider data.

ParameterTypeDescription
statestringState abbreviation filter
GET/api/reports/marketread

Market summary report: total providers, specialty breakdown, license status distribution, top organizations, DSO share, practice-size distribution, retirement-age count, HPSA designations. Supports state-wide and geo-scoped (city, ZIP+radius) queries.

Trial behavior: Trial users are capped at 1 downloaded PDF brief per trial. Previews (page loads without for_brief=1) are unlimited.

ParameterTypeDescription
statestringState abbreviation (default: TX)
citystringCity name for a 25-mile radius scope
zipstringCenter ZIP for radius search (use with radius)
radiusintegerRadius in miles from zip
GET/api/hpsa/mapread

HRSA Health Professional Shortage Area designations for dental care, deduplicated by county and enriched with county centroid coordinates for map overlays.

ParameterTypeDescription
statestringState abbreviation (default: TX)

Returns { data: [{ hpsa_id, designation_type, hpsa_score, county_name, county_fips, latitude, longitude }], error }

GET/api/cms/mapreadTeam+

Dental providers with CMS Medicare Part B billing history, geocoded for map overlay rendering. Returns lifetime billing aggregates per NPI (~3,000 providers nationally).

Plan requirement: Team tier or higher. Rep plan receives 403.

ParameterTypeDescription
statestringOptional state filter

Returns { data: [{ npi, first_name, last_name, specialty, latitude, longitude, total_paid_lifetime, total_paid_latest_year, latest_year, years_active }], error }

Triggers

GET/api/triggers/summaryread

Trigger feed summary for the dashboard home card. Returns a count-by-trigger-type breakdown for the requested state and lookback window. Excludes enumeration_backfill rows (heuristic snapshots). Use this for territory-level rollups; use /api/alerts/events for the underlying license-event detail.

ParameterTypeDescription
statestringState abbreviation filter (e.g. TX). Omit for territory-wide totals across all states the user covers.
daysintegerLookback window in days (1-90, default 7).

Returns { data: { total, days, state, byType: { dso_acquisition, new_associate, new_specialist, new_practice, practice_opened_new_location, provider_moved, provider_departed, practice_moved, practice_closed, license_renewal_window }, types: [string] } }. Field name is camelCase byType, not snake_case.

Alerts

GET/api/alerts/eventsread

License change events: expirations, status changes, new registrations, NPI deactivations, and disciplinary actions. Paginated.

ParameterTypeDescription
event_typestringFilter by event type
statestringFilter by state
pageintegerPage number (default: 1)
per_pageintegerResults per page (default: 25, max: 100)

Returns { data, error, meta: { total, page, per_page, total_pages } }

GET/api/alerts/grantsread

Dental-relevant federal grant opportunities from Grants.gov and SAM.gov. Paginated.

ParameterTypeDescription
sourcestringFilter by source: grants_gov or sam_gov
pageintegerPage number (default: 1)
per_pageintegerResults per page (default: 25, max: 100)
GET/api/alerts/ratesreadTeam+

Medicaid/Medicare reimbursement rate change events detected by the quarterly diff pipeline. Surfaces CPT/CDT codes whose rates moved significantly.

Plan requirement: Team tier or higher. Rep plan receives 403.

ParameterTypeDescription
statestringState abbreviation (default: TX)
significant_onlybooleanLimit to >=5% or >=$5 changes (default: true)
pageintegerPage number (default: 1)
per_pageintegerResults per page (default: 25, max: 100)
GET/api/alerts/npdbreadTeam+

NPDB malpractice trend anomaly events by state. Surfaces states whose latest-year adverse action count exceeds the 3-year rolling average by >20%.

Plan requirement: Team tier or higher. Rep plan receives 403.

ParameterTypeDescription
statestringState abbreviation filter
significant_onlybooleanLimit to significant anomalies only (default: true)
pageintegerPage number (default: 1)
per_pageintegerResults per page (default: 25, max: 100)

Account

GET/api/profileread

Retrieve the authenticated user's profile (role, territory, company).

PATCH/api/profilewrite

Update profile fields: role, display_name, company_name, territory_states, territory_zips, territory_metros.

GET/api/saved-searchesread

List the authenticated user's saved searches.

POST/api/saved-searcheswrite

Create a saved search. Body: { name, filters, notify_email }. Plan limits apply (Rep: 10, Team: unlimited).

DELETE/api/saved-searches/:idwrite

Delete a saved search by ID. Ownership is verified.

GET/api/saved-providersread

List the authenticated user's bookmarked providers (Save + Set alert from the triage slideout). Each row carries enrichment fields (specialty, license status, practice context) joined client-side from the providers table.

Returns { data: [{ id, npi, notify_on_change, created_at, ...enrichment }], error }

POST/api/saved-providerswrite

Bookmark a provider. Idempotent on (user_id, npi); calling twice toggles or refreshes the row instead of duplicating. Body: { npi, notify_on_change?: boolean }.

Plan requirement: Rep tier capped at 10 saved providers; Team and Enterprise uncapped.

DELETE/api/saved-providerswrite

Remove a saved provider. Pass ?npi=XYZ as a query parameter.

ParameterTypeDescription
npi*string10-digit NPI of the bookmarked provider to remove.
GET/api/preferencesread

Read digest email frequency preference.

PATCH/api/preferenceswrite

Update digest frequency. Body: { digest_frequency: 'off' | 'weekly' | 'daily' }.

Plan requirement: digest_frequency='daily' requires Team tier or higher. 'off' and 'weekly' are available on all plans.

System

GET/api/healthnone

Health check. Returns database connectivity status and latency. No authentication required.

GET/api/statsnone

Public statistics: provider counts, expiring licenses, DSO-flagged, solo practitioners, retirement-age providers, disciplinary actions, HPSA designations, active grants, enriched state counts. Cached 5 minutes. No authentication required.

ParameterTypeDescription
statesstringComma-separated state abbreviations to scope the counts (e.g. TX or TX,CA,FL). Omit for nationwide totals.
GET/api/data-statusread

Data source freshness status for all pipeline sources.

Error Codes

All error responses follow a consistent format:

{ "data": null, "error": "Human-readable error message" }
CodeStatusDescription
400Bad RequestInvalid parameters, missing required fields, or malformed request body.
401UnauthorizedNo valid authentication provided. Check your API key or session.
403ForbiddenValid authentication but insufficient scope. Your API key needs the required scope for this endpoint.
404Not FoundThe requested resource does not exist (e.g., invalid NPI).
429Too Many RequestsRate limit exceeded. Check X-RateLimit-* headers for reset timing.
500Internal Server ErrorAn unexpected error occurred. Details are logged server-side. Contact support if persistent.

Code Examples

cURL

bash
# Search TX solo dentists expiring within 12 months curl -s "https://providersignal.com/api/providers?state=TX&sole_prop=true&exp_months=12&per_page=10" \ -H "Authorization: Bearer ps_live_your_api_key_here" # Bulk export with cursor pagination (JSON) curl -s "https://providersignal.com/api/providers/export?state=TX&format=json&per_page=1000" \ -H "Authorization: Bearer ps_live_your_api_key_here" # Page 2: use next_cursor from previous response curl -s "https://providersignal.com/api/providers/export?state=TX&format=json&per_page=1000&cursor=1234567890" \ -H "Authorization: Bearer ps_live_your_api_key_here"

Python

python
import requests API_KEY = "ps_live_your_api_key_here" BASE = "https://providersignal.com/api" HEADERS = {"Authorization": f"Bearer {API_KEY}"} # Search providers r = requests.get(f"{BASE}/providers", headers=HEADERS, params={ "state": "TX", "sole_prop": "true", "exp_months": "12", "per_page": "50", }) data = r.json() print(f"Found {data['meta']['total']} providers") # Bulk iteration with cursor pagination cursor = None all_providers = [] while True: params = {"state": "TX", "format": "json", "per_page": "1000"} if cursor: params["cursor"] = cursor r = requests.get(f"{BASE}/providers/export", headers=HEADERS, params=params) page = r.json() all_providers.extend(page["data"]) cursor = page["meta"].get("next_cursor") if not cursor: break print(f"Exported {len(all_providers)} providers total")

JavaScript

javascript
const API_KEY = "ps_live_your_api_key_here"; const BASE = "https://providersignal.com/api"; const headers = { Authorization: `Bearer ${API_KEY}` }; // Search providers const res = await fetch( `${BASE}/providers?state=TX&sole_prop=true&exp_months=12`, { headers } ); const { data, meta } = await res.json(); console.log(`Found ${meta.total} providers`); // Bulk iteration with cursor let cursor = null; const allProviders = []; do { const params = new URLSearchParams({ state: "TX", format: "json", per_page: "1000", }); if (cursor) params.set("cursor", cursor); const r = await fetch( `${BASE}/providers/export?${params}`, { headers } ); const page = await r.json(); allProviders.push(...page.data); cursor = page.meta.next_cursor; } while (cursor); console.log(`Exported ${allProviders.length} providers`);

Example Response

GET /api/providers?state=TX&sole_prop=true&per_page=1

json
{ "data": [ { "npi": "1234567890", "first_name": "John", "last_name": "Smith", "specialty": "General Practice Dentistry", "city": "Austin", "state": "TX", "zip": "78701", "is_sole_proprietor": true, "parent_organization_name": null, "license_status": "Active", "license_expiration_date": "2027-03-31", "graduation_year": 1985, "has_disciplinary_action": false, "is_dso": false } ], "error": null, "meta": { "total": 14523, "page": 1, "per_page": 25, "total_pages": 581 } }

Get API access

API keys are available on all paid plans. Subscribe to generate your first key.

Start Free Trial