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

# Node SDK

> Changelog for @kelviq/node-sdk

<Update label="v2.7.3" description="July 21, 2026">
  ### New Features

  #### Activate Subscription Updates After Payment

  `client.subscriptions.update()` now supports `paymentBehavior`. Use the exported `PAYMENT_BEHAVIORS.ACTIVATE_ON_PAYMENT` constant to keep the current subscription active until payment for the update succeeds.

  ```typescript theme={null}
  import { CHARGE_PERIOD_CHOICES, PAYMENT_BEHAVIORS } from "@kelviq/node-sdk";

  const subscription = await client.subscriptions.update({
    subscriptionId: "78058918-9746-4280-9b9b-1bd5115eec6e",
    planIdentifier: "premium-plan",
    chargePeriod: CHARGE_PERIOD_CHOICES.MONTHLY,
    paymentBehavior: PAYMENT_BEHAVIORS.ACTIVATE_ON_PAYMENT,
  });
  ```

  The SDK serializes the option as `payment_behavior: "activate_on_payment"`. If payment is not completed and the pending update expires, the existing subscription remains active and unchanged.
</Update>

<Update label="v2.7.2" description="July 14, 2026">
  ### New Features

  #### List, Retrieve, and Create Subscriptions

  The subscriptions module now supports the complete read and create workflow:

  * `client.subscriptions.list()` retrieves a customer's paginated subscription list with optional pagination controls.
  * `client.subscriptions.retrieve()` retrieves a subscription by its Kelviq UUID.
  * `client.subscriptions.create()` creates a subscription directly for a customer.

  ```typescript theme={null}
  const page = await client.subscriptions.list({
    customerId: "cust_789",
    page: 1,
    pageSize: 20,
  });

  const subscription = await client.subscriptions.retrieve({
    subscriptionId: page.results[0].id,
  });

  const created = await client.subscriptions.create({
    planIdentifier: "plan-pro-monthly",
    chargePeriod: "MONTHLY",
    customerId: "cust_789",
  });
  ```

  New TypeScript models cover paginated results, create payloads, product and plan details, features, files, links, and issued licenses.
</Update>

<Update label="v2.7.0-beta.2" description="July 10, 2026">
  ### New Features

  #### Create One-Time Charges

  A new `client.charges.create()` method can immediately charge a customer's saved payment method without creating a checkout session.

  ```typescript theme={null}
  const charge = await client.charges.create({
    planIdentifier: "lifetime-access",
    chargePeriod: "ONE_TIME",
    customerId: "cust_789",
    currencyCode: "USD",
    features: [{ identifier: "seats", quantity: 5 }],
    ipAddress: "103.154.35.20",
  });

  console.log(charge.id, charge.status, charge.amount);
  ```

  The customer must already have a usable payment method on file. `chargePeriod` is restricted to `"ONE_TIME"`; use the subscriptions API for recurring billing. The SDK includes typed request and response models for charge records.
</Update>

