← Blog·Sep 24, 2026productbroadcasts

Let your subscribers choose what they get

Topics turn "unsubscribe" into a preference change instead of a lost contact — and contact properties give your merge tags a default that sticks.

SivertSivertFounder / Coffee DrinkerBuilding eusend — email infrastructure where your email data never leaves the EU.

When broadcasts shipped a month ago, every list had just one lever for the people on it: stay or leave. Someone who liked your monthly newsletter but not your weekly product update had no way to say so except to unsubscribe from both.

Two features that shipped this week fix that. Topics let a contact pick which kinds of your mail they want. Contact properties let you declare the custom fields your contacts carry, and give each one a fallback value so a broadcast never renders a hole.

Why this is a deliverability feature

Topics look like a marketing nicety. We built them because of what people do when leaving feels too blunt.

If the only choice is "stop everything", a recipient who still wants some of your mail doesn't unsubscribe. They hit Report spam on the message they didn't want and keep the rest. A spam complaint counts against your domain, and on shared infrastructure it counts against everyone sending from the same IPs. An unsubscribe costs nobody anything.

So the goal is simple: give people a lever small enough that they reach for it instead of the spam button.

Topics

A topic is a kind of email — "Monthly newsletter", "Product updates", "Beta programme" — that a contact can join or leave on its own. Scope a broadcast to a topic and it reaches only the people who want that kind of mail:

const { data: topic } = await eusend.topics.create({
  name: 'Product updates',
  description: 'New features and changes, roughly once a month.',
  defaultSubscription: 'opt_in',
  visibility: 'public',
})

await eusend.broadcasts.create({
  name: 'October product update',
  audienceId,
  topicId: topic.id,
  from: 'hello@acme.com',
  subject: 'What shipped in October',
  html: '<p>Hi {{first_name}}…</p>',
})

The recipient count on the broadcast reflects the topic, so the number you see before you press Send is the number that goes out.

Topics belong to the workspace, not to one audience, so "Product updates" means the same thing wherever a contact sits.

Opt-in or opt-out, decided once

Every topic has a default that decides what happens to a contact who has never said anything:

  • opt_in — they get it until they leave. Use this for mail the list signed up for, like your newsletter.
  • opt_out — they don't get it until they ask. Use this for anything people should choose: a beta programme, a daily digest, a regional announcement.

You can't change the default after you create the topic. That's deliberate:

Flipping an opt_out topic to opt_in would start mailing every contact who never asked for it. Nothing stored anywhere says which of them would have agreed, because they were never asked. It's a consent decision, not a setting. If you get it wrong, create a new topic. That takes a minute. Sending mail people didn't agree to can't be undone.

It also means neither kind needs a backfill. A new opt_in topic reaches everyone at once, a new opt_out topic reaches nobody, and we only store the choices people actually make.

The unsubscribe link now does the right thing

This is where most of the value is.

A broadcast sent under a topic carries a topic-scoped unsubscribe. The footer link and the List-Unsubscribe header both retire that topic, not the whole list. So when someone uses the one-click unsubscribe button Gmail shows on your October product update, they stop getting product updates and nothing else. You keep the contact, and they keep the newsletter they actually read.

The hosted unsubscribe page changes too. Recipients now see a checkbox for each topic and a Save preferences button next to the usual unsubscribe-from-everything button. There's still no login and no JavaScript, and it's still branded as you.

A few rules we won't bend:

  • The global unsubscribe still wins. Someone who unsubscribed from your list gets nothing, whatever their topic boxes say. Topics narrow a send. They never widen it.
  • Topics only apply to broadcasts. POST /emails is untouched. A receipt or a password reset must never be filtered by a marketing preference.
  • Private topics stay private. A topic is private by default and only shows on the unsubscribe page to people already on it. An unfamiliar name like "Enterprise outreach" on someone's unsubscribe page tells them something you probably didn't mean to, and most people untick names they don't recognise. Mark a topic public when you're happy for anyone to find it and join.

If you don't create a topic, nothing changes. Existing broadcasts still go to the whole audience, and your recipients see exactly the unsubscribe page they saw before.

Contact properties

The second half is smaller, but you'll feel it in every broadcast you write.

Contacts have always carried custom fields — plan, company, renews_on — that render as {{variables}} in a broadcast. Until now those fields just appeared whenever you sent one. Now you can declare them, give them a type, and set a fallback value:

await eusend.contactProperties.create({
  key: 'plan',
  type: 'string',
  fallbackValue: 'free',
})

Every contact without a plan now reads as "free" in every broadcast. Before, each broadcast body had to remember to write {{plan|free}}, and the one that forgot rendered "You're on the plan."

Declaring a property is optional. A field you send on a contact without declaring it first still works exactly as it did, and it now shows up in the list on its own. Existing integrations need no changes.

A few things we left out on purpose:

  • number is a check, not formatting. A merge tag has nowhere to say which thousands separator or locale it wanted, so {{order_total}} renders exactly the characters you sent. What the type gives you is catching a mistyped value when you write it, not in an email someone already received.
  • You can't rename a property. A rename would have to rewrite every contact and every broadcast body that uses the old merge tag, and we won't edit your drafts. Delete and create a new one when you mean it.
  • Deleting a property removes it from every contact. Leaving the values behind sounds safer but isn't: the next contact write carrying that key would quietly register the property again, undoing a delete you watched succeed. So the dashboard tells you how many contacts use a property before you delete it. Removing a field nobody uses is a different decision from removing one on 1,240 people.

Everywhere you already work

Both features are in the dashboard under Audiences → Topics and Audiences → Properties, in the REST API at /topics and /contact-properties, and in all three SDKs:

topic = eusend.Topics.create({"name": "Product updates", "default_subscription": "opt_in"})
eusend.Topics.subscribe(topic["id"], contact_id, True)
topic, _ := client.Topics.Create(&eusend.CreateTopicRequest{
	Name:                "Product updates",
	DefaultSubscription: "opt_in",
})
client.Topics.Subscribe(topic.Id, contactId, true)

The MCP server has them too. When an assistant creates a broadcast, it's told to check list_topics first, because a product announcement sent to everyone is exactly what drives unsubscribes.

The full reference is in the docs for Topics and Contact properties.


If you run more than one kind of mail through a single list, create your first topic before your next broadcast. It's the cheapest deliverability improvement we can offer. And if the unsubscribe page or a default gets in your way, tell us at support@eusend.dev.