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

# React SDK

> Changelog for @kelviq/react-sdk

<Update label="v2.1.1" description="May 9, 2026">
  ### Improvements

  #### `KelviqProvider` — `customerId` and `plansEnabled` Props for Pricing

  The pricing request now forwards two additional optional props to the product offerings API.

  **`customerId`** — when provided, the pricing response is personalized to that customer (e.g. reflecting their existing subscription state):

  ```tsx theme={null}
  <KelviqProvider
    accessToken="..."
    productId="your-product-id"
    customerId="cust_123"
    config={{ fetchPricingOnMount: true }}
  >
    <App />
  </KelviqProvider>
  ```

  **`plansEnabled`** — a comma-separated list of plan identifiers to include in the response. When omitted, all active plans are returned:

  ```tsx theme={null}
  <KelviqProvider
    accessToken="..."
    productId="your-product-id"
    plansEnabled="free-plan,pro-plan"
    config={{ fetchPricingOnMount: true }}
  >
    <App />
  </KelviqProvider>
  ```

  Both props are reactive — updating either value will trigger a new pricing fetch automatically.
</Update>

<Update label="v2.1.0" description="May 9, 2026">
  ### New Features

  #### Product Offerings — Pricing & Feature Components

  The SDK now supports fetching and displaying product pricing and plan features, with localized currency based on user location.

  ##### Setup

  Pass `productId` and enable `fetchPricingOnMount` in your provider config:

  ```tsx theme={null}
  <KelviqProvider
    accessToken="..."
    productId="your-product-id"
    config={{ fetchPricingOnMount: true }}
  >
    <App />
  </KelviqProvider>
  ```

  ##### `usePricing()` — Pricing Data Hook

  Returns the full pricing API response as an `AsyncState`. Only populated when `fetchPricingOnMount` is enabled, or after calling `refreshPricing()` from `useKelviq()`.

  ```ts theme={null}
  const { data, isLoading, error } = usePricing();
  console.log(data?.currencyCode, data?.plans);
  ```

  ##### `<KQPrice />` — Render-Prop Price Component

  Displays localized pricing for a plan and billing period:

  ```tsx theme={null}
  <KQPrice
    planIdentifier="pro-plan"
    billingPeriod="MONTHLY"
    loadingComponent={<Spinner />}
    fallback={<p>Unavailable</p>}
  >
    {({ formattedPrice, isFree, hasFreeTrial, trialPeriod }) => (
      <div>
        <span>{formattedPrice}</span>
        {hasFreeTrial && <span>Try free for {trialPeriod} days</span>}
      </div>
    )}
  </KQPrice>
  ```

  The render prop receives:

  | Field            | Type                       | Description                                       |
  | ---------------- | -------------------------- | ------------------------------------------------- |
  | `plan`           | `RawPricingPlan`           | Full plan object                                  |
  | `charge`         | `RawPricingCharge \| null` | Charge for the requested billing period           |
  | `amount`         | `number`                   | Raw numeric amount                                |
  | `formattedPrice` | `string`                   | Localized price string (e.g. `$9.99`) or `"Free"` |
  | `currencySymbol` | `string`                   | e.g. `$`                                          |
  | `currencyCode`   | `string`                   | e.g. `USD`                                        |
  | `pricingLocale`  | `string`                   | e.g. `en-US`                                      |
  | `isFree`         | `boolean`                  | `true` when `priceType === 'FREE'`                |
  | `hasFreeTrial`   | `boolean`                  | Whether a free trial is available                 |
  | `trialPeriod`    | `number`                   | Trial length in days                              |

  ##### `<KQFeatureList />` — Render-Prop Feature Component

  Iterates over enabled features for a plan. Supports optional filtering by feature type:

  ```tsx theme={null}
  <KQFeatureList
    planIdentifier="pro-plan"
    featureType="BOOLEAN"
    loadingComponent={<Spinner />}
    fallback={<p>No features available</p>}
  >
    {({ feature, plan, index }) => (
      <li key={feature.id}>
        {feature.displayName}
      </li>
    )}
  </KQFeatureList>
  ```

  | Prop               | Type                   | Description                                    |
  | ------------------ | ---------------------- | ---------------------------------------------- |
  | `planIdentifier`   | `string`               | The plan to list features for                  |
  | `featureType`      | `'BOOLEAN' \| 'METER'` | Optional filter by feature type                |
  | `loadingComponent` | `ReactNode`            | Shown while data loads                         |
  | `fallback`         | `ReactNode`            | Shown when plan not found or no features match |
  | `children`         | `(data) => ReactNode`  | Render prop called for each enabled feature    |

  ##### `kqFormatPrice()` — Price Formatter Utility

  Standalone utility for formatting a numeric amount with a currency symbol:

  ```ts theme={null}
  import { kqFormatPrice } from '@kelviq/react-sdk';
   
  kqFormatPrice(9.99, '$', { pricingLocale: 'en-US' });         // "$9.99"
  kqFormatPrice(1200, '€', { compact: true, pricingLocale: 'de-DE' }); // "€1,2K"
  kqFormatPrice(0, '$', { includeCurrencySymbol: false });       // "0"
  ```

  | Option                  | Type      | Default         | Description                           |
  | ----------------------- | --------- | --------------- | ------------------------------------- |
  | `compact`               | `boolean` | `false`         | Use compact notation (e.g. `1.2K`)    |
  | `locale`                | `string`  | `pricingLocale` | Override locale for number formatting |
  | `pricingLocale`         | `string`  | `'en-US'`       | Locale from the pricing API           |
  | `includeCurrencySymbol` | `boolean` | `true`          | Prepend the currency symbol           |

  ##### New Types

  ```ts theme={null}
  RawPricingFeature
  RawPricingCharge
  RawPricingPrice
  RawPricingPlan
  RawPricingBillingPeriod
  RawPricingApiResponse
   
  KQFormatPriceOptions
  KQPriceProps
  KQFeatureListProps
  ```

  ##### New Exports

  ```ts theme={null}
  export { usePricing } from './hooks/usePricing';
  export { KQPrice } from './components/KQPrice';
  export { KQFeatureList } from './components/KQFeatureList';
  export { kqFormatPrice } from './utils/formatPrice';
  export type * from './types/api.types';
  ```