<Update label="v2.7.0" description="July 4, 2026">
  ### New Features

  #### Caching & Offline Resilience

  The SDK now includes a two-tier cache that keeps entitlement checks fast and lets your app keep working during transient network/API outages.

  * **L1 — in-memory** (on by default): entitlements are cached per-process. Fresh entries (within the TTL) are served without a network call; if the API is unreachable or returns a 5xx, the last-known value is served as a fallback.
  * **L2 — distributed** (optional): a shared store (e.g. Redis) so the cache and the offline usage queue are shared across processes and containers, and survive restarts.

  Read-through order is **L1 → L2 → API**, with write-through to both on every successful fetch.

  ```typescript theme={null}
  const client = new Kelviq({
    accessToken: process.env.KELVIQ_API_KEY,
    enableCache: true,   // default true
    cacheTtlMs: 60_000,  // freshness window in ms (default 60000)
  });
  ```

  Set `enableCache: false` to disable caching entirely.

  #### Offline Usage Queue

  Usage/event reports that fail due to a network error are automatically **queued** and replayed on the next report call (or via `client.reporting.flush()`), so measurements are not lost during brief outages. Cached entitlement usage is updated optimistically so access checks stay consistent while offline.

  ```typescript theme={null}
  const remaining = await client.reporting.flush(); // returns count still queued
  ```

  #### Distributed Cache — `RedisStore`

  A ready-to-use `RedisStore` (backed by `ioredis`, an optional dependency) can be shared across multiple instances / containers so they use one cache and one durable usage queue.

  ```typescript theme={null}
  import { Kelviq, RedisStore } from '@kelviq/node-sdk';

  const store = new RedisStore({ url: 'redis://localhost:6379/0', prefix: 'kelviq:prod' });
  const client = new Kelviq({ accessToken: process.env.KELVIQ_API_KEY, cacheStore: store });
  ```

  You can also bring your own backend by implementing the public `CacheStore` interface and passing it as `cacheStore`. All instances that should share a cache must use the same `prefix`.
</Update>

<Update label="v2.6.0" description="June 5, 2026">
  ### New Features

  #### Checkout Session — `metadata` Parameter

  `CreateCheckoutSessionPayload` now accepts an optional `metadata` field for attaching arbitrary key-value pairs to a checkout session. The metadata is returned verbatim on the `checkout.completed` webhook payload's `metadata` field, letting you correlate a completed checkout back to your own records.

  ```typescript theme={null}
  const response = await client.checkout.createSession({
    planIdentifier: "plan-pro-monthly",
    chargePeriod: "MONTHLY",
    customerId: "cust_789",
    successUrl: "https://example.com/success",
    metadata: {
      orderId: "order_12345",
      referralCode: "SUMMER2026",
    },
  });
  ```

  Unlike other payload fields, the **keys inside `metadata` are preserved exactly as provided** — they are not converted to snake\_case on the way to the API, so they survive the round-trip back through webhooks unchanged.
</Update>

