A Zotero 7/8/9/10 plugin that goes further than the built-in "Find Available PDF" to attach full text to your library items.
- Download
better-find-full-text.xpifrom the latest release - In Zotero: Tools → Add-ons → gear icon → Install Add-on From File
- Select the downloaded
.xpifile and restart Zotero
Select one or more items in your library and right-click. Three menu entries are added:
- Better Find Full Text — runs the normal fetch flow
- Better Find Full Text (force retry) — deletes known-bad snapshots first, then re-runs (see below)
- Cancel Better Find Full Text — interrupts a running batch between items
A non-blocking progress window shows what's happening. When the batch finishes you'll see a summary like Done: 2 PDFs, 1 snapshot, 1 opened in browser.
For each selected item, the plugin works through a series of strategies in order, stopping as soon as one succeeds:
Runs the same lookup as Zotero's built-in "Find Available PDF": tries DOI resolution, Unpaywall (open-access PDFs), the item's URL field, and any custom resolvers you've configured in Zotero preferences. If this succeeds, you get a PDF silently with no further steps.
Fetches the article page and checks whether citation_pdf_url — a metadata tag that publishers embed in every article page — resolves to an actual PDF. If you have institutional access (e.g. via a university proxy configured in your browser or Zotero), the PDF downloads automatically. If the URL exists but isn't serving a PDF, the plugin knows the article is paywalled and moves to the next step.
When an article is paywalled, the plugin:
- Opens the article page in your browser (using the canonical DOI URL so the browser follows the publisher's redirect chain with its own cookies)
- Shows a non-blocking notification in Zotero: "N paywalled items opened in browser. If needed, complete your institutional or publisher login, then click the Zotero Connector to save the PDF."
In your browser, complete any required login (institutional proxy, publisher account, etc.) so you can see the full article, then click the Zotero Connector button. This normally creates a duplicate record in Zotero — but the plugin detects it automatically and merges it:
- Attachments (PDF, snapshot) are moved to your existing item
- The duplicate record is silently deleted
- A brief "Merged: 1 attachment added to existing item" notification confirms it worked
You don't need to do anything in Zotero — just click the Connector and the duplicate disappears.
For items that aren't journal articles (web pages, blog posts, reports, etc.) and for journal articles where the full article page is actually accessible, the plugin saves a web snapshot. This captures the page content locally in Zotero for offline reading and searching.
The plugin avoids saving useless snapshots — abstract-only pages (LWW /abstract/, PubMed, Oxford /article-abstract/), JavaScript-rendered shells, and Cloudflare bot-challenge pages are all detected and routed to the browser instead.
For patent items:
- US patents: attempts a direct PDF download from the USPTO image server
- All patents: falls back to a Google Patents snapshot
Use Better Find Full Text (force retry) when the normal entry won't make progress on an item. Force does two things the normal entry doesn't:
-
Clobbers obviously-bad snapshots. HTML attachments whose URL matches a known abstract-only pattern (LWW
/abstract/, PubMed, Oxford/article-abstract/) or whose content is a Cloudflare challenge page get deleted before the fetch runs. PDFs, user-uploaded files, linked attachments, and real full-text snapshots are left alone. -
Bypasses the "already has PDF" skip. Normally the plugin skips items that already have any valid PDF attached. That's usually correct, but sometimes a non-article PDF is present (supplementary materials, a different chapter, a cover page) and you still want the main article PDF. Force tries anyway.
After the cleanup pass and skip-bypass, the normal fetch flow runs. This can end up adding a second PDF if the item already had one — force is explicit opt-in for that trade-off.
For long batches, use Cancel Better Find Full Text from the context menu. The currently-running item finishes its in-flight work (HTTP request, snapshot verification, etc. — typically a few seconds to ~30s) and then the loop exits. The summary will read Cancelled at N/M: ... so you can see where it stopped.
| Item type | Has DOI / URL? | Likely outcome |
|---|---|---|
| Journal article (OA or institutional access) | DOI | PDF downloaded automatically |
| Journal article (paywalled, no access) | DOI | Opened in browser; use Connector to add PDF |
| Journal article (paywalled, you have access) | DOI | Opened in browser; log in, click Connector; PDF auto-merged |
| Web page, report, blog post | URL | Web snapshot saved |
| US patent | Patent number | PDF from USPTO, or Google Patents snapshot |
| Other patent | Patent number | Google Patents snapshot |
| Item with no DOI and no URL | — | Skipped |
- Institutional proxy: if your university uses a browser-based proxy (e.g. EZproxy), make sure you're logged in before clicking the Connector. The plugin opens the publisher page in your browser, so the proxy needs to be active there.
- Multiple items: select as many items as you want. The plugin processes them all, opens all paywalled articles in the browser at once, and shows one combined notification.
- Existing PDFs: items that already have a real PDF on disk are skipped automatically. Force retry does not override this — it only cleans up bad snapshots.
Getting full text reliably is an intrinsically hard problem. The plugin does what it can but some items will still fail, usually for one of these reasons:
- No access: the article is behind a paywall and neither you nor Unpaywall has a legal route to the PDF. The plugin routes these to the browser via the Connector workflow, but you have to have (or buy, or interlibrary-loan) access.
- Publisher anti-bot measures: some publishers (ScienceDirect/Elsevier is notable) serve Cloudflare challenges to Zotero's hidden browser. The plugin detects these and opens the item in your real browser instead, but completing the Connector workflow is then up to you.
- Metadata gaps: items without a DOI and without a URL are skipped — the plugin has nowhere to start.
- Dynamic-rendering sites: sites that require JavaScript to show content (PubMed, some publisher pages) can't be snapshotted usefully. The plugin recognizes these and routes to the browser.
- Unusual publishers: we only know about paywall patterns we've seen. If the plugin saves something useless on a publisher it doesn't recognize, open an issue with the item's DOI and the plugin can be taught the new pattern.
Think of the plugin as a force multiplier for the normal Zotero + Connector workflow, not a guaranteed-success tool. It converts the common cases into one click, and routes the hard cases to a workflow that's one click away from success when you have access.
The same batch the context menu runs can be driven over Zotero's own local HTTP server, so Zotero must be running with the library open.
POST http://127.0.0.1:23119/better-find-full-text/run
Content-Type: application/json
Supply either keys or collection:
| Field | Meaning |
|---|---|
keys |
Array of item keys |
collection |
Collection key, or its name (exact, case-insensitive; an ambiguous name is a 400 rather than a guess) |
libraryID |
Optional; defaults to My Library |
force |
Retry items already marked as tried, as the force-retry menu entry does |
limit |
Cap the batch; 0 means no cap. A capped response sets capped: true |
openPaywalled |
false runs the fetch but launches no browser tabs — the URLs come back in paywalledUrls instead |
dryRun |
true resolves and reports what it would do, fetching nothing |
The response is the same tally the menu command produces: ok, processed, total,
pdf, snapshot, paywall, skipped, failed, clobbered, openedInBrowser, and
paywalledUrls.
Zotero 10 requires an extra header for browser-shaped callers. Zotero 10 drops any request whose
User-Agentstarts withMozilla/, or that carries anOriginheader, with no response at all unless it also sendsZotero-Allowed-Request. A silent drop looks exactly like Zotero not running, so check this first.curland other script callers are unaffected; the header is harmless to send always.
curl -H 'Zotero-Allowed-Request: 1' -H 'Content-Type: application/json' \
-d '{"collection":"Inbox","limit":25,"openPaywalled":false}' \
http://127.0.0.1:23119/better-find-full-text/runZotero → Settings → Better Find Full Text
| Setting | Default | What it does |
|---|---|---|
| Per-item timeout | 60 s | Ceiling on one item before the batch moves on. A hung download can't be cancelled mid-flight, so this stops one bad item stalling everything. |
| Contact email for NCBI | (blank) | Sent as the email parameter when asking NCBI's PMC ID Converter to backfill a DOI for a PubMed ID. |
The NCBI address is optional and blank by default. NCBI asks tools for a contact address so they can warn a heavy user before throttling them, which means the only address worth sending is one you actually read — so when this is blank the plugin sends no address at all rather than a made-up one. The lookup works either way; NCBI just notes the omission in its response, which the plugin writes to the debug log.
No API key is involved: the ID Converter doesn't accept one, and it's the only NCBI API this plugin calls.
- Zotero 7.0, 8.x, 9.x, or 10.x
- Zotero Connector browser extension (for the paywall workflow)
bash build-xpi.sh # produces better-find-full-text.xpi.github/workflows/build.yml builds the XPI on every push and attaches it to the
GitHub release when a v* tag is pushed. Bump version in manifest.json and add a
matching entry to updates.json (with its update_link and an applications.zotero
block) — the workflow refuses a tag whose version, manifest and update manifest
disagree, and checks the built archive for the files it must contain. Zotero reads
updates.json from main to offer in-app updates.
git tag vX.Y.Z && git push origin vX.Y.Z