# Why magic links expire before users click them

Reproduce scanner clicks, single-use token reuse, and expiry in a local lab, then design a confirmation flow with clear recovery behavior.

Author: Stampwing
Canonical: https://www.stampwing.com/guides/magic-link-expired-before-click
Published: 2026-10-05
Reviewed: 2026-10-05
Category: Engineering

Reproduce an expired-link report and identify whether a scanner, reuse or elapsed time consumed the token.

## Short answer

Section link: https://www.stampwing.com/guides/magic-link-expired-before-click#short-answer

An expired or already-used magic link can mean different things: the token aged out, another request consumed it, or a scanner visited it before the person did. Trace token creation and consumption separately. A GET that only displays a confirmation page avoids one common premature-consumption path.

## Before you start

Section link: https://www.stampwing.com/guides/magic-link-expired-before-click#before-you-start

For: Developers building and operating application email.

Bring: Node.js 22+ and npm. No account system, mailbox, or database is required.

Scope: The download runs locally with synthetic data. All email delivery is simulated; no provider credentials or live sends are needed.

## Separate expiry from consumption

Section link: https://www.stampwing.com/guides/magic-link-expired-before-click#identify-the-failure

Begin with the token’s issue time, expiry time, and the first state transition recorded by your authentication service. Keep only a safe correlation ID in diagnostics. Do not paste the token or full magic-link URL into a support ticket, analytics event, or access log.

A delayed email can carry a token that has expired naturally. An already-used response suggests a different path: a previous browser tab, another device, a retry, or an automated visitor may have consumed it. Record what the backend knows instead of assuming every early visit came from a scanner.

Inspect whether your email provider rewrites links for click tracking. A secret-bearing authentication link should not acquire extra tracking intermediaries. Also check that the message’s stated lifetime matches the account service’s actual expiry.

## Reproduce the scanner sequence

Section link: https://www.stampwing.com/guides/magic-link-expired-before-click#run-locally

Unzip the lab, run npm ci, npm test, and npm run demo. The experiment issues three synthetic tokens under a controlled clock. It compares a GET that consumes a token with a GET that only returns “confirmation required,” then advances time to the exact expiry boundary.

The fixture stores token hashes in memory and never connects to an account, browser session, or provider. Restarting it clears its state. The experiment demonstrates lifecycle decisions; it is not a reusable authentication backend.

Run from the extracted example folder

```sh
npm ci
npm test
npm run demo
```

## Read the key part of the example

Section link: https://www.stampwing.com/guides/magic-link-expired-before-click#example-source

This excerpt comes from lab.mjs, lines 20–32, in the download. It shows the central decision; run the complete package with the commands above.

Local simulation · lab.mjs

```javascript
    visit(token, method, flow) {
      if (typeof token !== "string" || !/^[a-zA-Z0-9_-]{32}$/.test(token))
        return "invalid";
      const row = records.get(hash(token));
      if (!row) return "invalid";
      if (row.used) return "used";
      if (now() >= row.expires) return "expired";
      if (!["GET", "POST"].includes(method)) return "method rejected";
      if (flow === "confirm" && method === "GET")
        return "confirmation required";
      if (!["confirm", "consume-on-get"].includes(flow)) return "invalid flow";
      row.used = true;
      return "consumed";
```

## Find the files you’ll change

Section link: https://www.stampwing.com/guides/magic-link-expired-before-click#example-files

Pinned dependencies: Node.js built-ins only. Install with npm ci so the lockfile controls the resolved versions.

| File | Purpose |
| --- | --- |
| lab.mjs | The example behavior shown in this article. |
| test.mjs | Acceptance checks and synthetic failure cases. |
| demo.mjs / expected-output.json | A repeatable local experiment and its recorded result. |
| README.md | Setup commands, expected behavior and production boundaries. |
| BUILD-BRIEF.md | The coding-agent brief below. |
| package.json / package-lock.json | Pinned dependencies and runnable commands. |
| LICENSE | MIT license for adapting this example. |

## Compare the recorded local result

Section link: https://www.stampwing.com/guides/magic-link-expired-before-click#recorded-experiment

Captured from this package’s demo command on October 5, 2026. These results use synthetic fixtures and simulated delivery; they do not measure a live provider or inbox placement.

Recorded local simulation · expected-output.json

```json
{
  "simulated": true,
  "consumeOnGet": {
    "scanner": "consumed",
    "person": "used"
  },
  "confirmation": {
    "scanner": "confirmation required",
    "person": "consumed",
    "reuse": "used"
  },
  "delayed": "expired"
}
```

## What the local experiment establishes

Section link: https://www.stampwing.com/guides/magic-link-expired-before-click#interpret-results

