> ## 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.

# JavaScript SDK

> Documentation for the Kelviq JavaScript SDK

The `@kelviq/js-sdk` is a lightweight, plain TypeScript library designed for interacting with Kelviq entitlement and pricing services. It allows you to:

* Fetch all feature entitlements for a specific organization and customer.
* Cache these entitlements client-side for efficient and repeated access.
* Check if a user has access to a particular feature.
* Retrieve specific entitlement details, including boolean flags, numeric configurations, and metered usage data.
* Fetch product offering and pricing data for display on pricing pages.
* Format prices with locale-aware currency formatting.
* Automatically populate pricing into DOM elements via data attributes.
* Manually refresh entitlement and pricing data from the server.

<Note>If you are using React, you can use the <a href="/frontend-integration/react-sdk">React SDK</a> instead.</Note>

### Installation

Install the SDK using your preferred package manager or include it directly via CDN.

```bash theme={null}
npm install @kelviq/js-sdk
# or
yarn add @kelviq/js-sdk
# or
pnpm add @kelviq/js-sdk
```

**CDN Installation:**

```html theme={null}
<script src="https://unpkg.com/@kelviq/js-sdk@2.1.0/dist/kelviq-js-sdk.umd.js"></script>
```

### Basic Usage

Get started with the SDK in just a few lines of code.

```typescript theme={null}
import kelviqSDK from '@kelviq/js-sdk';

// Initialize the SDK (fetches entitlements automatically)
const kq = kelviqSDK({
  customerId: "your-customer-id",
  accessToken: "your-access-token",
});

// Wait for the initial fetch to complete
await kq.ready();

// Check if user has access to a feature
if (kq.hasAccess('premium-features')) {
  console.log("User has access to premium features!");
}

// Get specific entitlement details
const emailQuota = kq.getEntitlement('email-sends');
if (emailQuota && emailQuota.hasAccess) {
  console.log(`Emails remaining: ${emailQuota.remaining}`);
}
```

## API Reference

Complete documentation of all SDK methods and configuration options.

### Initialization

Create and configure your SDK instance with the required parameters.

