Why this exists

ClinicalTrials.gov API v2 is a genuinely public, unauthenticated JSON API run by the US National Library of Medicine, covering every registered clinical trial worldwide — roughly 597,000 studies as of this writing. It's well-engineered but its official docs are reference-dense rather than example-driven, and a few behaviors (how pagination actually ends, what an out-of-range pageSize does, exactly what AREA[] query syntax looks like in practice) are easier to get from a working example than from the spec alone. This page is that working example, built from calls we actually made.

The endpoint & real parameters

Verified live — 200 OK

Endpoint

GET https://clinicaltrials.gov/api/v2/studies

No authentication, no API key.

Example request

curl "https://clinicaltrials.gov/api/v2/studies?format=json&pageSize=2&filter.overallStatus=RECRUITING&query.term=AREA%5BStudyType%5DINTERVENTIONAL&fields=NCTId,BriefTitle,OverallStatus,Phase"

Real response (trimmed)

{
  "studies": [
    {
      "protocolSection": {
        "identificationModule": {
          "nctId": "NCT05879276",
          "briefTitle": "Effect at 3 Months of Early Empagliflozin Initiation in Cardiogenic Shock Patients..."
        },
        "statusModule": { "overallStatus": "RECRUITING" },
        "designModule": { "phases": ["PHASE3"] }
      }
    }
  ],
  "nextPageToken": "ZVdj7o2Elu8o3lp2Wsy8sLL2mpOQJJxuZfOp"
}

Key query parameters

ParameterNotes
formatjson or csv. Everything in this reference assumes json.
pageSizeRows per page. See the pageSize section — capped, but not the way you'd expect.
pageTokenOpaque cursor for the next page. See pagination.
filter.overallStatusExact status value(s), e.g. RECRUITING. See status section.
query.termFree-text query, including field-scoped AREA[FieldName]value syntax. See query syntax.
fieldsComma-separated list restricting which fields come back. See field selection.
sorte.g. LastUpdatePostDate:desc. Not the focus of this page but supported.

pageToken pagination and how it actually terminates

Verified live — confirmed empty-result page has no nextPageToken

This API uses cursor pagination, not offset/page numbers. Each response can include a nextPageToken string; pass it back as the pageToken query parameter to get the next page.

Termination condition

The loop ends when nextPageToken is absent from the response — not when studies is empty, and not via any count field. We confirmed this against a real narrow query that runs out of results: the final page's response is {"studies": [...]} with no nextPageToken key at all once you've reached the end. A query with zero matching studies at all returns {"studies": []}, also with no nextPageToken. Either way, the absence of the key — not an empty array, not a zero — is the real signal.

Worked pagination loop

async function fetchAllStudies(params) {
  const studies = [];
  let pageToken = null;

  while (true) {
    const url = new URL('https://clinicaltrials.gov/api/v2/studies');
    Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
    if (pageToken) url.searchParams.set('pageToken', pageToken);

    const res = await fetch(url).then(r => r.json());
    studies.push(...(res.studies ?? []));

    pageToken = res.nextPageToken ?? null;
    if (!pageToken) break; // the real stop condition
  }
  return studies;
}

The pageSize cap — and it doesn't error

Verified live — pageSize=1001 returns 200 OK with exactly 1000 rows
pageSize is capped at 1000, but an over-cap value doesn't error — it's silently clamped. We requested pageSize=1001 and got back 200 OK with exactly 1000 studies in the response, no error, no warning field. This is friendlier than USAspending's approach (which 422s on an over-limit limit) but worth knowing: if your code assumes it's getting pageSize rows back, count the actual array length rather than trusting the parameter you sent.

query.term and the AREA[] syntax

Verified live

query.term accepts free text, but its real power is field-scoped matching via AREA[FieldName]value. This is how the fetcher behind our own dataset (open-trials-data) restricts to interventional studies:

query.term=AREA[StudyType]INTERVENTIONAL

URL-encoded, the brackets become %5B/%5D:

curl "https://clinicaltrials.gov/api/v2/studies?format=json&pageSize=1&query.term=AREA%5BStudyType%5DINTERVENTIONAL"

The same syntax works for condition search, confirmed live:

curl "https://clinicaltrials.gov/api/v2/studies?format=json&pageSize=1&query.term=AREA%5BConditionSearch%5Ddiabetes"

You can combine AREA[] clauses with plain terms and boolean operators (AND/OR) inside a single query.term string — the field-scoped clause narrows to a specific structured field while the rest of the term can still be free text.

A query matching zero studies is a normal 200, not an error We confirmed this with a deliberately nonsense condition string — the API returned {"studies": []} at 200 OK, no error. Don't treat an empty result as a broken query; treat it as a real answer.

filter.overallStatus

Restricts to one or more exact recruitment status values, e.g. RECRUITING, COMPLETED, NOT_YET_RECRUITING, ACTIVE_NOT_RECRUITING, TERMINATED, WITHDRAWN, SUSPENDED. This is a separate parameter from query.term — it's a structured filter, not a free-text match, and combining it with query.term's AREA[] clauses (as in the example above) is the standard way to build a scoped pull like "recruiting AND interventional."

Field selection syntax

Verified live — fields param confirmed to restrict response shape