In the consume-on-GET flow, the first simulated scanner visit succeeds and the later person sees “used.” In the confirmation flow, that same GET leaves the token intact; the subsequent POST consumes it, and another POST fails. At the expiry boundary, consumption is rejected.

This does not prove that every scanner only performs GET requests. A scanner can behave differently, including following richer interactions. A confirmation page reduces one failure path; it is not proof of a human and does not replace the rest of your authentication controls.

## Match the symptom to evidence

Section link: https://www.stampwing.com/guides/magic-link-expired-before-click#diagnostic-matrix

| Symptom | Evidence to collect | Likely next investigation |
| --- | --- | --- |
| Expired on first use | Issue, acceptance, arrival, and expiry times | Delivery delay versus token lifetime. |
| Already used | First successful consumption timestamp | Other tabs, devices, retries, or automated visits. |
| Wrong destination | Final redirect and approved callback settings | Tracking rewrites and redirect allowlists. |
| Works once, fails concurrently | Token update transaction result | Atomic single-use enforcement. |

## Build the confirmation flow around your auth service

Section link: https://www.stampwing.com/guides/magic-link-expired-before-click#production-setup

Keep GET side-effect free: display a confirmation screen without changing authentication state. On an explicit, protected submission, validate the token, approved redirect, expiry, and session context; atomically mark the token consumed as part of the real operation. Two concurrent requests must not both succeed.

Use appropriate CSRF defenses and bind the flow to a requesting session where your product supports that model. A form with no such protections is not the production implementation. Store hashes rather than plaintext bearer tokens, suppress token-bearing logs, and avoid third-party scripts on the confirmation page.

Give expired and used links a clear next action that requests a fresh link without disclosing whether an account exists. Do not silently extend an expired token. Reconcile delayed or uncertain email requests separately from authentication recovery.

[Review password reset copy and security boundaries](https://www.stampwing.com/guides/password-reset-email-templates)

[Trace a missing or late email](https://www.stampwing.com/guides/email-delivered-but-not-received)

## Does a confirmation button stop every scanner?

Section link: https://www.stampwing.com/guides/magic-link-expired-before-click#common-question

No. A scanner capable of submitting the form can still consume the token. A POST changes the consumption point; it does not prove a human is present. Production authentication also needs secure session handling, CSRF protection, atomic token storage, and a recovery path.

## Use with your coding agent

Section link: https://www.stampwing.com/guides/magic-link-expired-before-click#coding-agent-brief

Download the example, then copy this brief into your coding agent. The same brief is included as BUILD-BRIEF.md.

Coding-agent build brief

```markdown
# Why magic links expire before users click them — coding-agent brief

## Objective

Compare token consumption on GET with a confirmation step that consumes the token only on explicit POST.

## Read first

Read README.md, package.json, lab.mjs, test.mjs and demo.mjs. Keep dependency versions pinned to package-lock.json. Use Node.js 22+.

## Dependencies

Node.js built-ins only. Install with npm ci and preserve the lockfile.

## Work

Start at `tokenLab` in lab.mjs. Run the existing synthetic fixtures before changing behavior. Preserve the guide's original scenario and add a regression check for each changed failure case.

## Constraints

All email is simulated. Do not add provider credentials, send email, deploy services, or use the application's database. Preserve unknown outcomes instead of claiming delivery. Do not turn the local demonstration into production authentication or a public mail relay. Explain the production setup separately.

## Verification

Run `npm ci`, `npm test`, and `npm run demo`.

## Acceptance

Prove scanner GET, expiry at the exact boundary, one-use behavior, invalid input, capacity limits and competing confirmations. Keep the output labelled as a token-lifecycle simulation.
No real tokens, signing secrets, or recipient data appear in logs. All fixtures use synthetic values.
```

## Check the download

Section link: https://www.stampwing.com/guides/magic-link-expired-before-click#download-integrity

The ZIP contains 5,591 bytes. Compare its SHA-256 digest with this value before extracting. Downloads are free and require no signup.

SHA-256 · magic-link-expired-before-click.zip

```text
8ffaae8839958c93494d20080ec94f16259d5f44a95a67199712233593160572
```

## Sources

Section link: https://www.stampwing.com/guides/magic-link-expired-before-click#sources

- [Supabase: email templates and email prefetching](https://supabase.com/docs/guides/auth/auth-email-templates)
- [OWASP: forgot password guidance](https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html)
- [RFC 9110: safe request methods](https://www.rfc-editor.org/rfc/rfc9110.html#name-safe-methods)

Example download: [Download the local example](https://www.stampwing.com/downloads/magic-link-expired-before-click.zip)

## Related guides

- https://www.stampwing.com/guides/password-reset-email-templates
- https://www.stampwing.com/guides/supabase-auth-email-templates
- https://www.stampwing.com/guides/email-delivered-but-not-received

Editorial policy: https://www.stampwing.com/resources/editorial
