Keep one claim you can check: the first open of the reader should load the shell page only. It should not fetch ciphertext yet. After you click Open and view, Network should show a request by id. After a successful fetch that uses up the count, the same id should return 410. The address bar may still hold the key after #. That same string should not appear on the request line.
The URL can still exist after the ciphertext is gone
People hear “burn after reading” and picture the blue link catching fire. The metaphor usually points at the wrong object. Chat clients do not erase a message because the destination returned 410. Browser history, the original email, and the ticket comment stay put. What burns is the short-lived ciphertext on the host. The full URL in someone’s hand is still a string of characters. Open it again and the id points at an object that is no longer there.
Split two credentials before you ask what remains. One is the locator: the id after the question mark, which tells the server which record to fetch. The other is the decrypt key: the fragment after the hash, which stays in the current tab. The host is designed to see the first and never see the second. After one successful read, the locator may still name an empty slot. If the key is still in chat history, it decrypts nothing, because the ciphertext is gone.
A file-share link works the other way. While that link is valid, the file is usually still on the drive. You revoke access; you do not delete the object on first open. A one-time secret binds “read” and “delete” to the same fetch. At create time you set a read count (1–10, default 1) and an expiry (1 hour, 24 hours, 7 days, or burn after reading only). The default path is: first successful fetch, ciphertext is deleted. Unread and expired, same deletion. Neither path leaves a plaintext backup you can recover later.
The server never held plaintext
“Delete plaintext after the first read” is the other common misread. The order is: plaintext exists only in the sender’s tab; the browser draws a 256-bit random key with Web Crypto and encrypts with AES-256-GCM; the outbound fields are ciphertext, expiry, and max reads. The server returns an unguessable id. The page then appends the key to s.html?id={id}#{key}. There is no step that writes the passphrase into a database and encrypts it afterward.
The parameters are checkable, not a slogan. NIST SP 800-38D recommends a 96-bit (12-byte) IV for GCM so implementations stay interoperable and simple; the authentication tag is commonly 128 bits (16 bytes). Web Crypto’s AesGcmParams matches that shape. On MakePwd, ciphertext is Base64 of 12-byte IV + ciphertext + 16-byte tag. The key is 32 bytes. Each item is capped at 32 KB. Those numbers sit on the create page. You can also inspect a link you just generated: the part after the hash is the key in Base64URL, not the sentence you typed.
GCM also gives integrity. If ciphertext or the tag is altered, decrypt fails in this tab. You do not get a “close enough” plaintext. That blocks tampering in transit. It does not block a screenshot after someone has already read the message. The server never has the key, so it cannot decrypt on your behalf, cannot scan the body, and cannot mint a plaintext copy after you read. What it can do is hand over ciphertext by id, then delete that ciphertext by count or TTL.
| Stage | In the browser | On the server |
|---|---|---|
| Create finishes | Plaintext you just typed, plus the full link | Ciphertext, id, TTL, remaining reads |
| Reader first paints | Id and # key in the address bar |
Still ciphertext; this paint did not delete it |
| Confirm fetch succeeds | Plaintext decrypted in this tab | Deleted if the count is exhausted; otherwise decremented |
| Same id opened again | The key may still be in the address bar | Burned or expired; a 410-class response |
Locator in the query, key in the hash
A one-time link has to do two jobs: tell the server which record to fetch, and tell the recipient’s browser which key to use. Those jobs should not share one HTTP field. The query after ? is written into the request line, so reverse proxies and access logs are designed to see it. The fragment after # is left for the client.
RFC 9110 section 7.1 is explicit: the target URI excludes the fragment, because fragment identifiers are processed by the client. The browser asks the server for s.html?id=…, not the whole address bar. Move the key to ?key= and “ciphertext only” is already false at the log layer: an operator who opens that day’s access log has the second half needed to decrypt.
The previous note already walked that layer. The boundary here is short: the split keeps the host from seeing the key. It does not make the full string safe to forward. Paste a URL that still contains # into chat, and both the recipient and the chat vendor can see the key. A fragment blocks HTTP. It does not block the clipboard. Checks you can run in Network are in Why a URL hash fragment is a fit place for a key—and when that protection fails.
The burn happens on the ciphertext fetch, not the first paint
Opening the reader and consuming a read are not the same event. The reader HTML can paint first: check that the address has ?id= and a # key, then stop on a confirm button. That step only costs static assets. The request that tells the server to delete ciphertext is the later fetch by id. After you click, the fetch succeeds, and the count is exhausted, the record moves from “still fetchable” to “burned.”
A missing key should not trigger a fetch. If the address has only an id—because a chat client stripped the hash—the reader should say the fragment is missing, not ask the server for ciphertext first. Otherwise you spend the only read: the server hands over ciphertext and deletes it, the browser cannot decrypt, and the sender thinks the other person already read it. The checkable behavior is: with no #, Network should not show the ciphertext API; with a full link and a confirm click, it should.
The count is not always 1. Create can set 1 through 10. Set 3, and after the first two successful fetches the ciphertext is still on the server; only the remaining count dropped. The third success deletes it. Default 1 matches “hand over one password,” not a protocol that can only be read once. TTL is a separate delete condition: unread after 24 hours, ciphertext is gone too, and the state should be distinguishable from “someone already read it.” MakePwd’s reader treats expired in a 410 response as expiry; other burned states are treated as already burned.
Do not test with a real password, a production API key, or an unredacted connection string. Use a disposable canary such as canary-burn-2026-do-not-reuse. You are checking status codes and the request line, not spreading another secret.
410 and 404: burned, expired, or never there
After ciphertext is gone, the server still has to answer the next request. RFC 9110 section 15.5.11 writes 410 Gone as: the target resource is no longer available at the origin server and that condition is likely permanent. If the origin cannot say whether it is permanent, it should use 404 instead. MDN’s note on 410 adds that clients should not keep retrying, and the site should drop links that still point at the resource.
For a one-time secret, 410 fits better than 404: this id once named a ciphertext object, then that object was deleted on purpose, and it will not come back. Expiry and a spent count can both return 410, with the response body separating expired from a normal burn so the recipient does not think they mistyped the link. 404 can still appear—an invalid id, or a record cleared so thoroughly the host no longer claims it ever existed. For the sender, both answers mean the same thing: do not expect the host to hand the content back.
410 also tells crawlers to stop coming. The reader is a temporary ciphertext landing page. It should be noindex for search engines and should not enter a sitemap. You do not want a one-time password page indexed. The create page can be indexed, because it explains how to generate a link. The reader only serves people who already hold the full URL.
Why a chat preview can spend the only read
The usual accident is not a cryptographer breaking AES. It is a preview bot beating a colleague to the click. You paste the full URL into chat. The client requests that address so it can draw a card. Slack’s unfurling docs say it plainly: by default, when a link appears in a message, Slack fetches it and offers a preview. Teams, Discord, and some mail security scanners do a similar fetch. They want a title and a summary, not your password. If “the first GET also fetches ciphertext and deletes it,” that crawl spends the only read. The colleague opens a burned page.
A key after # is one layer of protection against that server-side fetch: the bot requests s.html?id=…, and the fragment is not sent with HTTP. The preview usually cannot decrypt, so the card does not show the passphrase. What it can burn is the read count, not the key itself. If your reader fetches ciphertext on page load, the preview is equivalent to clicking Open and view for the recipient. They get 410. You think they already read it.
The workable fix is not “never paste a link.” It is to split “paint the page” from “fetch ciphertext.” The reader renders a confirm step first and says this click will spend a read; only then does it send the fetch. A preview that grabs HTML and does not click the button stops at confirm. That does not stop a scanner that runs the full script and synthesizes a click, and it does not stop a person who clicks the wrong card. It changes the common “the card opened it once” failure from the default outcome to a surviving link.
A second habit helps in channels that unfurl automatically: send “password is going over a one-time link; URL in a separate message,” or put the full string in a DM that does not preview. The full URL is still a bearer credential. Unfurl only spends a count. It does not change “whoever has the whole string can decrypt.”
Check it live: stop on confirm, then search Network
“Burn after reading” and “zero knowledge” do not prove themselves. Four things you can see: whether create sent plaintext; whether the first reader load fetched ciphertext; whether the confirm request line includes the key; and whether a second open after a spent count is 410.
Prepare a canary first. Open the Burn-Link create page, type a disposable sentence, set TTL to 1 hour, and leave reads at 1. Do not send the result to anyone. Look at the split in the result pane: query side is the id only; fragment side is the key. Then open DevTools Network, enable Preserve log, and search for the full canary. The create body should be ciphertext, not that sentence. Analytics should not carry the sentence either.
- Open the reader with the same full link. Do not click confirm yet. Network should show the document and static assets only—not a ciphertext fetch by id.
- Compare the address bar with the document request line. The address bar keeps
#and what follows. The request line should show path and?id=only. - Click confirm. The ciphertext fetch should appear now. After success, the page shows plaintext. Open the same link a second time. You should land on burned or 410, not the same canary again.
Three clean passes support a narrow claim only: in this browser, on this run, plaintext did not leave as an observed business field, the key did not enter the request line, and ciphertext was no longer fetchable after one successful read. It does not prove an extension never read the input box, and it does not prove a compromised disk has never held historical ciphertext. Change browser, and it is worth running the canary again. If you also need to check whether plaintext left any tool page, the steps are in How to verify browser encryption: prove plaintext never left.
It does not stop copy, screenshots, or forwarding the full URL
Burn-after-read reduces two risks: long-term plaintext on the server, and the same ciphertext being opened again and again. It does not reduce a third: what the recipient does after they can see plaintext. They can copy, screenshot, forward, or read it aloud. The link itself can be forwarded in full—key and id travel together—and the next person can decrypt on the reader as long as a read remains.
That is also why “the recipient must sign in” does not fix the handoff. Access control is the full URL. Putting an account gate on the reader does not keep the key off the server—the key should never be sent to the server. It only adds a door for the recipient and changes “who holds the link” into “who has an account.” MakePwd has no accounts and no password vault. Create and read both open with no sign-up. The reader is public for the recipient.
Capacity has a boundary too. 32 KB is enough for a passphrase, an API key, a recovery code, and a short note. It is not enough for a database dump or a full certificate bundle. A whole file should become .lock / .enc on this device and travel over a drive or mail. File Encryption Box accepts one file up to 5 GB, also with no account, and does not upload plaintext by default. Stuffing a large file into a one-time text channel is not “more secure.” It is outside what this path is built for.
Practice the burned state on a create page that opens with no account
If you want a tool that writes the query / fragment split into the result pane, start with MakePwd’s Burn-Link. It opens with no sign-up on either side. Encryption at create time happens in the current tab with AES-256-GCM. The server receives ciphertext, ttl_hours (0–168, default 24), and max_reads (1–10, default 1). The generated link is always s.html?id={id}#{key}. The reader stops on confirm, then fetches ciphertext and decrypts in this tab.
Use the canary above and leave reads at 1. Watch three places at once: the query / fragment split on create, whether Network fetches ciphertext on the first reader load, and whether the second open is burned. The split confirms the key did not enter the query. The first load confirms a preview will not burn by accident. The second open confirms “burn after reading” is not copy.
If the payload still needs tracking parameters stripped before you send anything else, run Privacy Cleaner first, then put the short string that actually has to stay secret into Burn-Link. Cleanup removes UTM and click IDs from a query. It does not replace a one-time ciphertext channel. None of those steps require an account, and none of them show a support inbox that is not connected.
FAQ
After one open, is plaintext still on the server?
Plaintext was never stored. The browser encrypts first. The server receives ciphertext only. After a successful fetch that exhausts the count, ciphertext is deleted. A later request for the same id should see 410 or a burned state. The URL string may still sit in chat history. That is not a plaintext copy on the host.
Will a chat preview burn the secret first?
It can spend the count. It usually cannot burn the plaintext. The preview fetches the page; the key after # does not enter HTTP. If the reader fetches ciphertext on load, the preview spends the only read. The correct implementation confirms first, then fetches. Slack and similar products fetch links in a message by default to draw a card.
What is the difference between 410 and 404?
410 means the resource used to be available and has been removed for good; clients should not retry. 404 is vaguer: the id may be invalid, or the record may already have been cleared. A one-time secret is clearer when 410 separates burned from expired. In either case there is no plaintext to restore.
Do I need to sign in to open the reader?
No. Create and read both open with no account. The recipient fetches ciphertext with the id and decrypts in this tab with the hash key. MakePwd has no accounts and no password vault. Access control is the full link itself.
Three things to remember the next time you send a one-time secret
First, when you ask “what remains after one read,” ask whether plaintext ever sat on the server. It did not. What is deleted is ciphertext. A URL still in chat decrypts nothing. Second, the burn point is the ciphertext fetch, not the first paint of the reader. The confirm button is there for preview bots, not for someone who already has the whole string. Third, check Network: search the canary on create, compare the request line with # on read, and look for 410 on the second open.
If you still want the reason a key can sit after the hash, read Why a URL hash fragment is a fit place for a key—and when that protection fails. This note only turns “what remains on the server after one read” into a range you can match against status codes and request timing.