# Europe PMC API 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: - **Keyword search of bioRxiv/medRxiv preprints.** The preprint servers' own APIs have no keyword search at all (see `references/biorxiv.md`). Europe PMC indexes them and filters with `SRC:"PPR"`. - **Search inside full text**, not just titles and abstracts, with the results-limiting filters (`HAS_FT:Y`, `OPEN_ACCESS:Y`) applied server-side. - **Full text that fails honestly.** `fullTextXML` returns a clean **404** when an article is not open access, where NCBI eFetch returns HTTP 200 with metadata and no `` (see the hazard section of `references/pmc.md`). All figures below verified 2026-07-27. ## Base URL ``` https://www.ebi.ac.uk/europepmc/webservices/rest ``` ## Authentication None. No key, no email parameter, no registration. ## Rate Limits 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. ## Key Endpoints ### 1. Search ``` 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: ```bash 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:** ```json { "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: ```json {"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. ### 2. Full text XML ``` GET /{PMCID}/fullTextXML ``` ``` https://www.ebi.ac.uk/europepmc/webservices/rest/PMC7029759/fullTextXML ``` Returns a JATS `
` (not wrapped in `` 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 ``. A 404 here is the honest answer, so prefer this endpoint when you need to *know* whether full text exists. ### 3. Citations and references ``` 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`. ### 4. Text-mined terms and supplementary files ``` 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. ## Query Language 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:** ```bash 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. ## Result Object (resultType=core, key fields) ```json { "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. ## Identifiers 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. ## Pagination and Count Reconciliation 1. `hitCount` on the first response is the total. 2. Request with `cursorMark=*`, then pass the returned `nextCursorMark` on each subsequent call. 3. **Stop when `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. 4. Reconcile retrieved count against `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.