Skip to main content
v2.1.1
May 9, 2026

Improvements

KelviqProvidercustomerId 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):
<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:
<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.
v2.1.0
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:
<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().
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:
<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:
FieldTypeDescription
planRawPricingPlanFull plan object
chargeRawPricingCharge | nullCharge for the requested billing period
amountnumberRaw numeric amount
formattedPricestringLocalized price string (e.g. $9.99) or "Free"
currencySymbolstringe.g. $
currencyCodestringe.g. USD
pricingLocalestringe.g. en-US
isFreebooleantrue when priceType === 'FREE'
hasFreeTrialbooleanWhether a free trial is available
trialPeriodnumberTrial length in days
<KQFeatureList /> — Render-Prop Feature Component
Iterates over enabled features for a plan. Supports optional filtering by feature type:
<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>
PropTypeDescription
planIdentifierstringThe plan to list features for
featureType'BOOLEAN' | 'METER'Optional filter by feature type
loadingComponentReactNodeShown while data loads
fallbackReactNodeShown when plan not found or no features match
children(data) => ReactNodeRender prop called for each enabled feature
kqFormatPrice() — Price Formatter Utility
Standalone utility for formatting a numeric amount with a currency symbol:
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"
OptionTypeDefaultDescription
compactbooleanfalseUse compact notation (e.g. 1.2K)
localestringpricingLocaleOverride locale for number formatting
pricingLocalestring'en-US'Locale from the pricing API
includeCurrencySymbolbooleantruePrepend the currency symbol
New Types
RawPricingFeature
RawPricingCharge
RawPricingPrice
RawPricingPlan
RawPricingBillingPeriod
RawPricingApiResponse
 
KQFormatPriceOptions
KQPriceProps
KQFeatureListProps
New Exports
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';
v2.0.0
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 truetrue) 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:
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:
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:
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.
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:
<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:
// 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:
// Before
<ShowWhenBooleanEntitled featureKey="my-feature">
  <PremiumContent />
</ShowWhenBooleanEntitled>
 
// After
<ShowWhenBooleanEntitled featureId="my-feature">
  <PremiumContent />
</ShowWhenBooleanEntitled>

Config Renamed to Customizable

BeforeAfter
useConfigEntitlementuseCustomizableEntitlement
ShowWhenConfigEntitledShowWhenCustomizableEntitled
ConfigEntitlement typeUnified Entitlement

type Field Renamed to featureType with Uppercase Values

BeforeAfter
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.
// 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:
// 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

BeforeAfterNotes
limitusageLimitAligned with the API
usedcurrentUsageAligned with the API
resetAt(removed)Now per-item: entitlement.items[].resetAt
hardLimithardLimitNow 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:
// 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 limitusageLimit, usedcurrentUsage
  9. Replace entitlement.resetAt with entitlement.items[].resetAt
  10. Use getEntitlements() instead of allEntitlements.data
  11. Use getRawEntitlements() for un-aggregated API data
v2.0.0
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 truetrue)
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:
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:
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:
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.
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:
<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:
// 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:
// Before
<ShowWhenBooleanEntitled featureKey="my-feature">
  <PremiumContent />
</ShowWhenBooleanEntitled>

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

Config Renamed to Customizable

BeforeAfter
useConfigEntitlementuseCustomizableEntitlement
ShowWhenConfigEntitledShowWhenCustomizableEntitled
ConfigEntitlement typeUnified Entitlement

type Field Renamed to featureType with Uppercase Values

BeforeAfter
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.
// 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:
// 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

BeforeAfterNotes
limitusageLimitAligned with the API
usedcurrentUsageAligned with the API
resetAt(removed)Now per-item: entitlement.items[].resetAt
hardLimithardLimitNow 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:
// 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 limitusageLimit, usedcurrentUsage
  9. Replace entitlement.resetAt with entitlement.items[].resetAt
  10. Use getEntitlements() instead of allEntitlements.data
  11. Use getRawEntitlements() for un-aggregated API data