Your unsubscribe endpoint redirects
By Jose Pollman · Published
Every header is correct. The DKIM signature covers them. You click the URL yourself and the unsubscribe page loads perfectly. And recipients still tell you they cannot unsubscribe. The difference between your test and theirs is that a browser follows redirects and a mailbox provider does not.
What the error means
A one-click unsubscribe is a single exchange. The provider sends one POST to your URL, reads the status code, and stops. There is no second request. RFC 8058 §3.2 describes that exchange and says nothing about following a Locationheader, and in practice receivers do not.
So when your endpoint answers 302 Found, that is the whole conversation. The provider has a 3xx, no confirmation that anything happened, and no reason to try again. The recipient is told the unsubscribe failed, or nothing happens at all. Meanwhile your server logs show a request arriving and being answered normally, which is why this can survive weeks of investigation.
Why it happens
Rarely because anyone chose to redirect the unsubscribe endpoint. It is almost always something in front of the application:
- Scheme or host canonicalisation. The edge redirects
http://tohttps://, orwwwto the apex domain. If the URL in your header is not already in the canonical form, every request is bounced before it reaches your code. - A trailing-slash rule.
/unsubredirecting to/unsub/, or the reverse, is enough. - A confirmation page. The handler records the unsubscribe and then redirects to a “you have been removed” page. Correct for a browser; invisible to a provider that never follows it. The unsubscribe may well have worked, but the provider cannot tell, and it will report failure.
- A framework redirecting POST after processing. The post/redirect/get pattern is good practice for forms and wrong here.
The fix
The URL in the header must be the final endpoint, and it must answer the POST with a 2xx directly.
- Find the real one. Follow the redirect chain yourself and use the URL it ends at — including the exact scheme, host and trailing slash.
- Handle POST at that URL without redirecting. Return
200on success, and also on a token that has already been used: a repeated unsubscribe is not an error, and a 4xx there is reported as a failure to the recipient. - Answer immediately. Validate the token, return the response, and queue the actual removal. You have 48 hours to complete it under the Gmail and Yahoo rules, but only a few seconds to answer.
- Keep the browser experience separately. If you want a confirmation page for people who paste the link, serve it on GET at the same URL and leave POST answering plainly.
Testing it the way a provider would
Reproduce the exact request, and do not let curl follow anything:
curl -i -X POST 'https://example.com/unsub?id=TOKEN' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'List-Unsubscribe=One-Click'You want HTTP/2 200 on the first line. Anything in the 300s is the problem, and the Location header will usually name its own cause — a scheme change, a host change, or a confirmation page. Note the absence of -L: adding it makes curl behave like a browser and hides exactly what you are trying to see.
Other endpoint failures worth ruling out
- 405. The URL exists but accepts only GET. Common when the unsubscribe was built as a link first.
- 403 from a WAF. Some rules block requests with no browser user-agent. A provider’s POST looks nothing like a browser.
- Slow responses. If the handler waits on a database write, it can exceed what a receiver will wait for. Answer first, remove afterwards.
Checking it worked
The checker can send this request for you and reports the status, any Location header, and how long the response took. It is off by default, because it sends a genuine unsubscribe request — use headers from a message you sent to a test address of your own, not to a real subscriber.
Related guides
- List-Unsubscribe isn't DKIM-signed — The headers are right, the h= tag does not cover them, and nothing reports it.
- Missing List-Unsubscribe-Post header — You have List-Unsubscribe, but no unsubscribe button appears.
- Your unsubscribe header needs an HTTPS URL — A mailto: address alone will not satisfy the one-click requirement.