The and cover the concepts behind this API.
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 theirstatus, 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 atopic 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.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 theread: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
itemsand failures inerrors.
Scope to a tenant
If your app uses , scope any read or write to one Tenant with atenant_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).