Anchor a file on Windows in two minutes — no API, no code

You have a file. You need to be able to prove later — to an auditor, a counterparty, or a court — that this exact file existed in this exact form at a particular moment. You do not want to write code, integrate an API, or install a runtime.

This post is the shortest path from that situation to a verifiable proof on Windows: one executable, one command. Total time is a couple of minutes of work, plus a wait for the timestamp batch.

What you need

  • A free API key — sign up, no card, 100 anchors a month.
  • The trustbeat.exe binary, about 3 MB. No .NET, no Python, no Rust toolchain, no admin rights.

Your file never leaves your machine. The binary computes a SHA-256 digest locally and sends only that — 64 hex characters. There is no upload, so file size and confidentiality are simply not part of the problem.

Step 1 — get the binary

If you use Scoop, register the bucket first:

scoop bucket add trustbeat https://github.com/TrustBeat/scoop-bucket

Then install, as a separate command:

scoop install trustbeat

Run them one at a time rather than pasting both together. The first clones a git repository and prints progress while it does; in the classic PowerShell console a second pasted line can be consumed by that output instead of executing. The result is quietly wrong — the bucket registers, nothing installs, and you only find out several steps later when the command isn't found. The install is done when you see Checking hash ... ok and Creating shim for 'trustbeat'.

After this, scoop update trustbeat keeps it current.

If you don't use Scoop, download and unzip the release archive directly:

# PowerShell, no package manager
$v = "0.1.1"
$zip = "trustbeat-$v-x86_64-pc-windows-msvc.zip"
Invoke-WebRequest -Uri "https://github.com/TrustBeat/trustbeat-cli/releases/download/v$v/$zip" -OutFile $zip
Expand-Archive $zip -DestinationPath .
cd "trustbeat-$v-x86_64-pc-windows-msvc"

# PowerShell does not search the current directory — put the folder on PATH
# for this session so the commands below work as written
$env:PATH += ";$PWD"

That last line matters more than it looks. PowerShell does not look in the current directory for executables, so being inside the extracted folder is not enough — a bare trustbeat still fails with CommandNotFoundException. Either put the folder on PATH as above, or invoke it explicitly as .\trustbeat.exe. To stop doing this in every new shell:

# make it permanent: move the exe somewhere stable and add that to PATH
$dest = "$HOME\bin"
New-Item -ItemType Directory -Force -Path $dest | Out-Null
Move-Item .\trustbeat.exe $dest -Force

# write the USER PATH only. Do NOT use `setx PATH "$env:PATH;..."` — $env:PATH
# is the merged machine+user value, so that copies every system entry into your
# user variable, and setx silently truncates past 1024 characters.
$user = [Environment]::GetEnvironmentVariable("PATH", "User")
[Environment]::SetEnvironmentVariable("PATH", "$user;$dest", "User")   # new shells only

Scoop users skip the PATH setup — the installer creates a shim for you. But an already-open shell keeps the PATH it started with, so it still won't see a freshly installed command.

Confirm step 1 before going on

Open a new PowerShell window — this matters for both install routes — and run:

trustbeat --version

If that prints a version, you're done here. If it reports CommandNotFoundException, jump to troubleshooting rather than continuing — every step below depends on this one.

Both routes land the same artifact. Scoop pins and checks the archive's SHA-256 for you; if you download by hand, every release ships a SHA256SUMS file so you can check it yourself. Those release artifacts are themselves anchored with a qualified timestamp by the build pipeline — the tool is dogfooded on its own releases.

Windows SmartScreen may warn on first run of a freshly downloaded, unsigned executable. That is expected for a small open-source binary; the SHA-256 check is the meaningful verification, not the reputation prompt.

Step 2 — set your API key

For the current PowerShell session:

$env:TRUSTBEAT_API_KEY = "tb_live_..."

To keep it across reboots:

# survives a reboot (new shells only — not the current one)
setx TRUSTBEAT_API_KEY "tb_live_..."

Or drop it in a config file. Note the path — the CLI uses the same .config layout on every platform, resolved from %USERPROFILE% on Windows:

# %USERPROFILE%\.config\trustbeat\credentials
api_key = tb_live_...

Precedence is --api-key flag → environment variable → config file.

Step 3 — anchor the file

trustbeat anchor contract.pdf --wait
✓ sha256   9f2a1c…4b7e   contract.pdf
✓ submitted 01KNBQMYC0AQ7KA561TNKK71GJ
✓ anchored
  time     2026-08-16T09:14:03Z
  tsa      SK TIMESTAMPING UNIT 2025E
  proof    contract.pdf.proof.json

