Skip to content
correct.email
Deliverability

One-click unsubscribe (RFC 8058) for developers

How to implement List-Unsubscribe and List-Unsubscribe-Post per RFC 8058: the HTTPS POST endpoint, DKIM coverage, the mailto fallback and testing.

By Exmoor Software6 min read

When a marketing message carries the right headers, mailbox providers such as Gmail can show an Unsubscribe button next to the sender’s name. When the recipient uses it, the provider’s servers — not the recipient’s browser — send an HTTPS POST request to your server, and you remove the address from the list. No landing page, no login, no “are you sure?”.

The mechanism is defined in RFC 8058, on top of the older List-Unsubscribe header from RFC 2369. Gmail and Yahoo require it from bulk senders for marketing and subscribed messages (see the sender requirements checklist). This guide covers what you need to build.

Why a POST and not just a link

List-Unsubscribe could carry HTTPS links long before RFC 8058. The problem, as the RFC explains, is that anti-spam software often fetches URLs in headers automatically, and your server can’t tell an automated request from a person clicking. Senders protected themselves with confirmation pages, which an automated system won’t click and which make unsubscribing more than one click.

RFC 8058 adds a separate signal: a POST request with a fixed body. A scanner that fetches a URL sends a GET; a mailbox provider acting on the user’s request sends the POST. Your server can treat the POST as a definite unsubscribe.

The two headers

List-Unsubscribe: <https://example.com/unsubscribe/7f3c9a1e2b>, <mailto:unsubscribe@example.com?subject=unsubscribe-7f3c9a1e2b>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

The rules from RFC 8058, section 3.1:

  • The message contains one List-Unsubscribe and one List-Unsubscribe-Post header field.
  • List-Unsubscribe must contain one HTTPS URI. It may also contain non-HTTP URIs such as mailto:.
  • List-Unsubscribe-Post must contain exactly the pair List-Unsubscribe=One-Click.
  • The URI must carry enough information to identify the recipient and the list. The POST has no other arguments, and there is nobody to ask.
  • The URI should contain an opaque, hard-to-forge component, and your server should verify it. Otherwise anyone could unsubscribe your subscribers by posting to guessed URLs, or put your URLs into spam to provoke unsubscriptions.

RFC 2369 syntax applies: each URI in angle brackets, several URIs separated by commas, in order of preference from left to right.

For the token, use a random identifier stored with the subscription, or an HMAC over the subscriber and list IDs. Don’t put a plain email address or a sequential ID in the URL.

What the mailbox provider sends

The request goes to the HTTPS URI from the header, with the key/value pair as the body. This is the example from RFC 8058, section 8.1, with our token:

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

List-Unsubscribe=One-Click
  • The body should be sent as multipart/form-data and may be sent as application/x-www-form-urlencoded. Accept both.
  • The request must not include cookies, HTTP authentication or any other context. Your endpoint can’t rely on a session.
  • The receiver must not send the POST without the user’s consent; how it obtains that consent is up to the provider.

Building the endpoint

// POST https://example.com/unsubscribe/<token>
export async function handleOneClick(token: string): Promise<Response> {
  const sub = await findSubscriptionByToken(token); // only tokens you issued
  if (!sub) return new Response(null, { status: 404 });
  await suppress(sub.subscriberId, sub.listId);     // idempotent: already removed = success
  return new Response(null, { status: 200 });       // no redirect, no confirmation page
}

The rules behind those few lines:

  1. The POST itself unsubscribes. No confirmation step. RFC 8058 was written for cases where nobody visits your website, for example when a provider combines “report spam” with an unsubscribe.
  2. No redirects. The sender must not return an HTTPS redirect, because redirected POSTs have historically been unreliable. The URL in the header must be the final one: not a click-tracking link, not an http:// URL that redirects to https://, not a path your framework redirects to add a trailing slash.
  3. No CSRF token, no login. The request comes from the provider’s servers, without cookies. If your framework enforces CSRF protection on POST requests, exempt this route; the hard-to-forge token in the URL does that job.
  4. Make it idempotent. The same request can arrive more than once: a user clicks twice, a request is retried. Treat an address that is already unsubscribed as success. A repeated request must never produce an error or a resubscription.
  5. Unsubscribe from the list the URI identifies. Google notes that one-click removes the recipient only from the mailing list associated with the message, so separate lists can stay separate.
  6. Act quickly. Yahoo requires unsubscribes to be honored within 2 days; Google recommends 48 hours. Remove the address from queued and scheduled sends too, not only from the next import.
  7. Return a success status. RFC 8058 doesn’t define the response, and nobody reads it; 200 or 204 is enough.

