Skip to content
Pidgely
  • deliverability
  • compliance
  • unsubscribe

One-Click Unsubscribe (RFC 8058): How to Implement It Right

Implement List-Unsubscribe and List-Unsubscribe-Post per RFC 8058: headers, DKIM signing, the POST endpoint, and how to avoid link scanners unsubscribing users.

Pidgely teamPublished 5 min readMarkdown version

On this page
  1. Why it exists and who requires it
  2. The headers
  3. What the client sends
  4. Designing the token
  5. The endpoint
  6. The link scanner problem
  7. Scope: what does unsubscribe mean?
  8. Common mistakes
  9. Checklist
  10. Frequently asked questions

One-click unsubscribe is defined by RFC 8058: you add a List-Unsubscribe header with an HTTPS URL and a List-Unsubscribe-Post: List-Unsubscribe=One-Click header, both covered by your DKIM signature, and your server unsubscribes the recipient when it receives a POST to that URL. Mail clients like Gmail and Yahoo then show a native "Unsubscribe" button that works without opening a web page. The key implementation rule is that only the POST may unsubscribe; a GET must never change state, because link scanners fetch URLs automatically.

Why it exists and who requires it

The List-Unsubscribe header itself is old (RFC 2369, 1998), but it originally just pointed at a mailto: address or a web page. Clients could not safely automate a web unsubscribe: a page might ask for a login, show a survey or need several clicks. RFC 8058 (2017) closed that gap with a precise, machine-executable contract.

Since February 2024, Google's and Yahoo's sender guidelines require bulk senders to support one-click unsubscribe on marketing and subscribed mail and to process requests within two days. Beyond compliance, an easy unsubscribe is a deliverability tool: every person who unsubscribes instead of clicking "Report spam" protects your complaint rate.

The headers

text
List-Unsubscribe: <https://example.com/unsubscribe/eyJzIjoibmV3cyIsInIiOiJhOWYz...>, <mailto:unsubscribe+a9f3@example.com?subject=unsubscribe>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
DKIM-Signature: v=1; a=rsa-sha256; d=example.com; s=s2026;
 h=from:to:subject:date:message-id:list-unsubscribe:list-unsubscribe-post; ...

Requirements from RFC 8058:

  • List-Unsubscribe must contain exactly one HTTPS URI. Additional mailto: URIs are allowed.
  • List-Unsubscribe-Post must contain exactly List-Unsubscribe=One-Click.
  • The message must have a valid DKIM signature covering both headers (in the h= tag). Receivers ignore one-click data they cannot authenticate.
  • The URI must identify the recipient and list on its own: the client will not send cookies or authentication.

What the client sends

When the user clicks the client's unsubscribe button, the mailbox provider issues a request like this:

HTTP
POST /unsubscribe/eyJzIjoibmV3cyIsInIiOiJhOWYz... HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 26

List-Unsubscribe=One-Click

RFC 8058 allows multipart/form-data too, so parse both encodings. Your server must not redirect to a login page, require a CAPTCHA or show a confirmation: the POST itself completes the unsubscribe. Return a 2xx status.

Designing the token

The URL must carry enough information to identify who is leaving which list, and it must be impossible to forge. Two good options:

  1. Opaque random token stored in a database and mapped to (recipient, stream, list).
  2. Signed token (an HMAC over the payload with a server-side secret), which needs no lookup table.
Python
import hmac, hashlib, base64, json

SECRET = b"rotate-me"

def make_token(recipient_id: str, stream: str) -> str:
    payload = base64.urlsafe_b64encode(json.dumps(
        {"r": recipient_id, "s": stream}, separators=(",", ":")
    ).encode()).rstrip(b"=")
    sig = hmac.new(SECRET, payload, hashlib.sha256).digest()[:16]
    return payload.decode() + "." + base64.urlsafe_b64encode(sig).rstrip(b"=").decode()

Avoid putting a raw email address in the URL. URLs end up in logs, proxies and analytics tools. RFC 8058 asks for an opaque, hard-to-forge identifier in the URI, so use an internal identifier, and verify the signature with a constant-time comparison.

Do not set a short expiry. People unsubscribe from messages that are months old, and an expired link that refuses the request generates a spam complaint instead.

The endpoint

