Security

    How One-Time Secret Links Work

    A clear, developer-friendly explanation of how one-time secret links work — from creation to atomic burn-after-read.

    By W. Miller · May 26, 2026 · 7 min read

    A one-time secret link is a URL that reveals its contents exactly once and then becomes permanently inert. The mechanism is simple, but the edge cases are where it gets interesting — and where sloppy implementations quietly leak.

    The basic flow

    1. Sender creates a secret. The server stores the payload (encrypted at rest, ideally in storage isolated from everything else) and returns a URL with a random, hard-to-guess slug.
    2. Sender shares the URL through any channel — email, chat, SMS.
    3. Recipient opens the URL. The server checks status, atomically marks the secret as consumed, and returns the payload in the same operation.
    4. Any subsequent visit gets a "burned" status page instead of content.

    The critical step is step 3. If the read and the status update happen as two separate queries — SELECT to check, then UPDATE to mark — two concurrent viewers can both pass the check before either one writes. Both get the payload. That's a classic TOCTOU (time-of-check to time-of-use) bug, and it converts your "exactly once" guarantee into "usually once."

    Atomicity in practice

    The fix is to collapse the check, the mark, and the read into one database operation. In Postgres that looks like:

    UPDATE secrets
    SET burned_at = now()
    WHERE slug = $1
      AND burned_at IS NULL
      AND expires_at > now()
    RETURNING payload;
    

    The row-level lock the UPDATE takes means concurrent callers serialize: the first one matches the WHERE clause, flips burned_at, and gets the payload back via RETURNING. Every later caller finds burned_at already set, matches zero rows, and gets the burned status. There is no window between "check" and "consume" because they are the same statement.

    LinkPilot records views atomically through a database function for exactly this reason — concurrent reveal attempts cannot leak the secret twice. If you're evaluating a tool, this is a fair question to ask the vendor directly: is the burn atomic with the read?

    What "burned" should look like: HTTP 410

    Once consumed, the link should fail loudly and permanently. LinkPilot does two things on burn:

    • The stored payload is overwritten with [REDACTED] in the same write that flips the status — so even a later database compromise can't recover a spent secret.
    • Subsequent reveal attempts return HTTP 410 Gone with a clear "already viewed and destroyed" page.

    410 is the right status code: it means the resource existed and is intentionally, permanently unavailable — as opposed to 404, which says "never heard of it." The distinction matters for humans too: a recipient who sees "this secret was already viewed" knows to tell the sender, who can check the audit trail and rotate if the viewer wasn't them.

    Hard-to-guess slugs, and why they aren't enough

    The slug is the only thing standing between the public internet and an unread secret, so it has to carry real entropy. A slug drawn from a cryptographically secure random source with, say, 22 characters of base62 carries roughly 128 bits — enumeration is not a realistic attack at any request rate a server would survive.

    But slug entropy only defends against guessing. It does nothing about the URL being observed: browser history, proxy logs, forwarded emails, screen shares, clipboard managers. That's why serious implementations layer on:

    • Expiry, so an observed URL goes dead on schedule.
    • A passphrase, so an observed URL alone isn't sufficient.
    • Burn-after-read, so an observed URL is worthless after the legitimate read.

    Chat clients (Slack, Teams, iMessage) and email security scanners unfurl URLs by fetching them with a GET request the moment a message is delivered. If your reveal endpoint burns on GET, the unfurler destroys the secret before the human ever sees it — the most common real-world failure of naive burn-after-read tools.

    The defenses, as LinkPilot implements them:

    • Separate metadata and reveal actions. A GET on the reveal endpoint returns only status and whether a passphrase is required. A view is recorded only on an explicit POST — the action a human takes when they click the reveal button.
    • Passphrase gate. When a passphrase is set, no payload is returned to anyone — bot or human — without the correct hash.
    • Cache-Control: no-store on every reveal response, so intermediaries never cache secret contents.

    LinkPilot's Security Architecture page documents this handling end to end.

    The lifecycle, state by state

    State Triggered by What a visitor sees Payload recoverable?
    Active Creation Reveal page (passphrase prompt if set) Yes — one reveal available
    Burned View-count limit reached "Already viewed and destroyed" — HTTP 410 No — payload overwritten
    Expired Expiry deadline passes Expired status page No — redacted on next access
    Revoked Sender kills it from the dashboard Refuses to reveal — HTTP 410 No — immediate and permanent

    Every transition lands in an audit timeline. LinkPilot's event types are created, viewed, failed_passphrase, burned, expired, and revoked, each with a timestamp and a hashed IP (SHA-256 with a daily-rotating salt — the raw IP is never stored). That trail is what turns "I think they got it" into "viewed at 10:05 from the expected country, one view, then burned."

    Common extensions

    • Passphrase gate — require an out-of-band passphrase before the reveal, so a leaked URL alone is not a leaked secret. Rate-limit attempts; LinkPilot allows 5 per IP per minute per secret and logs every failure.
    • Time-based expiry — the secret dies after a set window (5 minutes to 30 days in LinkPilot) even if never read. Covers the forgotten-in-inbox case that burn-after-read can't.
    • Manual revoke — the sender can kill an unread secret from the dashboard the moment something feels off. Cheap insurance for "sent it to the wrong Alex."

    Edge cases that separate good implementations

    The wrong person opens it first

    Burn-after-read doesn't stop the wrong viewer — it makes the misdelivery visible. The intended recipient hits a burned page, the audit trail shows one view you didn't expect, and you rotate the credential. Painful, but bounded: exactly one exposure, detected quickly. Compare that with a pasted-in-chat secret, where you'd never know.

    The screenshot problem

    One-time means one reveal, not one copy. The recipient can screenshot, copy, or retype the secret — nothing about the link prevents that, and vendors who imply otherwise are selling fiction. The honest framing: burn-after-read controls the delivery channel, not the recipient. Trust in the recipient is an input, not an output.

    Browser prefetch and speculative loading

    Some browsers and apps prefetch likely-clicked URLs. Because a well-built tool only burns on an explicit POST, prefetch behaves exactly like an unfurler: it can fetch the page shell, never the payload.

    • The value will be needed again. Recoverability is the whole design goal of a password manager and the anti-goal of a one-time link. Use a vault item.
    • Multiple recipients need it. One link, one read. Fan-out means either creating one link per person (fine, auditable) or admitting you need shared access control instead.
    • A machine is the consumer. Services and CI should read from a secrets manager, not click links.

    If you want to watch the mechanism in action, create a Secret Link — the lifecycle is logged, so you can see the created, viewed, and burned events fire in real time.

    Frequently asked questions

    Run smarter links with LinkPilot

    Tracking, UTMs, QR codes, AI insights, and white-label reporting — in one workspace. Free to start.

    Create your free account

    Read next