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.
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
| Header | Value your instance sends | What 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-Post | List-Unsubscribe=One-Click | Promises the URL above will accept a POST and act on it with no further interaction. Exactly that string, from RFC 8058. |
| The token | signed tracking token: emailId + workspaceId, HMAC prefix | Self-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.
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.
Nobody is looking at the POST response, so it contains nothing to look at: a 200, content-type: text/plain, and the word Unsubscribed.
# what a receiver sees POST /u/eyJlbWFpbElkIjoi… HTTP/1.1 200 OK content-type: text/plain Unsubscribed.
| What the POST path does not do | Why not |
|---|---|
| No form | A 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 redirect | There 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. |
- 200 with content-type: text/plain
- The body is the word Unsubscribed. and nothing else
- Acts immediately; the reader confirmed in their client
- 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.
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.
- 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
- 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.
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.
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.
- 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
- 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 message | What it actually is |
|---|---|
| A receipt with a “you might also like” block | A campaign, sent under a transactional label. |
| A shipping notification with a referral offer | A campaign, sent under a transactional label. |
| A password reset from a dormant account | A message the recipient did not expect, whatever you call it. |
| An unsubscribe from your receipts | A 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.
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.
- 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
- 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
| Address | What 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.
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.