Skip to content
RRAECHLY

Deliverability · 6 min read

The 405 that quietly breaks one-click unsubscribe

Gmail and Yahoo POST to your List-Unsubscribe URL. If your route only accepts GET, every one-click opt-out fails silently. We shipped this bug, then fixed it.

M

Mukesh Yadav

Founder, Raechly

· Updated 2026-08-10

We had a working unsubscribe link. It rendered in the footer, it resolved, it opted people out. We had also implemented List-Unsubscribe with the RFC 8058 one-click header, so Gmail showed its native unsubscribe control next to our sender name.

Everything looked right. One-click unsubscribe was completely broken.

What was wrong

Our unsubscribe route accepted GET. That is the obvious way to build it, because a link in an email body is a GET request, and that path worked exactly as intended.

RFC 8058 does not use GET. When you send List-Unsubscribe-Post: List-Unsubscribe=One-Click alongside an HTTPS List-Unsubscribe URL, the mailbox provider makes a POST request to that URL when the recipient clicks unsubscribe in the Gmail or Yahoo interface. No page load, no confirmation, no human ever visits the URL.

Our route returned 405 Method Not Allowed.

So the sequence was: recipient clicks Gmail’s unsubscribe button, Gmail POSTs, we reject the request, nothing is recorded, the recipient believes they have unsubscribed, and they receive the next message in the sequence anyway.

That is worse than having no unsubscribe at all. Someone who took the polite exit and got mail regardless does not try the link twice. They press the spam button.

Why it is so easy to miss

Every part of this fails quietly.

The GET link keeps working, so manual testing passes. You click the footer link, you get opted out, the feature appears correct.

The header renders, so inspection passes. Gmail shows the native unsubscribe control, which is the visible signal most people check for. The control being present says nothing about whether the endpoint behind it works.

And the failure is invisible from your side. A 405 to a Gmail-originated POST does not appear in campaign reporting. There is no bounce, no complaint attributed to it, no error surfaced to the sender. The only symptom is a complaint rate that is higher than it should be, months later, with no obvious cause.

Nobody tests this because the test requires you to think about it as a machine-to-machine endpoint rather than a link.

The fix

Three parts.

Accept POST on the same URL. The route needs both methods, and the POST handler must complete the opt-out without requiring any further interaction. Returning a page that asks “are you sure?” fails the requirement, because there is no browser on the other end and nobody will ever click the confirmation.

GET  /api/unsubscribe/:email   → serves the link in the email body
POST /api/unsubscribe/:email   → RFC 8058 one-click

Both suppress the address immediately.

Encode the address properly. The email address is embedded in the URL, so it has to be percent-encoded when the header is written and unescaped when the handler reads it. Plus signs are the usual casualty: first+tag@example.com decodes to a space in the local part if you skip this, and the opt-out silently applies to an address that does not exist.

Make suppression permanent and unresurrectable. An opt-out that a future CSV import can undo is not an opt-out. Our import path upserts by email and refuses to revive anyone who unsubscribed or bounced, because the alternative is emailing someone who explicitly asked you not to, six months later, from the same domain.

How to check yours in one command

You do not need to send a campaign to test this. Send a POST at your own unsubscribe URL:

curl -i -X POST "https://yourdomain.com/api/unsubscribe/test%40example.com" \
  -d "List-Unsubscribe=One-Click"

A 2xx is correct. A 405 means every one-click unsubscribe from Gmail and Yahoo is currently failing. A 404 means the route does not exist at all, which is the same problem wearing a different number.

Then check the header is actually on your messages. Open a delivered message, view the original, and confirm both lines are present:

List-Unsubscribe: <https://yourdomain.com/api/unsubscribe/...>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

If the second line is missing, providers will not use one-click at all, and the first line degrades to a plain link.

Why this is table stakes now

In February 2024, Gmail and Yahoo made this a requirement rather than a recommendation for bulk senders. The rules are short: authenticate with SPF, DKIM and DMARC, support one-click unsubscribe, and keep spam complaints under 0.3%.

Those requirements are related in a way that is easy to miss. One-click unsubscribe exists specifically to hold the complaint rate down. Every recipient who opts out cleanly is one who did not press spam, and 0.3% is three complaints per thousand delivered messages. It does not take many failed opt-outs to breach it.

A broken one-click endpoint therefore does not just fail a compliance checkbox. It actively converts people who wanted to leave quietly into people who report you.

The structural reason we could fix it

Setting List-Unsubscribe requires control of the message headers, which means assembling raw MIME rather than handing a template to a send API. That is why our SES integration builds MIME by hand: it is the only way to set List-Unsubscribe, List-Unsubscribe-Post, Precedence: bulk and a stable Message-ID per send.

If your sending platform does not expose those headers, you cannot implement one-click unsubscribe correctly no matter how well you understand it. Worth checking before you assume the checkbox in the settings page is doing what it says.


Related reading: one-click unsubscribe, the List-Unsubscribe header, and spam complaint rate.

Keep reading

Point your assistant at it and see.

Connect the MCP server, register a domain, publish the DNS records it hands you. You will know inside an hour whether this is the setup you want.