Skip to content

Unsubscribe done properly: RFC 8058 one-click and a preference centre

What Gmail and Yahoo actually require, why the one-click endpoint returns plain text with no form, and how to offer preferences without hiding the exit.

MarketingIntermediate10 min readUpdated
YOU'LL NEED

01The two headers, and what each one does

Two headers, added by the send path to every outgoing message. They are what turns “find the tiny grey link at the bottom” into a button in the mail client’s own interface, and that difference is the single largest lever you have on complaint rate.

TL;DR

List-Unsubscribe offers the routes; List-Unsubscribe-Post is the promise that makes the client show a native button. Gmail and Yahoo require the pair from bulk senders, and one without the other is decorative.

List-Unsubscribe: <https://…/u/<token>>, <mailto:[email protected]?subject=unsubscribe>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
HeaderValue your instance sendsWhat it does
List-Unsubscribe<https://…/u/<token>>, then <mailto:unsubscribe@<your sending domain>?subject=unsubscribe>Offers two routes out, an HTTPS one and a mail one, because they fail differently.
List-Unsubscribe-PostList-Unsubscribe=One-ClickPromises the URL above will accept a POST and act on it with no further interaction. Exactly that string, from RFC 8058.
The tokensigned tracking token: emailId + workspaceId, HMAC prefixSelf-validating, so the endpoint knows a request is genuine before it touches a database — and you cannot construct a link that unsubscribes someone else.

Scoped to one message, so it identifies not just who is leaving but what they were reading when they decided to.

Three routes out of a message
One-click POST
mailto: fallback
Link in the body
Carried by
List-Unsubscribe + List-Unsubscribe-Post
List-Unsubscribe
Your template
Who performs the action
The mail client, on its own network
The mail client, by sending a message
The reader, in a browser
Needs a human present
No
No
Yes
Works with no outbound HTTP
No
Yes
No
Earns a native control in Gmail and Yahoo
Yes
No
No

Both routes are there because they fail differently. The URL is what modern clients use and what the one-click flow posts to. The mailto is the fallback that has worked since the 1990s and is the one a corporate mail gateway with no outbound HTTP will use. Offering both costs nothing and removes a class of “the unsubscribe link does not work” report you would otherwise never reproduce.

02Why the POST endpoint returns plain text

The /u/ endpoint answers two different callers. A POST is a machine acting on a reader’s behalf, and gets plain text. A GET is a person who clicked a link, and gets a page. Everything odd-looking about the POST response follows from who is on the other end of it.

TL;DR

Nobody is looking at the POST response, so it contains nothing to look at: a 200, content-type: text/plain, and the word Unsubscribed.

READER
Presses unsubscribe
in their mail client
CLIENT
POST /u/<token>
no cookies, no session, no browser
ENDPOINT
Token verified
HMAC, before any database read
200
Unsubscribed.
text/plain
# what a receiver sees
POST /u/eyJlbWFpbElkIjoi…  HTTP/1.1

200 OK
content-type: text/plain

Unsubscribed.
What the POST path does not doWhy not
No formA form is a request for a second action from someone who is not there. The unsubscribe would never be recorded, the client would report success because it got a 200, and the reader would keep receiving mail they have already told two systems they do not want.
No redirectThere is nothing to redirect to and nobody to follow it. A 302 is at best ignored and at worst treated as a failure to honour the request.
No confirmation screen, no “are you sure”The reader already confirmed, in their client, before the POST was sent. Asking again would be asking the wrong party.
POST — the machine
  • 200 with content-type: text/plain
  • The body is the word Unsubscribed. and nothing else
  • Acts immediately; the reader confirmed in their client
GET — the person
  • Renders a small confirmation page
  • Warns that anything already queued can take a few minutes to stop
  • Also acts — no “click here to confirm” button, because clicking the link was the intent

An invalid or forged token gets a 400 and a plain sentence: This unsubscribe link is not valid. That is safe to be specific about, unlike the tracking pixel, because there is nothing to leak — a token either verifies against the signing secret or it does not, and saying so reveals nothing about which message ids exist.

03Offering preferences without hiding the exit

A preference centre is a genuinely good idea and a very common way to break the contract you just made. The rule that keeps it honest is short: unsubscribing must be one action on the page, and the header path has to keep working whatever the page does.

TL;DR

Build the preference page if you like — but the List-Unsubscribe URL must never route through it. Two surfaces, two callers, and only one of them has a person on the other end.

The case for preferences is real. Most people who unsubscribe are not rejecting you, they are rejecting the frequency or the category. Someone who wants the monthly product note but not three campaign sends a week will take that option if it is in front of them, and you keep a subscriber instead of losing one.

A preference centre
  • Offers frequency and category choices, and “none of it” among them
  • Is linked alongside the unsubscribe link, not instead of it
  • Leaves the header path untouched
  • Costs one action to leave from
