Topics
Categories of marketing mail a person can opt out of one at a time, and each person's preferences across them.
A topic is a kind of mail someone can opt out of on its own: a newsletter, product updates, tips. Send under a topic and the unsubscribe link in that message opts the person out of that topic only, so someone who wants fewer newsletters can say so without pressing the spam button or losing every other kind of mail. The Topics guide covers the unsubscribe page and how topics interact with suppressions.
A preference is stored on the combination of workspace, email address and topic, never on a contact row. It belongs to the person, not to whichever audience they are in, so someone on two lists has one preference per topic, and it survives the contact being deleted. When a person has no preference recorded for a topic, the topic's default_subscribed applies. Defaults are not written out as rows, so changing a topic's default moves everyone who never chose.
Topics are addressed by key, not by id.
The topic object
Every field is always present.
| Field | Type | Description |
|---|---|---|
id | string | The topic's id (a UUID). Not used by any endpoint. |
key | string | Lower-case letters, digits and hyphens. Unique within the workspace. The identifier used in paths, in topic on POST /v1/emails, in topic_key on campaigns and automations, and in unsubscribe links. |
name | string | Shown to the person on the preference page. |
description | string | null | Shown on the preference page under the name. null when not set, including on a topic created before descriptions were stored. |
default_subscribed | boolean | Whether someone with no recorded preference receives this topic. |
created_at | string (ISO 8601) | When the topic was created. |
List topics
GET /v1/topics · requires contacts:read
Returns every topic in the workspace, sorted by name, in the list envelope without paging. Up to 500 are returned; at 500, has_more is true.
Example
Response
200 OK
Create or update a topic
POST /v1/topics · requires contacts:write
Creates a topic, or updates the one with this key if it already exists, in one write. A new topic returns 201; an existing one returns 200 with the stored topic after the update.
On an update, name is replaced, and description and default_subscribed are changed only when the body includes them; a field left out keeps its stored value. Send description: null to clear a description. The defaults, default_subscribed: true and no description, apply only when the topic is created.
Default-on suits an existing newsletter people already expect. Default-off suits anything a person should have to ask for. Changing the default of an existing topic moves everyone who never chose, so only send default_subscribed on an update when that is what you mean.
Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
key | string | yes | Lower-case letters, digits and hyphens. | |
name | string | yes | 1 to 120 characters. | |
description | string | null | no | none on create, unchanged on update | Up to 300 characters. null clears it. |
default_subscribed | boolean | no | true on create, unchanged on update | Whether someone with no recorded preference receives this topic. |
Example
Response
201 Created for a new topic, 200 OK for an update.
Errors
| Status | Type | When |
|---|---|---|
| 422 | invalid_request | The body failed validation. details lists each field. |
Delete a topic
DELETE /v1/topics/{key} · requires contacts:write
Deletes the topic and every preference recorded against it.
A topic that something still sends under cannot be deleted: an active or paused automation with that topic_key, or a campaign with it that is draft, scheduled, sending, paused or testing. The call answers 409 invalid_state naming each one, and nothing is deleted. Change the automation's topic_key or delete it, and delete the campaign or let it finish, then delete the topic. Finished campaigns (sent, failed) and draft automations do not block it.
Deleting a topic erases every opt-out from it, and nothing can restore them. Mail that still names the key is not sent. Creating a send, campaign, automation or segment that names it is refused with 422 unknown_topic. A send already scheduled or held for approval under it skips every recipient, and a saved segment with that topic_key matches nobody. A draft automation with that topic_key answers enrolments with 409 (invalid_state, reason topic_missing) once it is activated. Recreating the key brings back the default for everyone, including the people who had opted out.
Path parameters
| Parameter | Description |
|---|---|
key | The topic's key. |
Example
Response
200 OK
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | No topic with this key in the workspace. |
| 409 | invalid_state | An active or paused automation, or a campaign that has not finished, still uses the topic. The message names each one (up to 20 of each) with its id and status. |
Get preferences
GET /v1/topics/preferences · requires contacts:read
Returns every topic in the workspace, sorted by name, each with subscribed resolved for one address: the person's recorded choice where there is one, the topic's default where there is not. Check this before concluding that someone's mail is failing to arrive; an opt-out looks the same as a delivery problem from the outside.
This reports topic preferences only. An address suppressed for marketing receives no marketing mail whatever it shows here; check suppressions as well.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
email | string | required | The address to look up. Matched case-insensitively. |
Example
Response
200 OK
Each entry is a topic object with subscribed added, a boolean, in the list envelope. One entry per topic, not paged: next_cursor is always null, and has_more is true only when the workspace has 500 topics or more, the most this returns.
Errors
| Status | Type | When |
|---|---|---|
| 422 | invalid_request | email is missing or empty. |
Set preferences
POST /v1/topics/preferences · requires contacts:write
Records one person's choice for one or more topics. Only do this when the person has asked: re-subscribing someone who opted out is what produces spam complaints.
Opting out (false) immediately ends the person's active enrolments in every automation sent under that topic, with reason unsubscribed_from_topic. Sends already scheduled under the topic are not cancelled, but each is checked again when it comes due and leaves out anyone opted out by then. Opting in (true) restarts nothing, and does not lift a suppression.
Every key is checked before anything is written. If any key names no topic, the request answers 422 unknown_topic and no preference is saved, so the call can be corrected and retried as a whole.
Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
email | string | yes | A single email address. Stored lower-cased. | |
topics | object | yes | Topic key to true (subscribed) or false (opted out), for example { "newsletter": false }. Topics not named are left as they are. |
Example
Response
200 OK
The person's preferences across every topic after the change, in the same list envelope as Get preferences.
Errors
| Status | Type | When |
|---|---|---|
| 422 | invalid_request | email is not an address, or topics is missing or has a value that is not a boolean. |
| 422 | unknown_topic | A key in topics names no topic in this workspace. The message starts with the field, for example topics.promos:. Nothing was saved. |
Import preferences
POST /v1/topics/{key}/import · requires contacts:write
Sets the same preference for many addresses on one topic. This is the migration path: a previous provider knows who opted out of which list, and that has to be in place before the first send under the topic. Existing preferences for those addresses are overwritten.
An import with subscribed: false immediately ends the active enrolments of every address it names in each automation sent under this topic, with reason unsubscribed_from_topic, as Set preferences does for one person. Sends already scheduled under the topic are checked again when they come due, and leave out the addresses opted out. An import with subscribed: true restarts nothing. It does not touch suppressions; import those separately.
The request body as a whole is limited to 5 MB. A larger body is answered with 413 payload_too_large, so split a very long list across several calls.
Path parameters
| Parameter | Description |
|---|---|
key | The topic's key. |
Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
emails | array of string | yes | 1 to 50,000 email addresses. Lower-cased, and duplicates written once. | |
subscribed | boolean | yes | The preference to record for every address: false for opted out, true for subscribed. |
Example
Response
201 Created
written is the number of distinct addresses whose preference was recorded.
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | No topic with this key in the workspace. Checked before the body. |
| 413 | payload_too_large | The request body is larger than 5 MB. |
| 422 | invalid_request | emails is missing, empty, longer than 50,000 or contains something that is not an address, or subscribed is missing. |