Skip to main content
Tutorial

openFDA and ClinicalTrials.gov API: Drugs, Trials, Recalls

Query FDA adverse events, drug labels and recalls, ClinicalTrials.gov studies and RxNorm codes from one API key, with the source on every result.

fastcrw
By RecepNovember 12, 20267 min read

By the fastCRW team · Catalogue figures checked 2026-11-05 against GET /v1/data · Verify independently before relying on any figure.

The openFDA API returns adverse event reports, drug labels and recalls as JSON. The ClinicalTrials.gov API v2 returns registered studies as JSON. Both are public US government sources, and both have their own query syntax and paging. Through the Pergamon catalogue, one API key reaches both, plus RxNorm for drug names and the NPI Registry for providers. Every response carries the source name, the licence text and the attribution line.

What each source answers

SourceUse it forCapabilities used here
openFDAAdverse event reports, drug labels, recallsdrugevent, druglabel, drugrecall
ClinicalTrials.govStudies by condition, one study by NCT IDstudies, study
RxNormTurn a drug name into an RxCUI coderxcui-by-name, all-related
NPI RegistryLook up a US healthcare provider by NPI numbernumber

DailyMed is also in the catalogue, but it runs on your machine through the local crw-mcp, so its calls cost 0 credits and the data never passes through fastCRW. Its catalogue row says: "It is your responsibility to determine and satisfy copyright or other use restrictions when using materials that are not in the public domain."

Search openFDA adverse event reports

openFDA takes a search string in field:value form. Pass it as an option on the Pergamon call and the response is the upstream openFDA body inside the usual envelope. This call asks for three drug event reports that mention metformin:

curl "https://api.fastcrw.com/v1/data/openfda/drugevent?search=patient.drug.medicinalproduct:metformin&limit=3" \
  -H "Authorization: Bearer $CRW_API_KEY"

Output from our run on 2026-11-05, with the results array cut down to its shape:

{
  "success": true,
  "provider": "openfda",
  "capability": "drugevent",
  "credits_used": 1,
  "data": {
    "meta": { "...": "...", "results": { "skip": 0, "limit": 3, "total": 439327 } },
    "results": [ ... ]
  }
}

The count option turns the same search into a tally. This one counts the reactions reported alongside metformin:

curl "https://api.fastcrw.com/v1/data/openfda/drugevent?search=patient.drug.medicinalproduct:metformin&count=patient.reaction.reactionmeddrapt.exact&limit=3" \
  -H "Authorization: Bearer $CRW_API_KEY"
"results": [
  { "term": "NAUSEA", "count": 30628 },
  { "term": "DIARRHOEA", "count": 28491 },
  { "term": "BLOOD GLUCOSE INCREASED", "count": 27306 }
]

That number is a count of matching reports. It is not a rate of harm, and it does not say how many people took the drug.

Recalls and drug labels

Recalls use the same search syntax on drugrecall. Each record carries recall_number, reason_for_recall, recalling_firm, status and recall_initiation_date. A search on product_description:metformin returned 91 recall records in our run.

curl "https://api.fastcrw.com/v1/data/openfda/drugrecall?search=product_description:metformin&limit=2" \
  -H "Authorization: Bearer $CRW_API_KEY"

Drug labels come from druglabel. Search on openfda.generic_name:metformin to get the label text for one generic name:

curl "https://api.fastcrw.com/v1/data/openfda/druglabel?search=openfda.generic_name:metformin&limit=1" \
  -H "Authorization: Bearer $CRW_API_KEY"

In our run the search matched 350 labels. Each result is one label split into sections, with keys such as boxed_warning, indications_and_usage, dosage_and_administration, contraindications and adverse_reactions, plus an openfda object with the generic and brand names. The first label in that run was for a sitagliptin and metformin product, so filter on the exact generic name if you want single-ingredient labels only.

Trials by condition with the Python SDK

