Skip to main content
The and cover the concepts behind this API.
The User Preferences API reads and writes one user’s topic opt-ins and per-topic channel choices, one topic at a time or the whole set at once. Call it from your backend with your workspace API key (the examples below), or from your frontend with a client SDK as the signed-in user. Every call needs a user_id, the same ID you send to, and single-topic calls also need a topic_id.

Prerequisites

Read a user’s preferences

Reading a user’s preferences returns every topic they have an override for, with their status, the topic’s default_status, and any custom_routing. Fetch them from the :

Update one topic

Updating one topic creates or changes a single preference. Wrap it in a topic object with a status of OPTED_IN or OPTED_OUT. REQUIRED is a topic default set in the editor, not a user choice, and the API rejects opting a user out of a required topic. To set the channels the user wants, add has_custom_routing: true and list them in custom_routing (email, sms, push, inbox, direct_message, webhook). To opt out entirely, send { "topic": { "status": "OPTED_OUT" } } with no routing. Call the :
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.
To reset a topic to its default, .

Client SDKs

From your frontend, a client SDK updates the signed-in user’s own preferences. It authenticates with a short-lived JWT rather than your API key, issued for that user with the read:preferences and write:preferences scopes. See for the token flow. It can only read and write that one user’s data. The calls this same client. Every client SDK exposes the same preferences methods: getUserPreferences() to list, getUserPreferenceTopic() to read one, and putUserPreferenceTopic() to opt a topic in or out and set its channels. On the web, React, Web Components, Vue, and Angular all call the shared @trycourier/courier-js client (React also through the useCourier() hook). The mobile SDKs expose an identical client.

Import a full preference set (bulk)

Two bulk endpoints set many topics for a user in one request, up to 50 topics each. They differ in what happens to the topics you do not send.
  • Replace makes the body the user’s complete override set. Topics you send are created or updated, and any existing override you leave out is reset to its default. Validation is atomic: one invalid topic fails the whole request.
  • Update is additive. It touches only the topics you send and processes each independently, returning successes in items and failures in errors.
Send the or bulk request:
Replace is the usual way to bring subscription data from another system into Courier. Send only the topics where a user differs from the default. The rest fall back automatically. Because it is idempotent, you can re-run the import safely, keep users in sync with a source of truth, or resume a backfill mid-loop. There is no across-users endpoint, so a backfill means iterating your user base with one bulk call per user (max 50 topics each). Throttle the loop as you go.

Scope to a tenant

If your app uses , scope any read or write to one Tenant with a tenant_id. On REST that is the ?tenant_id= query parameter. On the SDKs it is the tenant_id argument on any method above. A user’s per-tenant preferences are independent of their global preferences. The read endpoint returns the topic-level default_status, not a tenant’s default preferences, even with a tenant_id. To set what a tenant defaults to, see .

Verify

1

Update a topic and read it back

Update a topic to OPTED_OUT, then and confirm the status.
2

Confirm the send is blocked

Send a message mapped to that topic and confirm the log shows it blocked as UNSUBSCRIBED.
3

Re-run an import

For an import, re-run the same bulk replace and confirm the result is unchanged (it is idempotent).