Python
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/unsubscribe/<token>")
def one_click(token):
    data = verify_token(token)            # raises on bad signature
    if request.form.get("List-Unsubscribe") != "One-Click":
        abort(400)
    suppress(data["r"], stream=data["s"], source="one_click")   # idempotent
    return "", 200

@app.get("/unsubscribe/<token>")
def landing(token):
    data = verify_token(token)
    # No side effects. Show a page with a button that POSTs, plus preferences.
    return render_unsubscribe_page(data)

Make the suppression idempotent: clients and users may trigger it more than once, and the second call must succeed quietly.

Corporate security gateways, antivirus products and some mailbox providers fetch every URL in a message to check for malware, often within seconds of delivery. Many follow redirects, and some render pages in headless browsers that execute JavaScript.

If your unsubscribe action happens on GET, these systems unsubscribe your recipients before a human sees the message. Symptoms include a burst of unsubscribes from one corporate domain right after a send, or unsubscribes timestamped seconds after delivery.

Defenses:

  • Never change state on GET. RFC 8058 explicitly requires this.
  • Make the visible footer link land on a page with a button that sends a POST, rather than unsubscribing on load.
  • Avoid auto-submitting forms with JavaScript on the landing page; headless scanners execute it.
  • Log user agent and timing of unsubscribe events so you can detect scanner patterns.

The native client button is safe because it always sends a POST defined by RFC 8058, not a scanner-style GET.

Scope: what does "unsubscribe" mean?

Decide what one click removes. Usually it is the specific list or stream the message belonged to, such as a weekly newsletter, not all mail. Encode the stream in the token, and keep transactional messages (receipts, security alerts) flowing, because they are not subscriptions. If you offer a preferences page, link to it from the body, not from the one-click URL.

Common mistakes

  • Header added after DKIM signing by a downstream system, so it is not covered by the signature.
  • Only a mailto: in List-Unsubscribe, which does not satisfy one-click.
  • Unsubscribing on GET, causing scanner-triggered unsubscribes.
  • Requiring login or confirmation after the POST.
  • Tokens that expire after a few days.
  • Raw email addresses in URLs.
  • Processing in a nightly batch that misses the two-day window when it fails.
  • Adding the headers to transactional mail in a way that lets users silence password resets.

Checklist

  • [ ] HTTPS List-Unsubscribe URI and List-Unsubscribe-Post: List-Unsubscribe=One-Click
  • [ ] Both headers listed in DKIM h=
  • [ ] Signed or opaque token, no raw addresses
  • [ ] POST endpoint parses form-urlencoded and multipart bodies
  • [ ] GET has no side effects
  • [ ] Suppression is idempotent, scoped and immediate
  • [ ] Visible unsubscribe link in the body as well

Pidgely adds RFC 8058 headers to marketing streams, signs them with your domain's DKIM key, handles the POST endpoint, and feeds unsubscribes into the stream's suppression list, emitting a webhook so your own database stays in sync.

Frequently asked questions

What is RFC 8058 one-click unsubscribe?

RFC 8058 defines the List-Unsubscribe-Post header. Combined with an HTTPS List-Unsubscribe URI, it lets a mail client unsubscribe a user by sending a single POST request with the body List-Unsubscribe=One-Click, with no web page or confirmation step.

Is one-click unsubscribe required?

Google and Yahoo require it for bulk senders on marketing and subscribed messages since 2024, and unsubscribes must be honored within two days. Transactional messages are exempt.

Why must the unsubscribe action only happen on POST?

Security scanners and link-preview systems routinely issue GET requests to every URL in a message. If a GET unsubscribes the user, people get unsubscribed without ever clicking. RFC 8058 requires the POST to perform the action and GET to have no side effects.

Does the List-Unsubscribe header need to be signed with DKIM?

Yes. RFC 8058 requires a valid DKIM signature that covers both List-Unsubscribe and List-Unsubscribe-Post, so receivers can trust the URI was not injected.

Can I use only a mailto: List-Unsubscribe?

A mailto URI alone does not satisfy one-click unsubscribe. You need an HTTPS URI plus List-Unsubscribe-Post. Including a mailto URI as a second option is still useful for older clients.

Pidgely team

Published

We build Pidgely, the email API that gets you to the inbox. We write about what we learn delivering email, citing the standards and mailbox provider guidelines behind every claim.

Also available in: