Skip to main content
Build a preference center that lives inside your app instead of redirecting users to an external page. Courier provides two approaches: a pre-built PreferencesV4 component for quick setup, or low-level usePreferences hooks for full custom UI control. By the end of this tutorial, your users will be able to toggle notification topics on/off, choose delivery channels (Enterprise), and subscribe to digests directly in your app.

Prerequisites

If you haven’t created subscription topics and mapped templates yet, complete Steps 1-2 of the hosted preference center tutorial first. The topics and template mappings are the same regardless of how you surface preferences.

Step 1: Install Packages

Install the Courier React packages:
npm install @trycourier/react-provider @trycourier/react-preferences @trycourier/react-hooks

Step 2: Set Up the Courier Provider

Wrap your app (or the relevant section) with CourierProvider. This gives all child components access to Courier’s preference APIs.
import { CourierProvider } from "@trycourier/react-provider";

function App() {
  return (
    <CourierProvider
      clientKey="your_client_key_here"
      userId="current_user_id"
    >
      {/* Your app content */}
    </CourierProvider>
  );
}
Find your Client Key in the Courier dashboard under Settings > API Keys. The userId should match the user ID you use when sending notifications.
In production, you should also pass a userSignature (JWT) for authentication. See How to Send a JWT from Your Backend for details.

Option A: Pre-Built Component (Quick Setup)

The PreferencesV4 component renders a complete preference interface out of the box, including topic toggles, channel selection (Enterprise), and digest scheduling.

Basic Implementation

import React from "react";
import { CourierProvider } from "@trycourier/react-provider";
import { Header, PreferencesV4, Footer } from "@trycourier/react-preferences";

function PreferencesPage() {
  return (
    <CourierProvider
      clientKey="your_client_key_here"
      userId="current_user_id"
    >
      <Header />
      <PreferencesV4 />
      <Footer />
    </CourierProvider>
  );
}
The Header and Footer components are optional; they render brand-matched header/footer sections. You can omit them or replace them with your own.

PreferencesV4 Props

PropTypeDescription
tenantIdstring (optional)Show preferences for a specific tenant context
draftboolean (optional)Show draft preferences before publishing

Multi-Tenant Preferences

If your app uses tenants, pass the tenantId to show tenant-specific preference configuration:
<PreferencesV4 tenantId="tenant_abc" />

Option B: Custom Hooks (Full Control)

For a fully custom UI, use the usePreferences hook from @trycourier/react-hooks. This gives you raw preference data and update functions to build your own interface.

Fetching Preferences

import React, { useEffect } from "react";
import { usePreferences } from "@trycourier/react-hooks";

function CustomPreferences({ tenantId }) {
  const {
    recipientPreferences,
    preferencePage,
    fetchRecipientPreferences,
    fetchPreferencePage,
    isLoading,
  } = usePreferences();

  useEffect(() => {
    fetchRecipientPreferences(tenantId);
    fetchPreferencePage(tenantId);
  }, [tenantId]);

  if (isLoading || !preferencePage) {
    return <div>Loading preferences...</div>;
  }

  return (
    <div>
      <h2>Notification Preferences</h2>
      {preferencePage.sections?.map((section) => (
        <div key={section.sectionId}>
          <h3>{section.sectionName}</h3>
          {/* Render topics within this section */}
        </div>
      ))}
    </div>
  );
}

Hook Return Values

PropertyTypeDescription
recipientPreferencesArrayUser’s current preference status per topic
preferencePageObjectPreference page config (sections, topics, brand)
fetchRecipientPreferences(tenantId?) => voidFetch user’s preferences
fetchPreferencePage(tenantId?, draft?) => voidFetch page configuration
updateRecipientPreferences(payload) => voidUpdate a topic preference
isLoadingbooleanWhether data is still loading

Updating Preferences

Use updateRecipientPreferences to toggle topics or set custom routing:
import React, { useEffect } from "react";
import { usePreferences } from "@trycourier/react-hooks";

function PreferenceToggle({ tenantId }) {
  const {
    recipientPreferences,
    fetchRecipientPreferences,
    updateRecipientPreferences,
  } = usePreferences();

  useEffect(() => {
    fetchRecipientPreferences(tenantId);
  }, [tenantId]);

  const handleToggle = (templateId, currentStatus) => {
    updateRecipientPreferences({
      templateId,
      status: currentStatus === "OPTED_IN" ? "OPTED_OUT" : "OPTED_IN",
      hasCustomRouting: false,
      routingPreferences: [],
      digestSchedule: "",
      tenantId,
    });
  };

  return (
    <div>
      {recipientPreferences?.map((pref) => (
        <label key={pref.templateId} style={{ display: "block", margin: "8px 0" }}>
          <input
            type="checkbox"
            checked={pref.status === "OPTED_IN"}
            onChange={() => handleToggle(pref.templateId, pref.status)}
          />
          {pref.templateName || pref.templateId}
        </label>
      ))}
    </div>
  );
}

Update Payload

FieldTypeDescription
templateIdstringThe topic/template ID to update
statusstring"OPTED_IN" or "OPTED_OUT"
hasCustomRoutingbooleanWhether to use custom channel routing
routingPreferencesstring[]Array of channel types (e.g. ["email", "push"])
digestSchedulestringDigest frequency if applicable
tenantIdstring (optional)Tenant context for multi-tenant apps

Step 3: Test Your Implementation

1

Verify Topic Display

Confirm that all subscription topics from the Preferences Editor appear in your component.
If topics are missing or stale, make sure you’ve clicked Publish in the Preferences Editor. Changes are saved as a draft until you publish, so the component won’t reflect updates until then.
2

Test Opt-In/Out

Toggle a topic off, then send a test notification for a template linked to that topic. The notification should be suppressed for the opted-out user.
3

Test Channel Selection (Enterprise)

If channel selection is enabled, verify that users can choose delivery channels and that their choices appear in the API when you query preferences:
curl -X GET https://api.courier.com/users/{user_id}/preferences/{topic_id} \
  -H "Authorization: Bearer $COURIER_AUTH_TOKEN"

Local Development and CORS

The Courier React SDK sends requests to https://api.courier.com/client/q by default. During local development, browsers block these cross-origin requests. Use a dev server proxy to work around this.

Vite Proxy Setup

Add a proxy to your vite.config.js that forwards requests to Courier’s API:
// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      "/api": {
        target: "https://api.courier.com",
        changeOrigin: true,
        secure: true,
        rewrite: (path) => path.replace(/^\/api/, ""),
        headers: {
          origin: "https://app.courier.com",
        },
      },
    },
  },
});
Then set apiUrl on the provider to route through the proxy:
<CourierProvider
  clientKey="your_client_key_here"
  userId="current_user_id"
  apiUrl="/api/client/q"
>
apiUrl replaces the entire GraphQL endpoint URL; the SDK does not append /client/q for you. Always include the full path (e.g. /api/client/q, not just /api).
Remove the apiUrl prop before deploying to production. In production, the SDK uses https://api.courier.com/client/q by default and CORS is handled by Courier’s allowlisted domains.

Styling

The PreferencesV4 component uses CSS custom properties that you can override:
:root {
  --ci-title-color: #1a1a1a;
  --ci-text-color: #4a4a4a;
}
For deeper customization, use the hooks approach (Option B) and apply your own styles entirely.

What’s Next