---
title: "Subscription topics"
description: "Let contacts opt out of one kind of mail without leaving your list."
url: https://www.unitpost.com/docs/topics
section: Docs
updated: 2026-08-31
---
# Subscription topics

## Subscription topics

> Let contacts opt out of one kind of mail without leaving your list.

A topic is a subscription category — "Product updates," "Promotions" — that a contact can opt out of. It's independent of the global unsubscribe: a contact can stay on your list but silence one kind of mail.

### Opt-in vs. opt-out

The default_opt_in field decides what happens for a contact who has never touched the topic:

- default_opt_in: true (the default) — an opt-out topic. Contacts are auto-enrolled unless they opt out.
- default_opt_in: false — an opt-in topic. Contacts are silent until they explicitly subscribe.

> **An explicit choice always wins:** Once a contact subscribes or unsubscribes from a topic, that explicit choice overrides the default in both directions. The default only governs contacts with no recorded preference.

### Managing topics

Create, list, update, and archive topics under /api/v1/email/topics. Deleting a topic archives it (soft): it disappears from pickers, but campaigns that used it keep their history.

**cURL**

```bash
# Create an opt-out newsletter topic
curl https://www.unitpost.com/api/v1/email/topics \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Product updates", "default_opt_in": true }'

# Opt a contact out of it (id or email in the path)
curl -X PUT https://www.unitpost.com/api/v1/contacts/customer@acme.com/topics \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "topic_id": "top_123", "subscribed": false }'
```

**Node.js**

```ts
import { Unitpost } from "unitpost";

const unitpost = new Unitpost(process.env.UNITPOST_API_KEY);

// Create a topic
const { data: topic } = await unitpost.email.topics.create({
  name: "Product updates",
  default_opt_in: true,
});

// Opt a contact out of it (id or email in the path)
await unitpost.email.topics.setTopic("customer@acme.com", {
  topic_id: topic.id,
  subscribed: false,
});
```

**Python**

```python
from unitpost import Unitpost

unitpost = Unitpost()  # reads UNITPOST_API_KEY

# Create a topic
topic = unitpost.email.topics.create({
    "name": "Product updates",
    "default_opt_in": True,
}).data

# Opt a contact out of it (id or email in the path)
unitpost.email.topics.set_topic("customer@acme.com", {
    "topic_id": topic["id"],
    "subscribed": False,
})
```

**Ruby**

```ruby
require "unitpost"

unitpost = Unitpost::Client.new  # reads UNITPOST_API_KEY

# Create a topic
topic = unitpost.email.topics.create({
  name: "Product updates",
  default_opt_in: true,
}).data

# Opt a contact out of it (id or email in the path)
unitpost.email.topics.set_topic("customer@acme.com", {
  topic_id: topic["id"],
  subscribed: false,
})
```

**PHP**

```php
require 'vendor/autoload.php';

use Unitpost\Client;

$unitpost = new Client(); // reads UNITPOST_API_KEY

// Create a topic
$topic = $unitpost->email->topics->create([
    "name" => "Product updates",
    "default_opt_in" => true,
])->data;

// Opt a contact out of it (id or email in the path)
$unitpost->email->topics->setTopic("customer@acme.com", [
    "topic_id" => $topic["id"],
    "subscribed" => false,
]);
```

**Laravel**

```php
use Unitpost\Laravel\Facades\Unitpost;

// Create a topic
$topic = Unitpost::email()->topics->create([
    "name" => "Product updates",
    "default_opt_in" => true,
])->data;

// Opt a contact out of it (id or email in the path)
Unitpost::email()->topics->setTopic("customer@acme.com", [
    "topic_id" => $topic["id"],
    "subscribed" => false,
]);
```

**Go**

```go
import (
    "context"
    "github.com/unitpostcom/unitpost-go"
)

client := unitpost.New() // reads UNITPOST_API_KEY

// Create a topic
topic, err := client.Email.Topics.Create(ctx, map[string]any{
	"name":           "Product updates",
	"default_opt_in": true,
})

// Opt a contact out of it (id or email in the path)
client.Email.Topics.SetTopic(ctx, "customer@acme.com", map[string]any{
	"topic_id":   topic["id"],
	"subscribed": false,
})
```

**Java**

```java
import com.unitpost.Unitpost;

var unitpost = new Unitpost(); // reads UNITPOST_API_KEY

// Create a topic
var topic = unitpost.email.topics.create(java.util.Map.of(
    "name", "Product updates",
    "default_opt_in", true
));

// Opt a contact out of it (id or email in the path)
unitpost.email.topics.setTopic("customer@acme.com", java.util.Map.of(
    "topic_id", String.valueOf(((java.util.Map<?, ?>) topic).get("id")),
    "subscribed", false
));
```

**Rust**

```rust
use unitpost::Unitpost;

let unitpost = Unitpost::new(); // reads UNITPOST_API_KEY

// Create a topic
let topic = unitpost.email().topics().create(serde_json::json!({
    "name": "Product updates",
    "default_opt_in": true
})).await;

// Opt a contact out of it (id or email in the path)
unitpost.email().topics().set_topic("customer@acme.com", serde_json::json!({
    "topic_id": topic["id"],
    "subscribed": false
})).await;
```

**.NET**

```csharp
using Unitpost;

var unitpost = new UnitpostClient(); // reads UNITPOST_API_KEY

// Create a topic
var topic = await unitpost.Email.Topics.Create(new
{
    name = "Product updates",
    default_opt_in = true,
});

// Opt a contact out of it (id or email in the path)
await unitpost.Email.Topics.SetTopic("customer@acme.com", new
{
    topic_id = topic.GetProperty("id").GetString(),
    subscribed = false,
});
```

### Per-contact preferences

GET /api/v1/contacts/{id}/topics returns a contact's effective subscription for every topic — folding in each topic's default so you get the real answer, not just rows they've touched. PUT the same path with { topic_id, subscribed } to set one. The path accepts a contact id or email; an unknown topic returns 404. This is the same thing a contact does by toggling a topic on the unsubscribe preference page.

### Scoping a campaign

Pass topic_id when you create a campaign to scope it. At send, contacts opted out of that topic are skipped (and counted as suppressed in the validate report) — on top of the usual segment-membership and global-unsubscribe checks. You can't point a campaign at an archived or missing topic: create and edit return a 409, and validate/send report a TOPIC_ARCHIVED or TOPIC_MISSING blocker.

> **One-click unsubscribe respects the topic:** Mail sent for a topic-scoped campaign carries a topic-aware unsubscribe link and RFC 8058 List-Unsubscribe-Post header, so a recipient's one-click opt-out silences just that topic rather than all of your marketing.

## Related

- [Sending domains](https://www.unitpost.com/docs/domains): Add SPF, DKIM, and DMARC so mail sends from your domain.
- [Open & click tracking](https://www.unitpost.com/docs/tracking): Control engagement tracking per send, template, or category.
- [Batch sending](https://www.unitpost.com/docs/batch): Send up to 100 distinct messages — schedule and cancel as a unit.