The same URL can also serve people who open it in a browser: RFC 8058 notes that the POST target is the same as for a manual unsubscribe, so one handler can serve both. For a GET, show a page with an unsubscribe button (a form that POSTs to the same URL). Don’t unsubscribe on a GET alone — that is exactly what link scanners trigger.

DKIM must cover both headers

RFC 8058, section 4: the message needs at least one valid DKIM signature, and both List-Unsubscribe and List-Unsubscribe-Post must be listed in the h= tag of that signature. Without it, the receiver should not offer one-click unsubscribe for the message.

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=s2026;
        h=from:to:subject:date:message-id:list-unsubscribe:list-unsubscribe-post;
        bh=...; b=...

The usual failure is ordering: the headers are added after signing — by a relay, a filter on your mail server or a second service — so they are present but not covered. Add them in your application or in the email service’s API call, before the message is signed. For the Gmail and Yahoo requirements, the signature should also be aligned with your From domain. More about signing in how DKIM works.

The mailto fallback

RFC 8058 allows extra non-HTTP URIs in List-Unsubscribe, and mailto: is the original form from RFC 2369. Some mail clients still use it.

  • Gmail supports mailto: but doesn’t count it as one-click; you need the HTTPS URI.
  • Yahoo highly recommends the POST method and accepts mailto:.

If you include a mailto:, process those messages automatically and encode the same token, either in the subject as above or in the address (for example unsubscribe+7f3c9a1e2b@example.com, if your mail system supports plus addressing). An unsubscribe address that delivers into an unread inbox is worse than none.

Which messages need it

Marketing and subscribed messages. Google and Yahoo both exclude transactional messages such as password resets and order confirmations. Both also want a visible unsubscribe link in the body; that link may lead to a preferences page and doesn’t have to be one-click. For mail sent from your own code, see transactional email from your app.

Testing

  1. Check the headers. Send a campaign message to a mailbox you control, open the original (in Gmail: ⋮ → “Show original”) and paste the headers into the email header analyzer. The Unsubscribe panel lists the URIs and shows “one-click (RFC 8058)” when List-Unsubscribe-Post is correct. Under DKIM signatures, the Signed headers list must include list-unsubscribe and list-unsubscribe-post, and the authentication results must show dkim=pass.

  2. Simulate the provider’s request in both encodings:

    curl -i -X POST -H 'Content-Type: application/x-www-form-urlencoded' \
      --data 'List-Unsubscribe=One-Click' 'https://example.com/unsubscribe/7f3c9a1e2b'
    
    curl -i -F 'List-Unsubscribe=One-Click' 'https://example.com/unsubscribe/7f3c9a1e2b'
    

    Expect a 2xx status and no Location header. Send it again: still 2xx. A modified token should be rejected.

  3. Check the result. The address is unsubscribed from that list and excluded from the next send.

  4. Open the URL in a browser. You should see a page with a button, and nothing should change until you press it.

Don’t use the Gmail button as your test. Google shows it only for messages that pass its automated eligibility checks and meet its sender requirements, so a missing button doesn’t necessarily mean your headers are wrong.

Common mistakes

  • The header URL goes through a click-tracking redirect.
  • The endpoint answers POST with 405 because only GET was implemented.
  • CSRF middleware rejects the request, often with 403.
  • The headers are added after DKIM signing.
  • The URL contains the plain email address, so anyone can unsubscribe anyone.
  • Only a mailto: URI: some clients use it, but it doesn’t meet Gmail’s requirement.
  • Unsubscribes are applied only at the next weekly import.

References