Cross-checks the references in a bibliography against eight free bibliographic indexes, and reports whether each one resolves to an indexed record.
Reads BibTeX, RIS, CSL-JSON, LaTeX .bbl, Word .docx, PDF and plain text. Covers journals, preprints, books, CS conference proceedings, biomedical literature and datasets.
pip install citeverifyRequires Python 3.9 or later. The core has no third-party dependencies.
citeverify check refs.bib # BibTeX
citeverify check library.ris # Zotero / EndNote / Mendeley export
citeverify check refs.json # CSL-JSON
citeverify check paper.docx # reads the reference section
citeverify check paper.pdf # needs: pip install 'citeverify[pdf]'
citeverify check refs.txt # plain text, one per line
citeverify check refs.txt --json out.json # structured results
citeverify check refs.txt --cache .cv.json # memoise source responses
citeverify check refs.txt --strict # exit 1 on any suspicious verdict
citeverify parse refs.txt # parse only, no network calls
citeverify check - < refs.txt # read stdin| Flag | Default | Effect |
|---|---|---|
--format |
detected | Force an input format: bibtex, ris, csl-json, latex, docx, pdf, document, text |
--mailto |
unset | Sent as a contact address to OpenAlex and Crossref, which routes the request to their polite pool |
--cache PATH |
unset | JSON memo of source responses, keyed by source and identifier |
--delay |
0.12 |
Seconds between calls that hit the network. Cached lookups do not sleep |
--verified-threshold |
0.60 |
Title score at or above which a candidate can be verified |
--partial-threshold |
0.35 |
Title score at or above which a candidate is partial |
--year-tolerance |
1 |
Permitted absolute difference between cited and indexed year |
--strict |
off | Exit 1 if any reference is suspicious. error verdicts do not trigger it |
Exit codes: 0 completed, 1 suspicious references found under --strict, 2 usage or input error.
Detected from the extension, or from the content when reading stdin. Override with --format.
| Format | Extension | Notes |
|---|---|---|
| BibTeX | .bib |
Brace-matched, so nested braces in titles survive. LaTeX accents ({\'E}mile) are folded to Unicode so they match indexed records |
| RIS | .ris |
Zotero, Mendeley, EndNote, Web of Science, Scopus. Soft-wrapped fields are rejoined |
| CSL-JSON | .json |
Zotero and Pandoc. Accepts a bare array or a wrapped export |
| LaTeX | .bbl, .tex |
Splits on \newblock, which delimits author/title/venue explicitly and beats sentence segmentation |
| Word | .docx |
Paragraph boundaries are explicit, so hanging-indent bibliographies survive intact |
.pdf |
Requires pip install 'citeverify[pdf]' |
|
| Document | any | Finds the References heading in a longer text and parses only what follows, stopping at Appendix |
| Plain text | any | One reference per line. The heuristic fallback |
Structured formats set parse_confidence to high because field boundaries are given rather than inferred. That matters: a mis-segmented title is the main cause of a false unverified.
| Verdict | Condition |
|---|---|
verified |
Title score at or above the verified threshold, year within tolerance, author overlap present or uncheckable |
year_mismatch |
Title score at or above the verified threshold, year outside tolerance |
author_mismatch |
Title score at or above the verified threshold, no surname overlap with the indexed record |
partial |
Title score between the partial and verified thresholds |
unverified |
No candidate above the partial threshold, or no title or identifier to search on |
error |
Every configured source failed to respond |
Result.suspicious is true for year_mismatch, author_mismatch, partial and unverified. It is false for error.
Transport failures are separated from negative results. If every source raises, the verdict is error and suspicious is false. Failed lookups are not written to the cache, so they are retried on the next run.
Missing author data does not lower a verdict. author_overlap is tri-state: true, false, or None when either side lists no authors. None is treated as passing.
Candidate selection uses title score alone. Author and year are recorded as corroborating evidence and applied in classify, outside candidate ranking. A reference to a real work carrying an incorrect year therefore resolves to that work and returns year_mismatch rather than unverified.
Title scoring is Jaccard overlap of content tokens. Stopwords and tokens of two characters or fewer are dropped before comparison.
Containment is a guarded escape hatch, not a free pass. If either normalised title contains the other the score is 1.0, but only when the contained title carries at least 4 content tokens and accounts for at least 70% of the longer one. Without that guard, containment is exactly the shape a fabricated citation takes: an invented title opening with a real, short one. "Quantum entanglement" is contained in "Quantum Entanglement in Recurrent Transformer Memory Lattices", and scoring that 1.0 reports a fabrication as verified. Pairs that fail the guard fall through to Jaccard and are scored on the evidence actually shared.
An exact identifier outranks any title score. When a citation and a candidate carry the same DOI, PMID, arXiv id or ISBN, the match is identity rather than similarity and scores 1.0 regardless of title. A reference that cites by PMID alone, with no title, still verifies.
from citeverify import Cache, parse_references, summarize, verify_all
refs = parse_references(open("references.txt").read())
results = verify_all(refs, cache=Cache(".cv.json"))
summarize(results) # {'verified': 4, 'unverified': 1, ...}
[r for r in results if r.suspicious]verify() and verify_all() accept dicts directly, bypassing the parser:
verify({"title": "...", "authors": "...", "year": 2017, "doi": "10.1/x"})Recognised keys: title, authors, year, doi, arxiv_id, isbn, pmid, entry_type, venue. authors accepts a string or a list. entry_type (book, inproceedings, dataset, ...) steers source ordering.
from citeverify import Thresholds, verify_all
verify_all(refs, thresholds=Thresholds(verified=0.7, partial=0.4, year_tolerance=0))Thresholds validates 0 <= partial <= verified <= 1 and year_tolerance >= 0 at construction.
All network access goes through one callable with signature (url, headers) -> str. No other module performs I/O.
from citeverify import OpenAlexSource, verify
def fetcher(url, headers):
return proxy.get(url, headers=headers).text
verify(citation, sources=[OpenAlexSource(fetcher=fetcher)])Sources are tried in order and the search stops at the first verified result. Each source exposes last_error after a failed call.
default_sources() returns all eight, none of which require an API key:
| Source | Covers |
|---|---|
| OpenAlex | Broadest general index, queried first |
| Crossref | DOI-registered publisher records |
| arXiv | Preprints |
| Europe PMC | Life sciences and everything in PubMed; the only source that resolves a bare PMID |
| Semantic Scholar | Computer science, and preprints that never acquired a DOI |
| DBLP | CS conference proceedings, patchily covered by DOI-centric indexes |
| Open Library | Books, which have no DOI and are the largest blind spot in DOI-based checking |
| DataCite | Datasets, software and theses |
sources_for(citation) reorders that list by what the reference looks like, so a book reaches Open Library first rather than after seven journal databases have each spent a request failing to find it. Since verification stops at the first confirmation, this turns eight requests into one. verify() applies it automatically.
parse_references splits on newlines, rejoins soft-wrapped entries, and strips list markers ([1], 1., (1)). Each entry yields a Citation with parse_confidence set to:
high: title found via a quoted string, or via an author/parenthesised-year/title splitlow: title inferred by sentence segmentation. Recorded innotes
DOIs match 10.\d{4,9}/.... arXiv ids match both the NNNN.NNNNN and the older archive/NNNNNNN forms. A parenthesised year is preferred over a bare four-digit number, which may be a volume or page range.
- Title matching is lexical and does not model meaning.
- Coverage is bounded by the eight indexes. Standards, grey literature, and much non-English and pre-1970 work remain thinly indexed, so
unverifiedon those often reflects absence from the index rather than fabrication. - The free-text parser is heuristic. Structured input (
.bib,.ris, CSL-JSON) is read exactly and should be preferred when available; it is also what setsparse_confidencetohigh. - PDF extraction is only as good as the PDF. Two-column layouts and ligature-heavy fonts still produce broken reference strings. Export a
.bibwhere you can. - The tool determines whether a matching record exists. It does not evaluate whether a citation supports the claim it is attached to.
pip install -e ".[dev]"
pytest76 tests. No network access or API key required; sources are exercised through injected fetchers.
MIT.