Clinical Trials API - ClinicalTrials.gov, ISRCTN, EU CTIS avatar

Clinical Trials API - ClinicalTrials.gov, ISRCTN, EU CTIS

Pricing

from $3.30 / 1,000 results

Go to Apify Store
Clinical Trials API - ClinicalTrials.gov, ISRCTN, EU CTIS

Clinical Trials API - ClinicalTrials.gov, ISRCTN, EU CTIS

Clinical trials API: search ClinicalTrials.gov, ISRCTN and the EU CTIS register in one call and get deduplicated trial records in a unified schema, with per-registry status on every row and optional patent linkage. Query by condition, drug, sponsor or status. Keyless, official registries.

Pricing

from $3.30 / 1,000 results

Rating

0.0

(0)

Developer

Kyle Maloney

Kyle Maloney

Maintained by Community

Actor stats

0

Bookmarked

3

Total users

2

Monthly active users

21 days ago

Last modified

Share

Clinical Trials Meta-Search API - ClinicalTrials.gov + ISRCTN + EU CTIS

Search ClinicalTrials.gov, the ISRCTN registry and EU CTIS in one call and get back a single deduplicated table of trials in one schema: title, status, phase, study type, enrollment, sponsor, conditions, interventions, countries, dates, every registry the trial was found in, and every cross-registry identifier (NCT / ISRCTN / EudraCT / CTIS) attached to it. Built for clinical research associates and CRO analysts who want one trial list instead of three browser tabs, biotech and pharma competitive-intelligence teams tracking a drug or indication, systematic-review authors searching more than one registry to satisfy PRISMA-S, patient-recruitment teams hunting open sites, and pipeline analysts who want registry data as a clean API or MCP tool call. No API key for the registries.

Every row carries per-registry status, so a short result set is never ambiguous: you can always tell "that registry answered and found nothing" from "that registry did not answer".


Scope and cost - read this first

Required input: query. It is the only required field. A call omitting it is rejected by input-schema validation before the Actor starts, so nothing is billed. A query that is present but blank or whitespace-only fails the run.

Price: $0.006 per result ($6.00 per 1,000 results), Pay-Per-Event. One billable result = one unified trial row. Paid Apify plans get a graduated discount: Bronze $0.0048, Silver $0.0042, Gold $0.0033, Platinum $0.0024, Diamond $0.0018. The platform's Actor-start event ($0.00005 per GB of run memory) also applies to any run that starts, including one that then fails.

maxResults is a true emit cap, so it is also your bill cap. Every row is planned by a single emission function carrying the cap as a thrown invariant, so billable rows can never exceed it. Default 100 = up to $0.60. Schema maximum 5000 = up to $30.00, the worst case this Actor can bill. You may receive fewer rows than maxResults when the registries do not hold that many matches; you are billed only for rows actually written.

A measured run.

{"query": "melanoma", "sources": ["clinicaltrials", "isrctn", "euctis"], "maxResults": 100}
, measured 2026-08-25 against the live registries:

StageMeasured
Fetched per registry (over-fetch = 150)ClinicalTrials.gov 150, ISRCTN 150, EU CTIS 150
Registry reports matching in totalClinicalTrials.gov 4,539, ISRCTN 386, EU CTIS 211
Raw records -> unified rows450 -> 436
Genuinely merged, before any cap14 records absorbed; 10 rows span 2+ distinct registries
Written and billed100 rows, 100 result events - $0.60 plus $0.0002 Actor start
Wall clock2.7 seconds

What it searches, and what it does not

SourceEndpointKeyContribution
ClinicalTrials.gov (US NIH)clinicaltrials.gov/api/v2/studies (API v2)nonePaginated on nextPageToken, 100 studies per page, up to 200 pages. countTotal gives a free exact truncation guard. Registry size measured 2026-08-25: 600,192 studies.
ISRCTN (UK / international)www.isrctn.com/api/query/format/default (public XML)noneUK-led and international trials, many absent from ClinicalTrials.gov. Ceiling 500 records per run. The feed's <allTrials totalCount> is read and published, so you always know how many matched.
EU CTIS (EU / EEA)euclinicaltrials.eu/ctis-public-api/search (POST)noneEU Clinical Trials Regulation trials. Measured 2026-08-25: 211 matches for melanoma, 3,065 for cancer, unauthenticated.
PatentsView (optional)search.patentsview.org/api/v1/patent/free keyPatent columns only, when enabled.

