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. To put the user on one of the topic’s digest schedules, add digest_schedule_id, as in . Call the :
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.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 don’t 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 .
Track preference changes
Courier records each time a user opts in, opts out, or changes a topic’s channels.Read the change history via API
returns entries newest first. Each is one change to one topic, andprevious holds the value it replaced. Pass since for changes from a date or time onward, and user_id for one user’s history. Add tenant_id with user_id to narrow to one tenant.
limit. While paging.more is true, pass paging.cursor back as cursor to get the next page. The endpoint allows one request every two seconds, so fetch pages one at a time.
Receive changes as webhook events
Courier also sends each change to your as apreferences:user:updated . Its data is the same entry the change history returns, with the same id, so you can match an event to its entry.
Mirror preferences in another system
To keep a copy of users’ choices in your CRM or database:- Load each user’s current preferences with .
- Apply each
preferences:user:updatedevent as it arrives. - After your endpoint has been down, read the change history with
sinceset to the lasttimestampyou applied. Apply the entries oldest first, and skip anyidyou’ve already applied.
deleted. Apply those resets to your copy when your code makes the calls.
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).