> ## Documentation Index
> Fetch the complete documentation index at: https://docs.suprsend.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Designing Notification Preferences

> Practical guide to designing notification preference centres in SuprSend - channel opt-ins, category toggles, digest schedules, condition properties, and multi-tenant patterns.

Notification preferences give users control over what they hear from your app, on which channel, and how often. Get them right and users stay engaged - they can mute what's noisy without losing what matters. Skip them and users will unsubscribe from everything.

This guide covers the preference patterns that come up most in real products - from simple channel opt-in/opt-outs to multi-tenant setups - and ends with a worked example from a real project-management app. Each pattern explains what it is, when to use it, how to configure it in SuprSend, and the code to implement it in your app.

One rule runs through every pattern: a preference value resolves as **user choice → tenant default → category default**. The most specific setting wins. See [Preference Evaluation](/docs/preference-evaluation) for how and when SuprSend applies this.

<Note>
  **New to preferences in SuprSend?** Read the [User Preferences overview](/docs/user-preferences) first - it covers how categories, channels, and the preference API response are structured. For multi-tenant setups, see [Tenant Preferences](/docs/tenant-preference).

  Client-side code examples use the [`@suprsend/react`](/docs/preference-react-sdk) SDK. The same `user.preferences.*` methods exist in the [JavaScript web SDK](/docs/js-preferences), with equivalents for [Angular](/docs/preferences-angular), [iOS](/docs/ios-preferences), and [Expo](/docs/expo-preferences). Each pattern also shows the matching [REST API](/reference/get-user-full-preference) call - use it from your backend (it needs your workspace secret) or from platforms without an SDK. Digest schedules and condition properties (Sections 3-6) are REST-only today. If you want a ready-made preference UI without building from scratch, see the [Embedded Preference Centre](/docs/embedded-preference-centre).
</Note>

***

## 1. Channel opt-in / opt-out

Users control what they receive on each channel. For every channel your app uses (Email, SMS, WhatsApp, Push, etc.), the user picks one of two options:

* **All** - receive all notifications on this channel, except categories they've explicitly turned off
* **Required** - receive only notifications from categories marked as mandatory (Can't Unsubscribe)

Separately, users can also turn individual channels on or off per preference category - for example, keeping Email on for "Product Updates" but turning off WhatsApp for the same category. These are the per-category channel checkboxes shown in the **CHANNELS BY TOPIC** section of the screenshot.

The `channel_preferences` array in the API response drives the global All / Required control. Each item has a `channel` name and an `is_restricted` boolean: `false` means All, `true` means Required. The per-category channel preferences live under `sections[].subcategories[].channels`.

<Frame caption="Example app UI - global channel controls (mockup, not the SuprSend dashboard)">
  <img src="https://mintcdn.com/suprsend/esjzrvFHZzoyaiTT/images/channel.png?fit=max&auto=format&n=esjzrvFHZzoyaiTT&q=85&s=1ee62f7fa935673f0c55545199e2d36c" alt="Channel-level preferences" width="450" data-path="images/channel.png" />
</Frame>

**Configure in SuprSend**

1. Go to **Developers → [Preference categories](/docs/managing-notification-categories)** and select a root-category (System, Transactional, or Promotional)
2. Optionally, click **`+Section`** to create a section (for example "Product", "Billing"). Sections are used to group sub-categories on the preference page. Section doesn't have it's own preference.
3. Click **`+Sub-Category`**, fill in the name, select the section (if set), and set a default preference (**On**, **Off**, or **Can't Unsubscribe**)
4. Click **`Publish Changes`**

**Build in your app**

Call [`getPreferences()`](/docs/js-preferences#get-preferences-data) in your parent component on page load and pass the result down as `preferenceData`. The response gives you two independent parts:

* **`channel_preferences`** - render one row per channel with **All** / **Required** radio buttons. Read `is_restricted` to know which is currently selected: `false` = All, `true` = Required. Call [`updateOverallChannelPreference`](/docs/js-preferences#update-overall-channel-preference) with `ChannelLevelPreferenceOptions.ALL` or `ChannelLevelPreferenceOptions.REQUIRED` when the user changes their selection.
* **`sections[].subcategories[].channels`** - per-category channel checkboxes that let users turn individual channels on or off within a specific notification type. This is covered in detail in Section 2.

<CodeGroup>
  ```javascript React theme={"system"}
  import {
    ChannelLevelPreferenceOptions,
    useSuprSendClient,
  } from "@suprsend/react";

  function ChannelPreferences({ preferenceData, setPreferenceData }) {
    const suprSendClient = useSuprSendClient();

    // Global All vs Required control per channel
    // is_restricted: false = All, is_restricted: true = Required
    const handleOverallChannelChange = async (channel, status) => {
      const resp = await suprSendClient.user.preferences.updateOverallChannelPreference(
        channel.channel,
        status // ChannelLevelPreferenceOptions.ALL or ChannelLevelPreferenceOptions.REQUIRED
      );
      if (resp.status !== "error") setPreferenceData({ ...resp.body });
    };

    return (
      <>
        {/* Global channel controls - one row per channel */}
        {preferenceData?.channel_preferences?.map((channel, i) => (
          <div key={i}>
            <p>{channel.channel}</p>
            <p>{channel.is_restricted ? "Allow required notifications only" : "Allow all notifications"}</p>
            <label>
              <input
                type="radio"
                checked={!channel.is_restricted}
                onChange={() => handleOverallChannelChange(channel, ChannelLevelPreferenceOptions.ALL)}
              />
              All
            </label>
            <label>
              <input
                type="radio"
                checked={channel.is_restricted}
                onChange={() => handleOverallChannelChange(channel, ChannelLevelPreferenceOptions.REQUIRED)}
              />
              Required
            </label>
          </div>
        ))}
      </>
    );
  }
  ```

  ```bash cURL (backend) theme={"system"}
  # Same update server-side: is_restricted false = All, true = Required
  curl -X PATCH "https://hub.suprsend.com/v1/user/lisa.wang@acme.com/preference/channel_preference/" \
    -H "Authorization: Bearer {workspace_key}:{workspace_secret}" \
    -H "Content-Type: application/json" \
    -d '{
      "channel_preferences": [
        { "channel": "email", "is_restricted": false },
        { "channel": "iospush", "is_restricted": true }
      ]
    }'
  ```
</CodeGroup>

<Tip>
  **When to use:** Your app sends on multiple channels and users want coarse control over each one. Examples: "Email me everything, but push only for required notifications", "I only want SMS for critical alerts." Common in consumer apps, marketplaces, and collaboration tools.
</Tip>

***

## 2. Preference category toggles

Users turn each notification type on or off individually - for example, keeping "Invoices & receipts" on while turning off "Sales Newsletter" - and optionally choose which channels receive each type.

This is the standard notification settings page that most apps need. The response gives you sections (top-level groupings like Product, Billing, Sales) containing subcategories (the individual notification types). Each subcategory has a toggle and a list of channel checkboxes.

<Frame caption="Example app UI - category toggles with per-category channels (mockup)">
  <img src="https://mintcdn.com/suprsend/esjzrvFHZzoyaiTT/images/category.png?fit=max&auto=format&n=esjzrvFHZzoyaiTT&q=85&s=e9160b99bc580ce6f33bc420b32f55ad" alt="Category-level preferences" width="450" data-path="images/category.png" />
</Frame>

**Configure in SuprSend**

1. Go to **Developers → [Preference categories](/docs/managing-notification-categories)** and select a root-category
2. Click **`+Section`** to create sections (for example "Product", "Billing", "Sales"). Sections are used to group sub-categories on the preference page. Section doesn't have it's own preference.
3. Click **`+Sub-Category`** for each notification type, select its section (if set), and set a default preference (**On**, **Off**, or **Can't Unsubscribe**). You can also choose which channels should be opt\_in by default in case of `On` preference.
4. Click **`Publish Changes`**

**Build in your app**

Call [`getPreferences()`](/docs/js-preferences#get-preferences-data) in your parent component and pass `preferenceData` as a prop. Render sections as headings and subcategories as rows, each with a toggle and channel checkboxes below it.

Three things to handle carefully:

* **`is_editable: false` on a subcategory** means the category is set to **Can't Unsubscribe** in SuprSend. Disable the toggle - users cannot opt out.
* **`is_editable: false` on a channel inside a subcategory** means that specific channel is mandatory for that category and cannot be turned off. Disable the channel checkbox independently of the subcategory toggle. A category can have `is_editable: true` (user can toggle it off) while individual channels within it have `is_editable: false` (those channels cannot be removed).
* **`channels` may be empty or absent** (for example when `show_opt_out_channels` is `false` for opted-out categories). Use optional chaining (`subcategory?.channels?.map(...)`) to handle this safely.

<Note>
  All preference updates ([`updateCategoryPreference`](/docs/js-preferences#update-category-preference), [`updateChannelPreferenceInCategory`](/docs/js-preferences#update-channel-preference-in-category), [`updateOverallChannelPreference`](/docs/js-preferences#update-overall-channel-preference)) are **optimistic** - the UI updates immediately and the actual API call happens in the background with a 1-second debounce. Set up these emitter listeners once when loading preferences to handle the confirmed result and catch server-side errors:

  ```javascript React theme={"system"}
  // Fires after the background API call succeeds - sync your UI state with confirmed data
  suprSendClient.emitter.on("preferences_updated", (preferenceDataResp) => {
    setPreferenceData({ ...preferenceDataResp.body });
  });

  // Fires if the background API call fails - show an error toast or revert the UI
  suprSendClient.emitter.on("preferences_error", (errorResp) => {
    console.log("Error:", errorResp?.error?.message);
  });
  ```
</Note>

<CodeGroup>
  ```javascript React theme={"system"}
  import { PreferenceOptions, useSuprSendClient } from "@suprsend/react";

  function CategoryPreferences({ preferenceData, setPreferenceData }) {
    const suprSendClient = useSuprSendClient();

    // Toggle a category on or off
    // value: true = opt_in, false = opt_out
    const handleCategoryChange = async (subcategory, value) => {
      const resp = await suprSendClient.user.preferences.updateCategoryPreference(
        subcategory.category,
        value ? PreferenceOptions.OPT_IN : PreferenceOptions.OPT_OUT
      );
      if (resp.status !== "error") setPreferenceData({ ...resp.body });
    };

    // Toggle a specific channel on or off within a category
    // Skip if is_editable is false - that channel is mandatory for this category
    const handleChannelInCategoryChange = async (channel, subcategory) => {
      if (!channel.is_editable) return;
      const resp = await suprSendClient.user.preferences.updateChannelPreferenceInCategory(
        channel.channel,
        channel.preference === PreferenceOptions.OPT_IN
          ? PreferenceOptions.OPT_OUT
          : PreferenceOptions.OPT_IN,
        subcategory.category
      );
      if (resp.status !== "error") setPreferenceData({ ...resp.body });
    };

    return preferenceData?.sections?.map((section, index) => (
      <div key={index}>
        {/* Render section heading only if it has a name */}
        {section?.name && <h3>{section.name}</h3>}
        {section?.subcategories?.map((subcategory, i) => (
          <div key={i}>
            <div>
              <span>{subcategory.name}</span>
              <p>{subcategory.description}</p>
            </div>
            {/* Disable toggle if is_editable is false - this category is Can't Unsubscribe */}
            {/* Replace with your own toggle/switch component */}
            <button
              disabled={!subcategory.is_editable}
              onClick={() =>
                handleCategoryChange(
                  subcategory,
                  subcategory.preference !== PreferenceOptions.OPT_IN
                )
              }
            >
              {subcategory.preference === PreferenceOptions.OPT_IN ? "On" : "Off"}
            </button>
            {/* Channel checkboxes - channels may be empty or absent, handle with ?. */}
            <div>
              {subcategory?.channels?.map((channel, j) => (
                <button
                  key={j}
                  disabled={!channel.is_editable}
                  onClick={() => handleChannelInCategoryChange(channel, subcategory)}
                >
                  {channel.channel}
                </button>
              ))}
            </div>
          </div>
        ))}
      </div>
    ));
  }
  ```

  ```bash cURL (backend) theme={"system"}
  # Turn a category on/off and opt out of specific channels within it
  curl -X PATCH "https://hub.suprsend.com/v1/user/lisa.wang@acme.com/preference/category/product-updates/" \
    -H "Authorization: Bearer {workspace_key}:{workspace_secret}" \
    -H "Content-Type: application/json" \
    -d '{ "preference": "opt_in", "opt_out_channels": ["sms"] }'
  ```
</CodeGroup>

<Tip>
  **When to use:** The default for most apps - the standard notification settings page. Examples: e-commerce (order updates on, promotions off), SaaS (product updates via email, billing via email + SMS), social apps (comments on, likes off).
</Tip>

***

## 3. Digest schedules and condition properties

The first two patterns only need a category and its channels. The next three use two more settings that live on a sub-category, next to its default preference:

| Setting | What the user controls | Where the workflow reads it |
| - | - | - |
| **[Digest schedule](/docs/managing-notification-categories#configuring-digest-schedule)** | How often batched notifications arrive - Instant, Daily at 9 AM, Weekly on Monday | A [Digest node](/docs/digest) with schedule type **Preference Category** |
| **[Condition properties](/docs/managing-notification-categories#configuring-condition-properties)** | Values that decide *when* a notification fires - an alert threshold, a minimum severity, which roles are notified | Trigger or [Branch](/docs/branch) conditions, as `$category.properties.<key>` |

Both are configured under **Developers → [Preference categories](/docs/managing-notification-categories)** on the sub-category and go live when you click **`Publish Changes`**.

<Frame caption="Sub-categories in the SuprSend dashboard - the Conditions column shows condition properties configured on each">
  <img src="https://mintcdn.com/suprsend/_z1ttZCJ4f7Wr1-1/images/b2b-category-settings.png?fit=max&auto=format&n=_z1ttZCJ4f7Wr1-1&q=85&s=5294abf5c360af91049456cbf3e20600" alt="Preference categories in the SuprSend dashboard with a condition property on the Project Assigned sub-category" width="2940" height="1670" data-path="images/b2b-category-settings.png" />
</Frame>

Three things make them behave like preferences, not workflow settings:

* **They resolve through the same three layers.** At trigger time, SuprSend uses the recipient's own value if set, else the tenant default, else the category default. [Tenant admins can override](/docs/tenant-preference#overriding-digest-schedule-and-condition-defaults) the defaults for their users.
* **They travel in the preference API.** Each sub-category in the [get preference response](/reference/get-user-full-preference) carries `digest_schedule_options`, `digest_schedule`, and a `properties` array. Users change them through the [update category preference API](/reference/update-user-category-preference) - only fields marked `editable` accept overrides; `locked` fields stay fixed.
* **You control the input type.** A condition property is typed: `integer`, `string`, `string_choice` (single-select), `list_choice` (multi-select), or dynamic list variants - up to 25 per sub-category.

<Tip>
  Use **Preview** on the Preference categories page before publishing - it renders the full preference experience, including digest selectors and condition inputs, exactly as users will see them.
</Tip>

***

## 4. Digest frequency

Instead of one notification per event, users choose how often they want a summary - Instant, Daily, or Weekly. A user who picks Daily gets one email at 09:00 summarising everything that happened that day instead of a notification for every single event.

This is the digest schedule from Section 3: you define the cadence options on the sub-category, the user picks one on the preference page, and the workflow's [Digest node](/docs/digest) batches each recipient's notifications on whatever schedule they resolve to - in the recipient's timezone.

<Frame caption="Example app UI - digest frequency selector (mockup)">
  <img src="https://mintcdn.com/suprsend/esjzrvFHZzoyaiTT/images/t1.png?fit=max&auto=format&n=esjzrvFHZzoyaiTT&q=85&s=e457e11cb623596a83943147487e6a04" alt="Digest preferences" width="450" data-path="images/t1.png" />
</Frame>

**Configure in SuprSend**

<Steps>
  <Step title="Enable a digest schedule on the sub-category">
    Go to **Developers → [Preference categories](/docs/managing-notification-categories)**, select the sub-category (for example "Activity Summary") and enable **Digest schedule**. For each option, set:

    * **Label** - what the user sees ("Daily").
    * **Frequency** and **interval** - `instantly`, `hourly`, `daily`, `weekly`, or `monthly`; interval multiplies it (interval 2 + weekly = every 2 weeks).
    * **Time / weekdays / month days** - when the batch fires. Each field has an edit policy: **editable** (the user can change it) or **locked**.

    Mark exactly one option as the **default**, then click **`Publish Changes`**.

    <Frame caption="Digest schedule settings on a sub-category (SuprSend dashboard)">
      <img src="https://mintcdn.com/suprsend/LL6D4dXCaotlymSe/images/category-digest-setting.png?fit=max&auto=format&n=LL6D4dXCaotlymSe&q=85&s=ae1c4ff95b231e266b65f908ac130b65" alt="Digest schedule options configured on a sub-category" width="1724" height="1572" data-path="images/category-digest-setting.png" />
    </Frame>
  </Step>

  <Step title="Point the workflow's Digest node at the category">
    In the workflow linked to this category, add a **[Digest node](/docs/digest)** and set its schedule type to **Preference Category** (`category_digest_schedule` in the [management API](/reference/create-update-workflow)). SuprSend batches notifications on the option each recipient selected - or the default, if they haven't picked one.
  </Step>
</Steps>

<Note>
  If the digest schedule isn't enabled on the category, a Digest node set to **Preference Category** sends notifications immediately, without batching.
</Note>

**Build in your app**

The [preference response](/reference/get-user-full-preference) gives you everything to render: `digest_schedule_options` lists the options, `digest_schedule` is the recipient's resolved choice. Show the options as radio buttons, add a time picker where `time.edit_policy` is `"editable"`, and save with the [update category preference API](/reference/update-user-category-preference). This API uses your workspace secret, so call it from your backend.

<CodeGroup>
  ```json GET preference response (excerpt) theme={"system"}
  {
    "digest_schedule_options": {
      "options": [
        {
          "id": "instant",
          "label": "Instant",
          "frequency": "instantly",
          "is_default": true,
          "is_user_selected": false
        },
        {
          "id": "daily-summary",
          "label": "Daily at 9:00 AM",
          "frequency": "daily",
          "interval": 1,
          "time": {
            "edit_policy": "editable",
            "default_value": "09:00",
            "value": ""
          },
          "is_default": false,
          "is_user_selected": false
        }
      ]
    },
    "digest_schedule": {
      "id": "instant",
      "label": "Instant",
      "frequency": "instantly",
      "is_default": true,
      "is_user_selected": false
    }
  }
  ```

  ```bash Save the user's choice theme={"system"}
  curl -X PATCH "https://hub.suprsend.com/v1/user/lisa.wang@acme.com/preference/category/activity-summary/" \
    -H "Authorization: Bearer {workspace_key}:{workspace_secret}" \
    -H "Content-Type: application/json" \
    -d '{ "digest_schedule": { "id": "daily-summary", "time": "10:00" } }'
  ```
</CodeGroup>

Only pass `id` and the fields whose edit policy is `editable`. Pass `"digest_schedule": null` to clear the user's choice and fall back to the tenant or category default.

<Tip>
  **When to use:** Users get too many individual notifications and want a summary instead. Examples: daily activity digest ("Here's what happened in your workspace today"), weekly analytics reports, aggregated comment or mention notifications.
</Tip>

***

## 5. User-defined alert threshold

Some notifications are only useful past a user-defined threshold. A storage alert at 1% full is noise; at 80% it's urgent. Users set their own trigger point, and the workflow only fires when the incoming metric crosses it.

The threshold is a condition property (Section 3) on the sub-category. The category preference still controls whether the alert is on and which channels it uses; the condition property controls *when* the workflow fires.

<Frame caption="Example app UI - threshold input (mockup)">
  <img src="https://mintcdn.com/suprsend/esjzrvFHZzoyaiTT/images/thresholdimg.png?fit=max&auto=format&n=esjzrvFHZzoyaiTT&q=85&s=de4edf63a05fe55044ddde5ab6d9930e" alt="Threshold alerts" width="450" data-path="images/thresholdimg.png" />
</Frame>

**Configure in SuprSend**

<Steps>
  <Step title="Add a condition property to the sub-category">
    Go to **Developers → [Preference categories](/docs/managing-notification-categories)**. Click **`+Sub-Category`**, name it (for example "Usage alerts"), and add a condition property: key `threshold`, value type `integer`, a default value (say `80`), and edit policy **editable** so users can change it. Click **`Publish Changes`**.

    <Frame caption="Condition property settings on a sub-category (SuprSend dashboard)">
      <img src="https://mintcdn.com/suprsend/LL6D4dXCaotlymSe/images/category-condition-setting.png?fit=max&auto=format&n=LL6D4dXCaotlymSe&q=85&s=d10290e17b7e5b5caa97e24b1d7399ac" alt="Condition property configuration with key, value type, default value, and edit policy" width="1696" height="1546" data-path="images/category-condition-setting.png" />
    </Frame>
  </Step>

  <Step title="Reference it in the workflow">
    Compare the value from the trigger payload against `$category.properties.threshold`. Two places to put the check, depending on what you want to skip:

    * **Skip the whole workflow** - add it as a [trigger condition](/docs/workflows): `usage_percent >= $category.properties.threshold`. If it fails, nothing runs for that recipient.
    * **Skip one step** - put the same expression on a [Branch node](/docs/branch). The branch decides that step; the rest of the workflow still runs.

    At trigger time, SuprSend resolves the value per recipient: their own setting, else the tenant default, else the category default.
  </Step>
</Steps>

<Note>
  A property marked optional with no value resolves to `null` at runtime - handle that case in your branch logic.
</Note>

**Build in your app**

The sub-category's `properties` array in the [preference response](/reference/get-user-full-preference) gives you the input to render and the user's current value. Save changes with the [update category preference API](/reference/update-user-category-preference) from your backend. The category's toggle and channel checkboxes work exactly as in Section 2.

<CodeGroup>
  ```json GET preference response (excerpt) theme={"system"}
  {
    "properties": [
      {
        "key": "threshold",
        "label": "Alert me at (%)",
        "value_type": "integer",
        "default_value": "80",
        "is_optional": false,
        "edit_policy": "editable",
        "is_overridden": true,
        "value": "90"
      }
    ]
  }
  ```

  ```bash Save the user's threshold theme={"system"}
  curl -X PATCH "https://hub.suprsend.com/v1/user/lisa.wang@acme.com/preference/category/usage-alerts/" \
    -H "Authorization: Bearer {workspace_key}:{workspace_secret}" \
    -H "Content-Type: application/json" \
    -d '{ "properties": [ { "key": "threshold", "value": "90" } ] }'
  ```
</CodeGroup>

<Tip>
  **When to use:** Users need to set their own trigger point for an alert, not just whether to receive it. Examples: cloud storage ("alert me at 80% full"), API rate limits ("notify at 90% of quota"), budget tracking ("warn me when spend hits 75%").
</Tip>

***

## 6. Target recipients by role

In B2B apps, *who* receives a notification is a setting too. An admin decides that budget alerts go to project managers, adds one stakeholder who must always get them, and excludes a contractor who never should. The workflow triggers once for the whole team; each member is kept or dropped by rules the admin controls.

<Note>
  Different from tags (Section 7). Tags only filter which categories appear on the preference page - they never affect delivery. The condition properties here decide who actually receives the notification.
</Note>

Model the rules as condition properties on the sub-category:

* **`role`** (`list_choice`) - the roles that receive this notification.
* **`include`** (`list_dynamic`) - users who always receive it, whatever their role.
* **`except`** (`list_dynamic`) - users who never do.

Trigger the workflow on the group - typically an [object](/docs/objects) whose subscribers are the team, so SuprSend fans out to every member - and filter recipients with trigger conditions:

```
$category.properties.role     contains       $recipient.subscription.role   # role is targeted
$category.properties.except   not contains   $recipient.distinct_id         # not excluded
$category.properties.include  contains       $recipient.distinct_id         # always notify
```

Combine them as OR groups: *(role matches AND not excluded)* OR *(in the always-notify list)*. A recipient passes if either group holds. `$recipient.subscription.role` reads the role stored on the user's [object subscription](/docs/object-subscriptions).

<Warning>
  When trigger conditions reference recipient properties and you trigger on an object, add `$recipient.$type == "object"` as its own OR group. Without it, the object itself fails the condition and fan-out to its subscribers never happens.
</Warning>

Because these are preference-layer values, they resolve like any other preference: an admin sets the category defaults, a tenant admin can override them per tenant, and whether individual users can edit them depends on each property's edit policy. Most apps expose them on admin screens, not the end-user preference page.

<Frame caption="Example admin UI (mockup from Keystone) - default recipients per notification, backed by role, include, and except">
  <img src="https://mintcdn.com/suprsend/_z1ttZCJ4f7Wr1-1/images/b2b-category-tree.png?fit=max&auto=format&n=_z1ttZCJ4f7Wr1-1&q=85&s=f9d2ee859035ee9e3f52c023de534d5e" alt="Account-level notification defaults with recipient roles per category" width="3140" height="1920" data-path="images/b2b-category-tree.png" />
</Frame>

<Tip>
  **When to use:** Notifications aimed at a team where only some members should hear about each event. Examples: budget alerts to project managers and sponsors only; deployment failures to on-call engineers plus the team lead; compliance alerts to all admins with legal always copied.
</Tip>

***

## 7. Filter categories with modules, roles, or departments

Not every user needs to see every preference category. A sales user only cares about Sales Newsletter, Cart & checkout, and New Leads - they don't need Budget Alerts or Expense Reports. A finance user is the opposite. And in a multi-module product, each module's alerts should live on its own tab.

This uses **[tags](/docs/user-preferences#filter-categories-with-tags)**. You assign tags to preference categories or sections in SuprSend, then pass the relevant tag to [`getPreferences()`](/docs/js-preferences) - SuprSend returns only the matching categories. When a tag is assigned at the **section level**, it automatically applies to all subcategories under that section via `effective_tags`.

Tags are flexible - they can drive two common patterns:

* **Automatic filtering by role or department:** Your app reads the user's department/role/team from their profile or session and passes the tag silently. No filter UI is needed.
* **User-selected filtering by module:** Your app shows a visible tab selector and re-fetches preferences with the selected tag.

**Configure in SuprSend**

Go to **Developers → [Preference categories](/docs/managing-notification-categories)** and tag your sections or sub-categories. Two ways to structure this:

* **Tag per section:** Click **`+Section`** to create a section (for example "Sales", "Finance"). Add a tag to the section (for example `sales`, `finance`). All sub-categories under that section inherit the tag automatically via `effective_tags`. Click **`Publish Changes`**.
* **Tag per sub-category:** Create a section with all sub-categories under it. Select each sub-category and add its own tag (for example `sales`, `finance`, `product`). Filtering by tag returns that section with only the matching sub-categories.

**Build in your app**

Pass the tag to [`getPreferences()`](/docs/js-preferences) via the `tags` parameter. Where the tag comes from depends on your use case - either derived from the user's profile (automatic) or from a tab selector (user-selected). The `tags` parameter also supports logical filtering for more complex cases - see [Filter categories with tags](/docs/user-preferences#filter-categories-with-tags) for supported operators (`or`, `and`, `not`, `exists`).

<Tabs>
  <Tab title="Automatic (by role/department)">
    Read the user's department from their profile or session and pass it as the tag. No filter UI is needed - a sales user automatically sees only sales categories, a finance user sees only finance categories.

    <Frame caption="Example app UI (mockup) - Jane D. (Sales team) sees Sales Newsletter, Cart & checkout, and New Leads">
      <img src="https://mintcdn.com/suprsend/esjzrvFHZzoyaiTT/images/t2_sales.png?fit=max&auto=format&n=esjzrvFHZzoyaiTT&q=85&s=3b3074ecdaa486afd6814a61d8ef8642" alt="Sales department preferences" width="450" data-path="images/t2_sales.png" />
    </Frame>

    <Frame caption="Example app UI (mockup) - M. Kim (Finance team) sees Budget Alerts, Expense Reports, Invoice Reminders, and more">
      <img src="https://mintcdn.com/suprsend/esjzrvFHZzoyaiTT/images/t2_finance.png?fit=max&auto=format&n=esjzrvFHZzoyaiTT&q=85&s=5b10fc0e3f5605624da2020d56931d6f" alt="Finance department preferences" width="450" data-path="images/t2_finance.png" />
    </Frame>

    ```javascript React theme={"system"}
    import { PreferenceOptions, useSuprSendClient } from "@suprsend/react";
    import { useState, useEffect } from "react";

    function DepartmentPreferences({ user }) {
      const suprSendClient = useSuprSendClient();
      const [preferenceData, setPreferenceData] = useState();

      // Derive tag from user's department — for example "sales" or "finance"
      const tag = user?.department || "sales";

      useEffect(() => {
        // Sales user gets: Sales Newsletter, Cart & checkout, New Leads
        // Finance user gets: Budget Alerts, Expense Reports, Invoice Reminders, etc.
        suprSendClient.user.preferences
          .getPreferences({ tags: tag })
          .then((resp) => {
            if (resp.status !== "error") setPreferenceData({ ...resp.body });
          });
      }, [tag]);

      const handleCategoryChange = async (category, value) => {
        const resp = await suprSendClient.user.preferences.updateCategoryPreference(
          category,
          value ? PreferenceOptions.OPT_IN : PreferenceOptions.OPT_OUT
        );
        if (resp.status !== "error") setPreferenceData({ ...resp.body });
      };

      return preferenceData?.sections?.map((section, i) => (
        <div key={i}>
          {section?.name && <h3>{section.name}</h3>}
          {section?.subcategories?.map((sub, j) => (
            <div key={j}>
              <span>{sub.name}</span>
              <button
                disabled={!sub.is_editable}
                onClick={() => handleCategoryChange(sub.category, sub.preference !== PreferenceOptions.OPT_IN)}
                style={{ opacity: !sub.is_editable ? 0.5 : 1, background: sub.preference === PreferenceOptions.OPT_IN ? '#2563EB' : '#D1D5DB', borderRadius: '9999px', padding: '2px 8px', border: 'none', cursor: !sub.is_editable ? 'not-allowed' : 'pointer' }}
              >
                {sub.preference === PreferenceOptions.OPT_IN ? 'On' : 'Off'}
              </button>
            </div>
          ))}
        </div>
      ));
    }
    ```
  </Tab>

  <Tab title="User-selected (by module)">
    Render tab pills for each module. When the user switches tabs, re-fetch with the new tag.

    <Frame caption="Example app UI (mockup) - users switch between modules to manage preferences for each one">
      <img src="https://mintcdn.com/suprsend/esjzrvFHZzoyaiTT/images/t4.png?fit=max&auto=format&n=esjzrvFHZzoyaiTT&q=85&s=fd30511d041cb053468d1857c091f7e0" alt="Filter by module" width="450" data-path="images/t4.png" />
    </Frame>

    ```javascript React theme={"system"}
    import { PreferenceOptions, useSuprSendClient } from "@suprsend/react";
    import { useState, useEffect } from "react";

    function ModulePreferences() {
      const suprSendClient = useSuprSendClient();
      const [selectedTag, setSelectedTag] = useState("product");
      const [preferenceData, setPreferenceData] = useState();

      useEffect(() => {
        suprSendClient.user.preferences
          .getPreferences({ tags: selectedTag })
          .then((resp) => {
            if (resp.status !== "error") setPreferenceData({ ...resp.body });
          });
      }, [selectedTag]);

      const handleCategoryChange = async (category, value) => {
        const resp = await suprSendClient.user.preferences.updateCategoryPreference(
          category,
          value ? PreferenceOptions.OPT_IN : PreferenceOptions.OPT_OUT
        );
        if (resp.status !== "error") setPreferenceData({ ...resp.body });
      };

      return (
        <>
          {[
            { label: "Product", tag: "product" },
            { label: "Billing", tag: "billing" },
          ].map(({ label, tag }) => (
            <button
              key={tag}
              onClick={() => setSelectedTag(tag)}
              style={{ fontWeight: selectedTag === tag ? "bold" : "normal" }}
            >
              {label}
            </button>
          ))}

          {preferenceData?.sections?.map((section, i) => (
            <div key={i}>
              {section?.name && <h3>{section.name}</h3>}
              {section?.subcategories?.map((cat, j) => (
                <div key={j}>
                  <span>{cat.name}</span>
                  <button
                    disabled={!cat.is_editable}
                    onClick={() => handleCategoryChange(cat.category, cat.preference !== PreferenceOptions.OPT_IN)}
                    style={{ opacity: !cat.is_editable ? 0.5 : 1, background: cat.preference === PreferenceOptions.OPT_IN ? '#2563EB' : '#D1D5DB', borderRadius: '9999px', padding: '2px 8px', border: 'none', cursor: !cat.is_editable ? 'not-allowed' : 'pointer' }}
                  >
                    {cat.preference === PreferenceOptions.OPT_IN ? 'On' : 'Off'}
                  </button>
                </div>
              ))}
            </div>
          ))}
        </>
      );
    }
    ```
  </Tab>
</Tabs>

The API equivalent is the `tags` query parameter on the [get user preference API](/reference/get-user-full-preference) - a single tag, or a JSON expression with `or` / `and` / `not` / `exists`:

```bash cURL (backend) theme={"system"}
curl "https://hub.suprsend.com/v1/user/lisa.wang@acme.com/preference/?tags=sales" \
  -H "Authorization: Bearer {workspace_key}:{workspace_secret}"
```

<Tip>
  **When to use:** Users in different roles, departments, or product modules should see only the preference categories relevant to them. Examples: sales users see Sales Newsletter and Cart & checkout; finance users see Budget Alerts and Expense Reports; a multi-module product where users switch between modules to manage alerts separately.
</Tip>

***

## 8. Tenant admin controls

<Note>
  Different from Section 9 (platform admin restricting tenant access). This section is about tenant admins configuring defaults for their own users. For per-project independent preferences, see Section 10.
</Note>

In a B2B product, the person who configures notifications isn't the end user - it's generally pre-configured by account admin. This pattern lets [tenant](/docs/tenant-preference) admins control which categories are available to their users, what the defaults are, and which channels are enabled - all scoped to their account. End users then see only what their admin has left enabled.

The admin view shows a table of categories with a **default preference selector** (Opt-in / Opt-out / Mandatory) and a toggle per category. The toggle maps directly to `visible_to_subscriber` in the API.

<Frame caption="Example admin UI (mockup) - tenant admin sets defaults and visibility for their organization">
  <img src="https://mintcdn.com/suprsend/esjzrvFHZzoyaiTT/images/t3.png?fit=max&auto=format&n=esjzrvFHZzoyaiTT&q=85&s=def310c6e28f6f18d874808d68619cb6" alt="Tenant admin preferences" width="450" data-path="images/t3.png" />
</Frame>

**Configure in SuprSend**

1. First ensure your preference categories are set up under **Developers → [Preference categories](/docs/managing-notification-categories)** and published
2. Provide tenant admins access to configure categories via the [Tenant Preference API](/docs/tenant-preference). They can enable/disable categories, set default On/Off, and control channel availability - all scoped to their tenant

**Build in your app**

Build an admin settings page that calls the [Tenant Preference API](/reference/update-tenant-preference-single-category). Tenant admins control the following per category:

* **Default preference** (`preference: opt_in / opt_out / cant_unsubscribe`) - sets whether the category is on, off, or mandatory by default for all users in this tenant. SuprSend evaluates preferences in the order: user preference → tenant default → category default. If a user hasn't set their own preference, the tenant default applies.
* **Visibility** (`visible_to_subscriber: true / false`) - controls whether the category appears on the end user's preference page at all. Set to `false` to hide and turn off categories that aren't relevant to this tenant's users.
* **Channel controls** - `mandatory_channels` makes specific channels non-unsubscribable (when preference is `cant_unsubscribe`), `opt_in_channels` sets which channels are on by default, and `blocked_channels` disables channels across all categories for the tenant regardless of individual user preferences.

```bash theme={"system"}
# Set default preference and visibility for a category within a tenant
curl -X PATCH "https://hub.suprsend.com/v1/tenant/{tenant_id}/preference/category/{category}/" \
  -H "Authorization: Bearer {workspace_key}:{workspace_secret}" \
  -H "Content-Type: application/json" \
  -d '{
    "preference": "opt_in",
    "visible_to_subscriber": true
  }'
```

For the end-user side, pass `tenantId` to [`getPreferences()`](/docs/js-preferences#get-preferences-data) - the rendering is the same as Section 2.

<Tip>
  **When to use:** B2B or white-label products where each customer organisation's admin controls the notification experience for their users. Examples: one enterprise customer enables billing + security + product alerts while another only enables billing alerts; a white-label SaaS where each reseller configures the notification defaults for their own users.
</Tip>

***

## 9. Hide categories by tenant plan

<Note>
  This guide has three tenant-related patterns. **This section** is for platform admins restricting what a tenant can access. **Section 8** is for tenant admins setting defaults for their own users. **Section 10** is for modelling each project as an independent tenant.
</Note>

If you're building a multi-tenant app (for example a SaaS platform with free and paid plans), you probably don't want every tenant to see the same preference categories. A free-plan tenant shouldn't see categories for features they haven't subscribed to.

SuprSend lets you handle this with **[global tenant preferences](/docs/tenant-preference#platform-admin-controlling-tenant-access)**. As a platform admin, you can opt out specific categories or channels for any tenant. Once you do:

* The opted-out categories and channels are **removed from [`getPreferences()`](/docs/js-preferences)** responses for all users in that tenant - they won't see them on the preference page.
* **Notifications for those categories are not delivered** to the tenant's users.
* These are **hard overrides** - neither the tenant admin's default preferences nor individual user preferences can re-enable them. See [Preference Evaluation](/docs/preference-evaluation) for the full resolution order.

<Frame caption="Example app UI (mockup) - free-plan users see only core categories; paid-plan users see the full set">
  <img src="https://mintcdn.com/suprsend/esjzrvFHZzoyaiTT/images/tenant_res.png?fit=max&auto=format&n=esjzrvFHZzoyaiTT&q=85&s=255e7ae827b3eee0cb63adbb06cb51c5" alt="Tenant-restricted categories - free vs paid plan" width="560" data-path="images/tenant_res.png" />
</Frame>

**Configure in SuprSend**

Go to **Tenants → select your tenant → Tenant Preference** in the SuprSend dashboard. The **Set Global Preference for Tenant** section gives you three controls:

* **Global Channel Opt-out** — block one or more channels (for example Email, In-App Inbox) across all categories for the tenant. Notifications on blocked channels are never delivered to any user in the tenant.
* **Category Opt-out** — toggle a category off for the tenant. The category is removed from the tenant's preference API responses and notifications for that category are not delivered.
* **Opt-out Channels in the Category** — block specific channels within a single category (for example disable SMS for "Newsletter" but keep it enabled for other categories).

<Frame caption="Global tenant preference page in the SuprSend dashboard">
  <img src="https://mintcdn.com/suprsend/esjzrvFHZzoyaiTT/images/tenant-global-pref.png?fit=max&auto=format&n=esjzrvFHZzoyaiTT&q=85&s=e7aba588668fd22b458852e1f3dc42c2" alt="Set Global Preference for Tenant - category and channel opt-outs" width="560" data-path="images/tenant-global-pref.png" />
</Frame>

**Build in your app**

Pass `tenantId` to [`getPreferences()`](/docs/js-preferences#get-preferences-data). Categories opted out at the tenant level are excluded from the response server-side - **your rendering code stays the same regardless of tenant plan**.

<CodeGroup>
  ```javascript React theme={"system"}
  import { PreferenceOptions, useSuprSendClient } from "@suprsend/react";
  import { useState, useEffect } from "react";

  function TenantRestrictedPreferences({ tenantId }) {
    const suprSendClient = useSuprSendClient();
    const [preferenceData, setPreferenceData] = useState();

    useEffect(() => {
      // Categories opted out at the tenant level are excluded from the response
      suprSendClient.user.preferences
        .getPreferences({ tenantId })
        .then((resp) => {
          if (resp.status !== "error") setPreferenceData({ ...resp.body });
        });
    }, [tenantId]);

    const handleCategoryChange = async (category, value) => {
      const resp = await suprSendClient.user.preferences.updateCategoryPreference(
        category,
        value ? PreferenceOptions.OPT_IN : PreferenceOptions.OPT_OUT
      );
      if (resp.status !== "error") setPreferenceData({ ...resp.body });
    };

    // Render sections and subcategories - same pattern as Section 2
    return preferenceData?.sections?.map((section, i) => (
      <div key={i}>
        {section?.name && <h3>{section.name}</h3>}
        {section?.subcategories?.map((sub, j) => (
          <div key={j}>
            <span>{sub.name}</span>
            {/* Replace with your own toggle/switch component */}
            <button
              disabled={!sub.is_editable}
              onClick={() =>
                handleCategoryChange(
                  sub.category,
                  sub.preference !== PreferenceOptions.OPT_IN
                )
              }
              style={{
                opacity: !sub.is_editable ? 0.5 : 1,
                background: sub.preference === PreferenceOptions.OPT_IN ? "#2563EB" : "#D1D5DB",
                borderRadius: "9999px",
                padding: "2px 8px",
                border: "none",
                cursor: !sub.is_editable ? "not-allowed" : "pointer",
              }}
            >
              {sub.preference === PreferenceOptions.OPT_IN ? "On" : "Off"}
            </button>
          </div>
        ))}
      </div>
    ));
  }
  ```

  ```bash cURL (backend) theme={"system"}
  # Categories opted out globally for the tenant are excluded from this response
  curl "https://hub.suprsend.com/v1/user/lisa.wang@acme.com/preference/?tenant_id=acme" \
    -H "Authorization: Bearer {workspace_key}:{workspace_secret}"
  ```
</CodeGroup>

<Tip>
  **When to use:** Multi-tenant SaaS where each tenant's plan determines which notifications are available. Examples: free-plan tenants see only core alert categories, paid tenants see advanced analytics alerts and SLA breach alerts they've subscribed to.
</Tip>

***

## 10. Per-project preferences

<Note>
  Different from Section 9 (platform admin restricting tenant access) and Section 8 (tenant admin setting defaults). This section models each project as a separate tenant so preferences are fully independent per project.
</Note>

Each project needs a completely independent set of preferences - its own category toggles, its own channel settings, its own defaults. Tags won't work here because tags only filter categories; they don't give each project its own independent channel-level preferences or defaults.

The solution is to model each project as a **[tenant](/docs/tenant-preference)** in SuprSend. Preferences are then scoped per tenant. When the user switches projects, you switch the `tenantId` passed to [`getPreferences()`](/docs/js-preferences#get-preferences-data), and all reads and writes happen against that project's tenant.

<Frame caption="Example app UI (mockup) - independent preferences per project">
  <img src="https://mintcdn.com/suprsend/esjzrvFHZzoyaiTT/images/t5.png?fit=max&auto=format&n=esjzrvFHZzoyaiTT&q=85&s=43c1c346ccecd3287881ebbb17c11550" alt="Project-level preferences" width="450" data-path="images/t5.png" />
</Frame>

**Configure in SuprSend**

1. Create **one [tenant](/docs/tenant-preference) per project** in SuprSend (via **Tenants** in the dashboard or the [Tenants API](/docs/tenants)). You can also [create tenants via API](/reference/create-update-tenants)
2. For each tenant, configure which categories are enabled, what the defaults are, and which channels are available via the [Tenant Default Preference API](/reference/update-tenant-preference-single-category)
3. Ensure your preference categories are published under **Developers → [Preference categories](/docs/managing-notification-categories)**

**Build in your app**

Show a project selector. When the user picks a project, pass its tenant ID to [`getPreferences()`](/docs/js-preferences#get-preferences-data). Render the full preference UI - channel-level (All / Required per channel) and category-level (toggles + channel checkboxes).

<Note>
  Client SDK update methods take no `tenantId` - they apply to the **active tenant**: the one set at [identify](/docs/integrate-javascript-sdk#authenticate-user), or overridden by the most recent `getPreferences({ tenantId })` call. So fetch a project's preferences before writing to it, and keep one active project at a time. The `tenantId` must also match the `scope.tenant_id` in the user's [JWT token](/docs/client-authentication#2-creating-signed-user-jwt-token), or the call fails. On the REST API, scope every call explicitly with the `tenant_id` query parameter instead.
</Note>

<CodeGroup>
  ```javascript React theme={"system"}
  import { PreferenceOptions, useSuprSendClient } from "@suprsend/react";
  import { useState, useEffect } from "react";

  // Example projects - in production these come from your own data
  const PROJECTS = [
    { label: "Project Alpha", tenantId: "project-alpha" },
    { label: "Project Beta", tenantId: "project-beta" },
  ];

  function ProjectPreferences() {
    const suprSendClient = useSuprSendClient();
    const [selectedTenantId, setSelectedTenantId] = useState(PROJECTS[0].tenantId);
    const [preferenceData, setPreferenceData] = useState();

    // Switching projects re-fetches preferences scoped to that project's tenant.
    // Later update calls apply to this tenant, until the next getPreferences().
    useEffect(() => {
      suprSendClient.user.preferences
        .getPreferences({ tenantId: selectedTenantId })
        .then((resp) => {
          if (resp.status !== "error") setPreferenceData({ ...resp.body });
        });
    }, [selectedTenantId]);

    // No tenantId here - the update goes to the active tenant fetched above
    const handleCategoryChange = async (category, value) => {
      const resp = await suprSendClient.user.preferences.updateCategoryPreference(
        category,
        value ? PreferenceOptions.OPT_IN : PreferenceOptions.OPT_OUT
      );
      if (resp.status !== "error") setPreferenceData({ ...resp.body });
    };

    return (
      <>
        {PROJECTS.map(({ label, tenantId }) => (
          <button key={tenantId} onClick={() => setSelectedTenantId(tenantId)}>
            {label}
          </button>
        ))}
        {/* Render preferenceData.channel_preferences and preferenceData.sections
            exactly as in Sections 1 and 2 - the response shape is identical,
            just scoped to the selected project's tenant. */}
      </>
    );
  }
  ```

  ```bash cURL (backend) theme={"system"}
  # Read preferences scoped to one project's tenant
  curl "https://hub.suprsend.com/v1/user/lisa.wang@acme.com/preference/?tenant_id=project-alpha" \
    -H "Authorization: Bearer {workspace_key}:{workspace_secret}"

  # Update a category for that project only - other projects are untouched
  curl -X PATCH "https://hub.suprsend.com/v1/user/lisa.wang@acme.com/preference/category/comment-added/?tenant_id=project-alpha" \
    -H "Authorization: Bearer {workspace_key}:{workspace_secret}" \
    -H "Content-Type: application/json" \
    -d '{ "preference": "opt_out" }'
  ```
</CodeGroup>

<Tip>
  **When to use:** Each project or sub-account needs completely independent preferences - its own defaults, its own channel settings, everything separate. Examples: an ad platform where each ad account has independent budget alert settings; a cloud hosting provider where production, staging, and dev each have separate alert preferences.
</Tip>

***

## Worked example: Keystone

Keystone is the project-management app from the [multi-tenant B2B guide](/docs/guides/modelling-customers-b2b): each customer gets a workspace, and each project is both a tenant (for settings and preferences) and an object (whose subscribers are the project team). Its notification settings combine most of the patterns above, so two of its categories are worth reading end to end.

**Budget Alert - conditions.** The sub-category carries four condition properties: `role`, `cost` (the budget threshold), `include`, and `except`. The workflow triggers on the project object, and its trigger conditions keep a recipient if any of these OR groups pass:

1. Their role is in `role`, the project's cost exceeds `cost`, and they're not in `except`
2. They're in `include`
3. The recipient is the project object itself - the `$recipient.$type == "object"` fan-out guard from Section 6

Group 2 is why "always notify" beats every other rule, and group 3 is easy to forget - without it, no one gets notified at all.

**Comment Added - digest.** The sub-category offers three digest options: Instant, Hourly with a user-editable time, and Daily at a locked time - the default. The workflow is a Digest node with schedule type **Preference Category**, followed by a send step using a template that renders the batched comments. A user who picks Hourly and a user who left the default get different cadences from the same workflow, with no branching.

<Frame caption="Keystone's project notification settings - category toggle, digest frequency, and channel toggles, all from one preference API response">
  <img src="https://mintcdn.com/suprsend/_z1ttZCJ4f7Wr1-1/images/b2b-preferences.png?fit=max&auto=format&n=_z1ttZCJ4f7Wr1-1&q=85&s=33e6d7297761653b7b9603cfd63960d9" alt="Keystone per-project preference screen with a digest frequency selector on Comment Added" width="2940" height="1670" data-path="images/b2b-preferences.png" />
</Frame>

Every screen in the app sits on the preference APIs:

* A user's own settings per project - [update category preference API](/reference/update-user-category-preference), scoped by `tenant_id`
* A project's defaults, edited by admins and project managers - [tenant preference API](/reference/update-tenant-preference-single-category)
* Account-wide defaults - [create/update category API](/reference/create-update-category)

For the full data model - workspaces, tenants, objects, and how one workflow serves every project - see [Designing notifications for multi-tenant B2B applications](/docs/guides/modelling-customers-b2b).

***

## Related reading

<CardGroup cols={2}>
  <Card title="User Preferences" icon="sliders" href="/docs/user-preferences">
    How categories, channels, and the preference API response fit together.
  </Card>

  <Card title="Preference Evaluation" icon="list-checks" href="/docs/preference-evaluation">
    When SuprSend checks preferences, and how conflicts resolve.
  </Card>

  <Card title="Digest" icon="layers" href="/docs/digest">
    Everything the Digest node can do, beyond preference-driven schedules.
  </Card>

  <Card title="Multi-tenant B2B modelling" icon="building" href="/docs/guides/modelling-customers-b2b">
    The full Keystone data model this guide's example comes from.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.