List-Unsubscribe=One-Click: The Header Pair That Has to Match
The whole value of the List-Unsubscribe-Post header is one key/value pair and nothing else: list-unsubscribe=one-click. It is also the half of the pair that does the least work. The header that decides whether a mailbox provider shows an unsubscribe button next to your sender name is the other one, List-Unsubscribe, and unless it carries an HTTPS URI the pair means nothing at all.
That mismatch is where most broken implementations sit. Both headers are present, a syntax validator passes them, and the button still never appears — or it appears, someone taps it, and nobody gets unsubscribed. RFC 8058 is a short document and nearly every one of these failures traces back to six sentences in it. What follows is the failure list, not the definition.
What list-unsubscribe=one-click actually promises
The two headers do different jobs and fail for different reasons.
List-Unsubscribe is the older one, defined in RFC 2369, and it carries the destinations. Its contents are angle-bracket enclosed URLs, comma separated, and RFC 2369 is explicit that "the URLs have order of preference from left to right" — the client uses the leftmost protocol it supports. On its own it gets you the older two-step unsubscribe: the client opens the URL in a browser, the recipient lands on your page, and your page does whatever it does.
List-Unsubscribe-Post is the RFC 8058 addition, and it is a declaration rather than a destination. Section 3.1 says it "MUST contain the single key/value pair 'List-Unsubscribe=One-Click'". Sending it means: the HTTPS URI in the other header accepts a POST, and that POST completes the unsubscribe with no further interaction. A correct pair looks like this:
List-Unsubscribe: <https://send.yourdomain.net/u/9f3c1a2b4d>, <mailto:unsub@send.yourdomain.net?subject=unsub-9f3c1a2b4d>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
Note what is load-bearing there. The HTTPS URI is first. The value of the second header is exact — no quotes, no extra parameters, no trailing semicolon. And the URI ends in a token that identifies one recipient on one list, because section 3.1 requires the URI to "contain enough information to identify the mail recipient and the list from which the recipient is to be removed".
Nine pairs that look correct and are not
| Symptom | Cause | Fix |
|---|---|---|
| No unsubscribe button appears | List-Unsubscribe holds only a mailto: |
Add an HTTPS URI; the POST header is meaningless without one |
| Button appears, nobody unsubscribes | Endpoint answers the POST with a 301/302 | Handle the POST at the exact URI; return 200 |
| Button appears, nobody unsubscribes | Endpoint renders "click to confirm" | Complete the removal on the POST itself |
| POST returns 403 | CSRF middleware or a login wall on the route | Exempt the route; the POST carries no session |
| POST returns 415 | Endpoint parses JSON only | Accept multipart/form-data and application/x-www-form-urlencoded |
| Everyone unsubscribes to the same record | One campaign-wide URL | Mint a per-recipient token |
| Provider ignores the headers | Message unsigned, or h= omits the two headers |
DKIM-sign the message and include both header names |
| Recipients vanish without clicking | A GET on the URI performs the removal, and scanners prefetch links |
Make GET render a page; make POST do the work |
| Header present but malformed | Value quoted, cased differently, or carrying extra params | Emit List-Unsubscribe=One-Click byte for byte |
The rest of this guide is the four of those that cost the most time to diagnose.
The redirect that silently disarms the whole thing
RFC 8058 section 3.1 is blunt about this: "The mail sender MUST NOT return an HTTPS redirect, since redirected POST actions have historically not worked reliably." A client that follows a 302 usually converts the POST into a GET on the way, so your endpoint receives a request with no body, no key/value pair in it, and no reason to treat it as a one-click removal. From the provider's side the request succeeded. From yours, nothing happened.
Almost nobody writes this redirect on purpose. It arrives from infrastructure:
- A CDN or load balancer upgrading
http://tohttps://— which also means the URI in the header must already behttps://, not merely redirect there. - A trailing-slash canonicalisation rule, so
/u/9f3c1a2b4d301s to/u/9f3c1a2b4d/. - An apex-to-
www(orwww-to-apex) rule on the sending domain. - A framework's "force canonical host" middleware that fires before routing.
All four are invisible in a browser and fatal to the POST. Test the exact string you put in the header, not a tidied version of it.
A confirmation page is a failed unsubscribe
The spec's intent, in section 3.1, is that the sender can "handle it as a one-click unsubscription without manual intervention." There is no second step. If your POST handler responds with a page saying "are you sure?" or "click here to confirm", the removal did not happen, because the recipient is never going to see that page — the mail client made the request in the background and discarded the response body.
This is the failure mode that survives longest in production, because nothing errors. Your endpoint returns 200. Your logs show traffic. Your suppression list does not grow, and the complaints that the unsubscribe request was meant to prevent arrive anyway. The rule is simple: by the time you write the response, the recipient is already off the list.
The POST arrives stripped of everything your app expects
Section 3.1 again: "The POST request MUST NOT include cookies, HTTP authorization, or any other context information." The mailbox provider makes the request; the recipient's browser is not involved. So the request reaching your endpoint has no session cookie, no bearer token, no Referer your middleware recognises, and no CSRF token in the body — the body is only the key/value pair from the header.
In practice that means the unsubscribe route has to be exempted from the protections you apply everywhere else. In Rails that is skip_forgery_protection on the action; in Django, @csrf_exempt; in Laravel, an entry in the VerifyCsrfToken exception list; in Express, mounting the CSRF middleware after the route rather than globally. Skip that and your endpoint returns 403 to every provider that tries, forever.
The body encoding matters too. Section 3.2 says the POST content "SHOULD be sent as 'multipart/form-data' or MAY be sent as 'application/x-www-form-urlencoded'." An endpoint wired to a JSON-only body parser will reject both with a 415 and never reach your handler.
DKIM has to cover the headers, not just the message
Section 4 of RFC 8058 requires that "senders MUST apply at least one valid DKIM signature to the message." The reason is structural: without a signature, anything in the path could insert a List-Unsubscribe header pointing at a URL of its choosing, and a provider that acted on it would be handing unsubscribe traffic to a stranger.
The part that gets missed is the signed header list. A DKIM signature only covers the header names listed in its h= tag. If list-unsubscribe and list-unsubscribe-post are not in that list, the headers are technically unsigned even though the message is signed, and a provider is within its rights to distrust them. Check the h= tag of a real outgoing message — not the config you believe is deployed — and confirm both names are there.
Testing the pair before a campaign goes out
Two curl calls settle it. Use a real per-recipient URI from a message you actually received, not a hand-built one.
Check for redirects first:
curl -sS -o /dev/null -w '%{http_code} %{redirect_url}\n' \
-X POST 'https://send.yourdomain.net/u/9f3c1a2b4d' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'List-Unsubscribe=One-Click'
A 200 with an empty redirect URL is what you want. Any 301, 302, 307 or 308 is the failure described above, and the redirect URL it prints tells you which rule caused it.
Then confirm the removal actually landed — query your own suppression table for that recipient. That is the check that catches the confirmation-page failure, and it is the one people skip because the HTTP status looked fine.
Worth repeating as a sequence:
POSTthe exact URI from a delivered message, with the form-encoded body.- Confirm
200and no redirect. - Confirm the recipient is suppressed in your own data.
GETthe same URI and confirm it renders a page rather than removing anyone.- Inspect the delivered message's DKIM
h=tag for both header names.
The header pair does not replace the link in the body
Google's sender guidelines tie one-click unsubscribe to volume: "If you send more than 5,000 message per day, your marketing and subscribed messages must support one-click unsubscribe." The same guidance requires those messages to "include a clearly visible unsubscribe link in the message body." The headers are in addition to the link, not instead of it — clients that do not implement RFC 8058 still need somewhere to send people.
Yahoo's sender best practices call the RFC 8058 POST method "highly recommended" and the mailto: method "acceptable", and attach a clock to both: "Honor unsubscribes within 2 days." A POST handler that writes to a queue processed weekly satisfies the letter of the header and misses the requirement that matters.
Where these rules are written down
Every requirement above comes from one of four documents, and all four are short enough to read in an afternoon. The two RFCs define the headers themselves; the two provider pages define what happens to your mail when you get them wrong. If a vendor tells you something that contradicts one of these, the document wins.
- RFC 8058 — Signaling One-Click Functionality for List Email Headers, the source of the POST header, the HTTPS requirement, the redirect prohibition and the DKIM rule.
- RFC 2369 — The Use of URLs as Meta-Syntax for Core Mail List Commands, which defines
List-Unsubscribeitself and the left-to-right preference order of its URLs. - Google — Email sender guidelines, for the 5,000-messages-a-day threshold and the visible-link requirement.
- Yahoo — Sender Best Practices, for the two-day window on honouring an unsubscribe.
Frequently asked questions
Can List-Unsubscribe-Post be sent without an HTTPS URI in List-Unsubscribe?
No. RFC 8058 section 3.1 requires the List-Unsubscribe header to contain one HTTPS URI, and the POST header is a statement about that URI. If List-Unsubscribe carries only a mailto: address, the pair is incomplete and mailbox providers have nothing to POST to.
Why does the unsubscribe button appear but nobody gets removed from the list?
The two usual causes are a redirect and a confirmation page. RFC 8058 forbids answering the POST with an HTTPS redirect because redirected POSTs lose their body, and a handler that responds with a confirm page never completes the removal since the recipient never sees that page.
Does the POST include cookies or an authentication token?
No. Section 3.1 states the POST request must not include cookies, HTTP authorization, or any other context information. The mailbox provider makes the request, not the recipient's browser, so the unsubscribe route has to be exempt from CSRF protection and any login requirement.
What content type does the one-click POST body use?
RFC 8058 section 3.2 says the POST content should be sent as multipart/form-data or may be sent as application/x-www-form-urlencoded. An endpoint that only parses JSON will reject both with a 415 before your handler runs.
Is a visible unsubscribe link in the email body still required?
Yes. Google's sender guidelines require marketing and subscribed messages from senders above 5,000 messages per day to support one-click unsubscribe and to include a clearly visible unsubscribe link in the message body. The headers supplement that link rather than replacing it.
How quickly does an unsubscribe request have to be processed?
Yahoo's sender best practices say to honor unsubscribes within 2 days. Writing the request to a queue that is only processed weekly technically accepts the POST but misses that window, and the messages sent in the meantime are the ones that draw complaints.