For whoever would sign
For sellers
This page is for whoever sells files without DRM — a publisher, a developer on itch.io, a small ebook shop, a musician — and wants to know what signing a receipt would take. It says what you install, what you sign, what your buyer receives, and what is not asked of you. It says the missing part first, because you need it to decide: no seller issues attest receipts today. The standard, two independent implementations and the conformance suite are done and free to use. What is missing is the first seller who signs, and this page is meant to let you work out whether that could be you.
What you would be signing
A receipt is a small JSON document you sign at the moment of sale. It names you by your domain, names the work, and points at the licence terms by URI and by the hash of their text. The receipt carries a salted commitment to an identifier you record. A matching disclosure proves possession of the binding secret, never who bought. Your signature is Ed25519; when the bridge signs, it adds an ML-DSA-65 signature beside it, so the same receipt carries a post-quantum leg. Once signed, the receipt does not change. Refunds, revocations and key changes live in separate signed documents, never inside it.
The buyer receives two files. The shareable one, named after your domain and the receipt id and ending in .attest, carries the receipt with its salt stripped, your key manifest exactly as it was when you signed, the licence text the receipt's hash points at, and a generated page explaining how to check the receipt once your store is gone. The other, ending in .private.attest, holds the salt that lets a presenter reproduce the receipt's binding value; that result proves possession of the salt, not buyer identity. The name is deliberate, because the web verifier refuses that file on sight, and it is the one file they must never send to anyone. Anyone holding the shareable file can verify it offline — on this site, with the attest command, or with the npm package — and none of that needs you, your server or your bridge to still exist.
What you install
Two packages, and only one of them is published. attest-receipts is on PyPI and gives you the attest command: keygen, manifest, issue, export, verify, import, inspect, check-artifact and disclose, plus the transfer, grant, authority and log families for the parts of the standard beyond a plain sale. attest-bridge, the service that turns a paid order into a signed receipt, is not published. Its package metadata is marked Private :: Do Not Upload, and its own setup guide tells you never to run pip install attest-bridge, because that name could resolve to something unrelated. You clone the repository and install from the checkout with pip install ./bridge, which pulls in attest-receipts as a dependency. Both need Python 3.12 or newer.
The bridge runs wherever you can run a container. A Dockerfile builds it from the checkout, and the deployment guide writes out Docker Compose on a machine you own, Fly.io and Render. TLS is not optional on any of them: a webhook body and a downloaded receipt both carry a buyer's binding salt. The bridge keeps a Ledger, an SQLite file recording every receipt it has issued, and runs as one process per Ledger; that file holds each receipt with its salt, so it is a secret, created with owner-only permissions and backed up encrypted or not at all.
The key and the manifest
attest keygen --hybrid writes your signing keys — an Ed25519 seed and an ML-DSA-65 key, both secret — and the public half. The bridge refuses to start without the ML-DSA key. attest manifest init then writes your key manifest: a self-signed document naming your domain, your key identifier and the window in which the key is valid. You choose the window, and the bridge refuses to sign outside it. Rotation exists, as attest manifest rotate, and it requires the old key to sign the new manifest. That is the point of it, and it is also why a lost key cannot be rotated out of.
The specification says an issuer should publish that manifest at https://<your-domain>/.well-known/attest.json, and you should. Be clear about what it does today: nothing, for anyone verifying. The specification reserves its strongest trust level, verified, for key material fetched over TLS from the issuer's own domain, and no tool published today performs that fetch. Every receipt you issue carries a copy of your manifest inside it, so every verification anyone can actually run reports unauthenticated_tofu: the arithmetic is checked, and nobody confirms that the key belongs to the domain. Publishing at the well-known path is what would let a future verifier close that gap. Nothing you can hand a buyer closes it now.
Where the purchase comes from
The bridge signs when your payment platform says a purchase is paid, and it takes the platform's word only in the form the platform signs.
checkout.session.completed (and its asynchronous-payment sibling) to the bridge, which verifies the Stripe-Signature header with your webhook secret and issues only for a session whose payment status is paid. With your Stripe secret key configured, the bridge reads the session's line items itself to learn which price was bought; without it, you set the product key in the session's metadata yourself. A session with more than one purchasable line item is refused: one receipt per purchase.orders/paid webhook, checked against X-Shopify-Hmac-Sha256. The order carries its own line items, so there is no API token to configure and no follow-up request. Unpaid or cancelled orders are acknowledged without issuing, and an order with more than one line item is set aside rather than signed.attest command can sign a payload you write by hand and package it as the same two files. There is no guide for that path; the setup guides cover the platforms above.What you decide, per product
The bridge signs nothing it has not been told about. For every item you sell you write a table in bridge.toml, keyed by the platform's identifier for it — a Stripe price id, a Shopify variant id, an itch game id — with the title, the publisher, an identifier of your own, a URI for the licence terms, the SHA-256 of the licence text and the path to that text. The bridge reads the text at startup, hashes it, and refuses to start if the hash does not match what you declared. A purchase of anything not in the file is refused and set aside rather than issued with guessed terms. The text itself ships inside every buyer's bundle, so the deal travels with the signature.
The defaults are not modest, and you should read them before you accept them: a perpetual grant, DRM-free, irrevocable, with a right to re-download. The specification allows an irrevocable receipt only for a DRM-free sale with a redownload right and a named artifact series or list of files, and it treats such a receipt as evidence that a sale falls under laws like California's AB 2426 or Maryland's HB 208 — evidence, it says, not a compliance determination, and your storefront language stays your own duty. It also means a refund does not take the receipt back: a record with status: "revoked" aimed at an irrevocable receipt is ignored. A backed status: "transferred" record is a separate case and can retire even an irrevocable receipt under the issuer-mediated transfer rules. If you want refunds to reach the receipt, set revocability = "refund_window" with a number of days, and know what that buys you today: attest revoke signs a stand-alone revocation record and checks its signature, signing-key eligibility and refund-window timestamp against the receipt and manifest you supply. A Stage-2-capable verifier also requires evidence that the record was logged and anchored no later than the refund-window deadline; the command does not produce that evidence and warns about the extra requirement. Use attest log entry --type revocation-record and the log commands to prepare it, obtaining the external timestamp separately. Use attest revocation-view to assemble the record into the view a verifier accepts. The view and evidence must reach the buyer as files, and the verifier needs matching trusted log keys and an anchor policy. No shipped tool fetches a revocation feed, and the browser and desktop currently pin no block headers.
How the buyer receives it
By email or by link, and you choose by configuration. With a [delivery] section the bridge sends the two files as attachments over SMTP, TLS only — there is no code path that sends in clear — with a body that names which file is safe to share and which is not, and a link to a page explaining what the files are. That page is this site's unless you point info_url at your own. Without [delivery], the download link is the delivery: every receipt has a token link on your bridge, and Stripe's success URL can land the buyer directly on a page offering both files. Delivery is at-least-once — a crash between the mail server accepting and the Ledger recording can send the same email twice — and it never issues twice. On itch.io there is no link path; email is the only delivery.
One consequence for your customers, worth knowing before you sign: the buyer's address is sealed, not encrypted. Whoever holds the private file together with the receipt — the buyer, or anyone they hand both to — can test candidate email addresses against the receipt's commitment offline. Your Ledger holds the same salts, which is one more reason it is a secret.
What stays with you afterwards
The signing key. It lives only where your bridge runs; the bridge reads it to sign and never exports, logs or writes it back. There is no attest portal, no authority and no company holding a copy that can restore it, and that is by design: an authority able to hand your identity back to you could hand it to someone else. Back the seed up where a disk failure on the issuing machine cannot take it, never next to what it signs, and read the incident runbook before you need it. Its two cases differ in a way worth knowing now. A lost key leaves your old receipts valid and leaves a permanent, visible gap in your key history. A stolen key, once you declare it compromised, invalidates every receipt it signed. The standard defines a rescue for receipts logged and anchored before such a declaration, but the bridge does not log what it issues. attest log entry --type receipt turns a receipt into an entry and the log commands append it, so the evidence is something you can produce; nothing in the bridge produces it for you, and for a receipt nobody logged that declaration is final.
The Ledger. It is your memory of which purchases already have a receipt and the source of every download link. Losing it does not touch receipts already delivered, but a redeploy without it can issue a second receipt for a retried webhook.
Uptime, while you sell. Stripe and Shopify retry a webhook that fails transiently, and the bridge answers a transient failure with a 500 precisely so that they do. After the sale, nothing: a receipt in the buyer's hands verifies with no dependency on the bridge, its database or its uptime ever again.
What is not asked of you
Where this honestly stands
No store issues attest receipts yet, and no regulator mandates them. The standard, two independent implementations and the conformance suite are done and free to use. What is missing is the first seller who signs. On this page that sentence has concrete edges: the bridge is unreleased and installs from a checkout; the manifest you publish at the well-known path is fetched by no shipped tool; the log-and-anchor rescue that would let a receipt outlive a later compromise of your key is not wired into what the bridge issues. None of it stops a receipt signed today from verifying later, offline, with nothing of yours still running. If something on this page would stop you, say so in the project's Discussions. A first seller is worth more to this project than another feature.
Buying rather than selling? Start here · The questions people ask are in the FAQ · The normative text is the specification, and the seller-side service lives in bridge/.