Subscription management
Mimeo hosts no public pages. Your unsubscribe page is yours — you build it, brand it, and host it, against these endpoints.
That's a deliberate choice. The page where someone decides whether to keep hearing from you is part of your product, not part of your vendor's. The trade is that you have to build it — and this page has a complete working example you can copy.
Tokens
Every one of these endpoints is authenticated by a signed token:
- Signed server-side, so it can't be forged or guessed.
- Encodes the person and the exact email that brought them there. That's exact-email attribution — when someone unsubscribes, you know which specific message prompted it, not just that they left.
- Never expires.
Non-expiring is intentional. Someone finding an old email in their archive two years from now must still be able to unsubscribe. An expired opt-out link is a complaint waiting to happen.
Where the token comes from
Every email you send resolves {{ unsubscribe_url }} in its
footer to:
https://your-instance.com/api/v1/subscription/<token>
The token is that URL's final path segment. Operators either link
{{ unsubscribe_url }} directly from the footer, or point the
footer at their own hosted page and carry the token to it — which is what
the example below assumes.
Endpoints
All of these are public — no bearer token, because they run
in your subscribers' browsers. CORS is open (*) so your page
can call them from any origin you host it on.
Get status
GET /api/v1/subscription/:token
{
"email": "ada@example.com",
"unsubscribed": false,
"unsubscribed_at": null,
"reason": null
}
Call this on page load. Showing people the address they're managing prevents the most common confusion — someone with several addresses not knowing which one they just opted out.
Unsubscribe
POST /api/v1/subscription/:token/unsubscribe
Content-Type: application/json
{ "reason": "too_frequent" }
reason is optional. The call:
- Records the unsubscribe locally with its reason and attribution — which email, which send.
- Stops future sends immediately.
- Pushes the opt-out to your provider where the provider supports it.
- Is idempotent — unsubscribing an already-unsubscribed person succeeds rather than erroring, so a double-click or a retry is harmless.
Returns the same status shape as the GET, reflecting the new state.
Resubscribe
POST /api/v1/subscription/:token/resubscribe
Reverses it. Worth offering on the confirmation screen — accidental unsubscribes are common, and the alternative is that they're gone.
One-click unsubscribe (RFC 8058)
POST /u/:token
This is the target mail clients POST to when someone uses the unsubscribe button Gmail and Apple Mail show next to the sender name. You never call it yourself. It always returns 200, because a mail client treating an error as "unsubscribe failed" is worse than any failure it could report.
Your provider injects the List-Unsubscribe headers that point
here — with Bento, that's
automatic.
v1 unsubscribe is global only. There's one on/off state per person, not per list or topic. Granular preferences arrive with a later release. Don't build a preference-center UI with per-list toggles yet — there's nothing behind them.
Build your unsubscribe page
A complete working page, in one file, with no dependencies. It fetches
status on load, shows the address, offers a reason, and confirms. Change
BASE to your instance and style it as your own.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Email preferences</title>
</head>
<body>
<main>
<p id="loading">Loading…</p>
<section id="form" hidden>
<h1>Unsubscribe</h1>
<p>You're subscribed as <strong id="email"></strong>.</p>
<label for="reason">Why are you leaving? (optional)</label>
<select id="reason">
<option value="">Prefer not to say</option>
<option value="too_frequent">Too many emails</option>
<option value="not_relevant">Not relevant to me</option>
<option value="never_signed_up">I never signed up</option>
</select>
<button id="unsubscribe" type="button">Unsubscribe me</button>
</section>
<section id="done" hidden>
<h1>You're unsubscribed</h1>
<p>We won't email you again. Changed your mind?</p>
<button id="resubscribe" type="button">Resubscribe</button>
</section>
</main>
<script>
// Your Mimeo instance.
const BASE = "https://your-instance.com/api/v1/subscription";
// However you route people here, the token has to come along.
const token = new URLSearchParams(location.search).get("token");
const el = (id) => document.getElementById(id);
async function call(path, body) {
const res = await fetch(BASE + "/" + token + path, {
method: body === undefined ? "GET" : "POST",
headers: { "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
if (!res.ok) throw new Error("Request failed: " + res.status);
return res.json();
}
function render(state) {
el("loading").hidden = true;
el("email").textContent = state.email;
el("form").hidden = state.unsubscribed;
el("done").hidden = !state.unsubscribed;
}
el("unsubscribe").addEventListener("click", async () => {
render(await call("/unsubscribe", { reason: el("reason").value }));
});
el("resubscribe").addEventListener("click", async () => {
render(await call("/resubscribe", {}));
});
call("")
.then(render)
.catch(() => {
el("loading").textContent = "That link isn't valid.";
});
</script>
</body>
</html>
Things worth keeping when you restyle it
- Show the email address. People manage several; they need to know which one this is.
- One button, no confirmation step. Making unsubscribing hard produces spam complaints, which cost far more than the subscriber did.
- Keep the reason optional. A required reason is a barrier; an optional one still gets answered often enough to be useful, and the answers show up on the person's record.
- Offer resubscribe on the confirmation. It costs one button and recovers accidental clicks.
- Handle the invalid-token case. Links get mangled by mail clients; say so plainly rather than showing a blank page.