PlunkPlunk
Guides

Unsubscribe & preferences pages

Plunk's hosted pages for letting recipients unsubscribe, resubscribe, and manage their email preferences

Every email Plunk sends to a recipient who can unsubscribe (marketing or headless templates) carries a personalized link to a hosted page where they can manage their subscription. You don't need to build any of this — Plunk hosts the pages, handles the state changes, and tracks the events for you.

The three URL variables

Three template variables are auto-injected on every send and resolve to per-recipient signed URLs:

VariablePage it links toWhat the recipient can do
{{unsubscribeUrl}}One-click unsubscribe pageConfirm they want to stop receiving marketing emails
{{subscribeUrl}}Resubscribe pageOpt back in after previously unsubscribing
{{manageUrl}}Preferences pageView their current state and toggle subscription either way

Use them anywhere in a template's body or subject line. The placeholder is replaced with a unique URL when the email is rendered for that specific contact.

Default behaviour by template type

Where Plunk drops these links depends on the template type:

Template typeAuto footer with {{unsubscribeUrl}}?Notes
MarketingYes — Plunk appends a localized footerYou don't need to do anything. The footer respects the recipient's locale.
HeadlessNoYou must include {{unsubscribeUrl}} (or {{manageUrl}}) in your body or the recipient has no way to opt out.
TransactionalNoTransactional emails skip subscription checks; an unsubscribe link is not added (and shouldn't be needed).

If you build your own footer in a marketing template, you can still rely on the auto-injected one or override it by including your own link with {{unsubscribeUrl}}.

Inside transactional sends

/v1/send accepts the same template variables. Whether an unsubscribe footer is appended depends on the template you reference:

  • Sending with no template (just subject + body) → no auto footer.
  • Sending with a marketing template → footer auto-injected.
  • Sending with a headless or transactional template → no footer (you control the body).

You can always reference {{unsubscribeUrl}} / {{manageUrl}} in your inline body if you want to render your own link.

What happens when the recipient acts

When the recipient clicks one of the links and confirms:

ActionEffect
UnsubscribeContact's subscribed flips to false. contact.unsubscribed event fires.
SnoozeContact's subscribed flips to false and snoozedUntil is set. contact.unsubscribed event fires with reason: "snooze".
ResubscribeContact's subscribed flips to true. contact.subscribed event fires.
Update via preferencesSame as above, depending on which way they toggle.

You can drive workflows off contact.unsubscribed / contact.subscribed (e.g. send a "we're sorry to see you go" survey, or trigger a winback when they re-opt-in).

Snoozing

Not everyone who clicks unsubscribe wants to leave for good. Some are mid-launch-sequence, some are going on holiday, and some just want the volume to stop for a while. If the only option is permanent, a share of them take it — and a share of the rest reach for the spam button instead, which costs you far more.

So the unsubscribe and preferences pages offer a second, secondary option: snooze. The recipient picks a window, receives nothing for its duration, and is resubscribed automatically when it ends.

WindowValue passed to the API
2 weeks2_weeks
1 month1_month
6 months6_months
1 year1_year

Snooze is deliberately the quieter of the two choices. Unsubscribe keeps its position and weight on the page; a recipient who came to leave never has to hunt for the button that does it.

It is a real opt-out, not a softer one

A snoozed contact has subscribed: false for the whole window. Every send path — campaigns, workflows, and marketing templates sent through /v1/send — filters on that flag, so a snoozed contact receives nothing at all until they come back. Nothing is "held back and delivered later".

The contact also carries a snoozedUntil timestamp. That is the only thing distinguishing a snooze from a permanent unsubscribe, and it exists so the return can be scheduled — it plays no part in whether mail is sent.

Coming back

A background job resubscribes contacts whose window has elapsed, clears snoozedUntil, and fires contact.subscribed with reason: "snooze_expired". Returns are processed within a few minutes of the window ending.

A recipient can also come back early: the "resume" link on the preferences page, or any visit to {{subscribeUrl}}, ends the snooze immediately.

What ends a snooze early

Any other change to a contact's subscription cancels the snooze and takes over — the pending return is dropped, not queued behind it:

  • The recipient unsubscribes properly (their snooze becomes permanent — they will not be resubscribed later).
  • The recipient, or you, resubscribes them.
  • A hard bounce or spam complaint suppresses the address. A suppressed contact is never resurrected by a snooze that was running when it happened.
  • PATCH /contacts/:id with subscribed, a CSV import that sets the column, a bulk action, or a workflow Update contact step.

Reacting to a snooze

Snoozing reuses contact.unsubscribed and contact.subscribed rather than introducing new event names, so existing workflows, Wait for event steps and campaign statistics pick it up with no changes. Branch on the event's reason field to tell them apart:

// contact.unsubscribed — the recipient snoozed
{
  "reason": "snooze",
  "duration": "6_months",
  "snoozedUntil": "2027-03-11T09:14:22.104Z"
}

// contact.subscribed — the window ended
{
  "reason": "snooze_expired"
}

A "sorry to see you go" workflow triggered on contact.unsubscribed should check for reason and skip the ones that are snoozes — those people have not gone anywhere.

In campaign stats

A snooze counts toward the campaign's unsubscribe count, the same as a permanent opt-out. It is still a deliberate opt-out that the campaign prompted, and excluding it would let a campaign that burns through goodwill look clean simply because its recipients chose to pause rather than leave.

In the dashboard

Snoozed contacts show a distinct Snoozed badge in the contacts list, with their own option in the Status filter, so they are never counted as churn. A contact's detail page names the date they return, and the activity feed shows the snooze and the later resume as separate entries.

Knowing which email caused it

The links Plunk generates at send time carry an ?e=<emailId> parameter identifying the message they were sent in — on the List-Unsubscribe header, the auto-injected marketing footer, and the {{unsubscribeUrl}} / {{subscribeUrl}} / {{manageUrl}} variables. When the recipient acts, that email is recorded on the resulting event, and the activity feed names the campaign or workflow the opt-out came from.

The parameter is carried across the hosted pages, so a recipient who lands on the preferences page and then unsubscribes is still attributed to the email they started from.

A few cases are unattributed by design:

  • Links rendered before the email row exists — a campaign body's {{unsubscribeUrl}}, which is rendered once per recipient at queue time rather than at send time.
  • Changes with no email behind them: the dashboard toggle, a CSV import, and PATCH /contacts/:id.
  • Mail sent before this parameter existed.

Attribution is what lets a campaign report how many subscribers it cost: see campaign stats.

If you build your own opt-out surface, you can append ?e= yourself. The value is advisory — Plunk verifies the email belongs to the contact before recording it, and the contact id in the path is what governs the change, so a missing or wrong value costs attribution and nothing else.

Localization

The hosted pages and the auto-injected footer are localized into the contact's locale if set, or the project's default language otherwise. See Localization for the full list of supported languages.

Branding

The pages name you by your project name, inside the copy itself: a recipient sees "Stop receiving emails from Acme at jane@example.com?", not a logo or header. The same name already appears in the auto-injected marketing footer ("you agreed to receive emails from Acme").

Because recipients read it, use the name they know you by. A project called Test or acme-prod-eu is shown to them exactly as written. You can rename a project in its settings.

A small "Sent with Plunk" line below the page credits Plunk as the mail provider.

For full control over the look, send marketing as headless templates and build the unsubscribe surface into your own product UI. Link your in-product controls to the same {{unsubscribeUrl}} for one-click revocation.

API reference

If you need to drive subscription changes programmatically rather than through the hosted pages — for example because you've built your own preferences UI — use the contacts API:

  • PATCH /contacts/:id with { "subscribed": false } — unsubscribe a contact.
  • PATCH /contacts/:id with { "subscribed": true } — resubscribe.

Both flips automatically emit the corresponding contact.subscribed / contact.unsubscribed event, just like the hosted pages do. Either one also clears a running snooze.

Snoozing from your own preferences UI uses the same unauthenticated endpoint the hosted pages do:

POST /contacts/public/:id/snooze?e=<emailId>
Content-Type: application/json

{ "duration": "6_months" }

It responds with the contact's subscribed and snoozedUntil. As with the other public list-management routes, the contact id in the path is what governs the change and ?e= is advisory attribution only.