API Reference
REST API
A structured, versioned API for querying the full TwinFyRx drug intelligence platform. All endpoints return JSON and follow consistent patterns for pagination, errors, and resource nesting.
Authentication & Access Tiers
Data endpoints require an X-API-Key header; a small set of public endpoints needs none. A missing or wrong key returns 401.
| Surface | Access | Paths |
|---|---|---|
| Always public | No key, ever | /v1/demo/* /v1/data-quality/* /v1/metadata/* /health* |
| Data endpoints | X-API-Key required | /v1/drugs/* /v1/pricing/* /v1/formulary/* /v1/diseases/* /v1/ndc/* /v1/atc/* /v1/medicare/* /v1/pharmacies/* /v1/providers/* /v1/fhir/* /v1/canada/* /v1/ingredients/* |
Once you have a key, include it on every request to the data endpoints. In the interactive docs at /docs, click Authorize and paste it once:
curl -H "X-API-Key: your_api_key" \ https://api.twinfyrx.com/v1/drugs/search?q=atorvastatin
Key provisioning: One key per customer, scoped to the licensed tier. Keys are issued during onboarding and rotated on request. Request access to get started.
Backwards compatibility: The drug_id primary key, all enum values, and the v1 path prefix are stable. Breaking changes will be versioned under a new path prefix with a minimum six-month migration window.
Base URL
https://api.twinfyrx.com
All endpoints are versioned under /v1.
Response Headers
Every response carries three diagnostic headers:
| Header | Value | Notes |
|---|---|---|
| X-Schema-Version | 1.0 | Schema version in effect. Increments on breaking changes. |
| X-Data-As-Of | 2026-04-28T14:32:00Z | UTC timestamp of the most recent data sync to MotherDuck. |
| X-Request-ID | uuid-v4 | Echo of client-supplied X-Request-ID, or a server-generated UUID. Include in support tickets. |
Supply your own X-Request-ID on outbound calls to correlate requests across your own logs. X-Schema-Version and X-Data-As-Of are exposed via CORS so browser clients can read them.
Error Handling
All error responses follow a consistent structure. The request_id field matches the X-Request-ID header and is the primary reference for support:
{
"error": {
"code": "NOT_FOUND",
"message": "Drug with drug_id 999999 not found",
"request_id": "a3f2c1d8-7b4e-4e2a-9c1f-0d5e8a3b2f91"
}
}| Status | Code | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid API key |
| 404 | NOT_FOUND | Resource not found |
| 422 | VALIDATION_ERROR | Invalid query parameters |
| 500 | INTERNAL_ERROR | Unexpected server error |
Drugs
Search, retrieve, and explore the drug concept hierarchy. Every response anchors to a canonical RxNorm drug concept.
/v1/drugs/searchSearch drugs by name, ingredient, or brand name. Returns paginated results.
Parameters
qstringREQUIREDSearch query (min 2 characters)limitintegerResults per page (1–100, default 20)offsetintegerPagination offsetResponse
results[]DrugSummaryArray of matching drugs.drug_idintegerCanonical drug concept ID (RxNorm SCD/SBD RXCUI).drug_namestringFull clinical drug name.ingredientstringActive ingredient(s).is_genericbooleanWhether this is a generic concepttotalintegerTotal matching resultsExample Response
{
"results": [
{
"drug_id": 617314,
"drug_name": "atorvastatin 20 MG Oral Tablet",
"ingredient": "atorvastatin",
"dose_form": "Oral Tablet",
"clinical_drug_form": "atorvastatin Oral Tablet",
"brand_name": null,
"is_generic": true
}
],
"total": 12,
"limit": 20,
"offset": 0
}/v1/drugs/{drug_id}Full drug detail including RxNorm hierarchy, therapy attributes, and branded variants.
Parameters
drug_idintegerREQUIREDCanonical drug concept ID (path parameter)Response
drug_idintegerCanonical drug concept IDdrug_namestringFull drug nameingredient_rxcuiintegerIngredient concept IDclinical_drug_form_rxcuiintegerDrug form concept IDtherapy_patternstringTherapy behavior enumspecialty_flagbooleanSpecialty drug indicatorbenefit_channelstringPharmacy, medical, or dual/v1/drugs/{drug_id}/icd10-volumesUtilization-weighted drug-to-diagnosis mappings with percent volume estimates.
Response
drugDrugSummaryDrug summary objectis_single_indication_drugbooleanWhether drug maps to a single diagnosisicd10_volumes[]ICD10VolumeDiagnoses sorted by volume.icd10_codestringICD-10-CM code.pct_volumedecimalEstimated % of prescriptions for this diagnosis.is_dominant_indicationbooleanWhether this is the primary indication/v1/drugs/{drug_id}/hcc-suspectsRisk-adjustment categories a drug points at, for diagnosis suspecting from pharmacy claims. Optional ?model= cms_hcc_v28, cms_hcc_v24 or rxhcc_v08.
Response
drugDrugSummaryDrug summarysuspect_countintegerTotal categories returnedsuspects[]HccSuspectCategories, strongest first.model_keystringcms_hcc_v28, cms_hcc_v24, or rxhcc_v08.ccstringCondition category within the model.cc_labelstringCMS's name for the category.pct_volumedecimalEstimated % of the drug's prescriptions for diagnoses in the category.confidence_tierstringhigh (≥ 90%), moderate (70–90%), or unrated.contributing_icd10_codes[]stringICD-10-CM codes behind the categoryPricing
NADAC acquisition costs (weekly cadence) and Medicare Part B ASP payment limits (quarterly). Both resolve through the drug concept hierarchy.
/v1/drugs/{drug_id}/pricingCurrent pricing snapshot — latest NADAC week and latest ASP quarter for a drug.
Response
drugDrugSummaryDrug summarynadacNADACSnapshot | nullLatest NADAC pricing (if available).median_per_unitdecimalMedian acquisition cost per unit.pricing_unitstringUnit label (e.g., 'EACH', 'ML')aspASPSnapshot | nullLatest ASP pricing (if available).median_payment_limitdecimalMedian Part B payment limit/v1/drugs/{drug_id}/nadacHistorical NADAC pricing with weekly granularity. Defaults to 2 years of history.
Parameters
startdateStart date (YYYY-MM-DD, default: 2 years ago)enddateEnd date (YYYY-MM-DD, default: today)Response
pricing_unitstringUnit of measureweeks[]NADACWeekWeekly pricing data points.as_of_datedateEffective date.median_per_unitdecimalMedian cost per unit.ndc_countintegerNDCs contributing to this aggregateExample Response
{
"drug": { "drug_id": 617314, "drug_name": "atorvastatin 20 MG Oral Tablet" },
"pricing_unit": "EACH",
"weeks": [
{ "as_of_date": "2026-03-05", "median_per_unit": 0.0412, "min_per_unit": 0.0389, "max_per_unit": 0.0445, "ndc_count": 14 },
{ "as_of_date": "2026-02-26", "median_per_unit": 0.0415, "min_per_unit": 0.0391, "max_per_unit": 0.0448, "ndc_count": 14 }
]
}/v1/drugs/{drug_id}/aspHistorical ASP (Medicare Part B) payment limits by quarter with HCPCS code mappings.
Response
quarters[]ASPQuarterQuarterly ASP data points.effective_quarterstringe.g., '2026Q1'.median_payment_limitdecimalMedian payment limit per unit.hcpcs_codesstring[]Associated HCPCS codesDiagnoses
Search and explore ICD-10-CM diagnoses with reverse drug mappings — find which drugs are most commonly prescribed for a given condition.
/v1/diagnoses/searchSearch ICD-10 diagnoses by code or description text.
Parameters
qstringREQUIREDICD-10 code or description textlimitintegerResults per page (default 20)Response
results[]DiseaseSummaryMatching diagnoses.icd10_codestringICD-10-CM code.diagnosisstringDiagnosis description.is_billablebooleanWhether this is a billable code/v1/diagnoses/{icd10_code}Full diagnosis detail including ICD-10 hierarchy and top 20 drugs prescribed for this condition.
Response
icd10_codestringICD-10-CM codediagnosisstringFull descriptionchapter_namestringICD-10 chapteris_billablebooleanBillable indicatortop_drugs[]DrugForDiseaseTop 20 drugs by prescribing volume.pct_volumedecimalEstimated share of prescriptionsClassification & Identifiers
ATC therapeutic classification hierarchy and NDC identifier resolution.
/v1/atc/searchSearch ATC classification codes by code or name.
Parameters
qstringREQUIREDATC code or therapeutic class name/v1/atc/{atc_code_5}Full 5-level ATC hierarchy for a given code.
Response
atc_code_1 → atc_code_5stringAll 5 ATC hierarchy levelsatc_name_1 → atc_name_5stringNames for each level/v1/drugs/{drug_id}/atcEvery WHO ATC code the drug product carries, including combination and pack codes. primary_atc_code is the one code for the product's route and main use (null when the evidence doesn't settle one); it is listed first, flagged is_primary.
Response
primary_atc_codestring | nullThe product's one ATC level-5 codeatc_classifications[]arrayAll 5 levels per code, plus is_primary, source, is_combination/v1/ndc/{ndc_code}Resolve an NDC code to its drug concept with therapy attributes and specialty flags. NDCs of retired drug concepts still resolve, flagged obsolete, so historical claims join.
Response
ndcstringInput NDC codedrug_idintegerResolved canonical drug concept ID (last known concept for an obsolete NDC)drug_namestringDrug nametherapy_patternstringTherapy behavior classificationspecialty_flagbooleanSpecialty drug indicatoris_activebooleanNDC is in the current crosswalkis_obsoletebooleandrug_id is no longer a current RxNorm drug conceptobsolete_reasonstringretired | obsolete | not_prescribable (null when current)remapped_drug_idintegerCurrent drug concept RxNorm says replaces drug_id, when one existsPrescribers
Medicare Part D prescribing per NPI (2024), mapped onto RxNorm ingredients and WHO ATC classes: who prescribes a class or ingredient, how much, and each prescriber's specialty and volume decile. Claims are counted at ingredient and ATC levels 2–4 — CMS drug names carry no strength or form. CMS suppresses rows under 11 claims, so volumes are lower bounds.
/v1/providers/searchSearch prescribers, largest prescribing volume first. At least one filter is required.
Parameters
qstring10-digit NPI, or name words ("jane smith")statestringTwo-letter state codespecialtystringSpecialty key, name or group (e.g. cardiology)cms_prescriber_typestringCMS Part D prescriber typelimitintegerResults per page (1–500, default 50)offsetintegerPagination offset/v1/providers/{npi}One prescriber: name, location, best-available specialty, and Part D prescribing totals, breadth and volume deciles.
Response
specialtyobjectprovider_type, specialty_key / name / group, population, primary_care_specialty, practice_setting, specialty_confidence, cms_prescriber_typeprescribing.claimsintegerPart D claims in data_yearprescribing.drug_costdecimalTotal drug cost, USDprescribing.n_ingredients / n_atc2 / n_atc4integerBreadth of prescribingprescribing.brand_claim_sharedecimalShare of claims dispensed under a brand nameprescribing.decilesobjectVolume deciles 1–10: national, prescriber_type, stateExample Response
{
"npi": "1234567893",
"first_name": "JANE",
"last_name": "DOE",
"city": "BOSTON",
"state": "MA",
"specialty": {
"provider_type": "physician",
"specialty_key": "cardiology",
"specialty_name": "Cardiology",
"practice_setting": "office",
"specialty_confidence": "high",
"cms_prescriber_type": "Cardiology"
},
"prescribing": {
"data_year": 2024,
"claims": 9120,
"drug_cost": 1840250.12,
"n_ingredients": 64,
"n_atc4": 41,
"brand_claim_share": 0.21,
"top_atc2": "C10",
"top_atc2_share": 0.27,
"deciles": { "national": 10, "prescriber_type": 8, "state": 9 }
}
}/v1/providers/{npi}/prescribing/atcA prescriber's volume in each ATC class, largest first. Deciles rank the prescriber among everyone who prescribes the class.
Parameters
levelintegerATC level 2, 3 or 4 (default 4)limitintegerMax rows (1–1000, default 100)Response
results[]arrayatc_code, atc_name, claims, fills_30day, days_supply, drug_cost, brand_claims, share_of_prescriber, deciles/v1/providers/{npi}/prescribing/ingredientsA prescriber's volume per ingredient (RxNorm IN / MIN), largest first.
Parameters
limitintegerMax rows (1–1000, default 100)/v1/atc/{atc_code}/prescribersTop prescribers of an ATC level 2–4 class (e.g. C10, C10A, C10AA), ranked by claims.
Parameters
statestringTwo-letter state codecms_prescriber_typestringCMS Part D prescriber typespecialtystringSpecialty key, name or groupmin_decileintegerLowest national decile within the class (1–10)limit / offsetintegerPagination (1–500, default 50)/v1/atc/{atc_code}/prescriber-mixWho prescribes a class: prescribers, claims and share of the class's claims by CMS prescriber type.
/v1/ingredients/{ingredient_rxcui}/prescribersTop prescribers of an ingredient, ranked by claims. Same filters as the ATC class endpoint.
Pagination
List endpoints return a consistent envelope. The response also carries an X-Total-Count header with the untruncated count, useful for building UI paginators without parsing the body:
{
"results": [ ... ],
"total": 142,
"limit": 20,
"offset": 0
}
# Response header:
X-Total-Count: 142| Rule | Detail |
|---|---|
| Max limit | 1000 results per request. Default is 20. |
| Offset paging | Use limit + offset for result sets up to ~10,000 rows. |
| Bulk endpoint | POST /v1/drugs:lookup accepts up to 500 drug_ids per call — preferred over looping single-drug GETs. |
Rate Limits
Rate limits are enforced per API key. Specific limits are communicated during onboarding and vary by tier. When a limit is exceeded, the API returns 429 Too Many Requests with a Retry-After header indicating how many seconds to wait before retrying.
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-Request-ID: a3f2c1d8-7b4e-4e2a-9c1f-0d5e8a3b2f91
{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Retry after 30 seconds.",
"request_id": "a3f2c1d8-7b4e-4e2a-9c1f-0d5e8a3b2f91"
}
}Recommended retry strategy: exponential backoff starting at 1 second, capped at 60 seconds, with jitter. For batch workloads, use POST /v1/drugs:lookup to reduce request volume — 500 drugs in one call instead of 500 individual GETs.