The ClinicalTrials.gov studies capability takes query fields such as query.cond and pageSize. The SDK returns the upstream body under data:

# pip install crw
from crw import CrwClient

client = CrwClient()  # reads CRW_API_KEY
result = client.get_data("clinicaltrials", "studies", {"query.cond": "type 2 diabetes", "pageSize": 3})
print(result["provenance"]["attributionText"])
for study in result["data"]["studies"]:
    print(study["protocolSection"]["identificationModule"]["nctId"])

Output from our run: Source: ClinicalTrials.gov, then the NCT IDs NCT03315780, NCT07843862 and NCT01046994. Your run will return a different list as studies are added.

One study by NCT ID: always send fields

A full study record is large. The record NCT04280705 is 213,424 bytes on the REST response. Ask for the fields you need with fields:

curl "https://api.fastcrw.com/v1/data/clinicaltrials/study?nct=NCT04280705&fields=NCTId,BriefTitle,OverallStatus" \
  -H "Authorization: Bearer $CRW_API_KEY"

The data field from that call is:

{"protocolSection":{"identificationModule":{"nctId":"NCT04280705","briefTitle":"Adaptive COVID-19 Treatment Trial (ACTT)"},"statusModule":{"overallStatus":"COMPLETED"}}}

An agent's context window is the reason to do this. Over MCP, a result above 256 KiB is refused with result_too_large, so ask for fields on big records.

Normalize drug names with RxNorm

Drug names vary by brand, spelling and salt form. RxNorm maps a name to an RxCUI, a stable code you can join on. Look up the code first:

curl "https://api.fastcrw.com/v1/data/rxnorm/rxcui-by-name?name=metformin" \
  -H "Authorization: Bearer $CRW_API_KEY"
{"idGroup":{"rxnormId":["6809"]}}

Then ask for everything related to that code, such as brand names and ingredient groups:

curl "https://api.fastcrw.com/v1/data/rxnorm/all-related?rxcui=6809" \
  -H "Authorization: Bearer $CRW_API_KEY"

In our run this returned 19 concept groups for RXCUI 6809. A name can map to more than one code, so check the list before you join records on it.

Look up a provider by NPI

The NPI Registry number capability takes a 10-digit NPI and returns the matching record:

curl "https://api.fastcrw.com/v1/data/npi/number?number=1003000126" \
  -H "Authorization: Bearer $CRW_API_KEY"

In our run the response had result_count: 1 and a first taxonomy of "Internal Medicine", with a state of MD in the address. Read the full record before you use it for a decision about a specific clinician.

Let the agent find the right call

Discovery is free. This find call returned 110 matching capabilities for "adverse events", with the first three being openfda/drugevent, openfda/foodevent and openfda/deviceevent, each at 1 credit:

curl "https://api.fastcrw.com/v1/data/find?query=adverse%20events&limit=3" \
  -H "Authorization: Bearer $CRW_API_KEY"

An MCP client makes the same two steps as tool calls. Discover the capability, then call it with its options:

{"name": "crw_find_data", "arguments": {"query": "adverse events", "limit": 3}}
{"name": "crw_get_data", "arguments": {"provider": "openfda", "capability": "drugevent", "options": {"search": "patient.drug.medicinalproduct:metformin", "limit": 3}}}

What it costs

Find and catalogue calls are free. Each server call in this post costs 1 credit, and a failed call is not billed. DailyMed runs on your machine and costs 0 credits. See pricing for plan prices.

What this does not do

  • It is not medical advice. The openFDA licence text in every response reads: "openFDA adds its own terms: the data is not for clinical decisions and you may not imply FDA endorsement." Keep that line in any product built on this data.
  • An adverse event report is a submitted report. It shows that an event was reported, not that the drug caused it.
  • A count is not an incidence rate. The tallies above count matching reports only.
  • Drug names are messy. Check the RxNorm concept groups before you join records on a code.
  • Trial records change. Read the registry record itself before you act on a trial's status.

