Topics
Named categories of email your contacts can subscribe to and leave independently, so an unsubscribe becomes a preference change instead of losing the contact entirely.
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.
Without topics, a recipient who likes the monthly newsletter but not the weekly product blast has exactly one lever: leave. The lever they reach for when leaving feels too blunt is the spam button, and that costs every sender on the shared IPs — which is the real reason this feature exists.
Topics belong to the organization, not to one audience, so "Product updates" means the same thing wherever the contact sits.
Opt-in or opt-out
Every topic has a default that decides what a contact who has never expressed a preference receives:
| Default | A contact who has never chosen |
|---|---|
opt_in | receives it, until they explicitly unsubscribe |
opt_out | does not receive it, until they explicitly join |
Use opt_in for mail the list signed up for — your newsletter is the obvious case. Use
opt_out for anything a contact has to ask for: a beta programme, a high-frequency digest,
a regional announcement.
The default cannot be changed after creation. Flipping an opt_out topic to opt_in would
start mailing every contact who never asked for it, and nothing stored anywhere says which of them
would have agreed. It is a consent decision, so changing it means creating a new topic.
Because a new opt_in topic reaches everyone immediately and a new opt_out topic reaches
nobody, neither needs a backfill — only explicit choices are stored.
The global subscription still wins
A contact whose subscription is off receives nothing, whatever their topic preferences say. Topics narrow who gets a broadcast; they never widen it. Unsubscribing from every topic is also not the same as unsubscribing from the list — see Unsubscribes.
Visibility
private (the default) or public, controlling whether the topic is listed on the hosted
unsubscribe page to a contact who is not already opted in.
Keep a topic private when its name is an internal one. Advertising "Enterprise outreach" to
someone who is not on it tells them something you probably did not mean to, and the natural
response to an unfamiliar name on an unsubscribe page is to untick it. A public topic is
one you are happy for anyone to discover and join.
A private topic is always shown to a contact who explicitly opted in — otherwise they would have no way to leave it.
Sending to a topic
Pass topic_id when creating a broadcast:
curl -X POST https://api.eusend.dev/broadcasts \
-H "Authorization: Bearer eu_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "October product update",
"audience_id": "550e8400-e29b-41d4-a716-446655440000",
"topic_id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
"from": "hello@acme.com",
"subject": "What shipped in October",
"html": "<p>Hi {{first_name}}…</p>"
}'The recipient count shown on the broadcast reflects the topic, so it matches what is
actually sent. PATCH topic_id to null on a draft to widen it back to the whole audience.
A broadcast sent under a topic also carries a topic-scoped unsubscribe link: the footer
link and the List-Unsubscribe header both retire that topic rather than the whole list, so
one-click unsubscribe in Gmail becomes a preference change.
Topics scope broadcasts only. POST /emails is untouched — a receipt or a password reset must
never be filtered by a marketing preference.
/topics| Parameter | Type | Description |
|---|---|---|
namerequired | string | What the recipient sees on the unsubscribe page if the topic is visible to them. At most 50 characters, unique within the organization. |
default_subscriptionrequired | string | "opt_in" or "opt_out". Cannot be changed later. |
description | string | null | Shown under the name on the unsubscribe page. At most 200 characters. Worth writing — it is what tells a recipient whether to keep the box ticked. |
visibility | string | "private" (default) or "public". |
{
"id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
"name": "Product updates",
"description": "New features and changes.",
"defaultSubscription": "opt_in",
"visibility": "public",
"subscriberCount": 0,
"createdAt": "2026-09-22T10:14:52.110Z",
"updatedAt": "2026-09-22T10:14:52.110Z"
}Returns 409 CONFLICT if a topic with that name already exists, and 403 PLAN_LIMIT_EXCEEDED past 100 topics.
/topicsEvery topic in the organization, ordered by name. Unpaginated — the list is capped at 100 and both the composer's picker and the unsubscribe page want all of it.
subscriberCount is the number of contacts who have explicitly opted in. For an
opt_out topic that is the whole reachable audience; for an opt_in topic it is not, since
everyone who never chose is also subscribed.
/topics/:idOne topic by id.
/topics/:id| Parameter | Type | Description |
|---|---|---|
name | string | At most 50 characters. |
description | string | null | Send null to clear it. |
visibility | string | "public" or "private". |
default_subscription is rejected here — see the warning above.
/topics/:id{ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", "deleted": true }Every stored preference for the topic goes with it. Any draft or scheduled broadcast scoped to it widens back to its whole audience, so check before deleting a topic a scheduled send is pointing at. Broadcasts already sent keep their recipient records.
Setting a contact's preference
/topics/:id/contacts/:contactIdcurl -X PUT https://api.eusend.dev/topics/b6d24b8e/contacts/9f1c2d3e \
-H "Authorization: Bearer eu_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "subscribed": true }'/topics/:id/contacts/:contactIdClears the override so the contact falls back to the topic's default. This is different
from { "subscribed": false }: it forgets that they ever chose, so on an opt_in topic
they start receiving it again.
On the unsubscribe page
A recipient who clicks unsubscribe gets a checkbox per visible topic and a Save preferences button, alongside the usual unsubscribe-from-everything button. No JavaScript, no login — it is the same hosted page, branded the same way.
Organizations with no topics see exactly the page they saw before.
SDKs
const { data: topic } = await eusend.topics.create({
name: 'Product updates',
defaultSubscription: 'opt_in',
visibility: 'public',
})
await eusend.topics.subscribe(topic.id, contactId, true)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)