</Update>

<Update label="v2.0.0" description="February 27, 2026">
  ### New Features

  #### Duplicate `featureId` Aggregation

  The SDK now handles customers with multiple subscriptions that grant entitlements to the same feature. Raw entries are grouped by `featureId` and aggregated automatically:

  * **METER**: `usageLimit` and `currentUsage` are summed across entries; `remaining` is recalculated
  * **BOOLEAN / CUSTOMIZABLE**: `hasAccess` is OR'd across entries (any `true` → `true`)
    The raw un-aggregated entries are preserved in the `items` array on each `Entitlement`.

  #### `getEntitlements()` — Convenience Accessor

  Returns the aggregated entitlements map directly, without the `AsyncState` wrapper:

  ```ts theme={null}
  const { getEntitlements } = useKelviq();
  const entitlements = getEntitlements(); // Record<string, Entitlement>
   
  const emailFeature = entitlements["email-sends"];
  console.log(emailFeature.hasAccess, emailFeature.remaining);
  ```

  #### `getRawEntitlements()` — Raw API Response

  Access the un-aggregated API response with the `customerId` wrapper:

  ```ts theme={null}
  const { getRawEntitlements } = useKelviq();
  const raw = getRawEntitlements();
  // { customerId: "cust_123", entitlements: [...] }
  ```

  #### `getRawEntitlement(featureId)` — Raw Data for a Single Feature

  Returns the raw API response filtered to a specific featureId:

  ```ts theme={null}
  const { getRawEntitlement } = useKelviq();
  const raw = getRawEntitlement("email-sends");
  // { customerId: "cust_123", entitlements: [/* only "email-sends" entries */] }
  ```

  #### `updateEntitlement()` — Client-Side Entitlement Mutation

  Update an entitlement in-place, for example after recording a usage increment on the client side. The `remaining` field is automatically recalculated.

  ```ts theme={null}
  const { updateEntitlement } = useKelviq();
  updateEntitlement("email-sends", { currentUsage: 5 });
  // `remaining` is automatically recalculated
  ```

  Accepts `Partial<Omit<Entitlement, 'featureId' | 'featureType' | 'items'>>`.

  #### `environment` Prop on `KelviqProvider`

  Supports `'production'` (default) and `'sandbox'`, which selects the appropriate default API URL:

  ```tsx theme={null}
  <KelviqProvider environment="sandbox" customerId="cust_123" accessToken="...">
    <App />
  </KelviqProvider>
  ```

  ***

  ### Breaking Changes

  #### Unified `Entitlement` Type

  The three separate entitlement interfaces and their union type have been replaced by a single unified `Entitlement` interface:

  ```ts theme={null}
  // Before — three separate types
  interface BooleanEntitlement { type: 'boolean'; featureKey: string; hasAccess: boolean; }
  interface ConfigEntitlement  { type: 'customizable'; featureKey: string; hasAccess: boolean; configuration: number | null; }
  interface MeteredEntitlement { type: 'metered'; featureKey: string; hasAccess: boolean; limit: number | null; used: number; remaining: number | null; resetAt: string | null; hardLimit: boolean; }
   
  // After — one unified type
  interface Entitlement {
    featureId: string;
    featureType: 'METER' | 'BOOLEAN' | 'CUSTOMIZABLE';
    hasAccess: boolean;
    currentUsage: number;
    usageLimit: number | null;
    remaining: number | null;
    hardLimit: boolean;
    items: AnyRawEntitlementData[];
  }
  ```

  #### `featureKey` Renamed to `featureId`

  All props, parameters, and type fields now use `featureId` to match the backend API naming:

  ```tsx theme={null}
  // Before
  <ShowWhenBooleanEntitled featureKey="my-feature">
    <PremiumContent />
  </ShowWhenBooleanEntitled>
   
  // After
  <ShowWhenBooleanEntitled featureId="my-feature">
    <PremiumContent />
  </ShowWhenBooleanEntitled>
  ```

  #### `Config` Renamed to `Customizable`

  | Before                   | After                          |
  | ------------------------ | ------------------------------ |
  | `useConfigEntitlement`   | `useCustomizableEntitlement`   |
  | `ShowWhenConfigEntitled` | `ShowWhenCustomizableEntitled` |
  | `ConfigEntitlement` type | Unified `Entitlement`          |

  #### `type` Field Renamed to `featureType` with Uppercase Values

  | Before                 | After                         |
  | ---------------------- | ----------------------------- |
  | `type: 'boolean'`      | `featureType: 'BOOLEAN'`      |
  | `type: 'customizable'` | `featureType: 'CUSTOMIZABLE'` |
  | `type: 'metered'`      | `featureType: 'METER'`        |

  #### `configuration` Field Removed

  `ConfigEntitlement.configuration` has been removed. Customizable entitlements now use the same `usageLimit`, `currentUsage`, and `remaining` fields as metered entitlements.

  #### Hooks Return `Entitlement | null` Directly

  All entitlement hooks now return the entitlement object directly (or `null`) instead of an `AsyncState` wrapper. Use the top-level `isLoading` and `error` from `useKelviq()` for loading/error states.

  ```ts theme={null}
  // Before
  const { data, isLoading, error } = useMeteredEntitlement("my-feature");
  if (isLoading) return <Spinner />;
  console.log(data?.used);
   
  // After
  const entitlement = useMeteredEntitlement("my-feature");
  const { isLoading, error } = useKelviq();
  if (isLoading) return <Spinner />;
  console.log(entitlement?.currentUsage);
  ```

  #### `getEntitlement()` Simplified

  The generic type parameter and second argument have been removed:

  ```ts theme={null}
  // Before
  getEntitlement<MeteredEntitlement>("my-feature", "metered")
   
  // After
  getEntitlement("my-feature") // returns Entitlement | null
  ```

  #### `hasAccess()` Returns `boolean` (Never `undefined`)

  `hasAccess(featureId)` now returns `false` when data is unavailable instead of `undefined`. No need for nullish checks.

  #### Metered Field Renames

  | Before      | After          | Notes                                       |
  | ----------- | -------------- | ------------------------------------------- |
  | `limit`     | `usageLimit`   | Aligned with the API                        |
  | `used`      | `currentUsage` | Aligned with the API                        |
  | `resetAt`   | *(removed)*    | Now per-item: `entitlement.items[].resetAt` |
  | `hardLimit` | `hardLimit`    | Now aggregated: `true` if any item sets it  |

  #### `allEntitlements.data` Shape Changed

  The map is now keyed by `featureId` (previously `featureKey`) and contains unified `Entitlement` objects. Use the new `getEntitlements()` convenience method:

  ```ts theme={null}
  // Before
  const { allEntitlements } = useKelviq();
  const myFeature = allEntitlements.data?.["my-feature"]; // AnyEntitlement | undefined
   
  // After
  const { getEntitlements } = useKelviq();
  const entitlements = getEntitlements(); // Record<string, Entitlement>
  const myFeature = entitlements["my-feature"]; // Entitlement | undefined
  ```

  ***

  ### Migration Checklist

  1. Replace all `featureKey` props/params with `featureId`
  2. Replace `entitlement.type` with `entitlement.featureType` and update values to uppercase
  3. Replace `useConfigEntitlement` with `useCustomizableEntitlement`
  4. Replace `ShowWhenConfigEntitled` with `ShowWhenCustomizableEntitled`
  5. Replace `ConfigEntitlement.configuration` with `usageLimit` / `currentUsage` / `remaining`
  6. Update hook consumers: hooks now return `Entitlement | null` directly (not `AsyncState`)
  7. Remove type parameters from `getEntitlement()` calls
  8. Replace `limit` → `usageLimit`, `used` → `currentUsage`
  9. Replace `entitlement.resetAt` with `entitlement.items[].resetAt`
  10. Use `getEntitlements()` instead of `allEntitlements.data`
  11. Use `getRawEntitlements()` for un-aggregated API data
