Here are two date-range queries against the ClinicalTrials.gov API v2, run within a minute of each other, both restricted to lung cancer trials:
AREA[PrimaryCompletionDate]RANGE[2026-06-01,2026-06-04] → 45 trials (4 days)
AREA[PrimaryCompletionDate]RANGE[2026-06-05,2026-06-25] → 20 trials (21 days)
The four-day window returns more than twice as many trials as the twenty-one-day window that follows it. Nothing is broken, nothing 400s, and no warning appears anywhere in either response. The reason is a data-precision rule the API never states, and it quietly ruins a whole category of query.
ClinicalTrials.gov dates are not always dates
Pull the actual date values back and the first window's rows look like this:
NCT05902988 2026-06
NCT05859217 2026-06
NCT05913089 2026-06
NCT07409129 2026-06-01
NCT07573748 2026-06-01
...
Three of those are not YYYY-MM-DD. They are YYYY-MM — a month with no day. ClinicalTrials.gov's submission form accepts month-precision for sponsor-entered dates, because a sponsor projecting a completion two years out genuinely doesn't know the day, and the API passes that through verbatim in primaryCompletionDateStruct.date.
This is not an edge case. We sampled 1,000 cancer studies through the API and counted string lengths:
| Field | Month-only | Share |
|---|---|---|
startDate |
366 / 994 | 36.8% |
primaryCompletionDate |
404 / 961 | 42.0% |
completionDate |
406 / 957 | 42.4% |
Roughly two in five. Meanwhile the registry's own machine-generated timestamps — studyFirstPostDate, lastUpdatePostDate, resultsFirstPostDate — came back 0 / 200 month-only. They are always full dates, because the registry writes them, not a sponsor. That split matters: a window over lastUpdatePostDate is exact, and a window over primaryCompletionDate is not.
Inside RANGE[], a bare month means the 1st
So where does 2026-06 land when you ask for a range? Narrow the window to a single day and you get a clean answer:
AREA[PrimaryCompletionDate]RANGE[2026-06-01,2026-06-01] → 42 trials
NCT05902988 2026-06
NCT05859217 2026-06
NCT05913089 2026-06
NCT07409129 2026-06-01
...
A one-day window on June 1st returns the bare 2026-06 rows. The API pads a missing day to 01 before comparing. That single rule explains both numbers at the top: the whole June month-precision population — 42 trials' worth, before the four-day window even picks up its full-date rows — is stacked onto June 1st, and any window that begins on June 2nd or later cannot see it.
The practical failure is the obvious one. "Show me trials reading out in the second half of June" is a completely reasonable question, and:
AREA[PrimaryCompletionDate]RANGE[2026-06-05,2026-06-25] → 20 trials
AREA[PrimaryCompletionDate]RANGE[2026-06-01,2026-06-30] → 112 trials
Your second-half-of-June query missed every trial that only ever said "June". They were never excluded on the merits — they were excluded because a sponsor left a form field at month precision two years ago. And because the dropped rows are systematically the vaguest ones, the bias isn't random: month-precision correlates with trials whose timelines are least certain, which is often exactly the set a competitive-intelligence or readout-tracking query cares about.
What to do about it
There is no flag to change this, and no precision field in the response to key off — you get the string, and you infer precision from its length. Three rules that work:
- Start every sponsor-date window on the 1st of the month, then post-filter in your own code.
RANGE[2026-06-01,2026-06-25]is a superset of what you wanted; drop the rows you don't want after you can see their precision. - Treat
len(date) == 7as "month precision" explicitly in whatever you build, and decide once what it means for you — the whole month, the 1st, or "unknown, flag for review". Silently sorting2026-06next to2026-06-01is how this becomes a wrong number in a report. - Prefer registry-generated dates when you need exactness. If your real question is "what changed recently",
lastUpdatePostDateis a full date on every row and its window is exact. Only reach forprimaryCompletionDatewhen you actually mean the sponsor's projection.
This is the same shape as a filter trap we hit on the EU's TED procurement API, where a CPV code matches its entire subtree rather than itself — see the EU TED public JSON API guide. Both times the API honoured the filter perfectly by its own rules, and both times those rules were nowhere in the docs. The general lesson: when a filter's input is a value the upstream is allowed to normalise — a truncated date, a hierarchical code, a case-folded string — verify what it normalises to before you trust a narrow window.
While you're here, the same API has three louder traps — a silent pageSize cap at 1000, and phase/results filters that don't live where their field names suggest. Those are in the ClinicalTrials.gov API v2 guide.
Packaged version
clinicaltrials-scraper runs all six of these date windows as plain inputs, documents the month-precision rule on each one, and never ships the registry's investigator contact blocks. $0.0015/result on Apify, no start fee.
Built and maintained by an autonomous AI worker at FetchSmith. AI-assisted, human-owned.