Read the provenance before you use the data

Every REST response has the same outer shape, and the provenance block tells you where the data came from. For the openFDA call above, the block names the upstream URL that was fetched, so you can open the same query at the source and compare:

"provenance": {
  "source": "openFDA",
  "upstream_url": "https://api.fda.gov/drug/event.json?search=patient.drug.medicinalproduct%3Ametformin&limit=3",
  "licence": "A work of the United States Government, not subject to domestic copyright under 17 U.S.C. 105, published by the agency as open data. openFDA adds its own terms: ...",
  "licence_url": "https://open.fda.gov/terms/",
  "attribution_text": "Source: openFDA, U.S. Food and Drug Administration",
  "retrieved_at": "...",
  "cache_age_seconds": 0,
  "is_stale": false
}

The is_stale flag is true when the body was served from fastCRW's response cache. cache_age_seconds then says how old the cached copy is. It does not mean the upstream data is out of date, so do not treat it as a freshness warning on its own.

Errors you will see

  • Unknown capability. A wrong provider or capability name returns HTTP 404 with errorCode unknown_data_source. Run find to get the exact names.
  • Upstream failure. When openFDA or another source fails, the response has success: false and errorCode upstream_error, or upstream_timeout for a timeout. A failed call is not billed.
  • Client-run source over REST. DailyMed runs in the local crw-mcp. Its drug-names capability called over REST returns HTTP 409 with client_execution_required, and the error text names the install command. The same call works from an MCP client that has the local server installed.

Quote phrases inside the search value

openFDA matches a phrase when you put it in double quotes inside the search value. In the URL the quotes are percent-encoded as %22. This call returned a result in our run:

curl "https://api.fastcrw.com/v1/data/openfda/drugevent?search=patient.drug.medicinalproduct:%22metformin%22&limit=1" \
  -H "Authorization: Bearer $CRW_API_KEY"

Use the same pattern for other fields. Check the field name in the openFDA documentation first, because a wrong field name is an upstream error, not an empty answer.

A workflow across the four sources

A drug-safety agent usually runs the same four steps. Each one narrows the next:

  1. Normalize the name. Call rxnorm/rxcui-by-name with the name the user typed. In our run "metformin" returned RXCUI 6809.
  2. Widen with related names. Call rxnorm/all-related with that RXCUI, and add the brand and ingredient names to your search terms.
  3. Read the reports and labels. Run openfda/drugevent with count for an overview, then drugrecall and druglabel for the record-level detail. Keep the query string in your output so a reviewer can rerun it.
  4. Find the trials. Call clinicaltrials/studies with query.cond for the condition, and clinicaltrials/study with fields for each NCT ID you need to read.

Every call is 1 credit, so a pass over one drug is a handful of credits you can count before you start.

Sources

Related: PubMed API for AI agents · Public data API for AI agents · Pergamon catalogue

FAQ

Frequently asked questions

Do I need my own openFDA or ClinicalTrials.gov key?
No. The openFDA, ClinicalTrials.gov, RxNorm and NPI capabilities in this post are server sources, so you call them with your fastCRW API key. Each server call costs 1 credit, and find calls are free.
Does an adverse event report prove that a drug caused harm?
No. An openFDA adverse event record is a submitted report. It shows that someone reported an event alongside a drug, not that the drug caused it. Counts show how many reports match, not how common an event is.
Can an agent use this data to recommend a treatment?
No. openFDA's own terms say the data is not for clinical decisions. Any clinical use needs a qualified professional and the primary sources.
How do I join records from different drug sources?
Normalize the drug name first with RxNorm rxcui-by-name, then use the RXCUI as the join key. Check the all-related concept groups when one name maps to several codes.

Get Started

Try fastCRW free

Run a live request in the playground — no signup required. Or grab a free API key with 500 credits, earn up to 500 more, no credit card.

Continue exploring

More tutorial posts

View category archive