</Update>

<Update label="v2.0.0" description="February 27, 2026">
  ### New Features

  #### Duplicate `featureId` Aggregation

  The SDK now handles customers with multiple subscriptions that grant entitlements to the same feature. Raw entries are grouped by `featureId` and aggregated automatically:

  * **METER**: `usageLimit` and `currentUsage` are summed across entries; `remaining` is recalculated
  * **BOOLEAN / CUSTOMIZABLE**: `hasAccess` is OR'd across entries (any `true` → `true`)

  The raw un-aggregated entries are preserved in the `items` array on each `Entitlement`.

  #### `getEntitlements()` — Convenience Accessor

  Returns the aggregated entitlements map directly, without the `AsyncState` wrapper:

  ```ts theme={null}
  const { getEntitlements } = useKelviq();
  const entitlements = getEntitlements(); // Record<string, Entitlement>

  const emailFeature = entitlements["email-sends"];
  console.log(emailFeature.hasAccess, emailFeature.remaining);
  ```

  #### `getRawEntitlements()` — Raw API Response

  Access the un-aggregated API response with the `customerId` wrapper:

  ```ts theme={null}
  const { getRawEntitlements } = useKelviq();
  const raw = getRawEntitlements();
  // { customerId: "cust_123", entitlements: [...] }
  ```

  #### `getRawEntitlement(featureId)` — Raw Data for a Single Feature

  Returns the raw API response filtered to a specific featureId:

  ```ts theme={null}
  const { getRawEntitlement } = useKelviq();
  const raw = getRawEntitlement("email-sends");
  // { customerId: "cust_123", entitlements: [/* only "email-sends" entries */] }
  ```

  #### `updateEntitlement()` — Client-Side Entitlement Mutation

  Update an entitlement in-place, for example after recording a usage increment on the client side. The `remaining` field is automatically recalculated.

  ```ts theme={null}
  const { updateEntitlement } = useKelviq();
  updateEntitlement("email-sends", { currentUsage: 5 });
  // `remaining` is automatically recalculated
  ```

  Accepts `Partial<Omit<Entitlement, 'featureId' | 'featureType' | 'items'>>`.

  #### `environment` Prop on `KelviqProvider`

  Supports `'production'` (default) and `'sandbox'`, which selects the appropriate default API URL:

  ```tsx theme={null}
  <KelviqProvider environment="sandbox" customerId="cust_123" accessToken="...">
    <App />
  </KelviqProvider>
  ```

  ***

  ### Breaking Changes

  #### Unified `Entitlement` Type

  The three separate entitlement interfaces and their union type have been replaced by a single unified `Entitlement` interface:

  ```ts theme={null}
  // Before — three separate types
  interface BooleanEntitlement { type: 'boolean'; featureKey: string; hasAccess: boolean; }
  interface ConfigEntitlement  { type: 'customizable'; featureKey: string; hasAccess: boolean; configuration: number | null; }
  interface MeteredEntitlement { type: 'metered'; featureKey: string; hasAccess: boolean; limit: number | null; used: number; remaining: number | null; resetAt: string | null; hardLimit: boolean; }

  // After — one unified type
  interface Entitlement {
    featureId: string;
    featureType: 'METER' | 'BOOLEAN' | 'CUSTOMIZABLE';
    hasAccess: boolean;
    currentUsage: number;
    usageLimit: number | null;
    remaining: number | null;
    hardLimit: boolean;
    items: AnyRawEntitlementData[];
  }
  ```

  #### `featureKey` Renamed to `featureId`

  All props, parameters, and type fields now use `featureId` to match the backend API naming:

  ```tsx theme={null}
  // Before
  <ShowWhenBooleanEntitled featureKey="my-feature">
    <PremiumContent />
  </ShowWhenBooleanEntitled>

  // After
  <ShowWhenBooleanEntitled featureId="my-feature">
    <PremiumContent />
  </ShowWhenBooleanEntitled>
  ```

  #### `Config` Renamed to `Customizable`

  | Before                   | After                          |
  | ------------------------ | ------------------------------ |
  | `useConfigEntitlement`   | `useCustomizableEntitlement`   |
  | `ShowWhenConfigEntitled` | `ShowWhenCustomizableEntitled` |
  | `ConfigEntitlement` type | Unified `Entitlement`          |

  #### `type` Field Renamed to `featureType` with Uppercase Values

  | Before                 | After                         |
  | ---------------------- | ----------------------------- |
  | `type: 'boolean'`      | `featureType: 'BOOLEAN'`      |
  | `type: 'customizable'` | `featureType: 'CUSTOMIZABLE'` |
  | `type: 'metered'`      | `featureType: 'METER'`        |

  #### `configuration` Field Removed

  `ConfigEntitlement.configuration` has been removed. Customizable entitlements now use the same `usageLimit`, `currentUsage`, and `remaining` fields as metered entitlements.

  #### Hooks Return `Entitlement | null` Directly

  All entitlement hooks now return the entitlement object directly (or `null`) instead of an `AsyncState` wrapper. Use the top-level `isLoading` and `error` from `useKelviq()` for loading/error states.

  ```ts theme={null}
  // Before
  const { data, isLoading, error } = useMeteredEntitlement("my-feature");
  if (isLoading) return <Spinner />;
  console.log(data?.used);

  // After
  const entitlement = useMeteredEntitlement("my-feature");
  const { isLoading, error } = useKelviq();
  if (isLoading) return <Spinner />;
  console.log(entitlement?.currentUsage);
  ```

  #### `getEntitlement()` Simplified

  The generic type parameter and second argument have been removed:

  ```ts theme={null}
  // Before
  getEntitlement<MeteredEntitlement>("my-feature", "metered")

  // After
  getEntitlement("my-feature") // returns Entitlement | null
  ```

  #### `hasAccess()` Returns `boolean` (Never `undefined`)

  `hasAccess(featureId)` now returns `false` when data is unavailable instead of `undefined`. No need for nullish checks.

  #### Metered Field Renames

  | Before      | After          | Notes                                       |
  | ----------- | -------------- | ------------------------------------------- |
  | `limit`     | `usageLimit`   | Aligned with the API                        |
  | `used`      | `currentUsage` | Aligned with the API                        |
  | `resetAt`   | *(removed)*    | Now per-item: `entitlement.items[].resetAt` |
  | `hardLimit` | `hardLimit`    | Now aggregated: `true` if any item sets it  |

  #### `allEntitlements.data` Shape Changed

  The map is now keyed by `featureId` (previously `featureKey`) and contains unified `Entitlement` objects. Use the new `getEntitlements()` convenience method:

  ```ts theme={null}
  // Before
  const { allEntitlements } = useKelviq();
  const myFeature = allEntitlements.data?.["my-feature"]; // AnyEntitlement | undefined

  // After
  const { getEntitlements } = useKelviq();
  const entitlements = getEntitlements(); // Record<string, Entitlement>
  const myFeature = entitlements["my-feature"]; // Entitlement | undefined
  ```

  ***

  ### Migration Checklist

  1. Replace all `featureKey` props/params with `featureId`
  2. Replace `entitlement.type` with `entitlement.featureType` and update values to uppercase
  3. Replace `useConfigEntitlement` with `useCustomizableEntitlement`
  4. Replace `ShowWhenConfigEntitled` with `ShowWhenCustomizableEntitled`
  5. Replace `ConfigEntitlement.configuration` with `usageLimit` / `currentUsage` / `remaining`
  6. Update hook consumers: hooks now return `Entitlement | null` directly (not `AsyncState`)
  7. Remove type parameters from `getEntitlement()` calls
  8. Replace `limit` → `usageLimit`, `used` → `currentUsage`
  9. Replace `entitlement.resetAt` with `entitlement.items[].resetAt`
  10. Use `getEntitlements()` instead of `allEntitlements.data`
  11. Use `getRawEntitlements()` for un-aggregated API data
</Update>