Not covered at all: WHO ICTRP, EU CTR / EudraCT (the pre-CTIS register), jRCT, ChiCTR, CTRI, ANZCTR, DRKS, IRCT and Health Canada are not queried. EudraCT numbers appear in output only because the three searched registries cite them as secondary identifiers.

A note on EU CTIS, because earlier versions of this page said it was dead

They were wrong. The endpoint was being called with GET, and the AWS API Gateway in front of it answers an unmatched method with HTTP 403 {"message": "Missing Authentication Token"} - which reads like an authentication wall but is not one. That 403 was swallowed into an empty list, so it surfaced as an ordinary euctis: 0 records. It is a POST, needs no credentials, and returns real data. The GET still returns 403 today; the Actor no longer issues one.

Two consequences. EU CTIS now contributes roughly a third of a typical broad run. And, more durably, no registry here can silently return an empty list any more: a registry that fails is reported as failed with its error text, its record count is null rather than 0, and if every requested registry fails the run fails and bills nothing.


Input reference

PropertyTypeRequiredDefaultMeaning
querystringyes(prefill melanoma)Condition, drug, intervention or sponsor, as free text. Sent as query.term (CTG), q (ISRCTN) and searchCriteria.containAll (CTIS). All three are full-text, not field-scoped.
sourcesarray of stringno["clinicaltrials", "isrctn", "euctis"]Registries to include: clinicaltrials, isrctn, euctis. Anything else is dropped; if nothing known remains the run fails rather than returning an empty answer.
maxResultsintegerno100Maximum unified rows written and billed. Min 1, max 5000. Shared fairly across registries.
onlyMultiRegistrybooleannofalseKeep only trials found in more than one distinct registry. Applied before the cap.
includePatentsbooleannofalseAttach related US patents matched on interventions. Requires patentsViewApiKey; true without a key fails the run.
patentsViewApiKeystring (secret)no(none)Free key from https://search.patentsview.org/ . Used only when includePatents is true.

The cap is shared fairly, so one registry cannot fill it alone

Each registry is asked in parallel for max(10, ceil(maxResults * 1.5)) records so cross-registry duplicates can be found. Unified rows are then allocated in two steps:

  1. Trials found in 2+ distinct registries are reserved off the top. They are the reason a meta-search exists, so they are never cut and appear first in the dataset.
  2. The remainder is divided max-min fairly (water-filling) across the registries that answered. A registry wanting less than an equal share takes only what it has and the surplus is redistributed, so ClinicalTrials.gov cannot starve ISRCTN and a 13-record ISRCTN page cannot strand ClinicalTrials.gov.

Measured on melanoma at maxResults: 100: 10 cross-registry rows reserved, then 30 / 30 / 30. At maxResults: 3 the result is 1 row per registry; at 10 it is 4 / 3 / 3.

Two different counts, both true. The allocation above counts which bucket granted a row. Counting rows containing a leg from each registry gives ClinicalTrials.gov 36, EU CTIS 39, ISRCTN 35 on the same run, because the 10 reserved rows each carry two registries. row_source_bucket reports the first, distinct_registries the second.

The order you list sources in does not affect allocation - it only decides which registries are queried. Emission order is fixed: cross-registry rows first, then round-robin, so page one always shows every registry that answered.


Output fields

One row per unified trial. All 43 fields below are declared in the dataset schema.

Trial content

FieldTypeMeaning
titlestring / nullBrief title (briefTitle -> officialTitle) / ISRCTN <title> / CTIS ctTitle.
official_titlestring / nullFull scientific title.
statusstring / nullRecruitment status. Not harmonized across registries - see below.
phasestring / nullCTG phases joined with , (PHASE1, NA); ISRCTN and CTIS free text (Phase I/II, Therapeutic confirmatory (Phase III)).
study_typestring / nullINTERVENTIONAL / OBSERVATIONAL, or ISRCTN design text. Always null on an EU-CTIS-only row; the CTIS search payload omits it.
enrollmentnumber / nullParticipants enrolled or targeted. CTIS publishes a string; an unreadable value becomes null, never 0.
sponsorstring / nullLead sponsor.
conditionsarray of stringConditions studied, unioned across every merged registry.
interventionsarray of stringDrugs, devices or procedures, unioned across registries.
countriesarray of stringDistinct countries with sites. ISRCTN adds sub-national values (England beside United Kingdom); CTIS publishes Netherlands:5 and only the name is kept.
start_datestring / nullStudy start, as published. Always null on an ISRCTN-only row.
completion_datestring / nullStudy completion. Always null on an EU-CTIS-only row.
last_updatestring / nullRecord last updated. Recovered from the ISRCTN <trial lastUpdated=...> attribute, so ISRCTN rows carry it too.