<Warning>
  **Security Warning:** Never use your Server API Key in the browser. Use the **Client API Key** which is safe to expose in client-side code. You can find both keys in the [API keys](https://app.kelviq.com/settings/api-keys) section of your Kelviq dashboard.
</Warning>

```typescript theme={null}
// Initialize the SDK
const kq = kelviqSDK({
  customerId: "your-customer-id",
  accessToken: "your-access-token",
});
```

You can also initialize the SDK with just a `productId` to fetch pricing data only (without entitlements):

```typescript theme={null}
// Initialize for pricing only
const kq = kelviqSDK({
  productId: "your-product-id",
  accessToken: "your-access-token",
});

await kq.ready();
const pricing = kq.getPricing();
```

Or use both `customerId` and `productId` together:

```typescript theme={null}
// Initialize for both entitlements and pricing
const kq = kelviqSDK({
  customerId: "your-customer-id",
  productId: "your-product-id",
  accessToken: "your-access-token",
});

await kq.ready();
// Both entitlements and pricing are now available
```

#### Options

| Option               | Type                     | Description                                                         |                                                                                |
| -------------------- | ------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `customerId`         | `string`                 | Unique identifier for the customer (required for entitlements)      |                                                                                |
| `productId`          | `string`                 | Product identifier for fetching pricing data (required for pricing) |                                                                                |
| `accessToken`        | `string`                 | Authentication token for API requests                               |                                                                                |
| `environment`        | `sandbox\|production`    | 'production'                                                        | The environment to use. If you want to use the sandbox, you must provide this. |
| `initializeAndFetch` | `boolean`                | `true`                                                              | Automatically fetch data on initialization                                     |
| `onError`            | `(error: Error) => void` | Error callback function                                             |                                                                                |
| `mainApiUrl`         | `string`                 | Optional override for the main API base URL (used by pricing)       |                                                                                |

<Note>At least one of `customerId` or `productId` must be provided.</Note>

### Core Methods

#### `hasAccess(featureId: string): boolean`

Check if the user has access to a specific feature. Returns `false` if the feature doesn't exist or data hasn't been fetched.

```typescript theme={null}
if (kq.hasAccess('premium-features')) {
  // Show premium features
  showPremiumUI();
}
```

#### `getEntitlement(featureId: string): Entitlement | null`

Get the aggregated entitlement for a specific feature. Returns the unified `Entitlement` object which includes the `.items` array of raw API objects.

```typescript theme={null}
// Boolean entitlement
const canUseAnalytics = kq.getEntitlement('analytics');
if (canUseAnalytics?.hasAccess) {
  enableAnalytics();
}

// Metered entitlement
const emailQuota = kq.getEntitlement('email-sends');
if (emailQuota?.hasAccess && emailQuota.remaining !== null && emailQuota.remaining > 0) {
  console.log(`Usage: ${emailQuota.currentUsage} / ${emailQuota.usageLimit}`);
  console.log(`Emails remaining: ${emailQuota.remaining}`);
}
```

#### `getEntitlements(): Record<string, Entitlement> | null`

Get all aggregated entitlements as a map keyed by `featureId`.

```typescript theme={null}
const allEntitlements = kq.getEntitlements();
if (allEntitlements) {
  Object.entries(allEntitlements).forEach(([featureId, entitlement]) => {
    console.log(`${featureId}: ${entitlement.hasAccess ? 'Access granted' : 'Access denied'}`);
  });
}
```

#### `getRawEntitlement(featureId: string): RawEntitlement[] | null`

Get the raw, un-aggregated API items for a specific feature. Useful when you need per-item details like `resetAt` or `hardLimit`.

```typescript theme={null}
const rawItems = kq.getRawEntitlement('email-sends');
if (rawItems) {
  rawItems.forEach(item => {
    console.log(`Reset at: ${item.resetAt}, Limit: ${item.usageLimit}`);
  });
}
```

#### `getRawEntitlements(): RawEntitlementsApiResponse | null`

Get the full raw API response including the `customerId` wrapper, exactly as received from the server.

```typescript theme={null}
const raw = kq.getRawEntitlements();
if (raw) {
  console.log(`Customer: ${raw.customerId}`);
  console.log(`Total items: ${raw.entitlements.length}`);
}
```

#### `ready(): Promise<void>`

Wait for the initial fetch (triggered by `initializeAndFetch: true`) to complete. Resolves immediately if no initial fetch was triggered or if the data is already available.

```typescript theme={null}
const kq = kelviqSDK({
  customerId: "your-customer-id",
  accessToken: "your-access-token",
});

await kq.ready();
// Entitlements are now available
if (kq.hasAccess('premium-features')) {
  showPremiumUI();
}
```

#### `fetchAllEntitlements(forceRefresh?: boolean): Promise<Record<string, Entitlement>>`

Manually trigger a network request to refresh the entitlements. If a fetch is already in progress, the same promise is returned (calls are deduplicated).

```typescript theme={null}
// Fetch fresh data
try {
  const entitlements = await kq.fetchAllEntitlements(true);
  console.log('Entitlements refreshed:', entitlements);
} catch (error) {
  console.error('Failed to refresh entitlements:', error);
}
```

#### `isLoading(): boolean`

Check if entitlements are currently being fetched.

```typescript theme={null}
if (kq.isLoading()) {
  showLoadingSpinner();
} else {
  hideLoadingSpinner();
}
```

#### `getLastError(): Error | null`

Get the last error that occurred during entitlement fetching.

```typescript theme={null}
const lastError = kq.getLastError();
if (lastError) {
  console.error('Last SDK error:', lastError.message);
}
```

#### `clearCache(): void`

Clear the cached entitlements.

```typescript theme={null}
kq.clearCache();
// Next call to hasAccess or getEntitlement will need a new fetch
```

### Pricing Methods

#### `fetchPricing(forceRefresh?: boolean): Promise<RawPricingApiResponse>`

Fetch product offering and pricing data. Requires `productId` to be set. Results are cached; use `forceRefresh` to bypass the cache.

```typescript theme={null}
const pricing = await kq.fetchPricing();
console.log(`Plans: ${pricing.plans.length}`);
console.log(`Currency: ${pricing.currencySymbol}`);
```

#### `getPricing(): RawPricingApiResponse | null`

Get cached pricing data. Returns `null` if not yet fetched.

```typescript theme={null}
const pricing = kq.getPricing();
if (pricing) {
  pricing.plans.forEach(plan => {
    console.log(`${plan.displayName}: ${plan.price.priceType}`);
  });
}
```

#### `getPlan(identifier: string): RawPricingPlan | null`

Find an enabled plan by its identifier from cached pricing data.

```typescript theme={null}
const starterPlan = kq.getPlan('starter');
if (starterPlan) {
  const monthlyCharge = starterPlan.price.charges.find(c => c.chargePeriod === 'MONTHLY');
  console.log(`Monthly price: ${monthlyCharge?.priceData.amount}`);
}
```

#### `isPricingLoading(): boolean`

Check if pricing data is currently being fetched.

#### `getLastPricingError(): Error | null`

Get the last error from a pricing fetch.

#### `clearPricingCache(): void`

Clear cached pricing data and reset pricing state.

```typescript theme={null}
kq.clearPricingCache();
// Next call to getPricing will need a new fetch
```

### `renderPricing()` — DOM Binding

Automatically populate pricing into HTML elements using `data-kq-price` attributes. The SDK scans the DOM for elements with these attributes and fills in formatted prices.

```html theme={null}
<span data-kq-price="starter" data-kq-period="MONTHLY">Loading...</span>
<span data-kq-price="starter" data-kq-period="ANNUALLY">Loading...</span>
<span data-kq-price="free" data-kq-period="MONTHLY">Loading...</span>
```

```typescript theme={null}
const kq = kelviqSDK({
  productId: "your-product-id",
  accessToken: "your-access-token",
});

await kq.ready();
kq.renderPricing(); // Scans DOM and populates data-kq-price elements
```

* `data-kq-price` — the plan identifier (e.g., `"starter"`, `"pro"`)
* `data-kq-period` — the charge period (e.g., `"MONTHLY"`, `"ANNUALLY"`)
* Free plans display `"Free"` instead of a price
* Disabled or missing plans result in empty text

You can scope the scan to a container and pass formatting options:

```typescript theme={null}
kq.renderPricing({
  container: document.getElementById('pricing-section'),
  formatOptions: { compact: true },
});
```

### `kqFormatPrice` Utility

A standalone utility for formatting prices with locale-aware currency formatting.

```typescript theme={null}
import { kqFormatPrice } from '@kelviq/js-sdk';

kqFormatPrice(49.99, '$');                    // "$49.99"
kqFormatPrice(1299, '$', { compact: true });  // "$1.3K"
kqFormatPrice(100, '€', { locale: 'de-DE' }); // "€100"
kqFormatPrice(29, '$', { includeCurrencySymbol: false }); // "29"
```

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

## Entitlement Types

The SDK uses a single, unified `Entitlement` interface. When the API returns duplicate `featureId`s (e.g., from Subscriptions + Top-ups), they are aggregated automatically.

```typescript theme={null}
interface Entitlement {
  featureId: string;
  featureType: "METER" | "BOOLEAN" | "CUSTOMIZABLE";
  hasAccess: boolean;         // METER: remaining === null || remaining > 0; others: true if any item grants access
  hardLimit: boolean;         // true if any item has a hard limit
  currentUsage: number;       // METER: summed across items; CUSTOMIZABLE: from first item; BOOLEAN: 0
  usageLimit: number | null;  // METER: summed across items; CUSTOMIZABLE: from first item; BOOLEAN: null
  remaining: number | null;   // METER: usageLimit - currentUsage; CUSTOMIZABLE: from first item; BOOLEAN: null
  items: RawEntitlement[];    // The raw array of original API objects for this featureId
}
```

Per-item details such as `resetAt` and `hardLimit` are available on each item in the `items` array.

## Pricing Types

The pricing API returns a rich data structure with plans, charges, features, and billing periods.

```typescript theme={null}
interface RawPricingApiResponse {
  organizationId: number;
  countryCode: string;
  pricingLocale: string;
  currencyCode: string;
  currencySymbol: string;
  currencyConversionRate: number;
  baseCurrencyCode: string;
  baseCurrencySymbol: string;
  plans: RawPricingPlan[];
  subscription: { hasPreviousSubscription: boolean };
  isCustomerExists: boolean;
  billingPeriods: RawPricingBillingPeriod[];
}

interface RawPricingPlan {
  name: string;
  identifier: string;
  displayName: string;
  displayDescription: string;
  price: RawPricingPrice;
  features: RawPricingFeature[];
  enabled: boolean;
  shouldHighlight: boolean;
  // ... additional fields
}

interface RawPricingPrice {
  priceType: 'FREE' | 'PAID';
  charges: RawPricingCharge[];
  freeTrial: boolean;
  trialPeriod: number;
  currency: string;
}

interface RawPricingCharge {
  chargePeriod: string;      // e.g., "MONTHLY", "ANNUALLY"
  priceData: { amount: number };
}
```

All pricing types are exported from the SDK: `RawPricingApiResponse`, `RawPricingPlan`, `RawPricingPrice`, `RawPricingCharge`, `RawPricingFeature`, `RawPricingBillingPeriod`.

## Examples

Real-world usage patterns and common implementation scenarios.

### Feature Gating

Control access to features based on user entitlements.

```typescript theme={null}
// Simple feature gate
if (kq.hasAccess('beta-features')) {
  showBetaFeatures();
}

// Conditional rendering
const canUseAdvancedSearch = kq.hasAccess('advanced-search');
return (
  <div>
    <BasicSearch />
    {canUseAdvancedSearch && <AdvancedSearch />}
  </div>
);
```

### Usage-Based Features

Implement usage limits and quota management.

```typescript theme={null}
const apiCalls = kq.getEntitlement('api-calls');
if (apiCalls?.hasAccess) {
  if (apiCalls.remaining === null || apiCalls.remaining > 0) {
    // Allow API call
    makeApiCall();
    console.log(`Usage: ${apiCalls.currentUsage} / ${apiCalls.usageLimit}`);
  } else {
    // Show upgrade message
    showUpgradeMessage();
  }
}
```

### Configuration-Based Features

Apply dynamic configuration based on user entitlements.

```typescript theme={null}
const teamSizeEntitlement = kq.getEntitlement('max-team-size');

if (teamSizeEntitlement?.hasAccess) {
  if (currentTeam.length >= (teamSizeEntitlement.usageLimit ?? Infinity)) {
    showUpgradePrompt(); // or disable the "Add Member" button
  } else {
    enableTeamInvite();
  }
}
```

### Accessing Per-Item Details

When you need item-level details like `resetAt` or `hardLimit`, use the `items` array or the `getRawEntitlement` method.

```typescript theme={null}
// Via the aggregated entitlement's items array
const emailQuota = kq.getEntitlement('email-sends');
if (emailQuota) {
  emailQuota.items.forEach(item => {
    console.log(`Reset at: ${item.resetAt}, Hard limit: ${item.hardLimit}`);
  });
}

// Or via getRawEntitlement
const rawItems = kq.getRawEntitlement('email-sends');
if (rawItems) {
  rawItems.forEach(item => {
    console.log(`Reset at: ${item.resetAt}, Hard limit: ${item.hardLimit}`);
  });
}
```

### Error Handling

Robust error handling for production applications.

```typescript theme={null}
const kq = kelviqSDK({
  customerId: "user123",
  accessToken: "token456",
  onError: (error) => {
    // Log to monitoring service
    logError('kelviq SDK Error', error);

    // Show user-friendly message
    showNotification('Unable to load feature settings', 'error');
  }
});

// Handle fetch errors
try {
  await kq.fetchAllEntitlements();
} catch (error) {
  console.error('Failed to load entitlements:', error);
  // Fallback to default behavior
  showDefaultFeatures();
}
```

### Custom API Request Service

Integrate with your existing HTTP client or add custom logic.

```typescript theme={null}
// Custom API request function
async function customApiRequest<T>(config: ApiRequestConfig): Promise<ApiResponse<T>> {
  const response = await fetch(config.url, {
    method: config.method || 'GET',
    headers: {
      'Authorization': `Bearer ${config.accessToken}`,
      'Content-Type': 'application/json',
      ...config.headers
    },
    body: config.body ? JSON.stringify(config.body) : undefined
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  return {
    status: response.status,
    statusText: response.statusText,
    data: await response.json()
  };
}

// Use custom API request
const kq = kelviqSDK({
  customerId: "user123",
  accessToken: "token456"
}, customApiRequest);
```

## TypeScript Support

Full TypeScript support with comprehensive type definitions.

The SDK is written in TypeScript and provides full type safety:

```typescript theme={null}
import kelviqSDK, {
  KelviqClientOptions,
  Entitlement,
  RawEntitlement,
  RawEntitlementsApiResponse
} from '@kelviq/js-sdk';

const options: KelviqClientOptions = {
  customerId: "user123",
  accessToken: "token456"
};

const kq = kelviqSDK(options);

// Type-safe entitlement access
const emailQuota: Entitlement | null = kq.getEntitlement('emails');
```

## Advanced Configuration

Advanced options for customizing SDK behavior and API integration.

### API Configuration

Configure API request behavior and retry logic.

```typescript theme={null}
const kq = kelviqSDK({
  customerId: "user123",
  accessToken: "token456",
  apiConfig: {
    maxRetries: 5,
    timeout: 10000,
    backoffBaseDelay: 2000
  }
});
```

### Custom Error Handling

Implement sophisticated error handling and recovery strategies.

```typescript theme={null}
const kq = kelviqSDK({
  customerId: "user123",
  accessToken: "token456",
  onError: (error) => {
    // Send to error tracking service
    Sentry.captureException(error);

    // Retry logic
    if (error.message.includes('network')) {
      setTimeout(() => kq.fetchAllEntitlements(), 5000);
    }
  }
});
```

### Advanced Options

Additional configuration options for specialized use cases.

Default values are:

```typescript theme={null}
const kq = kelviqSDK({
  customerId: "",
  accessToken: "",
  apiUrl: "https://edge.api.kelviq.com/api/v1",
  entitlementsPath: "entitlements/",
  apiConfig: {
    maxRetries: 3,
    timeout: 5000,
    backoffBaseDelay: 1000
  }
});
```

| Option                       | Type     | Description                                 |
| ---------------------------- | -------- | ------------------------------------------- |
| `apiUrl`                     | `string` | Base URL for the API                        |
| `entitlementsPath`           | `string` | Path for fetching entitlements              |
| `apiConfig.maxRetries`       | `number` | Maximum number of retries for API requests  |
| `apiConfig.timeout`          | `number` | Timeout for API requests in milliseconds    |
| `apiConfig.backoffBaseDelay` | `number` | Base delay for exponential backoff strategy |

## Using the Sandbox

If you want to use the sandbox, you need to set the `environment` option to `sandbox` in the `init` function.

```typescript theme={null}
const kq = kelviqSDK({
  customerId: "your-customer-id",
  accessToken: "your-access-token",
  environment: "sandbox"
});
```
