Use it from your language

Every fixture is a plain file over HTTPS, so fetching one needs no library and no client: a GET, a SHA-256, and a comparison. Below is the same task in five languages — download pdf/minimal.pdf and check it against the hash this service publishes — using each language's standard library and nothing else. Every one of these runs in CI on each change, against this site, so what you are reading is what was last executed rather than what once worked.

Two files carry the hashes, and both are canonical. manifest.json is the full record — path, bytes, MIME type, tags and measured properties as well as sha256 — and it is what to read when you want to choose files or check anything besides the hash. sha256sums.txt is the same hashes in the format sha256sum -c reads, one line per file, which a two-line shell script or a language without a bundled JSON parser can consume directly. The snippets use whichever suits the language: Python, JavaScript and Go parse the manifest; shell and Java read the sums file.

Shell

#!/usr/bin/env sh
# Fetch one fixture and check it against the published hash. POSIX sh and curl.
# macOS ships `shasum` where Linux ships `sha256sum`; both read this format.
set -eu
check=$(command -v sha256sum || echo "shasum -a 256")

curl -fsS --create-dirs -o pdf/minimal.pdf https://loremfile.dev/pdf/minimal.pdf
curl -fsS https://loremfile.dev/sha256sums.txt | grep ' pdf/minimal\.pdf$' | $check -c -

Python

#!/usr/bin/env python3
"""Fetch one fixture and check it against the hash in the manifest. Standard library."""

import hashlib
import json
import sys
import urllib.request

BASE, WANTED = "https://loremfile.dev/", "pdf/minimal.pdf"

with urllib.request.urlopen(BASE + "manifest.json") as response:
    manifest = json.load(response)
entry = next(f for f in manifest["fixtures"] if f["path"] == WANTED)

with urllib.request.urlopen(BASE + WANTED) as response:
    body = response.read()

digest = hashlib.sha256(body).hexdigest()
if digest != entry["sha256"]:
    sys.exit(f"{WANTED}: got {digest}, the manifest says {entry['sha256']}")
print(f"{WANTED}: {len(body)} bytes, sha256 matches")

JavaScript

// Fetch one fixture and check it against the hash in the manifest. Node 18+, no packages.
import { createHash } from "node:crypto";

const BASE = "https://loremfile.dev/";
const WANTED = "pdf/minimal.pdf";

const manifest = await (await fetch(BASE + "manifest.json")).json();
const entry = manifest.fixtures.find((f) => f.path === WANTED);

const body = Buffer.from(await (await fetch(BASE + WANTED)).arrayBuffer());
const digest = createHash("sha256").update(body).digest("hex");

if (digest !== entry.sha256) {
  throw new Error(`${WANTED}: got ${digest}, the manifest says ${entry.sha256}`);
}
console.log(`${WANTED}: ${body.length} bytes, sha256 matches`);

Go

// Fetch one fixture and check it against the hash in the manifest. Standard library.
package main

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
)

const base, wanted = "https://loremfile.dev/", "pdf/minimal.pdf"

type manifest struct {
	Fixtures []struct {
		Path   string `json:"path"`
		SHA256 string `json:"sha256"`
	} `json:"fixtures"`
}

func get(url string) ([]byte, error) {
	response, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("%s answered %s", url, response.Status)
	}
	return io.ReadAll(response.Body)
}

func main() {
	raw, err := get(base + "manifest.json")
	if err != nil {
		log.Fatal(err)
	}
	var published manifest
	if err := json.Unmarshal(raw, &published); err != nil {
		log.Fatal(err)
	}
	want := ""
	for _, fixture := range published.Fixtures {
		if fixture.Path == wanted {
			want = fixture.SHA256
		}
	}
	body, err := get(base + wanted)
	if err != nil {
		log.Fatal(err)
	}
	sum := sha256.Sum256(body)
	if got := hex.EncodeToString(sum[:]); got != want {
		log.Fatalf("%s: got %s, the manifest says %s", wanted, got, want)
	}
	fmt.Printf("%s: %d bytes, sha256 matches\n", wanted, len(body))
}

Java

// Fetch one fixture and check it against the published hash. Run: java Verify.java
// Hashes come from sha256sums.txt rather than manifest.json: one line per file, so no
// JSON library is needed. Both are published and carry the same hashes.
import java.net.URI;
import java.net.http.*;
import java.security.MessageDigest;
import java.util.HexFormat;

public class Verify {
    static final String BASE = "https://loremfile.dev/", WANTED = "pdf/minimal.pdf";

    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        String sums = get(client, "sha256sums.txt", HttpResponse.BodyHandlers.ofString());
        String want = sums.lines()
                .filter(line -> line.endsWith("  " + WANTED))
                .findFirst().orElseThrow()
                .split(" ")[0];

        byte[] body = get(client, WANTED, HttpResponse.BodyHandlers.ofByteArray());
        String got = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(body));
        if (!got.equals(want)) throw new IllegalStateException(WANTED + ": " + got + " != " + want);
        System.out.println(WANTED + ": " + body.length + " bytes, sha256 matches");
    }

    static <T> T get(HttpClient client, String path, HttpResponse.BodyHandler<T> as)
            throws Exception {
        var request = HttpRequest.newBuilder(URI.create(BASE + path)).build();
        HttpResponse<T> response = client.send(request, as);
        if (response.statusCode() != 200) throw new IllegalStateException(path + " answered " + response.statusCode());
        return response.body();
    }
}

Any other language

Nothing above is special to these five. The contract is small enough to implement anywhere: a plain GET over HTTPS returns the bytes; CORS is open, so a browser may fetch any fixture from any origin; Range requests are supported, so you can take the first kilobyte of a 100 MB file; and every published hash is in manifest.json and sha256sums.txt. Paths never change meaning — the bytes at a URL are fixed forever, and a correction is published at a new path — so a hash you record today keeps matching.

Please stay under 30 requests a second, and prefer one request per file over retrying in a loop. If you would rather not write any of this, the GitHub Action and the loremfile command-line client do it for you, with the same verification.