Registry identity and merge evidence

FieldTypeMeaning
registriesarray of objectOne entry per merged record: {source, registry_id, url}. source is ClinicalTrials.gov, ISRCTN or EU-CTIS.
all_idsarray of stringEvery cross-registry identifier found: NCT\d{8}, ISRCTN\d{6,8}, EudraCT \d{4}-\d{6}-\d{2}.
primary_urlstring / nullURL of the first registry entry on the row.
distinct_registriesarray of stringEach distinct registry the trial was found in, once, sorted.
distinct_registry_countnumber / nullHow many distinct registries. The cross-registry measure to use.
cross_registryboolean / nullTrue only when distinct_registry_count > 1. The meta-search value-add.
multi_registryboolean / nullIdentical to cross_registry. Meaning corrected - see the behaviour-change note.
source_countnumber / nullNumber of registry entries merged. Two records from the same registry sharing an identifier count as 2.
registry_entry_countnumber / nullIdentical to source_count, named so the contrast with distinct_registry_count is unambiguous.
registry_status_codenumber / nullRegistry-native numeric code where one exists (EU CTIS ctStatus, 1-11). Null for text-status registries.
status_decodestring / nullHow status was produced: native_text, decoded_coarse, unmapped_code (status is null and was not guessed), or absent. Travels with the status value, so it always describes the leg that supplied it.

Budget, truncation and merge arithmetic

FieldTypeMeaning
row_source_bucketstring / nullWhich bucket granted this row: cross_registry, a registry name, or unattributed.
raw_records_fetchednumber / nullRecords received from all registries before merging.
unified_rows_availablenumber / nullUnified trials produced by merging, before the cap. Compare with your row count to see what was truncated.
rows_dropped_by_capnumber / nullTrials that existed but were not written because maxResults was reached.
results_truncatedboolean / nullTrue when rows_dropped_by_cap > 0.
max_resultsnumber / nullThe cap this run applied. Bounds both rows written and amount billed.
records_mergednumber / nullThe true merge count: raw records absorbed into an existing row. Computed before the cap, so it never mixes merging with truncation.
cross_registry_rowsnumber / nullUnified rows spanning 2+ distinct registries.
same_registry_merge_rowsnumber / nullRows merging 2+ records from the same registry, typically on a shared EudraCT number. Not cross-registry findings.

Per-registry status (read this before trusting a small result set)

FieldTypeMeaning
sources_requestedarray of stringRegistries this run was asked to search.
sources_okarray of stringRegistries that answered and were parsed. A zero count from one of these IS a verified negative.
sources_failedarray of stringRegistries asked that could not answer. Their absence from the results means nothing.
sources_degradedboolean / nullTrue when at least one requested registry failed, so the set is incomplete for reasons unrelated to your query.
source_statusobject / nullPer registry: ok, failed, or not_requested.
source_record_countsobject / nullRecords contributed per registry. A failed or not-requested registry reports null, never 0, because zero is a factual claim that we looked.
source_diagnosticsarray of objectPer registry: status, records returned, records_available (how many the registry says match in total), truncated, and error text if it failed.

Patents

FieldTypeMeaning
linked_patentsarray of objectPatents matched on interventions: {patent_id, title, date, url, matched_term}.
linked_patent_countnumber / nullLength of linked_patents. null when no lookup ran - never 0, which would claim we searched.
patent_enrichment_statusstring / nullnot_requested, ok, partial, failed: <error>, or no_interventions_to_match.

EU CTIS status-decode canary

EU CTIS publishes its trial status as a numeric code, not text, so this actor carries a code-to-label table (registry_status_code -> status, with status_decode: "decoded_coarse"). A table like that has a failure mode that is invisible from the outside: if EMA renumbers the codes, the table still recognises every code it sees and every row publishes a confident wrong status. A guard that only rejects codes it does not recognise cannot see that.

So before any billable row is written, this actor asks the registry to confirm its own mapping. For each of eight pinned trials it reads the numeric code off the search endpoint, then reads the /retrieve/<ctNumber> document, which publishes both the code (ctPublicStatusCode) and the status as text (ctStatus). It then checks that our label for the code the registry publishes matches the text the registry publishes next to it.

That is deliberately not "trial X should be Authorised". A trial legitimately changing status moves the code and the text together, so the pair still agrees and nothing flaps; a renumbering moves the code without moving the text, and the pair stops agreeing. Coverage is reported from the codes actually observed, so if a pinned trial moves on, the code it left shows up in ctis_decode_canary_codes_unverified rather than being silently claimed.

What a pass does not cover. Codes 2, 3, 4 and 5 all decode to the single label Authorised, and EMA publishes no finer public status text anywhere in the trial payload (checked across every status field in the document, not assumed). A renumbering confined to those four codes therefore cannot be detected here, and this is stated on every row in ctis_decode_canary_note rather than left for you to discover. A renumbering that moves a code across that block's boundary, or that touches any other code, is caught.

FieldTypeMeaning
ctis_decode_canary_statusstring / nullpass (at least one code+text pair verified, no drift measured), fail (drift measured - the run fails and nothing is billed, so you will not see this on a row), unavailable (CTIS could not be probed - the EU leg is then reported as failed, not as a result), not_checked (euctis was not requested).
ctis_decode_canary_probes_passednumber / nullPinned trials whose code+text pair was verified this run.
ctis_decode_canary_probes_failednumber / nullPinned trials that measurably disagreed with the table. Anything above zero fails the run.
ctis_decode_canary_probes_unavailablenumber / nullProbes that could not be completed - registry unreachable, payload incomplete, or the search filter could not be proven to have applied. Never counted as a pass.
ctis_decode_canary_codes_verifiedarray of number / nullThe codes actually verified this run. A short list means thin coverage, not a failure.
ctis_decode_canary_codes_unverifiedarray of number / nullTable codes no probe exercised. Codes 1 (Under evaluation), 6 (Halted) and 7 (Suspended) are normally here by design - they are transient states, so a trial pinned in one stops covering its own code within months.
ctis_decode_canary_measuredarray of object / nullPer pinned trial: the search code, the retrieve code and text, our table's label, whether the filter was proven to have applied, and the outcome with its reason. The measurements, not just the verdict.
ctis_decode_canary_notestring / nullThe coverage boundary above, on every row.

Why an unreachable CTIS does not fail the run, but a drift does. Not being able to reach a registry is an availability problem: ClinicalTrials.gov and ISRCTN can still answer honestly, so the run continues and the EU leg is marked failed - meaning its record count reaches the row as null, never 0. A measured drift is different. It says our published understanding of the registry's vocabulary is wrong, which no retry fixes, and because a merged trial inherits missing fields across registries a wrong EU status would not stay inside the EU rows. That run stops and bills nothing.

One measured detail worth knowing if you build on this API yourself: an unknown searchCriteria key is silently ignored, not rejected. {ctNumber: "..."} returns all 12,288 trials at HTTP 200 with a well-formed page; {number: "..."} returns the one you asked for. Every probe here therefore proves its filter applied - the filtered total must be strictly smaller than the unfiltered total, and the exact ctNumber must be found in the returned page - before its measurement counts for anything.

Measured coverage

Across the 100 rows of the melanoma run, by the bucket that produced them:

BucketRowsFields null or empty
cross_registry10registry_status_code 1 (the one row with no CTIS leg)
ClinicalTrials.gov30registry_status_code 30, phase 4, countries 3, completion_date 2, start_date 1, interventions 1
ISRCTN30status 30, start_date 30, status_decode 30, registry_status_code 30, phase 7
EU-CTIS30study_type 30, completion_date 30, start_date 3

title, official_title, sponsor, enrollment, conditions, registries, all_ids and primary_url were populated on 100 of 100.

Status vocabularies are not harmonized. CTG emits upper-case tokens (COMPLETED, RECRUITING, TERMINATED, WITHDRAWN, NOT_YET_RECRUITING, ACTIVE_NOT_RECRUITING, ENROLLING_BY_INVITATION, UNKNOWN); EU CTIS decodes to the labels below; ISRCTN supplies none. status_decode across the run: native_text 36, decoded_coarse 34, null 30.

Dates are passed through, not normalized. Across 4,409 populated date values: YYYY-MM-DD 2,818, YYYY-MM 691 (CTG month precision), full ISO timestamps 900 (ISRCTN, e.g. 2026-07-03T14:49:06.618822849Z). Parse defensively.


Worked examples

1. Everything on a condition

{ "query": "melanoma", "sources": ["clinicaltrials", "isrctn", "euctis"], "maxResults": 100 }

100 rows, $0.60. A genuine cross-registry row from that run, abridged:

{
"title": "A Study to Learn About the Long-Term Health of Adult Participants From a Previous Study of a New Melanoma Treatment",
"status": "ENROLLING_BY_INVITATION", "status_decode": "native_text",
"phase": "PHASE2", "enrollment": 48, "sponsor": "Regeneron Pharmaceuticals",
"countries": ["United States", "Ireland"],
"start_date": "2025-04-14", "completion_date": "2029-10-10", "last_update": "2026-07-13",
"registries": [
{ "source": "ClinicalTrials.gov", "registry_id": "NCT06848088",
"url": "https://clinicaltrials.gov/study/NCT06848088" },
{ "source": "EU-CTIS", "registry_id": "2024-517729-20-00",
"url": "https://euclinicaltrials.eu/ctis-public/view/2024-517729-20-00" }
],
"all_ids": ["NCT06848088", "2024-517729-20"],
"distinct_registries": ["ClinicalTrials.gov", "EU-CTIS"],
"distinct_registry_count": 2, "cross_registry": true, "source_count": 2,
"row_source_bucket": "cross_registry",
"sources_ok": ["clinicaltrials", "isrctn", "euctis"], "sources_failed": [],
"results_truncated": true, "rows_dropped_by_cap": 336
}

One row carrying the US and EU registrations together, instead of two lookups and a manual reconciliation. Note sources_failed: [] and rows_dropped_by_cap: 336 - the row states both that nothing was missing and that more existed.

2. Only the dual-registered trials

{
"query": "prostate cancer radiotherapy",
"sources": ["clinicaltrials", "isrctn"],
"onlyMultiRegistry": true,
"maxResults": 50
}

onlyMultiRegistry runs before the cap, so it sees every registry even when maxResults is small, and it keeps only genuinely cross-registry trials. A real merge from that query:

{
"title": "Prostate Reirradiation Toxicity Outcomes Feasibility Study",
"status": "NOT_YET_RECRUITING", "phase": "NA", "enrollment": 60,
"sponsor": "University of Leeds", "countries": ["United Kingdom"],
"start_date": "2022-11-15", "completion_date": "2026-11-15",
"registries": [
{ "source": "ClinicalTrials.gov", "registry_id": "NCT05614700",
"url": "https://clinicaltrials.gov/study/NCT05614700" },
{ "source": "ISRCTN", "registry_id": "ISRCTN12238218",
"url": "https://www.isrctn.com/ISRCTN12238218" }
],
"all_ids": ["NCT05614700", "ISRCTN12238218"],
"distinct_registry_count": 2, "cross_registry": true
}

That row's conditions array carries "Prostate cancer\n\tCancer" beside the clean CTG values - embedded tabs and newlines are how ISRCTN publishes the field, and the Actor does not rewrite it.

3. A small cap still returns every registry

{ "query": "melanoma", "sources": ["isrctn", "clinicaltrials", "euctis"], "maxResults": 3 }

Returns 1 row per registry. Under the old order-then-truncate behaviour a cap this small returned three rows from whichever registry was listed first.


How the registries are merged and deduplicated

Every record - its own registry_id plus all secondary identifiers - is scanned for NCT\d{8}, ISRCTN\d{6,8} and the EudraCT form \d{4}-\d{6}-\d{2}. CTG exposes these through secondaryIdInfos; for ISRCTN the whole XML block is regex-scanned, so a cross-listed NCT number mentioned anywhere is picked up; a CTIS number such as 2024-517729-20-00 carries its EudraCT-style trio in its first three groups, which is what lets a CTIS trial merge with its CTG or ISRCTN twin. Every harvested identifier becomes a merge key, so two records sharing any identifier collapse into one row; a record yielding no identifier falls back to a fingerprint of the first 12 words of its lower-cased, punctuation-stripped title.

