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:REQUIREDalways delivers.OPTED_OUTblocks the send for that topic. The message log shows the reasonUNSUBSCRIBED.- An opt-in topic whose resolved status is not
OPTED_INblocks the send with the reasonOPT_IN_REQUIRED. OPTED_INproceeds to channel routing.
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 indefault_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 toOPTED_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_routingis 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
REQUIREDtopic has no effect. Leave unsubscribe links off templates mapped to required topics.
FAQ
Why is my topic opted-in but nothing sends?
Why is my topic opted-in but nothing sends?
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.Do I need a user_id to read tenant preferences?
Do I need a user_id to read tenant preferences?
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.If a user unsubscribes under one tenant, are they unsubscribed everywhere?
If a user unsubscribes under one tenant, are they unsubscribed everywhere?
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.
Can a user opt out of a required topic?
Can a user opt out of a required topic?
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.