That is the entire operation. You now have contract.pdf.proof.json next to your file — a self-contained proof bundle holding the Merkle path and the qualified RFC 3161 timestamp token.

About the wait. Hashes are batched into a Merkle tree and one qualified timestamp is issued over the batch root roughly every ten minutes, so --wait can block that long. That batching is why a qualified timestamp — normally a per-request, per-contract purchase from a trust service provider — can be free at a hundred a month. If you'd rather not block, drop --wait and collect the proof later by tracking id:

trustbeat anchor contract.pdf
# ✓ submitted 01KNBQMYC0AQ7KA561TNKK71GJ

# ...later, from anywhere
trustbeat proof 01KNBQMYC0AQ7KA561TNKK71GJ -o contract.pdf.proof.json

If the batch hasn't run yet, trustbeat proof prints pending and exits 3 — deliberately distinct from 2 for a real error, so a polling script can tell "not ready yet" from "something is wrong" and just retry on 3.

Step 4 — verify it (the part that matters)

trustbeat verify contract.pdf.proof.json contract.pdf
✓ document   SHA-256 matches the anchored hash
✓ merkle     14 path step(s) re-derive the batch root
✓ timestamp  token imprint (SHA-256) equals the batch root
✓ signature  signed by SK TIMESTAMPING UNIT 2025E

PROOF VALID

trustbeat verify makes no network calls and needs no API key. It re-derives everything from the proof bundle and your file alone. Four independent checks:

  • document — the SHA-256 of your file equals the hash in the proof. Change one byte of the PDF and this fails.
  • merkle — the proof path re-derives the batch's Merkle root.
  • timestamp — the token's messageImprint is that Merkle root. This is the join; without it, a valid token could be paired with an unrelated tree.
  • signature — the trust service provider's signature verifies against the certificate embedded in the token.

verify exits 0 valid, 1 invalid, 2 usage or IO error — so this drops into a build script unchanged. Only proof uses the extra 3 above.

The consequence worth internalising: the proof outlives us. If TrustBeat vanished tomorrow, every proof already issued stays verifiable with this binary — or by hand with openssl, per the manual verification guide. You are not buying a subscription to your own evidence.

Two things worth knowing

You can hash without anchoring. Nothing is sent anywhere, no key needed — useful for checking what a file's digest is before deciding:

trustbeat hash contract.pdf

Every command takes --json, which makes batch work straightforward:

# anchor every PDF in a folder, collect the tracking ids
Get-ChildItem *.pdf | ForEach-Object {
    trustbeat anchor $_.FullName --json | ConvertFrom-Json | Select-Object -Expand id
}

Troubleshooting: "trustbeat is not recognized"

This is the one error worth documenting, because it has two completely different causes and the message looks identical either way. Find out which you have:

scoop list

If it says "There aren't any apps installed" — nothing was installed, so there is no PATH problem to chase. Re-run scoop install trustbeat on its own and watch for Creating shim. A populated scoop bucket list proves only that the bucket was added, not that the install ran.

If it lists trustbeat — it is installed and this is a stale PATH. Opening a new PowerShell window fixes it in almost every case. If a new window still fails, repair the user PATH and refresh the current session:

$user = [Environment]::GetEnvironmentVariable("PATH","User")
if ($user -notlike "*scoop\shims*") {
    [Environment]::SetEnvironmentVariable("PATH", "$user;$HOME\scoop\shims", "User")
}

# make the current session see it without reopening
$env:PATH = [Environment]::GetEnvironmentVariable("PATH","Machine") + ";" +
            [Environment]::GetEnvironmentVariable("PATH","User")

If neither applies, check the binary directly — this bypasses PATH entirely, so it separates "not installed" from "not findable" for good:

& "$HOME\scoop\shims\trustbeat.exe" --version

Note the leading &. PowerShell needs the call operator in front of a quoted path, otherwise it treats the string as a value and fails to parse the arguments after it.

What to keep

Keep the file and its .proof.json together. That pair is the evidence — no account, no service, and no internet connection is needed to check it later. Losing the file means losing the ability to prove what was anchored; the proof alone still establishes that some document with that digest existed at that time.

Where this fits

The timestamps come from EU-supervised qualified trust service providers under eIDAS — the mechanism the regulation attaches a legal presumption of accuracy to (Article 41). There is no blockchain involved; Merkle trees are just efficient cryptography and predate it.

If you later want this inside an application rather than at a prompt, the same infrastructure is available as a library in five languages, as a GitHub Action for CI artifacts, and as a REST API. The CLI is just the route that requires none of them.