Skip to content
Koltrix

Confirmation links that survive email security scanners

Why a one-time confirmation link can arrive already used, how mail scanners cause it, and a simple two-step design that works for real users and for scanners.

Koltrix Team4 min read
Screen showing lines of code
Photo by Chris Ried on Unsplash
On this page(9 sections)
  1. What is happening
  2. Why a longer expiry does not help
  3. The fix: a GET shows a page, a POST does the work
  4. Details that make it hold up
  5. What to put in the email
  6. Other things that open your links
  7. When the link was already used
  8. A quick way to test it
  9. The short version

A user signs up, opens the confirmation email and clicks the link. The page says "This link has already been used." They did nothing wrong. Something clicked it before they did.

That something is usually a security scanner. It is one of the most common support complaints about verification and sign-in links, and it has a clean fix.

What is happening

Many companies and some mail providers check links in incoming mail before the person sees them. A scanner follows each link to see whether the destination is malicious. Some mail apps also fetch links in advance to show a preview.

The scanner does not know your link is special. It sends a normal web request, usually a GET. If your server treats that request as "the user confirmed", the token is spent, and the real click finds an expired or used link.

You will see it in your logs as a click within a second or two of delivery, from a datacenter address, with an unusual or empty user agent. It tends to hit business addresses most, because that is where scanners are common.

Why a longer expiry does not help

Extending the token's lifetime does not fix this. The problem is not that the link expires; it is that the first request consumes it. Making it single-use is still right for security, so the answer is to change what a request is allowed to do.

The fix: a GET shows a page, a POST does the work

The rule is simple: a GET request must never change state. Opening the link should show a page, and only a deliberate action on that page should confirm anything.

  1. The link in the email points at a page on your site, for example /confirm?token=....
  2. A GET to that URL checks the token is valid and shows a short page: "Confirm your email address" with a button.
  3. Pressing the button sends a POST with the token. Only that request marks the address verified and spends the token.

Scanners follow links; they rarely submit forms. The person clicks the button and moves on, and the cost to them is one extra click.

For sign-in links, the same pattern works: show "Continue to your account" and create the session on the POST. Our comparison of magic links and email codes covers when a code is the simpler choice.

Details that make it hold up

  • Do not log the token. Put it in the query string if you must, but keep it out of access logs and analytics, and set a no-referrer policy on the page so it is not passed to other sites.
  • Make the POST idempotent. If the person double-clicks, or presses Back and tries again, the second request should say "already confirmed" and send them on, not show an error. The same idea is behind idempotency keys for email sends.
  • Keep expiry short but sensible. An hour is common for sign-in links, longer for first-time email confirmation, where the person may not check mail right away. Say the time in the email.
  • Bind the token to one purpose. A confirmation token must not work as a password reset or sign-in token. See password reset email security for the full list of properties a good token has.
  • Store a hash, not the token. If your database leaks, unhashed tokens are working keys.

What to put in the email

Keep the message plain and the link obvious. Say what the person is confirming, which address, and that they can ignore the email if they did not ask for it. Include the link as visible text as well as a button, so people whose mail app strips buttons can copy it.

Do not rewrite the link through a click-tracking redirect. Tracking wrappers add another hop that scanners follow, and they can break the token if the redirect changes the URL. For security emails, turn tracking off. The transactional email API design post explains why these messages should be sent separately from marketing mail.

Scanners are not the only machines. Link previews in chat apps, forwarding services that archive mail, and accessibility tools can all fetch a URL without a person choosing to. Some corporate gateways even rewrite the link and open it from several places. A design that does not depend on who made the first request, and that waits for a deliberate POST, copes with all of them the same way, so you do not need to list or block each tool.

Watch the numbers after the change. The share of confirmation emails that end in a resend request, and the number of "link already used" tickets, should both fall. If they do not, check whether another step, such as a redirect from your email provider, is still consuming the token.

Even with the two-step design, people will sometimes arrive at a spent link: they confirmed on another device, or an old email is still in their inbox. Make that page useful:

  • If the address is already confirmed, say so and offer to sign in.
  • If the token expired, offer a button to send a fresh link to the same address.
  • Never show a bare error code. The person needs the next step.

A quick way to test it

You can check your own flow without special tools. Send yourself a confirmation email, then request the link with curl before opening it in a browser. If the account shows as verified after the curl request, a scanner would have done the same. After the change, the request should return the page and leave the account untouched.

The short version

Treat every link in an email as something a machine may open first. Make GET requests safe, move the state change behind a button, make the confirmation repeatable and give a clear way forward when a link is spent. Your support queue will shrink, and the security of the token stays intact.

Start with Koltrix

Your domain, one inbox, and an API that sends.

A team inbox where AI sorts and drafts (nothing is sent without your click), plus the transactional API and SMTP relay your product sends with. 7 days free, no card.

SharePost on XLinkedIn