DEV Community

Omar Eldeeb
Omar Eldeeb

Posted on Originally published at datatooly.xyz

ClinicalTrials.gov API: Look Up a Study by NCT Number

To look up a clinical trial by NCT number, call https://clinicaltrials.gov/api/v2/studies/{nctId}, for example /api/v2/studies/NCT04437511. It returns the full record as JSON, with no API key and no signup. Add fields= to return only what you need. An unknown NCT number returns HTTP 404, and a malformed one returns HTTP 400.

This page covers the single-study endpoint of the ClinicalTrials.gov v2 API: the NCT number format, the response, the errors, aliases, and how to fetch many trials at once. Everything below was run against the live API on 2026-10-14 (API version 2.0.5).

The NCT number format

An NCT number is the ClinicalTrials.gov Identifier. The API's own field definition (from /api/v2/studies/metadata) says: "The format is "NCT" followed by an 8-digit number." For example, NCT04437511 is Eli Lilly's TRAILBLAZER-ALZ 2 donanemab trial.

What the endpoint accepts, as tested:

Input Result
NCT04437511 HTTP 200, the study
nct04437511 (lowercase) HTTP 200, the same study
NCT4437511 (leading zero dropped) HTTP 200, nctId comes back as NCT04437511
NCT99999999 (well-formed, doesn't exist) HTTP 404, text body NCT number NCT99999999 not found
04437511 (no NCT prefix) HTTP 400, text body Parameter `nctId` has incorrect format
NCT00009113 (an old alias) HTTP 301 redirect to NCT00010647

Error bodies are text/plain, not JSON, so check the status code before you call .json().

The endpoint

GET https://clinicaltrials.gov/api/v2/studies/{nctId}
Enter fullscreen mode Exit fullscreen mode
Parameter Values Notes
format json (default), csv, json.zip, fhir.json, ris fhir.json ignores fields. ris is a citation record.
fields Field, piece or module names, comma- or pipe-separated JSON uses names like NCTId, BriefTitle, ConditionsModule. CSV uses column names like NCT Number.
markupFormat markdown (default), legacy Applies to JSON only.

These are the parameters listed for /studies/{nctId} in the official OpenAPI spec.

What the JSON contains

The full record for NCT04437511 has five top-level keys: protocolSection, resultsSection, documentSection, derivedSection and hasResults. The spec also defines an annotationSection, which appears only on some studies.

Section What's in it
protocolSection Registration data in modules: identificationModule, statusModule, sponsorCollaboratorsModule, descriptionModule, conditionsModule, designModule, armsInterventionsModule, outcomesModule, eligibilityModule, contactsLocationsModule, and more.
resultsSection Posted results: participant flow, baseline, outcome measures, adverse events. Present only when hasResults is true.
documentSection Uploaded protocol, statistical analysis plan and consent PDFs.
derivedSection Fields ClinicalTrials.gov adds, such as MeSH terms for conditions and interventions.
hasResults true when results are posted.

A full record can be large (the API's /api/v2/stats/size reported an average of 17,304 bytes per study and a 99th percentile of 149,567 bytes), so use fields when you only need a few values.

curl: one study, a few fields

curl -s "https://clinicaltrials.gov/api/v2/studies/NCT04437511?fields=NCTId,BriefTitle,OverallStatus,Phase,LeadSponsorName,HasResults"
Enter fullscreen mode Exit fullscreen mode

Result:

{"protocolSection":{"identificationModule":{"nctId":"NCT04437511","briefTitle":"A Study of Donanemab (LY3002813) in Participants With Early Alzheimer's Disease (TRAILBLAZER-ALZ 2)"},"statusModule":{"overallStatus":"ACTIVE_NOT_RECRUITING"},"sponsorCollaboratorsModule":{"leadSponsor":{"name":"Eli Lilly and Company"}},"designModule":{"phases":["PHASE3"]}},"hasResults":true}
Enter fullscreen mode Exit fullscreen mode

Even with fields, the response keeps the nested path of each field. Phase comes back as designModule.phases, a list.

The same lookup as a one-row CSV:

curl -s "https://clinicaltrials.gov/api/v2/studies/NCT04437511?format=csv&fields=NCT%20Number,Study%20Title,Study%20Status,Phases,Sponsor"
Enter fullscreen mode Exit fullscreen mode
NCT Number,Study Title,Study Status,Sponsor,Phases
NCT04437511,A Study of Donanemab (LY3002813) in Participants With Early Alzheimer's Disease (TRAILBLAZER-ALZ 2),ACTIVE_NOT_RECRUITING,Eli Lilly and Company,PHASE3
Enter fullscreen mode Exit fullscreen mode

The columns come back in the API's order, not the order you asked for.

Python: a lookup that handles every case

import re
import requests

BASE = "https://clinicaltrials.gov/api/v2/studies"

def normalize_nct(raw):
    """'nct4437511', ' NCT04437511 ' -> 'NCT04437511' (NCT + 8 digits)."""
    s = raw.strip().upper().replace(" ", "")
    m = re.fullmatch(r"NCT(\d{1,8})", s)
    if not m:
        raise ValueError(f"not an NCT number: {raw!r}")
    return "NCT" + m.group(1).zfill(8)

def get_study(nct_id, fields=None):
    nct_id = normalize_nct(nct_id)
    params = {"fields": ",".join(fields)} if fields else {}
    r = requests.get(f"{BASE}/{nct_id}", params=params, timeout=30)
    if r.status_code == 404:
        return None  # body is plain text: "NCT number ... not found"
    r.raise_for_status()
    # An old alias NCT ID answers 301 to the current record. The redirect drops
    # the query string, so re-request the canonical ID to keep `fields`.
    if r.history and fields:
        canonical = r.url.rstrip("/").rsplit("/", 1)[-1]
        r = requests.get(f"{BASE}/{canonical}", params=params, timeout=30)
        r.raise_for_status()
    return r.json()

def get_many(nct_ids, fields):
    """Up to 1,000 trials in one call with filter.ids."""
    ids = [normalize_nct(x) for x in nct_ids]
    r = requests.get(BASE, params={
        "filter.ids": ",".join(ids),
        "fields": ",".join(fields),
        "pageSize": 1000,
    }, timeout=60)
    r.raise_for_status()
    return r.json()["studies"]

if __name__ == "__main__":
    FIELDS = ["NCTId", "BriefTitle", "OverallStatus", "Phase", "LeadSponsorName", "HasResults"]
    s = get_study("nct4437511", FIELDS)
    p = s["protocolSection"]
    print(p["identificationModule"]["nctId"], "|", p["identificationModule"]["briefTitle"])
    print(p["statusModule"]["overallStatus"], p["designModule"]["phases"],
          p["sponsorCollaboratorsModule"]["leadSponsor"]["name"], "hasResults:", s["hasResults"])

    print(get_study("NCT99999999"))  # -> None

    alias = get_study("NCT00009113", ["NCTId", "NCTIdAlias", "BriefTitle"])
    print(alias["protocolSection"]["identificationModule"])

    for st in get_many(["NCT04437511", "NCT04368728", "NCT00010647"], ["NCTId", "OverallStatus"]):
        p = st["protocolSection"]
        print(p["identificationModule"]["nctId"], p["statusModule"]["overallStatus"])
Enter fullscreen mode Exit fullscreen mode

Output on 2026-10-14:

NCT04437511 | A Study of Donanemab (LY3002813) in Participants With Early Alzheimer's Disease (TRAILBLAZER-ALZ 2)
ACTIVE_NOT_RECRUITING ['PHASE3'] Eli Lilly and Company hasResults: True
None
{'nctId': 'NCT00010647', 'nctIdAliases': ['NCT00009113'], 'briefTitle': 'Cost-Effectiveness of and Long-Term Outcomes Following Acupuncture Treatment for Osteoarthritis of the Knee'}
NCT00010647 COMPLETED
NCT04437511 ACTIVE_NOT_RECRUITING
NCT04368728 COMPLETED
Enter fullscreen mode Exit fullscreen mode

Details that trip people up

  • Aliases redirect and drop your query string. Some studies were registered twice and merged. The old ID is kept in nctIdAliases, and requesting it returns a 301 to the current ID. In the test above, the redirect target had no ?fields= on it, so following it blindly returns the full record. On 2026-10-14 there were 3,164 studies with at least one alias.
  • Batch lookups don't keep your order. filter.ids returned the three studies in the API's order, not the order I sent. Index the results by nctId.
  • NCT IDs in a search query switch filters off. The /studies docs say: "If any query parameter contains only NCT IDs (comma- and/or space-separated), filters are ignored." I sent query.term=NCT04437511 NCT04368728 with filter.overallStatus=RECRUITING and got both studies back, although neither is recruiting. Use filter.ids when you want the filters applied.
  • Document PDFs live on a CDN. documentSection.largeDocumentModule.largeDocs lists file names such as Prot_000.pdf. For NCT04437511 the file was at https://cdn.clinicaltrials.gov/large-docs/11/NCT04437511/Prot_000.pdf, where 11 is the last two digits of the NCT number. That path pattern is what I observed; it isn't documented in the API spec.
  • There is no history endpoint. The v2 API returns only the current version. Past versions are on the website's History of Changes tab (https://clinicaltrials.gov/study/NCT04437511?tab=history). The internal endpoint the site appears to use, /api/int/studies/{nctId}/history, returned HTTP 403 to a script.

Related guides for this API

Do it without code

The free ClinicalTrials.gov search tool calls this same API live from your browser. Search by condition or drug and filter by phase and status. It shows each study's NCT ID, title, status, phase and lead sponsor. No login, no key.

At scale

To look up hundreds of NCT numbers and join the trials with FDA approvals, device clearances, adverse events and recalls, use the Clinical Trials & FDA Pipeline actor on Apify. Its trial_details mode takes a list of NCT IDs (nctIds) and returns one flat row per trial: status, phase, enrollment, primary and secondary endpoints, eligibility criteria, sponsors and locations. It exports JSON, CSV or Excel. It's free to start, then pay-as-you-go.

Disclosure: I build the datatooly tool and the Apify actor. Everything above works with the public API alone.

Sources: ClinicalTrials.gov API (interactive OpenAPI docs), Study data structure, and live responses from clinicaltrials.gov/api/v2 on 2026-10-14.

Top comments (0)