<Update label="v2.5.0" description="May 10, 2026">
  ### New Features

  #### Subscription Update — `trialEnd` Parameter

  `subscriptions.update()` now accepts an optional `trialEnd` field on `UpdateSubscriptionPayload` to control the trial period when updating a subscription.

  Pass either:

  * The literal string `"now"` to end any active trial immediately.
  * An ISO 8601 datetime string (e.g. `"2025-12-31T23:59:59Z"`) to set a new trial end date. The datetime must be in the future. Naive datetimes (no timezone designator) are interpreted as UTC.
  * Omit the field to preserve the existing trial behavior on the subscription.

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

  // End the trial now
  await client.subscriptions.update({
    subscriptionId: "sub_node_78058918",
    planIdentifier: "premium-plan-node",
    chargePeriod: CHARGE_PERIOD_CHOICES.MONTHLY,
    trialEnd: "now",
  });

  // Extend the trial to a specific future date
  await client.subscriptions.update({
    subscriptionId: "sub_node_78058918",
    planIdentifier: "premium-plan-node",
    chargePeriod: CHARGE_PERIOD_CHOICES.MONTHLY,
    trialEnd: "2025-12-31T23:59:59Z",
  });
  ```

  The SDK validates `trialEnd` client-side — invalid datetime strings or past datetimes throw `InvalidRequestError` before the request is sent.
</Update>

<Update label="v2.4.0" description="May 4, 2026">
  ### New Features

  #### Checkout Session — New Optional Parameters

  Three new optional fields on `CreateCheckoutSessionPayload` give you more control over the hosted checkout page.

  **`discountsEnabled`** — controls whether the coupon/discount code field is shown. Defaults to `true`.

  **`lockEmail`** — when `true`, the email field is pre-filled and locked so the customer cannot change it. Defaults to `false`.

  **`defaultBillingCountry`** — ISO 3166-1 alpha-2 country code (e.g. `"US"`, `"GB"`) used to pre-fill the billing address country field.

  ```typescript theme={null}
  const response = await client.checkout.createSession({
    planIdentifier: "plan-pro-monthly",
    chargePeriod: "MONTHLY",
    customerId: "cust_789",
    successUrl: "https://example.com/success",
    discountsEnabled: false,
    lockEmail: true,
    defaultBillingCountry: "US",
  });
  ```
</Update>

<Update label="v2.3.0" description="April 25, 2026">
  ### New Features

  #### Webhook Verification

  A new `validateEvent` helper lets you securely verify incoming webhook requests from Kelviq in one step. It validates the HMAC-SHA256 signature using the three headers Kelviq attaches to every webhook delivery, then returns the parsed event object.

  ```typescript theme={null}
  import { validateEvent, WebhookVerificationError } from '@kelviq/node-sdk';
  import express from 'express';

  const app = express();

  app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    try {
      const event = validateEvent(
        req.body,
        req.headers,
        '<YOUR_WEBHOOK_SECRET>',
      );

      // Process the event
      console.log('Event type:', event.type);

      res.sendStatus(202);
    } catch (err) {
      if (err instanceof WebhookVerificationError) {
        return res.sendStatus(403);
      }
      throw err;
    }
  });
  ```

  **`validateEvent(payload, headers, secret)`**

  * `payload` — Raw request body as a `string` or `Buffer`. Must be the unparsed body — do not pass a pre-parsed JSON object.
  * `headers` — The request headers object (e.g. `req.headers` in Express). Header lookup is case-insensitive.
  * `secret` — Your webhook signing secret (`kq_whsec_...`) from the Kelviq dashboard.

  Returns the parsed event as `Record<string, unknown>`. Throws `WebhookVerificationError` if a required header is missing, the signature format is invalid, or the signature does not match.

  #### `WebhookVerificationError`

  New error class thrown by `validateEvent` when verification fails. Extends `Error`.
</Update>

<Update label="v2.2.0" description="April 13, 2026">
  ### New Features

  #### License Management Module

  A new `client.license` module provides full lifecycle management for software licenses.

  **`license.activate({ licenseKey, customerId?, instanceName?, metadata? })`**

  Activates a license key and creates a new instance. Returns a `LicenseActivateResponse` containing the `instanceId`, `activatedAt`, `expiresOn`, and the full `LicenseDetails` object.

  ```typescript theme={null}
  const response = await client.license.activate({
      licenseKey: "LIC-XXXX-YYYY-ZZZZ",
      customerId: "cust_789",
      instanceName: "My MacBook Pro",
      metadata: { os: "macOS", arch: "arm64" },
  });

  console.log(response.instanceId);
  console.log(response.license.activationUsage); // e.g. 1
  console.log(response.license.plan.product.name); // e.g. "Kelviq Engine"
  ```

  **`license.deactivate({ licenseKey, instanceId })`**

  Deactivates a specific license instance. Returns a `LicenseDeactivateResponse` with `message` and `deactivatedAt`.

  ```typescript theme={null}
  const response = await client.license.deactivate({
      licenseKey: "LIC-XXXX-YYYY-ZZZZ",
      instanceId: "8f3e2b1a-5c6d-4e9f-8a0b-1c2d3e4f5g6h",
  });
  console.log(response.message); // "License instance deactivated successfully."
  ```

  **`license.validate({ licenseKey, instanceId? })`**

  Validates a license key and optionally a specific instance. Returns a `LicenseValidateResponse` with `valid`, `code`, `detail`, `metadata`, and the full `LicenseDetails`.

  ```typescript theme={null}
  const response = await client.license.validate({
      licenseKey: "LIC-XXXX-YYYY-ZZZZ",
      instanceId: "8f3e2b1a-5c6d-4e9f-8a0b-1c2d3e4f5g6h",
  });

  if (response.valid) {
      console.log(`Valid — code: ${response.code}`);
  } else {
      console.log(`Invalid — ${response.detail}`);
  }
  ```

  #### New TypeScript Interfaces

  * **`LicenseDetails`** — Full license object with `id`, `licenseKey`, `activatedOn`, `expiresOn`, `activationUsage`, `activationLimit`, `enabled`, `customer`, `plan`, and `subscription`.
  * **`LicenseCustomer`** — `{ customerId, name?, email? }` nested in `LicenseDetails`.
  * **`LicensePlan`** — Expanded with `description`, `version`, `isLatest`, and `product` (nested `LicensePlanProduct`).
  * **`LicensePlanProduct`** — `{ id, identifier, name, taxCode?, createdOn?, modifiedOn? }`.
  * **`LicenseActivateResponse`**, **`LicenseDeactivateResponse`**, **`LicenseValidateResponse`** — Typed responses for each operation.

  #### `SubscriptionData` New Fields

  Three new fields derived from the subscription's `recurrence` string are now included in `SubscriptionData` (returned within `LicenseDetails.subscription` and customer subscription summaries):

  * **`billingType`** — `"SUBSCRIPTION"` if the plan has a recurrence, `"ONE_TIME"` otherwise.
  * **`recurrenceUnit`** — Integer unit from the recurrence string (e.g. `1` from `"1 month"`), or `null`.
  * **`recurrenceType`** — Recurrence period in uppercase (e.g. `"MONTH"`), or `null`.
</Update>

<Update label="v2.1.0" description="April 2, 2026">
  ### New Features

  #### Customer Portal Module

  A new `client.portal` module lets you create pre-authenticated customer portal sessions server-side and redirect customers directly to their self-serve portal.

  **`portal.createSession({ customerId })`**

  ```typescript theme={null}
  const session = await client.portal.createSession({
      customerId: "cust_789",
  });

  // Redirect the customer — no login required
  res.redirect(session.customerPortalUrl);
  ```

  Returns `CreatePortalSessionResponse` with:

  * `token` — Session token authenticating the portal session.
  * `email` — The customer's email address.
  * `customerPortalUrl` — Pre-authenticated URL to redirect the customer to.
</Update>

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

  #### Entitlements Aggregation Engine

  When a customer has multiple subscriptions, the API can return duplicate `featureId` entries. The SDK now automatically aggregates them into a single `Entitlement` object per feature:

  * Numeric fields (`usageLimit`, `currentUsage`, `remaining`) are **summed** across all entries
  * `hasAccess` is `true` if **any** raw entry grants access
  * `hardLimit` is `true` if **any** entry sets it
  * All raw entries are preserved in the `.items[]` array

  ```typescript theme={null}
  const entitlement = await client.entitlements.getEntitlement({
    customerId: "cust_123",
    featureId: "email-sends",
  });

  // Aggregated values across all subscriptions
  console.log(entitlement.hasAccess);    // true
  console.log(entitlement.usageLimit);   // 1500 (e.g., 1000 from base plan + 500 from top-up)
  console.log(entitlement.currentUsage); // 200
  console.log(entitlement.remaining);    // 1300

  // Per-subscription details
  entitlement.items.forEach(item => {
    console.log(item.usageLimit, item.resetAt);
  });
  ```

  #### `Entitlement` Interface

  New type representing an aggregated entitlement with an `items: EntitlementDetail[]` field containing the raw entries that were aggregated.

  #### `getRawEntitlement({ customerId, featureId })`

  Returns the raw API response for a specific feature (`CheckEntitlementsResponse` with `customerId` wrapper), without any aggregation:

  ```typescript theme={null}
  const raw = await client.entitlements.getRawEntitlement({
    customerId: "cust_123",
    featureId: "email-sends",
  });
  // { customerId: "cust_123", entitlements: [/* raw entries for email-sends */] }
  ```

  #### `getRawEntitlements({ customerId })`

  Returns the raw API response for all entitlements, without any aggregation:

  ```typescript theme={null}
  const raw = await client.entitlements.getRawEntitlements({
    customerId: "cust_123",
  });
  // { customerId: "cust_123", entitlements: [/* all raw entries */] }
  ```

  #### `client.subscription` Deprecated Alias

  A backward-compatible getter that maps `client.subscription` to `client.subscriptions`, so existing code continues to work during migration.

  ***

  ### Breaking Changes

  #### `client.subscription` Renamed to `client.subscriptions`

  The subscriptions module now uses the plural form for consistency with other modules (`client.customers`, `client.entitlements`, etc.):

  ```typescript theme={null}
  // Before
  const sub = await client.subscription.get({ subscriptionId: "sub_123" });

  // After
  const sub = await client.subscriptions.get({ subscriptionId: "sub_123" });
  ```

  The old `client.subscription` accessor still works as a deprecated alias.

  #### `getEntitlement()` Returns `Entitlement | null`

  Previously returned `CheckEntitlementsResponse` (the raw API shape with a `customerId` wrapper). Now returns a single aggregated `Entitlement` object with an `.items[]` array, or `null` if the feature is not found:

  ```typescript theme={null}
  // Before
  const response = await client.entitlements.getEntitlement({
    customerId: "cust_123",
    featureId: "email-sends",
  });
  // { customerId: "cust_123", entitlements: [...] }

  // After
  const entitlement = await client.entitlements.getEntitlement({
    customerId: "cust_123",
    featureId: "email-sends",
  });
  // { featureId: "email-sends", hasAccess: true, currentUsage: 200, ... }
  ```

  For the original raw API response, use `getRawEntitlement()`.

  #### `getAllEntitlements()` Removed

  Replaced by `getEntitlements()`, which returns `Record<string, Entitlement>` — a record keyed by `featureId` with aggregated values and an `.items[]` array:

  ```typescript theme={null}
  // Before
  const response = await client.entitlements.getAllEntitlements({ customerId: "cust_123" });

  // After
  const entitlements = await client.entitlements.getEntitlements({ customerId: "cust_123" });
  // { "email-sends": Entitlement, "api-calls": Entitlement, ... }
  ```

  For the original raw API response, use `getRawEntitlements()`.

  #### `FeatureType` Changed: `"LIMIT"` → `"CUSTOMIZABLE"`

  The `FeatureType` union no longer includes `"LIMIT"`. Update any code that matches on this value:

  ```typescript theme={null}
  // Before
  if (entitlement.featureType === "LIMIT") { ... }

  // After
  if (entitlement.featureType === "CUSTOMIZABLE") { ... }
  ```

  #### `resetAt` Removed from Aggregated `Entitlement`

  Since `resetAt` can differ across subscriptions, it is only available on individual items:

  ```typescript theme={null}
  // Before
  console.log(entitlement.resetAt);

  // After
  entitlement.items.forEach(item => {
    console.log(item.resetAt); // per-subscription reset date
  });
  ```

  ***

  ### Fixed

  * Fixed Python-style `try/except` syntax in subscription update documentation — replaced with JavaScript `try...catch`.
  * Added missing `try/catch` error handling to subscription cancel documentation example.
  * Removed invalid JSON comments (`// Server-generated UUID`) from documentation response blocks.
  * Fixed trailing comma in checkout session JSON response example.
  * Fixed `"Node SDK Use"` typo → `"Node SDK User"` in create customer documentation.
  * Fixed `True` → `true`, `bool` → `boolean` in `hasAccess` documentation.
  * Replaced "Pydantic model" references with "TypeScript Interface" or "Object".
  * Replaced "Dictionary" with "Object" in parameter descriptions.
  * Fixed Python-style `ACCESS_TOKEN = "..."` → `const ACCESS_TOKEN = "...";` in setup example.
</Update>
