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 cannot opt out. Courier ignores any user record for the topic and always delivers. Use it for critical messages like security alerts.

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 is not 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, applied when has_custom_routing is true) overrides the template’s routing. Without custom routing, the template’s channel routing applies.
Where custom routing over the API is not 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.

Limits & behavior

The read API returns the topic default, not the tenant default

Reading a user’s preferences returns the workspace topic default in default_status, even when you pass a tenant_id. Courier applies tenant default preferences in the send pipeline, not on the read endpoint. So with a tenant_id reflects the topic-level default and the user’s own overrides. It never shows a tenant’s default_preferences as the default. To see what a tenant sets, read the . To see what actually sends, trust the resolution order above.

user_id is required for tenant-context lookups

A preferences read is keyed by the user, and by the tenant only when you supply one. The record key is {tenant_id}#{user_id} when tenant_id is present, and {user_id} otherwise. Omitting user_id returns the topic defaults, not a resolved preference, because there is no user to resolve against. Always pass user_id. Pass tenant_id for a tenant-scoped result.

Opt-outs are scoped per tenant

A user’s opt-out applies to the tenant context it was set in. If a user opts out under tenant B, a send for the same topic under tenant C is unaffected. The two contexts store separate records. Global preferences (set with no tenant) and per-tenant preferences are independent. Setting a global preference does not propagate into a tenant context. (Honoring a global opt-out across tenants is off by default and enabled per tenant by Courier, contact support if you need it.)

An opted-in topic with no deliverable channel is unroutable, not dropped

A topic can resolve to OPTED_IN and still select no valid channel or provider, for example a default-on topic whose only channel is disabled. The message is unroutable, not silently dropped. The log shows an undeliverable reason of MISSING_PROVIDER_SUPPORT, distinct from the preference block UNSUBSCRIBED. That tells you routing stopped the message, not a preference. Add a fallback channel or fix the provider config.

Usage still counts for opted-out sends

Courier evaluates the recipient’s preferences to route a send, so an opted-out send still counts toward usage even though it is blocked. To skip a message before any routing runs, use instead of a preference.

Troubleshooting

  • A tenant default is not taking effect. Confirm you send with the tenant context (tenant_id). Check that the user has no per-tenant override, which wins over a tenant default. The read API shows the topic default, not the tenant default, so verify with a test send rather than a preferences read.
  • Custom routing is ignored. Channel choices apply only when has_custom_routing is true on the user’s record and the topic has channel selection enabled in the editor. Without both, the template’s routing applies.
  • A required topic still shows an unsubscribe link. Unsubscribe links render for any topic, but opting out of a REQUIRED topic has no effect. Leave unsubscribe links off templates mapped to required topics.

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. 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. Tenant defaults apply at send time.
Opt-outs are scoped to the tenant context they were set in, so an opt-out under tenant B does not affect sends under tenant C. Global and per-tenant preferences are stored separately and do not propagate into one another.
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.