# 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.

Source: https://pidgely.com/blog/one-click-unsubscribe-rfc-8058/ · Published: 2026-10-12 · Pidgely

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.

## The link scanner problem

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.