The first record to arrive establishes the row; later records only fill scalar fields still null or empty. Array fields are unioned. registries gains one entry per distinct (source, registry_id) pair; all_ids accumulates every identifier seen.

Behaviour change: multi_registry now means distinct registries

multi_registry used to be registries.length > 1, counting registry entries, so two records from the same registry sharing an identifier made it true. Not hypothetical: a melanoma run produced a row whose two legs were ISRCTN30841680 and ISRCTN15406513, merged on the shared EudraCT number 2022-000455-36 - a field advertising "found in more than one registry" reading true for a trial present in exactly one.

multi_registry is now identical to cross_registry. If you relied on the old meaning, read source_count or registry_entry_count, which are unchanged. onlyMultiRegistry: true now returns fewer rows, all genuinely cross-registry. Same-registry merges remain visible through same_registry_merge_rows.

Merge counts are computed before the cap

The run log used to report a merge figure computed after truncation, mixing the two. It is now computed before any cap exists and published on the rows. On the melanoma run: 450 raw records -> 436 unified, 14 genuinely merged, 10 spanning 2+ distinct registries, 3 same-registry merges. Everything else that did not reach the dataset was truncation: rows_dropped_by_cap: 336.

Cross-registry overlap, measured

Measured 2026-08-25 against the live registries at maxResults: 100:

QueryCTG / ISRCTN / CTIS fetchedUnifiedMergedCross-registrySame-registry
melanoma150 / 150 / 15043614103
semaglutide150 / 13 / 9324115132
prostate cancer radiotherapy150 / 150 / 0299110
cystic fibrosis150 / 150 / 39333641

Dual registration on a single query slice is a handful to a few dozen trials, not a majority. The value is a merged single-schema list plus recovered cross-identifiers, not a large duplicate count. Note the prostate cancer radiotherapy row: EU CTIS returned 0 records with status ok and records_available: 0 - a verified negative you can act on, not silence.


EU CTIS status codes

CTIS publishes status as a numeric ctStatus code, not text. The decode table was derived by measurement: for each code the search API was filtered to that code and the first returned trial was fetched from the per-trial retrieve endpoint, which publishes the status as text.

CodePublished asCodePublished as
1Under evaluation7Suspended
2, 3, 4, 5Authorised8Ended
6Halted9Expired
10Revoked11Not authorised

Honest boundary: codes 2 through 5 all resolve to Authorised on the retrieve endpoint. The portal shows finer labels ("Authorised, not started", "Ongoing, recruiting" and so on) and those strings exist in its translation bundle, but their order could not be verified against these codes, so the finer distinction is deliberately not published rather than guessed. If you need it, read registry_status_code and resolve it yourself. An unrecognised code sets status to null with status_decode: "unmapped_code"; it is never turned into a plausible-looking label.


Optional patent linkage

Set includePatents: true and supply a free patentsViewApiKey. For each trial the Actor takes up to its first 3 interventions, phrase-searches each against patent titles in the PatentsView Search API v1, and attaches up to 5 distinct patents per trial.

  • Patents add columns, not rows - they do not change how many results you are billed for.
  • includePatents: true with no key now fails the run before anything is billed. It used to write linked_patent_count: 0 on every row, indistinguishable from "searched and found nothing". If no lookup runs the count is null, and patent_enrichment_status says why.
  • If every attempted lookup errors the count is null and the status reads failed: <error>; a partial failure reads partial; a trial with no interventions reads no_interventions_to_match.
  • Matching is a title phrase match on the intervention string: a lead, not a legal FTO determination. A generic name such as Placebo, or a long ISRCTN free-text intervention description, matches poorly or not at all. For real freedom-to-operate work use malonestar/uspto-patent-lapse-fto-monitor and malonestar/ptab-trial-tracker.

