Integrity & Checksums

Fixity information for the research objects attached to a DOCiD

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:

Request
GET /api/v1/verify?identifier=20.500.14351/e7d1b5c9a30a45bc2858
GET /api/v1/verify/20.500.14351/e7d1b5c9a30a45bc2858

Prefer 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.

200 OK
{
    "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

Compare a local copy
$ shasum -a 256 abstract_1_1.docx
a9cc46dc31ff8a39c9bfdc33f76f308a3cf0b190676630d79f0087c495d4eb8a  abstract_1_1.docx

A 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.