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. 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.
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 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 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. Each bulk call covers one user and up to 50 topics, so a backfill iterates your user base. 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 .

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, and previous 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.
A page holds 25 entries by default and up to 100 with 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 a preferences: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:
  1. Load each user’s current preferences with .
  2. Apply each preferences:user:updated event as it arrives.
  3. After your endpoint has been down, read the change history with since set to the last timestamp you applied. Apply the entries oldest first, and skip any id you’ve already applied.
Resetting a topic to its default removes the user’s override, so it doesn’t add an entry or send an event. That includes calls to and the topics a bulk replace lists in 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).