Limitations and honest boundaries

  • ISRCTN publishes no recruitment status and no start date in this feed. Verified by enumerating every tag in a real ISRCTN record: there is no recruitmentStatus or overallStatus tag and no overall start date. status and start_date are null on every ISRCTN-only row (30 of 30 in the measured run) and nothing is invented to fill them. This is more visible than it used to be, precisely because ISRCTN is now about a third of a broad run instead of being truncated away. Use primary_url for the full record.
  • EU CTIS status is coarse, and EU CTIS rows carry no study_type or completion_date - those fields are absent from the CTIS search payload.
  • ISRCTN and EU CTIS are each fetched in a single request, ceiling 500 records. ISRCTN ignores offset and page entirely, so there is no pagination to walk. source_diagnostics reports records_available and truncated per registry, so you always know what you did not see.
  • Results are not relevance-ranked, and status and phase vocabularies are not harmonized. maxResults truncates by fair share, not by "the best N"; harmonizing vocabularies would mean inventing a mapping the registries do not publish.
  • query is free text, so recall is broad and precision is loose, especially on ISRCTN, which matches across the whole record - a semaglutide search can return a ketogenic-diet trial that merely mentions the drug. The title-fingerprint fallback can also over-merge two identically-titled trials carrying no identifier, though all three registries publish one on essentially every record.
  • This is registry metadata, not results data. No outcome measures, adverse-event tables, results postings, eligibility criteria, per-site addresses or contact names - use primary_url for the full record. It is a search and reconciliation tool, not a regulatory or medical deliverable; verify anything decision-critical against the registry itself.

Use as an MCP tool

This Actor is callable by any MCP-compatible AI agent through Apify's hosted MCP server. There is no server to run and no integration code to write - the tool schema an agent sees is generated from this Actor's own input and dataset schemas.

Endpoint

https://mcp.apify.com?tools=malonestar/clinical-trials-meta-search

Claude Desktop, Claude Code or Cursor - add to claude_desktop_config.json, .mcp.json or .cursor/mcp.json respectively:

{
"mcpServers": {
"apify": {
"url": "https://mcp.apify.com?tools=malonestar/clinical-trials-meta-search",
"headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" }
}
}
}

Get a token at https://console.apify.com/settings/integrations . Claude Desktop can also authenticate interactively via OAuth against https://mcp.apify.com with no headers block. Full reference: https://docs.apify.com/platform/integrations/mcp

Try asking your agent

Find clinical trials for prostate cancer radiotherapy registered in more than one registry, give me the sponsor, phase and every registry ID for each, and tell me if any registry failed to answer.

That last clause is answerable: sources_failed and source_diagnostics are on every row, so an agent can distinguish a real negative from a degraded run without reading the log.

Chains well with - comma-separate the tools parameter:

https://mcp.apify.com?tools=malonestar/clinical-trials-meta-search,malonestar/drug-shortage-delta-monitor,malonestar/nadac-price-movers,malonestar/cms-open-payments

Billing is unchanged as an MCP tool: an agent pays the same per-result price a human does.


FAQ

Which registries are actually searched? ClinicalTrials.gov (API v2), ISRCTN (public XML feed) and EU CTIS (public search API). All three are keyless and return real data. No other national or WHO registry is queried. A free PatentsView key is needed only for optional patent linkage.

Why did my run return rows from only one registry? Check sources_ok and sources_failed. A registry in sources_failed could not answer and its absence means nothing; one in sources_ok with a count of 0 is a verified negative. The cap itself no longer favours any registry.

How do I get only trials registered in more than one place? Set onlyMultiRegistry: true - it is applied before the cap and filters on distinct registries. You can also read cross_registry on a normal run.

What changed about multi_registry? It now means "found in 2+ distinct registries" instead of "2+ registry entries". Rows whose two legs came from the same registry no longer set it. Use source_count or registry_entry_count for the old meaning.

How is billing calculated? One result event per unified row written, at $0.006 on the free tier. maxResults caps rows before they are written, so it caps the bill. Patent enrichment adds columns, not rows. Maximum maxResults 5000 = $30.00 plus the Actor-start event.

Can I use it for a systematic review search? It helps with the multi-registry search step, and all_ids gives the cross-registry identifiers a PRISMA-S appendix wants. It is not a complete registry search: ISRCTN and EU CTIS are each read in a single request capped at 500 records, and WHO ICTRP is not queried. Cite source_diagnostics.records_available rather than guessing.

What happens on a bad or empty query? No query is rejected by the input schema before the run starts. A blank or whitespace-only query fails the run, as does a sources list containing no known registry. If every requested registry fails to answer the run fails and bills nothing, because a zero-row result there would be a wrong answer rather than a negative. A valid query matching nothing on registries that all answered produces a successful run with zero rows and zero charges.


Same buyer, adjacent question - all verified published:

All measurements in this README were taken on 2026-08-25 by running this Actor's own exported modules against the live registries.