skills/paper-lookup/references/europepmc.md
Europe PMC is a single search surface over PubMed abstracts, PMC full text, preprints (bioRxiv, medRxiv, Research Square, SSRN and others), patents, NHS guidelines, and theses. It is the one API in this skill that does keyword search across those corpora at once.
Reach for it when you need something the others cannot do:
references/biorxiv.md). Europe PMC indexes them and filters with SRC:"PPR".HAS_FT:Y, OPEN_ACCESS:Y) applied server-side.fullTextXML returns a clean 404 when an article is not
open access, where NCBI eFetch returns HTTP 200 with metadata and no <body> (see the hazard
section of references/pmc.md).All figures below verified 2026-07-27.
https://www.ebi.ac.uk/europepmc/webservices/rest
None. No key, no email parameter, no registration.
No published per-second limit. Europe PMC asks for reasonable use and recommends cursorMark
pagination over deep page offsets for large walks. Keep concurrency low and serialize long walks.
GET /search?query={query}&format=json&pageSize={n}&resultType={type}
| Parameter | Default | Description |
|---|---|---|
query | required | Query language below. URL-encode it. |
format | xml | json, xml, or dc |
resultType | lite | idlist (IDs only), lite (core bibliographic), core (adds abstract, full-text links, MeSH, grants) |
pageSize | 25 | Max 1,000. Over that is rejected, not clamped -- see the error shape below. |
cursorMark | * | Deep pagination -- use this, not page |
page | 1 | 1-based. Only for shallow paging. |
sort | relevance | CITED desc, P_PDATE_D desc (publication date), TITLE asc -- note the space before the direction, not a colon |
Example -- preprints about CRISPR:
curl -s --get "https://www.ebi.ac.uk/europepmc/webservices/rest/search" \
--data-urlencode 'query=CRISPR AND SRC:"PPR"' \
--data-urlencode 'format=json' \
--data-urlencode 'pageSize=2' \
--data-urlencode 'resultType=lite'
Returns hitCount 13341 with resultList.result[] entries whose id values look like PPR1283561
and source is PPR.
Response envelope:
{
"version": "6.9",
"hitCount": 13341,
"nextCursorMark": "AoIIQExCVyg1NTg2NjE3NQ==",
"nextPageUrl": "https://www.ebi.ac.uk/europepmc/webservices/rest/search?...",
"request": {"queryString": "CRISPR AND SRC:\"PPR\"", "resultType": "lite", "cursorMark": "*", "pageSize": 2},
"resultList": {"result": [...]}
}
The echoed request.queryString is the query as parsed -- diff it against what you sent to catch
a mangled or truncated query before trusting hitCount.
Errors arrive with HTTP 200 and no resultList. pageSize=1001 returns:
{"errCode": 404, "errMsg": "Invalid page size provided. Valid size is between 1 and 1000"}
Note the errCode is 404 inside a 200 response. Check for errCode / the absence of resultList
before indexing into results -- neither the HTTP status nor an exception will tell you.
GET /{PMCID}/fullTextXML
https://www.ebi.ac.uk/europepmc/webservices/rest/PMC7029759/fullTextXML
Returns a JATS <article> (not wrapped in <pmc-articleset> the way eFetch is). Pipe it through
scripts/jats_to_text.py, which handles both wrappers.
404 means not open access -- verified on PMC1500000, the same article for which eFetch returns a
200 with no <body>. A 404 here is the honest answer, so prefer this endpoint when you need to
know whether full text exists.
GET /{source}/{id}/citations?format=json&pageSize={n}&page={n}
GET /{source}/{id}/references?format=json&pageSize={n}&page={n}
source is the corpus code: MED (PubMed), PMC, PPR (preprints), PAT (patents), AGR, CBA,
CTX, ETH, HIR, NBK.
https://www.ebi.ac.uk/europepmc/webservices/rest/MED/32117569/citations?format=json&pageSize=1
Returns hitCount plus citationList.citation[] (or referenceList.reference[]). Both wrap the
list in a corpus-specific key, so parse by endpoint rather than assuming resultList.
GET /{source}/{id}/textMinedTerms/{semanticType}?format=json
GET /{source}/{id}/supplementaryFiles
Both are per-article optional and return 404 when the article has none. A 404 here means "this article has no such data", not a broken request -- do not treat it as an outage or retry it.
Verified on MED/32117569: resultType=core reports hasSuppl: "N", and supplementaryFiles 404s,
consistent with each other. Read hasSuppl from a core search first and skip the call when it is
"N"; there is no equivalent pre-check for textMinedTerms, which 404s for the same article.
Field-prefixed terms combined with AND / OR / NOT (uppercase), quoted phrases, and
parentheses.
| Field | Matches | Example |
|---|---|---|
SRC | Corpus | SRC:"PPR" (preprints), SRC:"MED", SRC:"PMC" |
PUBLISHER | Preprint server or publisher | PUBLISHER:"bioRxiv", PUBLISHER:"medRxiv" |
AUTH | Author name | AUTH:"Doudna J" |
TITLE | Title | TITLE:"gene editing" |
ABSTRACT | Abstract | ABSTRACT:organoid |
PUB_YEAR | Publication year | PUB_YEAR:2023, PUB_YEAR:[2020 TO 2024] |
HAS_FT | Full text indexed | HAS_FT:Y |
OPEN_ACCESS | Open access | OPEN_ACCESS:Y |
IN_EPMC | Full text hosted in Europe PMC | IN_EPMC:Y |
DOI | DOI | DOI:"10.1038/nature12373" |
EXT_ID | PMID | EXT_ID:32117569 |
JOURNAL | Journal title | JOURNAL:"Nature" |
MESH | MeSH term | MESH:"CRISPR-Cas Systems" |
LANG | Language | LANG:eng |
A bare term with no prefix searches title, abstract, and full text together.
The pattern that closes the preprint gap:
curl -s --get "https://www.ebi.ac.uk/europepmc/webservices/rest/search" \
--data-urlencode 'query=(SRC:"PPR" AND PUBLISHER:"bioRxiv" AND "organoid")' \
--data-urlencode 'format=json&pageSize=2&resultType=lite'
hitCount 1972, with bookOrReportDetails.publisher confirming bioRxiv on each hit. Take the
doi (a 10.1101/... preprint DOI) from these results and hand it to the bioRxiv API for
preprint-specific metadata such as the published-version link.
{
"id": "37917583",
"source": "MED",
"pmid": "37917583",
"pmcid": "PMC10680139",
"doi": "10.1016/j.celrep.2023.113339",
"title": "...",
"authorString": "Smith J, Jones A.",
"journalInfo": {"volume": "42", "journal": {"title": "Cell reports"}},
"pubYear": "2023",
"abstractText": "...",
"isOpenAccess": "Y",
"inEPMC": "Y",
"hasPDF": "Y",
"hasSuppl": "Y",
"citedByCount": 14,
"fullTextUrlList": {"fullTextUrl": [{"documentStyle": "pdf", "url": "..."}]}
}
The boolean-ish fields are the strings "Y" / "N", not JSON booleans. A truthiness test
passes for "N", so compare explicitly. pmcid is absent -- not null -- when the article is not in
PMC.
Preprint (SRC:"PPR") records differ in shape: the server name lives in
bookOrReportDetails.publisher, and journalInfo is absent. Do not assume one schema across corpora.
Every result carries id + source, and that pair is the key -- id alone is not unique across
corpora. Endpoints that take an article path want {source}/{id}, e.g. MED/32117569. Preprint IDs
are PPR-prefixed (PPR1283561) and are Europe PMC's own, not bioRxiv's; use the record's doi to
cross-reference.
hitCount on the first response is the total.cursorMark=*, then pass the returned nextCursorMark on each subsequent call.resultList.result is empty or nextCursorMark equals the cursor you sent. There is
no null terminator: at exhaustion Europe PMC returns an empty result list and echoes your own
cursor back. Detecting the end therefore costs one extra empty request -- expected, not a fault.hitCount and report both.Verified walk (AUTH:"Doudna J" AND PUB_YEAR:2013 AND SRC:"MED", pageSize=5): pages of 5, 5, 5, 4,
then a 5th request returning 0 results with the cursor unchanged. Retrieved 19, hitCount 19.
scripts/paginate.py --api europepmc implements this, including the repeated-cursor stop condition.
Deep page offsets degrade and are capped; cursorMark is the supported path for anything past a
few pages.