The public /api/v2/studies endpoint — real pagination, real query syntax, the actual nested response shape, and an explicit note on the personal data it discloses that most integrations shouldn't touch.
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.
GET https://clinicaltrials.gov/api/v2/studies
No authentication, no API key.
curl "https://clinicaltrials.gov/api/v2/studies?format=json&pageSize=2&filter.overallStatus=RECRUITING&query.term=AREA%5BStudyType%5DINTERVENTIONAL&fields=NCTId,BriefTitle,OverallStatus,Phase"
{
"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"
}
| Parameter | Notes |
|---|---|
format | json or csv. Everything in this reference assumes json. |
pageSize | Rows per page. See the pageSize section — capped, but not the way you'd expect. |
pageToken | Opaque cursor for the next page. See pagination. |
filter.overallStatus | Exact status value(s), e.g. RECRUITING. See status section. |
query.term | Free-text query, including field-scoped AREA[FieldName]value syntax. See query syntax. |
fields | Comma-separated list restricting which fields come back. See field selection. |
sort | e.g. LastUpdatePostDate:desc. Not the focus of this page but supported. |
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.
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.
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;
}
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[] syntaxquery.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.
{"studies": []} at 200 OK, no error. Don't treat an empty result as a broken query; treat it as a real answer.
filter.overallStatusRestricts 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."
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.
Every study is nested under protocolSection, itself broken into named modules. The ones you'll touch most:
| Module | Contains |
|---|---|
identificationModule | nctId, briefTitle, officialTitle |
statusModule | overallStatus, startDateStruct, completionDateStruct |
designModule | studyType, phases (array), enrollmentInfo (count, type) |
conditionsModule | conditions (array) |
armsInterventionsModule | interventions (array of objects with name) |
sponsorCollaboratorsModule | leadSponsor (name, class) |
contactsLocationsModule | locations (array), centralContacts, overallOfficials — see the ethics note before touching this module |
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.
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.
/stats/size endpointGET 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%.
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.
The endpoint on this page is public and unauthenticated — no login, no API key, no session cookie. It's the same API that powers ClinicalTrials.gov's own search interface.
contactsLocationsModule beyond a single deliberate per-study lookup — the API discloses real personal contact information for named individuals.clinicaltrials.gov/study/{nctId}) where practical.