Skip to main content
Preferences resolve at send time. Courier decides whether each recipient gets the message, and on which channels. Courier layers the topic’s default, any tenant defaults, and the user’s own choices. You write no routing logic. Map a template to a subscription topic and Courier enforces every layer.

How it works

Courier resolves a preference in two stages. First it works out the topic’s effective status for that user. Then it decides whether that status delivers or blocks the send, and on which channels.

Status precedence

The effective status starts from the topic’s default. Each layer below overrides the one above it, and only for fields that were explicitly set: Layer 5 is what gives one person separate settings in each organization they belong to. Someone who works with two of your customers can leave alerts on at one and off at the other, from the same account. A default is one of three states:
  • OPTED_IN (on): users receive the topic unless they opt out.
  • OPTED_OUT (off): users receive nothing until they opt in.
  • REQUIRED: users can’t opt out. Courier ignores any user record for the topic and always delivers. Use it for critical messages like security alerts.
Unsubscribe links still render for a REQUIRED topic, but opting out has no effect. Leave them off templates mapped to one.

Delivery and channel routing

Courier then decides delivery:
  • REQUIRED always delivers.
  • OPTED_OUT blocks the send for that topic. The message log shows the reason UNSUBSCRIBED.
  • An opt-in topic whose resolved status isn’t OPTED_IN blocks the send with the reason OPT_IN_REQUIRED.
  • OPTED_IN proceeds to channel routing.
For an opted-in topic, channels resolve the same way. A user’s per-topic channel choice (custom_routing) overrides the template’s routing when has_custom_routing is true and the topic has channel selection enabled in the editor. Without both, the template’s channel routing applies. Courier evaluates preferences to route a send, so a blocked send still counts toward usage. To skip a message before any routing runs, use instead of a preference.
Where custom routing over the API isn’t available, the single-topic write returns 402, the bulk PUT returns 400, and the bulk POST returns 200 with a per-item error. Only status is written.

FAQ

The topic likely resolves to no deliverable channel. An opted-in topic whose channels are all disabled or unsupported produces an unroutable message with the reason MISSING_PROVIDER_SUPPORT, not a delivery. That’s different from the preference block reason UNSUBSCRIBED, so the log tells you routing stopped it, not preferences. Add an available channel to the topic or template, or configure a provider for the channel you expect.
Preferences are keyed by user, and by tenant only when you pass tenant_id alongside the user_id. Without a user_id you get topic defaults, not a resolved preference. The read API returns the topic-level default even with a tenant_id. To see what a tenant sets, read the instead. Tenant defaults apply at send time, so confirm with a test send rather than a preferences read.
Opt-outs are scoped to the tenant context they were set in, so an opt-out under tenant B doesn’t affect sends under tenant C. Global and per-tenant preferences are stored separately and don’t propagate into one another. Honoring a global opt-out across every tenant is off by default. Contact support to enable it per tenant.
A topic whose default is REQUIRED always delivers, and the API rejects an attempt to opt a user out of it. Only a tenant or workspace default can change a topic’s required status, not a user.