A verified receipt in five minutes.
GACreate a log, mint a token, pin the keys once, then submit a file hash and hold a witness-cosigned receipt that verifies on your machine — online and fully offline.
Create a log in the dashboard
Go to /<org>/notary/logs in the dashboard and create a log. Pick a slug (it becomes part of the log’s permanent origin, e.g. notary.tilldev.dev/acme-releases) and a signature suite — the standard SHA-256 suite unless you need the government profile. The suite is fixed for the log’s lifetime, so choose deliberately. All fields are explained in Logs & entries. From a terminal: tilldev notary logs create acme-releases.
Mint a submit token
Go to /<org>/notary/tokens and mint a token with the submit scope. Tokens are shaped tnot_<32 hex> and scopes are strictly ordered: read < submit < admin. Give CI a submit token, not an admin one — submitting is the only thing a pipeline should be able to do. If your log is public, reads need no token at all. From a terminal: tilldev notary tokens create --name ci --scope submit --log acme-releases prints the token once.
Pin the keys — once, carefully
Everything downstream — every verify, every receipt, every offline check — is verified against the keys in your local trust file. Pin them once:
# One-time setup per machine.
npm i -g @tillstack/cli # or: pnpm add -g @tillstack/cli
# Fetch the log and witness public keys and pin them in a local trust file.
tilldev notary trust-init acme-releases --out notary-trust.json
OK Pinned acme-releases into notary-trust.json
origin notary.tilldev.dev/acme-releases
log key fp 1388 3e7f fc10 01f9 32c0 e322 13f6 aba7
witness tillnotary-witness/w1 29ac d643 062e 5a23 ed8c 7c94 ea41 bc43
required 1
! TRUST ON FIRST USE: new keys came from the log itself. Verify the fingerprints out of band (dashboard, ops channel) before relying on this pin — a pinned lie stays a lie.trust-init fetches keys over the network, so on its own it only proves you talked to a server. Before relying on the pin, compare the printed fingerprints against a second channel — the log’s page in the dashboard, or a fingerprint your team published elsewhere. Once confirmed, the trust file is the anchor: from then on a substituted key or forged proof is detected, not obeyed.The CLI finds your pin and token via --trust / --token flags or the TILLDEV_NOTARY_TRUST_FILE, TILLDEV_NOTARY_TOKEN, and TILLDEV_NOTARY_URL environment variables.
Submit a hash, get a receipt
submit --file hashes the file locally with the log’s suite hash and sends only the digest. The response — leaf index, inclusion proof, cosigned checkpoint — is verified against your pin before the CLI prints a single line of it.
# Hash the file locally and submit only the digest: the file never
# leaves your machine.
export TILLDEV_NOTARY_TOKEN=tnot_… # the submit token you minted
export TILLDEV_NOTARY_TRUST_FILE=notary-trust.json
tilldev notary submit acme-releases --file release.tar.gz --out bundle.json
OK Logged at index 41 of notary.tilldev.dev/acme-releases — receipt VERIFIED locally
leaf c53d57898e94df8d3fd05e41456bb17357992da9c36bab87edda8fe75f9acafb
origin notary.tilldev.dev/acme-releases
tree size 42
root d49832b2ac4e981bce2a74ea576ac48905fefd2639ec5e74652639b9b0f03e7d
timestamp 2026-09-27T07:03:41.478Z
cosignatures tillnotary-witness/w1
status COMPLETE (witness-cosigned)
OK Evidence bundle written to bundle.json
# Submitting the same value again returns the original receipt, not a new entry.
tilldev notary submit acme-releases --file release.tar.gz
OK Already logged at index 41 of notary.tilldev.dev/acme-releases — original receipt, VERIFIED locally
# Already have the digest? Submit it directly:
tilldev notary submit acme-releases \
--hash 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824Keep bundle.json with the artifact it notarizes. It is the portable, self-contained evidence: leaf, proof, checkpoint, cosignatures.
The log waits a few seconds for its witnesses before it answers, so a receipt is normally COMPLETE. If a witness is slow, the status reads incomplete — awaiting witness: the entry is logged and its proof is valid, and verify passes once the witness cosigns a head that includes it.
Verify it — online, then offline
Re-check any entry against the live log:
# Fetch the entry, its inclusion proof and the newest cosigned checkpoint,
# and check them all against your pinned keys.
tilldev notary verify acme-releases 41
OK VERIFIED — leaf 41 of notary.tilldev.dev/acme-releases
leaf c53d57898e94df8d3fd05e41456bb17357992da9c36bab87edda8fe75f9acafb
content class hash-only
origin notary.tilldev.dev/acme-releases
tree size 43
root 2f45debf01c9c04b5004461162ae5bdd38d3bc68e8bec462e272651b2011bf66
timestamp 2026-09-27T07:03:42.215Z
cosignatures tillnotary-witness/w1
status COMPLETE (witness-cosigned)
# Lost the index? Look the entry up by its value:
tilldev notary lookup acme-releases --file release.tar.gz
OK FOUND and VERIFIED — leaf 41 of notary.tilldev.dev/acme-releasesAnd the stronger claim — the receipt stands on its own, with no server in the loop:
# No network, no server, no token: the bundle and your trust file are enough.
tilldev notary verify bundle.json
OK VERIFIED (offline) — leaf 41 of notary.tilldev.dev/acme-releases
leaf c53d57898e94df8d3fd05e41456bb17357992da9c36bab87edda8fe75f9acafb
origin notary.tilldev.dev/acme-releases
tree size 42
root d49832b2ac4e981bce2a74ea576ac48905fefd2639ec5e74652639b9b0f03e7d
timestamp 2026-09-27T07:03:41.478Z
cosignatures tillnotary-witness/w1
status COMPLETE (witness-cosigned)The same loop from Node
@tillstack/sdk-notary-node has the same posture as the CLI: verification is not a method you call, it is always on. Every response is proven against your pinned keys before it is returned.
import { NotaryClient, parseTrustFile, trustFromFile, verifyEvidence } from '@tillstack/sdk-notary-node'
import { createHash } from 'node:crypto'
import { readFile, writeFile } from 'node:fs/promises'
// Keys YOU pinned with `tilldev notary trust-init` and verified out of band,
// never keys the server hands you at call time.
const logs = trustFromFile(parseTrustFile(await readFile('./notary-trust.json', 'utf8')))
const notary = new NotaryClient({
baseUrl: 'https://notary.tilldev.dev',
token: process.env.TILLDEV_NOTARY_TOKEN, // tnot_… with the submit scope
logs,
})
const hash = createHash('sha256')
.update(await readFile('./release.tar.gz'))
.digest('hex')
// Verification is always on: the receipt is checked BEFORE it is returned.
// A lying server throws NotaryVerificationError — it never returns data.
const receipt = await notary.submit('acme-releases', hash)
console.log(receipt.leafIndex, receipt.checkpoint.treeSize)
// The receipt carries a portable evidence bundle; store it next to the artifact…
await writeFile('./release.evidence.json', JSON.stringify(receipt.evidence))
// …and anyone can verify it later, offline, with just the pinned keys:
verifyEvidence(receipt.evidence, logs['acme-releases'])Straight HTTP, if you need it
The full REST surface is under https://notary.tilldev.dev/v1 (spec at /v1/openapi.json). Every error uses one envelope — {error, code, request_id} — and every response carries an x-request-id header to quote when you contact support.
# Submit a hash (submit scope): 64 hex characters on a SHA-256 log, 96 on SHA-384.
curl -s https://notary.tilldev.dev/v1/logs/acme-releases/leaves \
-H "authorization: Bearer $TILLDEV_NOTARY_TOKEN" \
-H "content-type: application/json" \
-d '{"leaf":"d7439bee24773bcbfa2d0a97947ee36227b10d1022b1a55847e928965bb6bfde"}'
# 201 for a new entry, 200 with "duplicate": true if the log already holds the value:
{
"leaf_index": 43,
"leaf": "d7439bee24773bcbfa2d0a97947ee36227b10d1022b1a55847e928965bb6bfde",
"duplicate": false,
"inclusion_proof": { "leaf_index": 43, "tree_size": 44, "path": ["07636ca8…", "ebf62045…", "40487d6d…", "a2ee9134…"] },
"checkpoint": {
"origin": "notary.tilldev.dev/acme-releases",
"tree_size": 44,
"root_hash": "a947f0647ece35bcbe53f1bd5fb925e380920c0be9db0855faab3cc022f66669",
"timestamp": "2026-09-27T07:03:43.107Z",
"suite": "sig-ed25519-mldsa65-sha256.v1",
"key_name": "tillnotary-log/acme-releases",
"signature": "eWZSL4fl…",
"cosignatures": [
{ "witness": "tillnotary-witness/w1", "observed_at": 1790492623, "signature": "rD8ZqWe0…" }
],
"complete": true
},
"complete": true
}
# An entry, a fresh inclusion proof and the best checkpoint (read scope; no token on a public log):
curl -s https://notary.tilldev.dev/v1/logs/acme-releases/leaves/43
# The newest witness-cosigned checkpoint:
curl -s https://notary.tilldev.dev/v1/logs/acme-releases/checkpointWhat's next
- Logs & entries — log fields, hash-only vs public-attestation submissions, caps, and freezing.
- Inclusion & consistency — what your receipt actually proves, and the periodic consistency check that catches tampering.
- Run your own witness — require a cosignature from a key you hold, so no checkpoint completes without you.
- Keep a mirror — a full copy of the log, every entry checked against the signed roots.
- Overview — the trust model and where TillNotary fits.