A maze
  • Requires signing in before you can leave
  • Offers six frequency options and no “none”
  • Is the only route out of the message
  • Wants twelve categories unticked individually
  • Adds a confirmation step after the confirmation step

Every item in the right-hand column converts a person who would have unsubscribed into a person who marks you as spam. The receiver does not record that you offered them choices, only that a reader called your mail junk.

THE TEST

Open your own message, find the unsubscribe, and count the actions between the click and being off the list. One is correct. Two is defensible if the second is a single confirm button. Anything requiring a login, a category-by-category pass, or a reason you must supply is not a preference centre, it is a maze — and the complaint rate will say so before the legal team does.

04Transactional mail gets headers too

Every message gets the headers. Receipts, password resets, invoices, shipping notifications, the lot. This surprises people, and the reasoning is worth stating because the alternative sounds sensible right up until you look at what it requires.

TL;DR

A header that is sometimes present is worse than one that always is, because the flag deciding “sometimes” is set by hand and is wrong in the sender’s favour every time.

The argument for stripping them
  • This message is not marketing
  • The reader needs it
  • An exit invites them to break their own account
  • Every clause is a decision made on the reader’s behalf about what they are allowed to leave
Why it does not survive contact
  • The category boundary is not real, and the examples below are ordinary
  • Classification at send time ends up as a hand-set flag
  • Readers do not casually unsubscribe from mail they want
  • The only other tool they have is the spam button, which damages every message to everyone else on that domain
The messageWhat it actually is
A receipt with a “you might also like” blockA campaign, sent under a transactional label.
A shipping notification with a referral offerA campaign, sent under a transactional label.
A password reset from a dormant accountA message the recipient did not expect, whatever you call it.
An unsubscribe from your receiptsA real signal — usually that they have stopped using the product, occasionally that your receipts have been carrying marketing.

Adding the headers unconditionally removes the flag and the argument it causes.

A practical consequence: separate your sending domains. Marketing volume on one, transactional on another. It does not change the headers, but it does mean a campaign’s complaint rate cannot decide whether password resets arrive — and it lets you look at two unsubscribe rates that mean different things instead of one that means nothing. See why email goes to spam for the rest of that argument.

05What an unsubscribe writes down

An unsubscribe writes two things: a suppression keyed on the normalised address, and a flag on the contact. They are separate on purpose, and knowing which is which is what makes the import question answerable.

TL;DR

The suppression is the authority and the contact flag is the reporting view — which is why suppression works for addresses that were never contacts at all.

The suppression — the authority
  • Stored per workspace, keyed on the address after normalisation
  • Reason unsubscribe, with a source recording that it came from the one-click path
  • Written for every recipient of the message the token identified
  • Mirrored into a fast lookup the send path consults before it does anything else
  • Checkable without loading a contact — plenty of mail goes to addresses that are not contacts
The contact flag — the reporting view
  • Set only if the message was tied to a contact, with a timestamp
  • Removes them from broadcasts: a page query filters on unsubscribed = 0 as it walks
  • Is what makes subscribed mean something in a segment expression
  • Idempotent: unsubscribing twice does not create a second suppression, and a retried POST is harmless
AddressWhat happens
[email protected]Unsubscribes. The suppression is keyed on the normalised form, and the original is kept alongside it.
[email protected]Arrives on a later list and is still suppressed — the normalised key is the same one.

The original is retained because when someone asks why they are still not receiving mail, the address they typed is the one they will quote at you.

This is the rule that will feel wrong at some point, usually when a colleague has a spreadsheet from a conference and three hundred of the addresses on it are suppressed. The suppression list is the record of people who told you to stop, and a CSV is not consent — it is a list of addresses. If someone genuinely opted back in, re-subscribe them individually, from a signal you could point to a year later.

ONE MORE MESSAGE AFTER LEAVING IS NOT A BUG

The suppression is written the instant the request lands, but messages already accepted into the send queue are past that check. Someone who unsubscribes and then receives one more message a few minutes later has found the queue depth, not a defect. It is why the confirmation page says so in as many words, and it is worth having the same sentence in your own preference page if you build one.

What just happened

Every message your instance sends carries both headers, the one-click endpoint answers a machine in plain text and a human in HTML, and an unsubscribe writes a suppression that the send path checks before it checks anything else. The thing most likely to bite you later is an import: a suppression is cleared only by an explicit re-subscribe, never as a side effect of uploading a CSV, and if you ever find yourself writing code to “merge” a spreadsheet over the suppression list, stop — that is the exact mechanism by which senders reach blocklists.

Common questions

Read next

YOUR ACCOUNT, YOUR MAIL

Nothing to sign up for. Just deploy it.

Every guide on this site describes software you run yourself.