Integrity & Checksums
Overview
DOCiD stores a SHA-256 fingerprint of the bytes of every file it hosts, so that anyone who downloads a research object can prove their copy is identical to the one registered against the DOCiD.
Checksums cover file content only. DOCiD metadata is intentionally mutable — a title can be corrected and a creator added without invalidating the fingerprint of the deposited file. The fingerprint answers one question: are these the same bytes?
What this is, and what it is not
The public verification endpoint is a view-and-compare facility. It returns the fingerprint DOCiD recorded; it does not re-read the stored file at request time. Server-side tamper detection is a separate, offline job (backfill_checksums.py --recheck). Do not describe the endpoint to end users as "live verification".
Where checksums are stored
Seven columns carry fixity information. They exist identically on both object tables — publications_files (uploaded files) and publication_documents (documents and external references).
| Column | Type | Meaning |
|---|---|---|
checksum |
String(64), nullable | Lowercase hex SHA-256 digest of the stored bytes |
checksum_algorithm |
String(20), nullable | Always SHA-256 at present; the column exists so the algorithm can be migrated without a schema change |
file_size |
BigInteger, nullable | Size in bytes, counted during hashing |
checksum_status |
String(30), nullable | One of the five values in the table below |
checksum_error |
Text, nullable | Diagnostic message when hashing failed |
checksum_generated_at |
DateTime, nullable | When the digest was first computed |
checksum_last_checked_at |
DateTime, nullable | When the object was last examined, whether or not a digest resulted |
None of these columns carry an index, a constraint, or a server default. A row created before this subsystem existed has NULL throughout.
The checksum_status vocabulary
Five values are defined. Treat only verified as a trustworthy fingerprint.
| Value | Meaning | Is checksum populated? |
|---|---|---|
verified |
Digest computed from stored bytes and recorded | Yes |
pending |
Row exists but has not been hashed yet | No |
external_not_supported |
The bytes live off-platform (a video URL, a repository link), so DOCiD never had them to hash | No |
missing_bytes |
The row references a local file that could not be found on disk | No |
failed |
Hashing raised an error, or a re-check found a digest that no longer matches the stored bytes | Possibly — but the value is stale and must not be trusted |
failed can carry a checksum
A row whose re-check detected a mismatch keeps its original digest and moves to failed. Clients must therefore branch on checksum_status, never on the mere presence of a checksum value. This is why the API reports verifiable_object_count separately from object_count.
How the digest is computed
Files are streamed from disk in 1 MiB chunks, so an arbitrarily large deposit never loads into memory in full. The function returns the hex digest and the byte count together, and the byte count recorded in file_size is the one observed during hashing rather than a separately reported value.
Only files this deployment actually hosts can be hashed. A stored URL is resolved to a path under the uploads directory, guarded against path traversal, and only when its host matches this deployment's configured base URLs. Anything else — an external video, a link to another repository — is recorded as external_not_supported. DOCiD never fetches remote bytes in order to fingerprint them.
Checksums are generated at deposit time, not only by backfill: the publish path, the edit path, and version creation all hash newly attached objects. The backfill script exists for rows predating the subsystem, defaults to a dry run, and on --recheck never silently overwrites an existing digest — a mismatch is recorded as an integrity event.
The verification endpoint
Two equivalent forms are exposed, both unauthenticated:
GET /api/v1/verify?identifier=20.500.14351/e7d1b5c9a30a45bc2858
GET /api/v1/verify/20.500.14351/e7d1b5c9a30a45bc2858Prefer the query form when scripting: a DOCiD contains a slash, and the query parameter avoids any ambiguity about path segments.
A live response follows. It shows the two states an integrator must handle side by side — an uploaded file with a usable digest, and an external object that can never have one.
{
"algorithm": "SHA-256",
"docid": "20.500.14351/e7d1b5c9a30a45bc2858",
"note": "Compare these stored fingerprints against your downloaded files (e.g. `shasum -a 256 <file>`). A match confirms the file is identical to the version registered in DOCiD.",
"object_count": 2,
"objects": [
{
"checksum": "a9cc46dc31ff8a39c9bfdc33f76f308a3cf0b190676630d79f0087c495d4eb8a",
"checksum_algorithm": "SHA-256",
"checksum_generated_at": "2026-07-08T13:54:01.095359",
"checksum_last_checked_at": "2026-07-08T13:54:01.095359",
"checksum_status": "verified",
"file_name": "c6930b0a35d1_abstract_1_1.docx",
"file_size": 20475,
"handle_identifier": "20.500.14351/9ba16c85d34594d212f2",
"id": 271,
"object_class": "file",
"title": "Publication title"
},
{
"checksum": null,
"checksum_algorithm": "SHA-256",
"checksum_generated_at": null,
"checksum_last_checked_at": "2026-07-08T13:54:01.104038",
"checksum_status": "external_not_supported",
"file_name": null,
"file_size": null,
"handle_identifier": "20.500.14351/a7b8e32199bcdbf899cf",
"id": 190,
"object_class": "document",
"title": "vid title"
}
],
"title": "Collection title",
"verifiable_object_count": 1
}| Status | Condition |
|---|---|
200 |
DOCiD resolved; objects[] may still be empty |
400 |
No identifier supplied |
404 |
No publication with that document_docid |
410 |
The DOCiD has been retired (soft-deleted); file metadata is withheld |
Drafts are stored in a separate table and are never reachable through this endpoint.
Verifying a downloaded file
$ shasum -a 256 abstract_1_1.docx
a9cc46dc31ff8a39c9bfdc33f76f308a3cf0b190676630d79f0087c495d4eb8a abstract_1_1.docxA match confirms the file is byte-identical to the version registered against the DOCiD. A mismatch means the copy differs — through truncation, transcoding, or modification — and should not be cited as the registered object.
Known gaps
Stated plainly so integrators are not surprised. These are current limitations, not design intentions.
Checksums are absent from publication metadata responses
The publication detail endpoints — get-publication, the DOCiD resolvers, and the DOI resolver — do not include any checksum field. Their file entries stop at generated_identifier. Fixity information is available only from /api/v1/verify and the owner-only edit endpoint. A client that needs both metadata and fixity must make two calls.
Downloads are not verified in transit
The upload-serving route is a plain static file serve with no checksum logic. Nothing recomputes or compares a digest while a file is being delivered; verification is an action the recipient takes afterwards using the endpoint above.
External objects can never be verified
Because DOCiD does not fetch remote bytes, any object hosted elsewhere is permanently external_not_supported. For collections dominated by external references, verifiable_object_count will legitimately be far below object_count.