Clinical Trials API - ClinicalTrials.gov, ISRCTN, EU CTIS
Pricing
from $3.30 / 1,000 results
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
Maintained by CommunityActor stats
0
Bookmarked
3
Total users
2
Monthly active users
21 days ago
Last modified
Categories
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}| Stage | Measured |
|---|---|
| Fetched per registry (over-fetch = 150) | ClinicalTrials.gov 150, ISRCTN 150, EU CTIS 150 |
| Registry reports matching in total | ClinicalTrials.gov 4,539, ISRCTN 386, EU CTIS 211 |
| Raw records -> unified rows | 450 -> 436 |
| Genuinely merged, before any cap | 14 records absorbed; 10 rows span 2+ distinct registries |
| Written and billed | 100 rows, 100 result events - $0.60 plus $0.0002 Actor start |
| Wall clock | 2.7 seconds |
What it searches, and what it does not
| Source | Endpoint | Key | Contribution |
|---|---|---|---|
| ClinicalTrials.gov (US NIH) | clinicaltrials.gov/api/v2/studies (API v2) | none | Paginated 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) | none | UK-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) | none | EU 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 key | Patent 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
| Property | Type | Required | Default | Meaning |
|---|---|---|---|---|
query | string | yes | (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. |
sources | array of string | no | ["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. |
maxResults | integer | no | 100 | Maximum unified rows written and billed. Min 1, max 5000. Shared fairly across registries. |
onlyMultiRegistry | boolean | no | false | Keep only trials found in more than one distinct registry. Applied before the cap. |
includePatents | boolean | no | false | Attach related US patents matched on interventions. Requires patentsViewApiKey; true without a key fails the run. |
patentsViewApiKey | string (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:
- 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.
- 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_bucketreports the first,distinct_registriesthe 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
| Field | Type | Meaning |
|---|---|---|
title | string / null | Brief title (briefTitle -> officialTitle) / ISRCTN <title> / CTIS ctTitle. |
official_title | string / null | Full scientific title. |
status | string / null | Recruitment status. Not harmonized across registries - see below. |
phase | string / null | CTG phases joined with , (PHASE1, NA); ISRCTN and CTIS free text (Phase I/II, Therapeutic confirmatory (Phase III)). |
study_type | string / null | INTERVENTIONAL / OBSERVATIONAL, or ISRCTN design text. Always null on an EU-CTIS-only row; the CTIS search payload omits it. |
enrollment | number / null | Participants enrolled or targeted. CTIS publishes a string; an unreadable value becomes null, never 0. |
sponsor | string / null | Lead sponsor. |
conditions | array of string | Conditions studied, unioned across every merged registry. |
interventions | array of string | Drugs, devices or procedures, unioned across registries. |
countries | array of string | Distinct countries with sites. ISRCTN adds sub-national values (England beside United Kingdom); CTIS publishes Netherlands:5 and only the name is kept. |
start_date | string / null | Study start, as published. Always null on an ISRCTN-only row. |
completion_date | string / null | Study completion. Always null on an EU-CTIS-only row. |
last_update | string / null | Record last updated. Recovered from the ISRCTN <trial lastUpdated=...> attribute, so ISRCTN rows carry it too. |
Registry identity and merge evidence
| Field | Type | Meaning |
|---|---|---|
registries | array of object | One entry per merged record: {source, registry_id, url}. source is ClinicalTrials.gov, ISRCTN or EU-CTIS. |
all_ids | array of string | Every cross-registry identifier found: NCT\d{8}, ISRCTN\d{6,8}, EudraCT \d{4}-\d{6}-\d{2}. |
primary_url | string / null | URL of the first registry entry on the row. |
distinct_registries | array of string | Each distinct registry the trial was found in, once, sorted. |
distinct_registry_count | number / null | How many distinct registries. The cross-registry measure to use. |
cross_registry | boolean / null | True only when distinct_registry_count > 1. The meta-search value-add. |
multi_registry | boolean / null | Identical to cross_registry. Meaning corrected - see the behaviour-change note. |
source_count | number / null | Number of registry entries merged. Two records from the same registry sharing an identifier count as 2. |
registry_entry_count | number / null | Identical to source_count, named so the contrast with distinct_registry_count is unambiguous. |
registry_status_code | number / null | Registry-native numeric code where one exists (EU CTIS ctStatus, 1-11). Null for text-status registries. |
status_decode | string / null | How 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
| Field | Type | Meaning |
|---|---|---|
row_source_bucket | string / null | Which bucket granted this row: cross_registry, a registry name, or unattributed. |
raw_records_fetched | number / null | Records received from all registries before merging. |
unified_rows_available | number / null | Unified trials produced by merging, before the cap. Compare with your row count to see what was truncated. |
rows_dropped_by_cap | number / null | Trials that existed but were not written because maxResults was reached. |
results_truncated | boolean / null | True when rows_dropped_by_cap > 0. |
max_results | number / null | The cap this run applied. Bounds both rows written and amount billed. |
records_merged | number / null | The true merge count: raw records absorbed into an existing row. Computed before the cap, so it never mixes merging with truncation. |
cross_registry_rows | number / null | Unified rows spanning 2+ distinct registries. |
same_registry_merge_rows | number / null | Rows 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)
| Field | Type | Meaning |
|---|---|---|
sources_requested | array of string | Registries this run was asked to search. |
sources_ok | array of string | Registries that answered and were parsed. A zero count from one of these IS a verified negative. |
sources_failed | array of string | Registries asked that could not answer. Their absence from the results means nothing. |
sources_degraded | boolean / null | True when at least one requested registry failed, so the set is incomplete for reasons unrelated to your query. |
source_status | object / null | Per registry: ok, failed, or not_requested. |
source_record_counts | object / null | Records contributed per registry. A failed or not-requested registry reports null, never 0, because zero is a factual claim that we looked. |
source_diagnostics | array of object | Per registry: status, records returned, records_available (how many the registry says match in total), truncated, and error text if it failed. |
Patents
| Field | Type | Meaning |
|---|---|---|
linked_patents | array of object | Patents matched on interventions: {patent_id, title, date, url, matched_term}. |
linked_patent_count | number / null | Length of linked_patents. null when no lookup ran - never 0, which would claim we searched. |
patent_enrichment_status | string / null | not_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.
| Field | Type | Meaning |
|---|---|---|
ctis_decode_canary_status | string / null | pass (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_passed | number / null | Pinned trials whose code+text pair was verified this run. |
ctis_decode_canary_probes_failed | number / null | Pinned trials that measurably disagreed with the table. Anything above zero fails the run. |
ctis_decode_canary_probes_unavailable | number / null | Probes 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_verified | array of number / null | The codes actually verified this run. A short list means thin coverage, not a failure. |
ctis_decode_canary_codes_unverified | array of number / null | Table 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_measured | array of object / null | Per 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_note | string / null | The 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:
| Bucket | Rows | Fields null or empty |
|---|---|---|
cross_registry | 10 | registry_status_code 1 (the one row with no CTIS leg) |
ClinicalTrials.gov | 30 | registry_status_code 30, phase 4, countries 3, completion_date 2, start_date 1, interventions 1 |
ISRCTN | 30 | status 30, start_date 30, status_decode 30, registry_status_code 30, phase 7 |
EU-CTIS | 30 | study_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:
| Query | CTG / ISRCTN / CTIS fetched | Unified | Merged | Cross-registry | Same-registry |
|---|---|---|---|---|---|
melanoma | 150 / 150 / 150 | 436 | 14 | 10 | 3 |
semaglutide | 150 / 13 / 93 | 241 | 15 | 13 | 2 |
prostate cancer radiotherapy | 150 / 150 / 0 | 299 | 1 | 1 | 0 |
cystic fibrosis | 150 / 150 / 39 | 333 | 6 | 4 | 1 |
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.
| Code | Published as | Code | Published as |
|---|---|---|---|
| 1 | Under evaluation | 7 | Suspended |
| 2, 3, 4, 5 | Authorised | 8 | Ended |
| 6 | Halted | 9 | Expired |
| 10 | Revoked | 11 | Not 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: truewith no key now fails the run before anything is billed. It used to writelinked_patent_count: 0on every row, indistinguishable from "searched and found nothing". If no lookup runs the count isnull, andpatent_enrichment_statussays why.- If every attempted lookup errors the count is
nulland the status readsfailed: <error>; a partial failure readspartial; a trial with no interventions readsno_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 usemalonestar/uspto-patent-lapse-fto-monitorandmalonestar/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
recruitmentStatusoroverallStatustag and no overall start date.statusandstart_datearenullon 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. Useprimary_urlfor the full record. - EU CTIS status is coarse, and EU CTIS rows carry no
study_typeorcompletion_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
offsetandpageentirely, so there is no pagination to walk.source_diagnosticsreportsrecords_availableandtruncatedper registry, so you always know what you did not see. - Results are not relevance-ranked, and status and phase vocabularies are not harmonized.
maxResultstruncates by fair share, not by "the best N"; harmonizing vocabularies would mean inventing a mapping the registries do not publish. queryis free text, so recall is broad and precision is loose, especially on ISRCTN, which matches across the whole record - asemaglutidesearch 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_urlfor 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.
Related Actors
Same buyer, adjacent question - all verified published:
- Drug Shortage API - Delta Monitor & FDA Shortage Tracker - openFDA shortage feed with change detection.
- Medicare Part D Drug Spending Delta & Top Movers API (CMS) - year-over-year Part D spending movement by drug.
- NADAC Drug Pricing API - Weekly NDC Price Movers - weekly national acquisition cost movement by NDC.
- CMS Open Payments API - Physician Roll-up (Sunshine Act) - industry payments to physicians; investigator due diligence.
- License Verification API - Nurses, MDs & OIG Exclusions - licence status plus OIG exclusion screening for site staff.
- HRSA NHSC & NURSE Corps Clinician Workforce Site Monitor - clinician site rosters for recruitment-site targeting.
- USPTO Patent Maintenance-Fee Lapse Monitor - FTO Tracker - the serious tool for the patent question this Actor only gestures at.
- USPTO PTAB Trial Tracker - IPR/PGR/CBM API - post-grant challenges against the patents behind a trial's interventions.
All measurements in this README were taken on 2026-08-25 by running this Actor's own exported modules against the live registries.