The fields parameter takes a comma-separated list of field names (not full JSON paths — short names like NCTId, BriefTitle, OverallStatus) and restricts the response to only those. Omitting it returns the full record, which is large — restricting fields is worth doing for any real pull.

curl "https://clinicaltrials.gov/api/v2/studies?format=json&pageSize=1&fields=NCTId,BriefTitle"
{
  "studies": [
    {
      "protocolSection": {
        "identificationModule": {
          "nctId": "NCT02841735",
          "briefTitle": "Young Adult Naturalistic Alcohol Study (YANAS)..."
        }
      }
    }
  ],
  "nextPageToken": "..."
}

Note that even with field selection, the response keeps the full nested protocolSection structure — you get fewer fields, not a flat object. See the flattening example below.

The nested response & a worked flattening example

Every study is nested under protocolSection, itself broken into named modules. The ones you'll touch most:

ModuleContains
identificationModulenctId, briefTitle, officialTitle
statusModuleoverallStatus, startDateStruct, completionDateStruct
designModulestudyType, phases (array), enrollmentInfo (count, type)
conditionsModuleconditions (array)
armsInterventionsModuleinterventions (array of objects with name)
sponsorCollaboratorsModuleleadSponsor (name, class)
contactsLocationsModulelocations (array), centralContacts, overallOfficials — see the ethics note before touching this module

Worked flattening example (JavaScript)

function flatten(study) {
  const ps = study.protocolSection ?? {};
  const id = ps.identificationModule ?? {};
  const status = ps.statusModule ?? {};
  const design = ps.designModule ?? {};
  const sponsor = ps.sponsorCollaboratorsModule?.leadSponsor ?? {};
  const conditions = ps.conditionsModule?.conditions ?? [];
  const interventions = (ps.armsInterventionsModule?.interventions ?? [])
    .map(i => i.name).filter(Boolean);
  // Institutional location data only — see the ethics note before
  // pulling anything from contactsLocationsModule.locations[].contacts
  // or .centralContacts.
  const countries = [...new Set(
    (ps.contactsLocationsModule?.locations ?? []).map(l => l.country).filter(Boolean)
  )];

  return {
    nctId: id.nctId ?? null,
    briefTitle: id.briefTitle ?? null,
    overallStatus: status.overallStatus ?? null,
    phase: (design.phases ?? []).join('; ') || null,
    studyType: design.studyType ?? null,
    conditions,
    interventions,
    leadSponsorName: sponsor.name ?? null,
    leadSponsorClass: sponsor.class ?? null,
    enrollmentCount: design.enrollmentInfo?.count ?? null,
    enrollmentType: design.enrollmentInfo?.type ?? null,
    locationCountries: countries,
    url: id.nctId ? `https://clinicaltrials.gov/study/${id.nctId}` : null,
  };
}

This is the same shape (minus dates, trimmed for space) as the real normalizer behind open-trials-data.

Getting counts

There's no dedicated "count only" endpoint for an arbitrary filtered query — the way to get a count for a specific filter/query combination is to page through with fields=NCTId (minimizing payload size) and count rows, using the pagination loop above. For the registry's total size (unfiltered), see /stats/size below.

The /stats/size endpoint

Verified live — 200 OK

GET https://clinicaltrials.gov/api/v2/stats/size exists and returns registry-wide size statistics — not a per-query count, but a genuinely useful sanity check for "how big is the whole registry right now."

curl "https://clinicaltrials.gov/api/v2/stats/size"
{
  "totalStudies": 597296,
  "averageSizeBytes": 17274,
  "percentiles": { "50%": 9823, "90%": 28319, "99%": 149565 },
  "ranges": [ { "sizeRange": "0 - 10 kb", "studiesCount": 315938 }, ... ],
  "largestStudies": [ { "id": "NCT02723955", "sizeBytes": 3596689 }, ... ]
}

Useful mainly as a scale reference: as of this run, 597,296 total registered studies, of which our own dataset's recruiting+interventional scope (see below) is roughly 8%.

Ethics note: this API exposes personal contact data

Individual study records include real names, phone numbers, and email addresses of investigators and site contacts. We confirmed this directly. Requesting ContactsLocationsModule on a live recruiting study returned a centralContacts array containing entries like:
{
  "name": "Antoine KIMMOUN, MD PhD",
  "role": "CONTACT",
  "phone": "3 83 15 40 79",
  "phoneExt": "+33",
  "email": "a.kimmoun@chru-nancy.fr"
}
This is real personal contact information for a real named individual, returned by an unauthenticated public API call. It's published deliberately by ClinicalTrials.gov so that prospective trial participants can reach a real person to ask about enrolling — that's a legitimate and important use. It is not published so that it can be scraped in bulk into a marketing list, a recruiting database, or any other secondary use the individual didn't consent to.

Responsible consumers of this API should exclude centralContacts, overallOfficials, and any per-location contacts field from anything beyond a single, deliberate per-study lookup done for the study's own intended purpose (helping someone decide whether to enroll). Don't request these fields in a bulk/aggregate pull, don't store them, and don't republish them in a dataset. This is exactly the line open-trials-data draws — its field list is deliberately institutional-only (sponsor organization name/class, country-level locations) and explicitly omits contactsLocationsModule's personal fields entirely, even though the API would hand them over on request.

Auth, rate limits, being polite