# Docs for agents
Source: https://docs.kelviq.com/ai/docs-for-agents
Give your coding agent the Kelviq docs through llms.txt or the docs MCP server
Point your agent at the Kelviq docs so it answers from them instead of guessing.
| Resource | Use it for |
| - | - |
| [`llms.txt`](https://docs.kelviq.com/llms.txt) | An index of every docs page, in Markdown |
| [`llms-full.txt`](https://docs.kelviq.com/llms-full.txt) | All the docs in one file |
| `https://docs.kelviq.com/mcp` | Docs MCP server: search and read the docs from any MCP client |
In Claude Code, add the docs MCP server with:
```bash theme={null}
claude mcp add --transport http kelviq-docs https://docs.kelviq.com/mcp
```
In other MCP clients, add `https://docs.kelviq.com/mcp` as a remote HTTP server.
# Kelviq and AI
Source: https://docs.kelviq.com/ai/overview
Set up Kelviq with your coding agent, or connect your agent to Kelviq and its docs
Kelviq works with the AI tools you already use. Your coding agent can create your plans, add checkout and feature checks to your app, and wire up webhooks. You review and approve each change.
## For humans
> **[Set up with AI](/guides/set-up-with-ai)**
>
> Copy a prompt from the dashboard and paste it into Claude Code, Cursor, Codex, Lovable, Replit, or Bolt.
> **[CLI](/cli/overview)**
>
> Define products, features, and plans as code and sync them from the terminal.
The quickest start:
1. Open **Settings → Set up with AI** in the dashboard.
2. Copy the keys block into your project's `.env.local`.
3. Copy the prompt and paste it into your agent as the first message.
Your agent can build and test everything with sandbox keys, then you go live when it's ready. You can also set it up directly in production. Either way, business verification and your keys stay with you.
## For agents
> **[Agent Toolkit](/guides/agent-toolkit)**
>
> The MCP server plus skills that teach Claude Code how Kelviq works.
> **[MCP server](/guides/mcp-server)**
>
> Manage customers, plans, subscriptions, and more from any MCP client.
> **[Docs for agents](/ai/docs-for-agents)**
>
> `llms.txt` and a docs MCP server, so your agent answers from the Kelviq docs.
# Authentication
Source: https://docs.kelviq.com/api-reference/authentication
Authenticate securely with environment-scoped Kelviq API keys
The Kelviq API uses API keys to authenticate requests. All API requests must be made over HTTPS; calls made over plain HTTP will fail. API requests without authentication will also fail.
## API Keys
Kelviq provides two types of API keys for different use cases.
> **Client API Key**
>
> Used for frontend applications where the key is publicly accessible.
> **Server API Key**
>
> Used for backend implementations. This key has more privileges and must be kept secret.
> [!NOTE]
>
> **Environment Scoping:** API keys are scoped per environment. This means your **production** and **sandbox** environments will have separate and distinct API keys.
[Learn how the sandbox works →](/guides/sandbox)
> [!WARNING]
>
> **Important Security Notice:** Do not share your **Server API Key** in any publicly accessible areas such as frontend code, client-side applications, or public repositories.
### Obtaining Your API Key
You can get your API key from the Kelviq application dashboard.
1. Navigate to **Settings**.
2. Go to the [**API keys**](https://app.kelviq.com/settings/api-keys) section.
3. Copy the key appropriate for your integration:
* **Client API Key** for supported calls made from browsers or distributed applications.
* **Server API Key** for calls made from your trusted backend.
***
## Authorization
To authenticate your requests, pass the appropriate API key as a Bearer token in the `Authorization` header.
```bash theme={null}
Authorization: Bearer YOUR_API_KEY
```
Most management APIs require a Server API Key. The License Activate, Validate, and Deactivate APIs accept either a Client API Key or a Server API Key, allowing license lifecycle calls from both customer-facing software and trusted backends. Check each endpoint's authentication note before using a Client API Key.
[Learn about license API authentication →](/product-catalog/license-keys#authentication)
# Create a charge
Source: https://docs.kelviq.com/api-reference/charges/create-a-charge
/api-reference/openapi.json post /charges/
Creates a one-time charge billed immediately against the customer's default payment method, without requiring a checkout session.
The customer must already have a usable payment method on file (a saved payment method or an active subscription). If no default payment method exists, or the off-session charge is declined or requires authentication, the request fails with a `400`.
> **Note:** `chargePeriod` must be `ONE_TIME`. Recurring charge periods are not supported by this endpoint — use [Create a subscription](/api-reference/subscriptions/create-a-subscription) instead.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List checkout session events
Source: https://docs.kelviq.com/api-reference/checkout-session-events/list-checkout-session-events
/api-reference/openapi.json get /checkout/sessions/{session_id}/events/
Returns a paginated list of timeline events for a checkout session (e.g. session created, customer details updated, payment attempted, payment completed), newest first.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List checkout sessions
Source: https://docs.kelviq.com/api-reference/checkout-sessions/list-checkout-sessions
/api-reference/openapi.json get /checkout/sessions/
Returns a paginated list of checkout sessions for the authenticated organization, newest first.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Retrieve a checkout session
Source: https://docs.kelviq.com/api-reference/checkout-sessions/retrieve-a-checkout-session
/api-reference/openapi.json get /checkout/sessions/{session_id}/
Returns a single checkout session by ID, including the order, subscription, and payment method it produced (if any).
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Create a checkout session
Source: https://docs.kelviq.com/api-reference/checkout/create-a-checkout-session
/api-reference/openapi.json post /checkout/
Creates a new checkout session for a customer to proceed with a purchase or subscription.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Create a customer
Source: https://docs.kelviq.com/api-reference/customers/create-a-customer
/api-reference/openapi.json post /customers/
Creates a new customer record within kelviq.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List customers
Source: https://docs.kelviq.com/api-reference/customers/list-customers
/api-reference/openapi.json get /customers/
Retrieves a paginated list of customers for your organization, ordered by creation date (newest first). You can search across the customer's name, email and `customerId`.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Update a customer
Source: https://docs.kelviq.com/api-reference/customers/update-a-customer
/api-reference/openapi.json patch /customers/{customerId}/
Performs a partial update on an existing customer using their client-defined `customerId`.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Create a discount
Source: https://docs.kelviq.com/api-reference/discounts/create-a-discount
/api-reference/openapi.json post /discount/
Creates a new discount code.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Delete a discount
Source: https://docs.kelviq.com/api-reference/discounts/delete-a-discount
/api-reference/openapi.json delete /discount/{pk}/
Soft-deletes (archives) the discount. Already-archived discounts return 400.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List discounts
Source: https://docs.kelviq.com/api-reference/discounts/list-discounts
/api-reference/openapi.json get /discount/
Returns a paginated list of active, non-archived discounts for the authenticated organization, newest first.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Retrieve a discount
Source: https://docs.kelviq.com/api-reference/discounts/retrieve-a-discount
/api-reference/openapi.json get /discount/{pk}/
Fetches a single discount by ID.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Update a discount
Source: https://docs.kelviq.com/api-reference/discounts/update-a-discount
/api-reference/openapi.json patch /discount/{pk}/
Toggles a discount on or off. Every other field is immutable after creation.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Retrieve customer entitlements
Source: https://docs.kelviq.com/api-reference/entitlements/retrieve-customer-entitlements
/api-reference/openapi.json get /entitlements/
Retrieves detailed entitlement information for a customer.
- If `feature_id` is provided as a query parameter, returns details for that specific feature.
- If `feature_id` is omitted, returns all entitlements for the customer.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Archive a feature
Source: https://docs.kelviq.com/api-reference/features/archive-a-feature
/api-reference/openapi.json delete /catalog/features/{pk}/
Soft-deletes the feature and updates any published pricing tables that referenced it.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Create a feature
Source: https://docs.kelviq.com/api-reference/features/create-a-feature
/api-reference/openapi.json post /catalog/features/
Creates a new feature. For METER features, `featureDetails` must include a `featureSubType` (PRE_AGGREGATED_USAGE or RAW_EVENTS) and `units`. RAW_EVENTS additionally requires a `meter` object with `aggregation` and `filters`.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List features
Source: https://docs.kelviq.com/api-reference/features/list-features
/api-reference/openapi.json get /catalog/features/
Returns a paginated list of features for the organization. Archived features are excluded.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Retrieve a feature
Source: https://docs.kelviq.com/api-reference/features/retrieve-a-feature
/api-reference/openapi.json get /catalog/features/{pk}/
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Introduction
Source: https://docs.kelviq.com/api-reference/introduction
Base URLs, environments, and authentication for the Kelviq API
The Kelviq API is available in two environments. Use the sandbox while building and testing your integration, then switch to production when you are ready to accept live payments.
## Base URLs
### Production
Use for live customers, production data, and real payments.
| API | Base URL |
| - | - |
| Core API | `https://api.kelviq.com/api/v1` |
| Edge API | `https://edge.api.kelviq.com/api/v1` |
### Sandbox
Use for development, integration testing, and test payments.
| API | Base URL |
| - | - |
| Core API | `https://sandboxapi.kelviq.com/api/v1` |
| Edge API | `https://edge.sandboxapi.kelviq.com/api/v1` |
The Core API manages resources such as products, plans, customers, subscriptions, and checkout sessions. The Edge API serves latency-sensitive features such as entitlements and promotions.
> [!NOTE]
>
> Sandbox and production are isolated. API keys and data created in one environment do not work or appear in the other.
[Learn how the sandbox works →](/guides/sandbox)
## Authentication
Kelviq uses API keys. Send your Server API Key as a Bearer token when calling the API from your backend:
```bash theme={null}
curl https://sandboxapi.kelviq.com/api/v1/catalog/products/ \
--header "Authorization: Bearer $KELVIQ_SANDBOX_SERVER_API_KEY" \
--header "Accept: application/json"
```
Use a Client API Key for supported browser and distributed-app integrations. For example, the license activation, validation, and deactivation endpoints accept either a Client API Key or a Server API Key. Never expose a Server API Key in client-side code.
[Read the authentication guide →](/api-reference/authentication)
## Switching environments
For direct API calls, change the base URL and use the matching environment's API key. Official Kelviq SDKs accept an `environment` option:
```typescript Node.js theme={null}
import { Kelviq } from "@kelviq/node-sdk";
const client = new Kelviq({
accessToken: process.env.KELVIQ_SANDBOX_SERVER_API_KEY,
environment: "sandbox",
});
```
```python Python theme={null}
from kelviq_sdk import Kelviq
client = Kelviq.create_sync_client(
access_token=KELVIQ_SANDBOX_SERVER_API_KEY,
environment="sandbox",
)
```
Omit `environment` or set it to `production` for live traffic.
## Pagination
List endpoints that support pagination use the `page` and `page_size` query parameters.
| Parameter | Type | Description |
| - | - | - |
| `page` | integer | Page number to return. |
| `page_size` | integer | Number of results to return per page. The maximum is `100`. |
Pagination defaults depend on the resource:
| Endpoints | Default `page_size` | Maximum `page_size` |
| - | -: | -: |
| Products, Plans, Plan Versions, and Features | `100` | `100` |
| All other paginated APIs | `10` | `100` |
You can request fewer results by setting `page_size`. Values above `100` are capped at `100`.
```bash theme={null}
curl "https://sandboxapi.kelviq.com/api/v1/customers/?page=2&page_size=10" \
--header "Authorization: Bearer $KELVIQ_SANDBOX_SERVER_API_KEY"
```
Paginated responses contain the total result count, links to adjacent pages, and the current page of results:
```json theme={null}
{
"count": 250,
"next": "https://sandboxapi.kelviq.com/api/v1/customers/?page=3&page_size=10",
"previous": "https://sandboxapi.kelviq.com/api/v1/customers/?page=1&page_size=10",
"results": []
}
```
## Filtering
Search and filter parameters vary by endpoint and can be combined with pagination when supported.
Use the `search` query parameter for free-text search and endpoint-specific query parameters for filtering.
```bash theme={null}
# Search customers
curl "https://sandboxapi.kelviq.com/api/v1/customers/?search=acme&page=1&page_size=10" \
--header "Authorization: Bearer $KELVIQ_SANDBOX_SERVER_API_KEY"
# Filter subscriptions by customer
curl "https://sandboxapi.kelviq.com/api/v1/subscriptions/?customer_id=customer_123" \
--header "Authorization: Bearer $KELVIQ_SANDBOX_SERVER_API_KEY"
```
Consult the endpoint reference before adding a query parameter; unsupported parameters may be ignored or rejected.
## Rate limits
To keep the API responsive for everyone, requests are rate limited per organization. The limit applies to all endpoints authenticated with a Server or Client API Key, and differs by environment:
| Environment | Limit |
| - | - |
| Production | `500 requests` / minute |
| Sandbox | `100 requests` / minute |
The limit applies to your whole organization, not to an individual API key — Server and Client API Key requests share the same quota. Sandbox and production are isolated environments, so each maintains its own limit.
If you exceed the limit, the API returns a `429 Too Many Requests` response with a `Retry-After` header indicating how many seconds to wait before retrying:
```json theme={null}
{
"detail": "Request was throttled. Expected available in 42 seconds."
}
```
> [!TIP]
>
> Honor the `Retry-After` header and back off before retrying. If your integration regularly approaches the limit, batch or cache requests where possible rather than polling.
## Error handling
The API uses standard HTTP status codes and returns detailed error information in the response body.
| HTTP Status | Name | Description |
| - | - | - |
| `400` | Bad Request | The request body or parameters are invalid, or a business rule prevented the operation. |
| `401` | Unauthorized | The API key is missing, invalid, or belongs to a different environment. |
| `403` | Forbidden | The credentials are valid, but the requested action is not allowed. |
| `404` | Not Found | The requested resource does not exist. |
| `405` | Method Not Allowed | The HTTP method is not supported for the requested endpoint. |
| `409` | Conflict | The request conflicts with an existing resource, such as a duplicate account. |
| `422` | Unprocessable Entity | The request is well-formed but contains invalid or unsupported values. |
| `428` | Precondition Required | A required account configuration, such as billing-provider authentication, is incomplete. |
| `429` | Too Many Requests | The [rate limit](#rate-limits) has been exceeded. |
| `500` | Internal Server Error | Kelviq could not complete the request because of an unexpected server error. |
| `502` | Bad Gateway | Kelviq received an invalid response from an upstream service. |
| `503` | Service Unavailable | The service is temporarily unavailable. |
| `504` | Gateway Timeout | An upstream service did not respond in time. |
# List invoices
Source: https://docs.kelviq.com/api-reference/invoices/list-invoices
/api-reference/openapi.json get /invoices/
Returns a paginated list of invoices for the authenticated organization, newest first. Optionally filter by subscription or customer.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Retrieve an invoice
Source: https://docs.kelviq.com/api-reference/invoices/retrieve-an-invoice
/api-reference/openapi.json get /invoices/{invoiceId}/
Retrieves an invoice by its Kelviq invoice ID.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Activate License
Source: https://docs.kelviq.com/api-reference/license/activate-license
/api-reference/openapi.json post /license/activate/
Activates a license key for a specific instance. Accepts either a Client API Key or a Server API Key as a Bearer token.
> [!NOTE]
>
> This endpoint accepts either a **Client API Key** or a **Server API Key** as a Bearer token. Use a Client API Key in browser or distributed client software, and keep Server API Keys on trusted backends. Select the matching Sandbox or Production host and key.
# Deactivate License
Source: https://docs.kelviq.com/api-reference/license/deactivate-license
/api-reference/openapi.json post /license/deactivate/
Deactivates a specific license instance. Accepts either a Client API Key or a Server API Key as a Bearer token.
> [!NOTE]
>
> This endpoint accepts either a **Client API Key** or a **Server API Key** as a Bearer token. Use a Client API Key in browser or distributed client software, and keep Server API Keys on trusted backends. Select the matching Sandbox or Production host and key.
# Validate License
Source: https://docs.kelviq.com/api-reference/license/validate-license
/api-reference/openapi.json post /license/validate/
Validates a license key and, when provided, a specific instance. Accepts either a Client API Key or a Server API Key as a Bearer token.
> [!NOTE]
>
> This endpoint accepts either a **Client API Key** or a **Server API Key** as a Bearer token. Use a Client API Key in browser or distributed client software, and keep Server API Keys on trusted backends. Select the matching Sandbox or Production host and key.
# Generate a presigned upload URL
Source: https://docs.kelviq.com/api-reference/media/generate-a-presigned-upload-url
/api-reference/openapi.json post /media/
Returns a presigned S3 POST policy that lets you upload a single file directly to kelviq's storage bucket (max 5 GB, expires in 1 hour). Use the returned `fileKey` as the value of `imageKeys` / `fileKeys` on the catalog product/plan endpoints.
Upload flow:
1. `POST /media/` with `fileName` and `fileType`.
2. Build a `multipart/form-data` request and send every key/value from `fields` as a form field, plus a final `file` field containing the binary.
3. `POST` that form to the returned `url`. A `204 No Content` response means the upload succeeded.
4. Pass `fileKey` to the relevant catalog endpoint (e.g. `POST /catalog/products/` with `imageKeys: [fileKey]`).
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Get Product Offering
Source: https://docs.kelviq.com/api-reference/monetization/get-product-offering
/api-reference/openapi.json get /monetization/product-offering/{product_id}/
Retrieves the public pricing details for a specific product, including all eligible plans, localized pricing, and the customer's current subscription state. Use this to render a pricing page or paywall scoped to a product.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List order events
Source: https://docs.kelviq.com/api-reference/order-events/list-order-events
/api-reference/openapi.json get /orders/{id}/events/
Returns a paginated list of timeline events for an order (e.g. order created, order completed, order receipt sent, invoice paid), newest first. This includes every order tied to the same underlying subscription as the given order, not just that single order record.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List orders
Source: https://docs.kelviq.com/api-reference/orders/list-orders
/api-reference/openapi.json get /orders/
Returns a paginated list of orders for the authenticated organization, newest paid first. Orders in `PENDING` status (payment never attempted/completed) and orders with no attached customer are never included.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Retrieve an order
Source: https://docs.kelviq.com/api-reference/orders/retrieve-an-order
/api-reference/openapi.json get /orders/{id}/
Returns a single order by ID.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Get a partner organization by external ID
Source: https://docs.kelviq.com/api-reference/partner/get-a-partner-organization-by-external-id
/api-reference/openapi.json get /partner/organizations/by-external-id/{externalId}/
Retrieve a single organization owned by the authenticated partner using the partner-side `externalId`.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Get a partner organization by identifier
Source: https://docs.kelviq.com/api-reference/partner/get-a-partner-organization-by-identifier
/api-reference/openapi.json get /partner/organizations/{identifier}/
Retrieve a single organization owned by the authenticated partner, looked up by its kelviq UUID.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List partner organizations
Source: https://docs.kelviq.com/api-reference/partner/list-partner-organizations
/api-reference/openapi.json get /partner/organizations/
Returns organizations owned by the authenticated partner. Optionally filter by the partner-side `externalId`.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Provision an organization
Source: https://docs.kelviq.com/api-reference/partner/provision-an-organization
/api-reference/openapi.json post /partner/organizations/
Creates a new kelviq user + organization on behalf of the partner, issues API keys, and emails the user a link to set their password. Fails with `409` if `email` belongs to an existing kelviq user, or with `400` if `externalId` already exists for this partner.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List payment methods
Source: https://docs.kelviq.com/api-reference/payment-methods/list-payment-methods
/api-reference/openapi.json get /payment-methods/
Returns a paginated list of saved payment methods for the authenticated organization, ordered by creation date (newest first). Only payment methods with status `succeeded` are included. Narrow results with `customer_id` and/or `customer_email`.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Add plan entitlements
Source: https://docs.kelviq.com/api-reference/plan-entitlements/add-plan-entitlements
/api-reference/openapi.json post /catalog/plans/{identifier}/features/
Appends one or more feature entitlements to the plan. If the plan's latest version is published, a new draft version is created automatically.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Delete plan entitlement
Source: https://docs.kelviq.com/api-reference/plan-entitlements/delete-plan-entitlement
/api-reference/openapi.json delete /catalog/plans/{identifier}/features/{feature_id}/
Removes the named feature from the plan's entitlements. A new draft version is created if the latest is published.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List plan entitlements
Source: https://docs.kelviq.com/api-reference/plan-entitlements/list-plan-entitlements
/api-reference/openapi.json get /catalog/plans/{identifier}/features/
Returns the feature entitlements (with per-entitlement `details`) configured on the plan.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Update plan entitlement
Source: https://docs.kelviq.com/api-reference/plan-entitlements/update-plan-entitlement
/api-reference/openapi.json patch /catalog/plans/{identifier}/features/{feature_id}/
Updates only the `details` (value, reset, hardLimit, rollover, etc.) of one feature entitlement on the plan. A new draft version is created if the latest is published.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Delete a plan file
Source: https://docs.kelviq.com/api-reference/plan-files/delete-a-plan-file
/api-reference/openapi.json delete /catalog/plans/{identifier}/file/{pk}/
Deletes the plan file. If the plan's latest version is published, a new draft version is created and the file is removed from the new draft.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Download a plan file
Source: https://docs.kelviq.com/api-reference/plan-files/download-a-plan-file
/api-reference/openapi.json get /catalog/plans/{identifier}/file/{pk}/download/
Public download endpoint for a plan file. Responds with a 302 redirect to a short-lived forced-download URL on the storage backend. Returns 404 if the file does not exist.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Update a plan file
Source: https://docs.kelviq.com/api-reference/plan-files/update-a-plan-file
/api-reference/openapi.json patch /catalog/plans/{identifier}/file/{pk}/
Updates display metadata on a plan file (name, ordering, enabled). If the plan's latest version is published, a new draft version is created and the file is updated on the new draft.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Archive a plan
Source: https://docs.kelviq.com/api-reference/plans/archive-a-plan
/api-reference/openapi.json delete /catalog/plans/{identifier}/
Archives all versions of the plan (sets `isArchived=true`).
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Create a plan
Source: https://docs.kelviq.com/api-reference/plans/create-a-plan
/api-reference/openapi.json post /catalog/plans/
Creates a new plan against a product. The new plan starts as an unpublished draft (`isLatest=false`) until `POST /plans/{identifier}/publish/` is called.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Discard draft plan
Source: https://docs.kelviq.com/api-reference/plans/discard-draft-plan
/api-reference/openapi.json delete /catalog/plans/{identifier}/discard/
Permanently deletes the latest draft (non-published) version of the plan. The previously published version remains untouched. Returns 400 if the latest version is already published.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List plan prices
Source: https://docs.kelviq.com/api-reference/plans/list-plan-prices
/api-reference/openapi.json get /catalog/plans/{identifier}/prices/
Returns the prices configured on the plan (one per currency), each with its full per-feature charge configuration.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List plans
Source: https://docs.kelviq.com/api-reference/plans/list-plans
/api-reference/openapi.json get /catalog/plans/
Returns one row per plan identifier, picking the most recent version for each. Archived plans, or plans on archived products, are excluded.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Publish a plan
Source: https://docs.kelviq.com/api-reference/plans/publish-a-plan
/api-reference/openapi.json post /catalog/plans/{identifier}/publish/
Promotes the most recent draft version of the plan to `isLatest=true`, demotes the previous published version. Optionally migrates existing customers' features or pricing. Fails with 400 if no price exists or required currencies are missing.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Retrieve a plan
Source: https://docs.kelviq.com/api-reference/plans/retrieve-a-plan
/api-reference/openapi.json get /catalog/plans/{identifier}/
Fetches a plan by identifier. Pass `?version=N` to fetch a specific version; otherwise the latest version is returned.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Update a plan
Source: https://docs.kelviq.com/api-reference/plans/update-a-plan
/api-reference/openapi.json patch /catalog/plans/{identifier}/
Partial update. If the latest version is already published (`isLatest=true`), a new draft version is created automatically; otherwise the existing draft is mutated.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Update plan prices
Source: https://docs.kelviq.com/api-reference/plans/update-plan-prices
/api-reference/openapi.json post /catalog/plans/{identifier}/prices/bulk/
Atomically replaces the plan's prices with the supplied list. If the plan's latest version is published, a new draft version is created and the new prices are attached to it. Duplicate currencies among PAID prices are rejected.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Create a portal session
Source: https://docs.kelviq.com/api-reference/portal/create-a-portal-session
/api-reference/openapi.json post /portal/session/
Creates a new customer portal session, returning a token and URL to redirect the customer to their billing portal. The selected customer must have an email address because the signed session token is tied to that email.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Delete a product image
Source: https://docs.kelviq.com/api-reference/product-files/delete-a-product-image
/api-reference/openapi.json delete /catalog/products/{product_pk}/image/{pk}/
Permanently deletes the product image record.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Update a product image
Source: https://docs.kelviq.com/api-reference/product-files/update-a-product-image
/api-reference/openapi.json patch /catalog/products/{product_pk}/image/{pk}/
Updates image metadata such as `name`, `ordering`, `thumbnail`, or `enabled`. Setting `thumbnail` to true automatically clears the flag on the product's other images.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Retrieve product settings
Source: https://docs.kelviq.com/api-reference/product-settings/retrieve-product-settings
/api-reference/openapi.json get /catalog/products/{pk}/settings/
Retrieves the VPN/TOR/proxy and presentation settings for a product. Settings are auto-created if they don't already exist.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Update product settings
Source: https://docs.kelviq.com/api-reference/product-settings/update-product-settings
/api-reference/openapi.json patch /catalog/products/{pk}/settings/
Updates one or more product settings fields. Changing `currency` re-enables previously disabled prices in the new currency on the latest plans of that product.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Archive a product
Source: https://docs.kelviq.com/api-reference/products/archive-a-product
/api-reference/openapi.json delete /catalog/products/{pk}/
Soft-deletes (archives) the product. Already-archived products return 400.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Create a product
Source: https://docs.kelviq.com/api-reference/products/create-a-product
/api-reference/openapi.json post /catalog/products/
Creates a new product.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List products
Source: https://docs.kelviq.com/api-reference/products/list-products
/api-reference/openapi.json get /catalog/products/
Returns a paginated list of all products in the organization's catalog. Archived products are excluded.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Retrieve a product
Source: https://docs.kelviq.com/api-reference/products/retrieve-a-product
/api-reference/openapi.json get /catalog/products/{pk}/
Fetches a product by ID along with its currently published plans.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Update a product
Source: https://docs.kelviq.com/api-reference/products/update-a-product
/api-reference/openapi.json patch /catalog/products/{pk}/
Partially updates a product. New images and files can be attached by sending temporary S3 keys in `imageKeys` / `fileKeys`.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Get promotions
Source: https://docs.kelviq.com/api-reference/promotions/get-promotions
/api-reference/openapi.json get /promotions
Resolves the active promotion for the caller's detected location. Returns the applicable discount percentage, coupon code, banner widget configuration, and security check results.
Use your **Client API Key**, not your Server API Key. Find it under **Settings → API keys** in the Kelviq dashboard.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Create a refund
Source: https://docs.kelviq.com/api-reference/refunds/create-a-refund
/api-reference/openapi.json post /refunds/
Creates a full or partial refund for an order owned by the authenticated organization. Specify either `amountUnits` or `amount` for a partial refund; omit both to refund the entire remaining refundable balance. The amount must be positive and cannot exceed the order's remaining refundable balance.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List refunds
Source: https://docs.kelviq.com/api-reference/refunds/list-refunds
/api-reference/openapi.json get /refunds/
Returns a paginated list of refunds for the authenticated organization, ordered newest first.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Retrieve a refund
Source: https://docs.kelviq.com/api-reference/refunds/retrieve-a-refund
/api-reference/openapi.json get /refunds/{refundId}/
Returns a refund owned by the authenticated organization.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Report pre-aggregated usage
Source: https://docs.kelviq.com/api-reference/reporting/report-pre-aggregated-usage
/api-reference/openapi.json post /report/usage/
Reports pre-aggregated feature usage from your application to kelviq.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Accept a retention offer
Source: https://docs.kelviq.com/api-reference/subscriptions/accept-a-retention-offer
/api-reference/openapi.json post /subscriptions/{subscriptionId}/retention/accept/
Applies the retention offer a customer accepted instead of cancelling.
Pass the cancellation reason the customer picked and the offer it maps to. `offerType` has to be the offer configured for that reason in the dashboard retention settings; any other pairing is rejected with `400`. Eligibility is checked again at this point, so an offer that was eligible when you fetched it can still be refused if, for example, the subscription was paused or scheduled to cancel in between.
What happens depends on the offer:
- `DISCOUNT` creates a single-use coupon for the configured percentage and billing cycles and attaches it to the subscription. The response includes the discounted amount and when the discount ends.
- `PAUSE` pauses the subscription for `pauseMonths`, which must be one of the durations returned by [Get retention offers](/api-reference/subscriptions/get-retention-offers). Billing and access resume on the returned date, exactly as a merchant-initiated pause would.
- `SUPPORT` records that the customer took the support offer and returns the configured support URL to send them to. Nothing changes on the subscription.
Every acceptance is recorded against the subscription and counted by the organization's offer repeat policy, so a customer cannot stack discounts or pause indefinitely by reopening the cancellation flow.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Cancel a subscription
Source: https://docs.kelviq.com/api-reference/subscriptions/cancel-a-subscription
/api-reference/openapi.json post /subscriptions/{subscriptionId}/cancel/
Cancels an active subscription for a customer.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Create a subscription
Source: https://docs.kelviq.com/api-reference/subscriptions/create-a-subscription
/api-reference/openapi.json post /subscriptions/create/
Creates a new subscription for a customer directly, without requiring a checkout session.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Get retention offers
Source: https://docs.kelviq.com/api-reference/subscriptions/get-retention-offers
/api-reference/openapi.json get /subscriptions/{subscriptionId}/retention/
Returns what this subscription can be offered for each cancellation reason, so you can build the cancellation flow in your own app.
Every cancellation reason is listed with the offer configured for it in the Kelviq dashboard retention settings: a discount, a pause, a link to support, or nothing. Each offer carries an eligibility verdict for this particular subscription. An ineligible offer is still returned with `ineligibleReason` set, so you can show it greyed out with an explanation rather than hide it.
When retention is switched off, `enabled` is `false` and every reason comes back with `offer: null`. The reason list is still returned so the same reason picker can be rendered either way.
To apply the offer the customer chooses, call [Accept a retention offer](/api-reference/subscriptions/accept-a-retention-offer). To record the cancellation instead, call [Cancel a subscription](/api-reference/subscriptions/cancel-a-subscription) with the same `cancellationFeedback`.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List Subscriptions
Source: https://docs.kelviq.com/api-reference/subscriptions/list-subscriptions
/api-reference/openapi.json get /subscriptions/
Retrieves a paginated list of subscriptions. Optionally filter the results by customer or status.
By default (no `status` filter), the response contains exactly one row per subscription: its most recent state, excluding records that have expired (`end_date` in the past) or been superseded by a later update. When `status` is supplied, that deduplication and exclusion is skipped and matching subscription records are returned directly — so a subscription that has passed through the requested status more than once, or whose only matching record has since expired or been superseded, can appear more than once or in a state you didn't expect. Use `status` for point-in-time queries (e.g. "show me everything currently `active`"); omit it when you want the current, deduplicated view of each subscription.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Preview a subscription update
Source: https://docs.kelviq.com/api-reference/subscriptions/preview-a-subscription-update
/api-reference/openapi.json post /subscriptions/{subscriptionId}/update/preview/
Calculates the recurring price, next invoice line items and total, and any amount that would be charged immediately for a proposed subscription update. The request uses the same plan, charge-period, and feature fields as [Update a subscription](/api-reference/subscriptions/update-a-subscription). This endpoint does not change the subscription, create an invoice, charge the customer, or write the proposed update to Kelviq.
Amount fields without the `Units` suffix use the currency's major unit; fields ending in `Units` use its minor unit.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Retrieve a Subscription
Source: https://docs.kelviq.com/api-reference/subscriptions/retrieve-a-subscription
/api-reference/openapi.json get /subscriptions/{subscriptionId}/
Retrieves a subscription by its Kelviq subscription ID.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Update a subscription
Source: https://docs.kelviq.com/api-reference/subscriptions/update-a-subscription
/api-reference/openapi.json post /subscriptions/{subscriptionId}/update/
Updates an existing subscription to a new plan, charge period, and/or features. Set `paymentBehavior` to `activate_on_payment` when the change should take effect only after payment succeeds.
> **Warning:** Free plans are created as one-time charges. Upgrading from a free plan is not supported via this endpoint. Use the [Create Checkout Session](/api-reference/checkout/create-a-checkout-session) endpoint instead.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List transactions
Source: https://docs.kelviq.com/api-reference/transactions/list-transactions
/api-reference/openapi.json get /transaction/
Returns a paginated list of financial transactions for the authenticated organization, ordered newest first. Each transaction includes an itemized Merchant of Record fee breakdown when applicable.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Create a webhook endpoint
Source: https://docs.kelviq.com/api-reference/webhook-endpoints/create-a-webhook-endpoint
/api-reference/openapi.json post /webhook/endpoints/
Registers a new destination URL and its event subscriptions. A signing secret is generated automatically and returned once in the response.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Delete a webhook endpoint
Source: https://docs.kelviq.com/api-reference/webhook-endpoints/delete-a-webhook-endpoint
/api-reference/openapi.json delete /webhook/endpoints/{pk}/
Permanently removes the webhook endpoint. It will stop receiving deliveries immediately.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List webhook endpoints
Source: https://docs.kelviq.com/api-reference/webhook-endpoints/list-webhook-endpoints
/api-reference/openapi.json get /webhook/endpoints/
Returns a paginated list of webhook endpoints configured for the authenticated organization.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Retrieve a webhook endpoint
Source: https://docs.kelviq.com/api-reference/webhook-endpoints/retrieve-a-webhook-endpoint
/api-reference/openapi.json get /webhook/endpoints/{pk}/
Fetches a single webhook endpoint by ID.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Update a webhook endpoint
Source: https://docs.kelviq.com/api-reference/webhook-endpoints/update-a-webhook-endpoint
/api-reference/openapi.json patch /webhook/endpoints/{pk}/
Partially updates a webhook endpoint's URL, event subscriptions, or enabled state.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# List webhook logs
Source: https://docs.kelviq.com/api-reference/webhooks/list-webhook-logs
/api-reference/openapi.json get /webhook/logs/
Returns a paginated list of webhook delivery attempts (including retries) for the authenticated organization, newest first. Results are always limited to the trailing 30 days — `start_date`/`end_date` can narrow that window further but can't widen it beyond 30 days back. Note that `start_date`/`end_date` filter against `lastAttempt` (when that specific delivery attempt happened), not `createdOn` (when the underlying event was first created) — the two can differ for retried deliveries.
> [!NOTE]
>
> Click the base URL in the API playground and select the **Sandbox** host for test data or the **Production** host for live data. Use credentials from the same environment.
# Node SDK
Source: https://docs.kelviq.com/backend-integration/node-sdk
Documentation for the Kelviq Node SDK
## Overview
The Kelviq Node SDK provides a convenient way to interact with the Kelviq REST APIs from your Node application.
## Installation
Install the Kelviq SDK using npm or yarn:
```shell theme={null}
npm install @kelviq/node-sdk
```
or
```shell theme={null}
yarn add @kelviq/node-sdk
```
## Prerequisites
Before you can initialize the client and use the SDK methods, you need a **Server API Key**.
You can obtain this key from the Kelviq application:
1. Navigate to Settings.
2. Go to the [API keys](https://app.kelviq.com/settings/api-keys) section.
3. Copy the **Server API Key**.
> [!WARNING]
>
> **Security Warning:** The Server API Key should never be exposed in client-side code. Use it only in your backend services. For client-side applications, use the Client API Key with the [React SDK](/frontend-integration/react-sdk) or [JavaScript SDK](/frontend-integration/js-sdk).
Once copied, add this key to your environment variables (recommended for security):
```typescript theme={null}
// Example of setting it in your code (environment variables recommended)
const ACCESS_TOKEN = process.env.KELVIQ_SERVER_API_KEY;
```
## Configuring the Client
The SDK provides a main `Kelviq` client class. You instantiate it directly with your configuration options.
```typescript theme={null}
const ACCESS_TOKEN: string = "YOUR_SECRET_ACCESS_TOKEN";
```
#### How to Create a Client
```typescript theme={null}
import { Kelviq } from '@kelviq/node-sdk';
const client = new Kelviq({ accessToken: ACCESS_TOKEN });
```
## Caching and offline resilience
Entitlement caching is enabled by default. The SDK keeps a per-process in-memory cache with a 60-second freshness window. Reads follow this order: fresh memory cache, optional distributed cache, then the Entitlement API. A successful API response refreshes both cache levels.
If the API is unreachable or returns a server error, the SDK returns the last known cached entitlement response when one exists. If the customer or feature has never been cached, the original request error is thrown.
```typescript theme={null}
const client = new Kelviq({
accessToken: process.env.KELVIQ_SERVER_API_KEY,
enableCache: true, // default: true
cacheTtlMs: 60_000, // default: 60 seconds
});
```
Set `enableCache: false` to disable caching.
To drop a customer's cached entitlements before the window expires, for example in a `subscription.paused`, `subscription.resumed`, or `subscription.cancelled` webhook handler:
```typescript theme={null}
await client.entitlements.invalidateCache({ customerId });
```
### Share the cache across instances
The in-memory cache and offline reporting queue belong to one Node process and disappear when it restarts. Use the optional Redis store when multiple processes or containers need one shared, durable cache and queue.
```shell theme={null}
npm install ioredis
```
```typescript theme={null}
import { Kelviq, RedisStore } from '@kelviq/node-sdk';
const store = new RedisStore({
url: process.env.REDIS_URL,
prefix: 'kelviq:production',
});
const client = new Kelviq({
accessToken: process.env.KELVIQ_SERVER_API_KEY,
cacheStore: store,
});
```
Use the same prefix for every instance that should share state.
### Replay queued usage
When a usage or event report fails because of a network problem, the SDK queues it and retries automatically on the next report call. You can also flush the queue explicitly:
```typescript theme={null}
const remaining = await client.reporting.flush();
```
`remaining` is the number of reports still queued. Without Redis, the queue exists only in the current process.
See [When the Entitlement API is unavailable](/product-catalog/entitlements#when-the-entitlement-api-is-unavailable) for access-control fallback guidance.
## Supported Functionalities
The SDK currently supports the following operations:
1. [Customers](/backend-integration/node-sdk#customers)
2. [Checkout](/backend-integration/node-sdk#checkout)
3. [Entitlements](/backend-integration/node-sdk#entitlements)
4. [Reporting](/backend-integration/node-sdk#reporting)
5. [Subscriptions](/backend-integration/node-sdk#subscriptions)
6. [Charges](/backend-integration/node-sdk#charges)
7. [License](/backend-integration/node-sdk#license)
8. [Portal](/backend-integration/node-sdk#portal)
9. [Refunds](/backend-integration/node-sdk#refunds)
10. [Payment Methods](/backend-integration/node-sdk#payment-methods)
11. [Transactions](/backend-integration/node-sdk#transactions)
***
## Customers
The `customers` module allows you to manage customer records within Kelviq. You can access these operations via the `customers` attribute on an initialized `Kelviq` client instance.
***
### Creates a new customer
```typescript theme={null}
const newCustomer = await client.customers.create({
customerId: "unique-customer-id-789",
email: "new.node.customer@example.com",
name: "Node SDK User",
metadata: { source: "sdk_import", priority: "high" }
});
```
> **Response**
>
> ```json theme={null}
> {
> "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
> "customerId": "unique-customer-id-789",
> "name": "Node SDK User",
> "email": "new.node.customer@example.com",
> "details": {},
> "metadata": {
> "source": "sdk_import",
> "priority": "high"
> },
> "createdOn": "2025-06-04T06:03:30.195790Z",
> "modifiedOn": "2025-06-04T06:03:30.195831Z"
> }
> ```
**Required Parameters:**
* `customerId` : A unique identifier for the customer that you define. This ID will be used to reference the customer in subsequent API calls.
**Optional Parameters:**
* `email` : The email address of the customer. Must be a valid email format.
* `name` : The name of the customer.
* `metadata` : An object of custom key-value pairs to store additional information about the customer.
**Returns:**
An instance of `CustomerResponse` (TypeScript Interface), representing the newly created customer record. Key attributes include:
* `id` : The server-generated unique UUID for the customer record.
* `customerId` : The client-provided customer identifier.
* `name` : The customer's name.
* `email` : The customer's email.
* `details` : Any server-added details about the customer (typically read-only).
* `metadata` : The metadata associated with the customer.
* `createdOn` : ISO 8601 timestamp of when the customer was created.
* `modifiedOn` : ISO 8601 timestamp of when the customer was last modified.
### Updates an existing customer
This operation performs a partial update (PATCH), so you only need to provide the fields you want to change.
```typescript theme={null}
const updatedCustomer = await client.customers.update({
customerId: "unique-customer-id-789",
name: "Node SDK User (Updated)",
metadata: { tier: "premium_plus" }
});
```
> **Response**
>
> ```json theme={null}
> {
> "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
> "customerId": "unique-customer-id-789",
> "name": "Node SDK User (Updated)",
> "email": "new.node.customer@example.com",
> "details": {},
> "metadata": {
> "tier": "premium_plus"
> },
> "createdOn": "2025-06-04T06:03:30.195790Z",
> "modifiedOn": "2025-06-04T06:03:30.195831Z"
> }
> ```
**Parameters:**
**Required Parameters:**
* `customerId` : A unique identifier for the customer that you define. This ID will be used to reference the customer in subsequent API calls.
**Optional Parameters:**
* `email` : The email address of the customer. Must be a valid email format.
* `name` : The name of the customer.
* `metadata` : An object of custom key-value pairs to store additional information about the customer.
**Returns:**
An instance of `CustomerResponse` (TypeScript Interface), representing the updated customer record. Key attributes include:
* `id` : The server-generated unique UUID for the customer record.
* `customerId` : The client-provided customer identifier.
* `name` : The customer's name.
* `email` : The customer's email.
* `details` : Any server-added details about the customer (typically read-only).
* `metadata` : The metadata associated with the customer.
* `createdOn` : ISO 8601 timestamp of when the customer was created.
* `modifiedOn` : ISO 8601 timestamp of when the customer was last modified.
***
## Checkout
The checkout module provides functionalities for creating and managing checkout sessions. You can access these operations via the checkout attribute on an initialized Kelviq client instance.
This operation creates a new checkout session for a customer, allowing them to proceed with a purchase or subscription.
```typescript theme={null}
import { Kelviq, CHARGE_PERIOD_CHOICES } from '@kelviq/node-sdk';
const response = await client.checkout.createSession({
planIdentifier: "plan-pro-monthly",
chargePeriod: CHARGE_PERIOD_CHOICES.MONTHLY,
successUrl: "https://yourwebsite.com/checkout/success",
customerId: "cust_789", // Recommended, otherwise it will create a new customer
trialPeriod: 14,
});
```
> **Response**
>
> ```json theme={null}
> {
> "checkoutUrl": "https://www.kelviq.com/checkout/cs_fBhCIBerpe1pAsRW6P4wYc8WHqMdfdr457hd3oOLjhAFx/",
> "checkoutSessionId": "cs_test_b1sNi7D6u9iMCUFV1UZi9ZwerfaiXKdOOmr3DCUW6XdCZIr5Id1F7G"
> }
> ```
**Required Parameters:**
* `planIdentifier` : The identifier of the specific plan the customer is checking out with. planIdentifier is mandatory.
* `successUrl` : The URL to which the user will be redirected after a successful checkout.
* `chargePeriod` : The billing cycle for the subscription. Must be one of:
* `"ONE_TIME"`
* `"MONTHLY"`
* `"YEARLY"`
* `"WEEKLY"`
* `"DAILY"`
* `"TWENTY_EIGHT_DAYS"`
* `"THREE_MONTHS"`
* `"SIX_MONTHS"`
**Optional Parameters:**
* `offeringId` : The ID (uuid) of the offering the customer is checking out with.
* `pricingTableId` : The id (uuid) of the pricing table being used for this checkout. (Considered only if `offeringId` is not provided)
* `ruleId` : The id (uuid) of the pricing rule being applied. (Considered only if `offeringId` is not provided)
* `customerId` : The ID of the customer initiating the checkout. (If not provided, a new customer will be created)
* `features`: A list of objects, where each object represents a feature and its desired quantity. Each object **must** have two keys:
* `"identifier"` : The unique identifier for the feature.
* `"quantity"`: The desired quantity for this feature.
* Example: `[{"identifier": "seats", "quantity": 10}, {"identifier": "api-calls-tier1", "quantity": 5000}]`
* `ipAddress` : The IP Address of the customer, used for location based pricing.
* `trialPeriod` : Number of trial days to apply. Must be an integer greater than or equal to `1` and overrides the selected plan's configured trial period.
* `currencyCode` : The currency code for the checkout session (e.g. `"USD"`). Required when `customAmount` is provided.
* `customAmount` : A custom amount to charge for this checkout session. When provided, `taxBehavior` and `currencyCode` are also required.
* `taxBehavior` : Specifies how taxes are applied to the `customAmount`. Required when `customAmount` is provided. Must be one of:
* `"INCLUSIVE"` — Tax is included in the custom amount.
* `"EXCLUSIVE"` — Tax is added on top of the custom amount.
* `discountsEnabled` : Whether the discount/coupon code field is shown on the checkout page. 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 on the checkout page.
* `metadata` : An object of arbitrary key-value pairs to attach to the checkout session. The keys are preserved exactly as supplied and the object is returned unchanged on the `checkout.completed` webhook's `metadata` field. Example: `{ "order_ref": "ABC-123", "source": "pricing_page" }`.
**Returns**:
An instance of CreateCheckoutSessionResponse (Object), which includes:
* `checkoutSessionId` : The unique ID for the created checkout session.
* `checkoutUrl` : The URL that the customer should be redirected to in order to complete the payment and activate the subscription/purchase.
***
## Entitlements
The entitlements module allows you to check and retrieve customer entitlements for various features. These operations target a specific edge API endpoint ( `https://edge.api.kelviq.com` by default) and use the GET HTTP method with query parameters.
### Checks if a specific customer has access to a particular feature.
This method directly returns a boolean indicating access status.
```typescript theme={null}
const hasAccess = await client.entitlements.hasAccess({
customerId: "cust_node_ent_123",
featureId: "premium-reporting-node"
});
```
> **Response**
>
> ```shell theme={null}
> true
> ```
**Required Parameters**:
* `customerId` : The unique identifier for the customer.
* `featureId` : The unique identifier for the feature whose access is being checked.
**Returns**:
* `boolean`: `true` if the customer has access to the specified feature (considering feature type, limits, etc.), `false` otherwise or if the feature is not found in their entitlements.
### Retrieves the aggregated entitlement for a specific feature for a given customer.
```typescript theme={null}
const entitlement = await client.entitlements.getEntitlement({
customerId: "cust_node_ent_123",
featureId: "premium-reporting-node"
});
```
> **Response**
>
> ```json theme={null}
> {
> "featureId": "advanced-analytics",
> "featureType": "METER",
> "hasAccess": true,
> "hardLimit": false,
> "usageLimit": 3,
> "currentUsage": 0,
> "remaining": 3,
> "items": [
> {
> "featureId": "advanced-analytics",
> "featureType": "METER",
> "hasAccess": true,
> "resetAt": "2025-05-22 08:27:45",
> "hardLimit": false,
> "usageLimit": 3,
> "currentUsage": 0,
> "remaining": 3
> }
> ]
> }
> ```
**Required Parameters**:
* `customerId` : The unique identifier for the customer.
* `featureId` : The unique identifier for the feature whose access is being checked.
**Returns**:
An aggregated `Entitlement` object, or `null` if the feature is not found. If the customer has multiple subscriptions, there can be multiple raw entries for the same featureId. The SDK aggregates them and keeps raw entries in the `items` array. Note: `resetAt` is only available on individual items (not at the top level) because it can differ across subscriptions. The structure includes:
* `featureId`: string
* `hasAccess`: boolean — For **METER** features, computed as `remaining === null || remaining > 0`. For **BOOLEAN** and **CUSTOMIZABLE** features, `true` if at least one item grants access.
* `featureType`: string — `"METER"`, `"BOOLEAN"`, or `"CUSTOMIZABLE"`.
* `hardLimit`: boolean | null — For **METER**, `true` if any item has a hard limit. For **BOOLEAN**, always `false`.
* `usageLimit`: number | null — For **METER**, summed across all items. For **CUSTOMIZABLE**, taken from the first item. For **BOOLEAN**, always `null`.
* `currentUsage`: number | null — For **METER**, summed across all items. For **CUSTOMIZABLE**, taken from the first item. For **BOOLEAN**, always `0`.
* `remaining`: number | null — For **METER**, computed as `usageLimit - currentUsage`. For **CUSTOMIZABLE**, taken from the first item. For **BOOLEAN**, always `null`.
* `items` (`EntitlementDetail[]`): An array containing all raw entitlement entries for this featureId. Each item includes `resetAt`.
### Retrieves all aggregated entitlements for a given customer.
```typescript theme={null}
const entitlements = await client.entitlements.getEntitlements({
customerId: "cust_node_ent_123"
});
// Access a specific feature directly by featureId
const analytics = entitlements["advanced-analytics"];
```
> **Response**
>
> ```json theme={null}
> {
> "advanced-analytics": {
> "featureId": "advanced-analytics",
> "featureType": "METER",
> "hasAccess": true,
> "hardLimit": false,
> "usageLimit": 3,
> "currentUsage": 0,
> "remaining": 3,
> "items": [
> {
> "featureId": "advanced-analytics",
> "featureType": "METER",
> "hasAccess": true,
> "resetAt": "2025-05-22 08:27:45",
> "hardLimit": false,
> "usageLimit": 3,
> "currentUsage": 0,
> "remaining": 3
> }
> ]
> }
> }
> ```
**Required Parameters**:
* `customerId` : The unique identifier for the customer.
**Returns**:
A `Record` keyed by `featureId`. If the customer has multiple subscriptions, there can be multiple raw entries for the same featureId. The SDK aggregates them and keeps raw entries in the `items` array. Note: `resetAt` is only available on individual items (not at the top level) because it can differ across subscriptions. Each `Entitlement` value has the same structure as described in `getEntitlement` above.
### Retrieves the raw entitlement from the API for a specific feature.
```typescript theme={null}
const response = await client.entitlements.getRawEntitlement({
customerId: "cust_node_ent_123",
featureId: "premium-reporting-node"
});
```
> **Response**
>
> ```json theme={null}
> {
> "customerId": "cust_456",
> "entitlements": [
> {
> "featureId": "advanced-analytics",
> "featureType": "METER",
> "hasAccess": true,
> "resetAt": "2025-05-22 08:27:45",
> "hardLimit": false,
> "usageLimit": 3,
> "currentUsage": 0,
> "remaining": 3
> }
> ]
> }
> ```
**Required Parameters**:
* `customerId` : The unique identifier for the customer.
* `featureId` : The unique identifier for the feature.
**Returns**:
An instance of `CheckEntitlementsResponse` (Object). This returns the raw API response without aggregation. The structure includes:
* `customerId` : The customer's ID.
* `entitlements` (`EntitlementDetail[]`): A list containing raw entitlement details. Each `EntitlementDetail` has fields like:
* `featureId`: string
* `hasAccess`: boolean
* `featureType`: string
* `resetAt`: string
* `hardLimit`: boolean | null
* `usageLimit`: number | null
* `currentUsage`: number | null
* `remaining`: number | null
### Retrieves all raw entitlements from the API without aggregation.
```typescript theme={null}
const response = await client.entitlements.getRawEntitlements({
customerId: "cust_node_ent_123"
});
```
> **Response**
>
> ```json theme={null}
> {
> "customerId": "cust_456",
> "entitlements": [
> {
> "featureId": "advanced-analytics",
> "featureType": "METER",
> "hasAccess": true,
> "resetAt": "2025-05-22 08:27:45",
> "hardLimit": false,
> "usageLimit": 3,
> "currentUsage": 0,
> "remaining": 3
> }
> ]
> }
> ```
**Required Parameters**:
* `customerId` : The unique identifier for the customer.
**Returns**:
An instance of `CheckEntitlementsResponse` (Object). This returns the raw API response without aggregation. The structure includes:
* `customerId` : The customer's ID.
* `entitlements` (`EntitlementDetail[]`): A list containing raw entitlement details. Each `EntitlementDetail` has fields like:
* `featureId`: string
* `hasAccess`: boolean
* `featureType`: string
* `resetAt`: string
* `hardLimit`: boolean | null
* `usageLimit`: number | null
* `currentUsage`: number | null
* `remaining`: number | null
***
## Reporting
### Reporting pre-aggregated usages for customer
This endpoint is used for reporting the pre-aggregated feature usage from your application (client-level) to the Kelviq application. It allows you to update the usage count for a specific feature associated with a customer.
```typescript theme={null}
import { BEHAVIOUR_CHOICES } from '@kelviq/node-sdk';
const response = await client.reporting.reportUsage({
value: 150,
customerId: "customer_001",
featureId: "seats",
behaviour: BEHAVIOUR_CHOICES.SET
});
```
> **Response**
>
> ```json theme={null}
> {
> "value": 150,
> "customerId": "customer_001",
> "featureId": "seats",
> "behaviour": "SET",
> "orgId": "1",
> "eventName": "aggregated.usage",
> "idempotencyKey": "597ee95063c744ed9bcc9b1cf5676a8a",
> "timestamp": "2025-05-22 08:27:45.430732"
> }
> ```
**Required Parameters**:
* `value`: The usage value being reported.
* `customerId` : The unique identifier for the customer associated with this usage.
* `featureId` : The unique identifier for the feature for which usage is being reported.
* `behaviour` parameter dictates how the usage is updated:
* `SET`: This will replace the current usage value for the feature with the new `value` provided.
* `DELTA`: This will increment the existing usage value for the feature by the amount specified in the `value` parameter
**Returns**:
An instance of ReportUsageResponse (Object), which includes:
* `value`: The usage value that was recorded.
* `customerId` : The customer ID associated with the usage.
* `featureId` : The feature Identifier for which usage was recorded.
* `behaviour` : The behaviour type ("SET" or "DELTA") that was processed.
* `orgId` : The organization ID associated with this record, as determined by the server.
* `eventName` : An internal event name generated by the server for this usage report (e.g., "aggregated.usage").
* `idempotencyKey` : A unique idempotency key generated by the server for this specific usage report instance.
* `timestamp` : The server-generated UTC timestamp (string format) indicating when the usage report was processed.
## Subscriptions
The subscriptions module allows you to list, retrieve, create, preview updates, update, and cancel customer subscriptions. You can access these operations via the `subscriptions` attribute on an initialized Kelviq client instance.
### List subscriptions
Retrieves a paginated list of subscriptions for a customer.
```typescript theme={null}
const page = await client.subscriptions.list({
customerId: "cust_789",
page: 1,
pageSize: 20,
});
for (const subscription of page.results) {
console.log(subscription.id, subscription.status);
}
```
> **Response**
>
> ```json theme={null}
> {
> "count": 1,
> "next": null,
> "previous": null,
> "results": [
> {
> "id": "09d706ca-58f3-4fb2-818f-5b8623d67e6e",
> "startDate": "2025-07-28",
> "endDate": null,
> "status": "active",
> "trialDaysRemaining": 0,
> "customerId": "cust_789",
> "billingType": "SUBSCRIPTION",
> "files": [],
> "links": [],
> "license": []
> }
> ]
> }
> ```
**Required Parameters:**
* `customerId` (string): The client-defined identifier of the customer whose subscriptions will be returned.
**Optional Parameters:**
* `page` (number): Page number to retrieve.
* `pageSize` (number): Number of subscriptions to return per page.
* `modifiedOnAfter` (string): Only return subscriptions modified on or after this ISO 8601 timestamp.
* `modifiedOnBefore` (string): Only return subscriptions modified on or before this ISO 8601 timestamp.
**Returns:**
A `PaginatedSubscriptionResponse` containing `count`, `next`, `previous`, and a `results` array of `SubscriptionData` objects.
### Retrieve a subscription
Retrieves one subscription using its Kelviq subscription ID.
```typescript theme={null}
const subscription = await client.subscriptions.retrieve({
subscriptionId: "09d706ca-58f3-4fb2-818f-5b8623d67e6e",
});
console.log(subscription.id, subscription.status);
subscription.files?.forEach((file) => console.log(file.name, file.downloadUrl));
subscription.links?.forEach((link) => console.log(link.name, link.url));
subscription.license?.forEach((license) =>
console.log(license.licenseKey, license.activationLimit),
);
```
> **Response**
>
> ```json theme={null}
> {
> "id": "09d706ca-58f3-4fb2-818f-5b8623d67e6e",
> "externalSubscriptionId": "sub_1RpoZoSBstCzyocS7iPmith3",
> "startDate": "2025-07-28",
> "endDate": null,
> "billingPeriodStartTime": "2025-07-28T10:46:34Z",
> "billingPeriodEndTime": "2025-08-28T10:46:34Z",
> "amount": "261.00",
> "recurrence": "month",
> "currency": "usd",
> "status": "active",
> "trialDaysRemaining": 0,
> "customerId": "cust_789",
> "billingType": "SUBSCRIPTION",
> "recurrenceUnit": 1,
> "recurrenceType": "MONTH",
> "files": [],
> "links": [],
> "license": []
> }
> ```
**Required Parameters:**
* `subscriptionId` (string): The Kelviq UUID of the subscription.
**Returns:**
A `SubscriptionData` object. A missing subscription throws `NotFoundError`.
### Create a subscription
Creates a subscription directly for a customer without requiring a checkout session.
```typescript theme={null}
import { CHARGE_PERIOD_CHOICES } from '@kelviq/node-sdk';
const subscription = await client.subscriptions.create({
planIdentifier: "plan-pro-monthly",
chargePeriod: CHARGE_PERIOD_CHOICES.MONTHLY,
customerId: "cust_789",
successUrl: "https://example.com/subscription/success",
features: [{ identifier: "seats", quantity: 5 }],
ipAddress: "103.154.35.20",
});
console.log(subscription.id, subscription.status);
```
**Required Parameters:**
* `planIdentifier` (string): Identifier of the plan to subscribe to.
* `chargePeriod` (`ChargePeriod`): By default, organizations can use `MONTHLY`, `THREE_MONTHS`, `SIX_MONTHS`, `YEARLY`, and `ONE_TIME`. To enable `DAILY`, `WEEKLY`, or `TWENTY_EIGHT_DAYS`, contact [hi@kelviq.com](mailto:hi@kelviq.com).
* `customerId` (string): Client-defined identifier of the customer.
**Optional Parameters:**
* `successUrl` (string): URL to redirect to after successful subscription creation.
* `features` (`FeatureListItem[]`): Feature identifiers and requested quantities.
* `ipAddress` (string): Customer IP address used for location-based pricing.
**Returns:**
A `CreateSubscriptionResponse` containing the created subscription, including its plan files, links, and issued licenses when available.
### Updates an existing subscription to a new plan
```typescript theme={null}
import { CHARGE_PERIOD_CHOICES, PAYMENT_BEHAVIORS, PRORATION_BEHAVIORS } from '@kelviq/node-sdk';
try {
const response = await client.subscriptions.update({
subscriptionId: "sub_node_78058918",
planIdentifier: "premium-plan-node",
chargePeriod: CHARGE_PERIOD_CHOICES.MONTHLY,
prorationBehavior: PRORATION_BEHAVIORS.PRORATE_NEXT_INVOICE,
trialEnd: "2027-12-31T23:59:59Z", // or "now" to end the trial immediately
paymentBehavior: PAYMENT_BEHAVIORS.ACTIVATE_ON_PAYMENT
});
} catch (e) {
console.error(`Error updating subscription: ${e}`);
}
```
> **Response**
>
> ```json theme={null}
> {
> "subscriptionId": "dffaf07e-4517-47db-ba3a-59a05aa2d465"
> }
> ```
**Required Parameters**:
* `subscriptionId` : The unique identifier of the subscription to be updated.
* `planIdentifier` : The identifier of the new plan.
* `chargePeriod` : The new charging period for the subscription. Must be one of:
* `"ONE_TIME"`
* `"MONTHLY"`
* `"YEARLY"`
* `"WEEKLY"`
* `"DAILY"`
* `"TWENTY_EIGHT_DAYS"`
* `"THREE_MONTHS"`
* `"SIX_MONTHS"`
**Optional Parameters:**
* `offeringId` : The ID of the new offering, if applicable.
* `pricingTableId` : The ID of the new pricing table, if applicable.
* `ruleId` : The ID of the new pricing rule, if applicable.
* `ipAddress` : The IP Address of the customer, used for location based pricing.
* `features`: An array of objects, where each object represents a feature and its desired quantity to update for the subscription. Each object **must** have two keys:
* `"identifier"` : The unique identifier for the feature.
* `"quantity"` : The desired quantity for this feature.
* Example: `[{"identifier": "seats", "quantity": 10}, {"identifier": "projects", "quantity": 5}]`
* `trialEnd` (string): Controls the trial period for the updated subscription. Accepts either:
* The literal string `"now"` to end any active trial immediately.
* An ISO 8601 datetime string (e.g. `"2027-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.
* If omitted, the existing trial behavior on the subscription is preserved.
* `paymentBehavior` (`PaymentBehavior`): Set to `PAYMENT_BEHAVIORS.ACTIVATE_ON_PAYMENT` (the value `"activate_on_payment"`) to keep the current subscription active while payment is pending. The new plan and features are applied only after payment succeeds. If the pending update expires, the current subscription remains unchanged. Omit this field to use the default immediate-update behavior.
* `prorationBehavior` (`ProrationBehavior`): Controls how a mid-period change is billed. Use `PRORATION_BEHAVIORS.IMMEDIATE_CHARGE`, `PRORATION_BEHAVIORS.PRORATE_NEXT_INVOICE`, or `PRORATION_BEHAVIORS.NO_PRORATION`. When omitted, the organization's default is used.
The SDK validates `trialEnd` client-side — invalid datetime strings or past datetimes throw `InvalidRequestError` before the request is sent.
**Returns**:
An instance of `UpdateSubscriptionResponse`, which includes:
* `subscriptionId` (string): UUID of the updated subscription.
### Preview a subscription update
Calculates the financial effect of an update without changing the subscription, creating an invoice, or charging the customer.
```typescript theme={null}
import { CHARGE_PERIOD_CHOICES, PRORATION_BEHAVIORS } from '@kelviq/node-sdk';
const preview = await client.subscriptions.previewUpdate({
subscriptionId: "78058918-9746-4280-9b9b-1bd5115eec6e",
planIdentifier: "premium-plan",
chargePeriod: CHARGE_PERIOD_CHOICES.YEARLY,
prorationBehavior: PRORATION_BEHAVIORS.PRORATE_NEXT_INVOICE,
features: [{ identifier: "seats", quantity: 10 }],
});
console.log(preview.recurringAmount, preview.amountChargedImmediately);
console.log(preview.nextInvoice?.lineItems);
```
Required parameters are `subscriptionId`, `planIdentifier`, and `chargePeriod`. Optional parameters are `prorationBehavior`, `features`, `ipAddress`, and `trialEnd`.
**Returns:** A `SubscriptionUpdatePreviewResponse` with recurring and immediate amounts in major and minor units, plus an optional next invoice and its line items.
### Cancel an active subscription for a customer.
```typescript theme={null}
import { CANCELLATION_TYPES } from '@kelviq/node-sdk';
try {
const response = await client.subscriptions.cancel({
subscriptionId: "sub_node_78058918",
cancellationType: CANCELLATION_TYPES.CURRENT_PERIOD_ENDS
});
} catch (e) {
console.error(`Error cancelling subscription: ${e}`);
}
```
> **Response**
>
> ```json theme={null}
> {
> "message": "Subscription cancellation processed successfully."
> }
> ```
**Required Parameters**:
* `subscriptionId` : The unique identifier of the subscription to be cancelled.
* `cancellationType` : The type of cancellation to perform. Must be one of:
* `"IMMEDIATE"`: The subscription is cancelled immediately.
* `"CURRENT_PERIOD_ENDS"`: The subscription will remain active until the end of the current billing period and then cancel.
* `"SPECIFIC_DATE"`: The subscription will be cancelled on the specified `cancellationDate`.
* `cancellationDate` : The specific date for cancellation if `cancellationType` is `"SPECIFIC_DATE"`. Must be in `YYYY-MM-DD` format. This parameter is **required** if `cancellationType` is `"SPECIFIC_DATE"`.
**Returns**:
An instance of `CancelSubscriptionResponse`, which includes:
* `message` : A confirmation message indicating the result of the cancellation request.
***
## Charges
The `charges` module immediately charges a customer's saved payment method for a one-time plan. Use sandbox mode when testing this operation.
```typescript theme={null}
const charge = await client.charges.create({
planIdentifier: "lifetime-access",
chargePeriod: "ONE_TIME",
customerId: "cust_789",
currencyCode: "USD",
customAmount: 49.99,
taxBehavior: "INCLUSIVE",
features: [{ identifier: "seats", quantity: 5 }],
});
console.log(charge.id, charge.status, charge.amount);
```
`planIdentifier`, `chargePeriod: "ONE_TIME"`, and `customerId` are required. `customAmount` must be positive and requires `currencyCode`. `taxBehavior` accepts `INCLUSIVE` or `EXCLUSIVE` and defaults to `EXCLUSIVE` on the API. Optional fields also include `features` and `ipAddress`.
**Returns:** A `CreateChargeResponse` containing the charge record, amount, currency, status, product, plan, features, and customer details.
***
## Refunds
The `refunds` module allows you to list, create, and retrieve refunds for orders owned by your organization. You can access these operations via the `refunds` attribute on an initialized `Kelviq` client instance.
### List refunds
```typescript theme={null}
const page = await client.refunds.list({
status: "SUCCEEDED",
startDate: "2026-07-01",
page: 1,
pageSize: 10,
});
```
> **Response**
>
> ```json theme={null}
> {
> "count": 1,
> "next": null,
> "previous": null,
> "results": [
> {
> "id": "7c2f3a91-2d4e-4a8b-9b1c-6f0a2e5d9c11",
> "reason": "REQUESTED_BY_CUSTOMER",
> "amountUnits": 2500,
> "amount": 25,
> "internalNote": "Customer requested a partial refund.",
> "failureReason": null,
> "status": "SUCCEEDED",
> "order": { "id": "ORD-20260715123000-A1B2C" }
> }
> ]
> }
> ```
**Optional Parameters:**
* `search` : Search by refund ID, order ID, customer name, email, or ID.
* `status` : Filter by refund status. One of `"PENDING"`, `"PROCESSING"`, `"SUCCEEDED"`, `"FAILED"`, `"CANCELED"`.
* `startDate` : Include refunds created on or after this date (`YYYY-MM-DD`).
* `endDate` : Include refunds created on or before this date (`YYYY-MM-DD`).
* `page` : Page number to return.
* `pageSize` : Number of results per page. Defaults to 10, maximum 100.
**Returns:** A `RefundListResponse` (TypeScript Interface) with `count`, `next`, `previous`, and `results: RefundResponse[]`.
### Create a refund
```typescript theme={null}
const refund = await client.refunds.create({
orderId: "ORD-20260715123000-A1B2C",
amountUnits: 2500,
reason: "REQUESTED_BY_CUSTOMER",
internalNote: "Customer requested a partial refund.",
});
```
> **Response**
>
> ```json theme={null}
> {
> "id": "7c2f3a91-2d4e-4a8b-9b1c-6f0a2e5d9c11",
> "reason": "REQUESTED_BY_CUSTOMER",
> "amountUnits": 2500,
> "amount": 25,
> "internalNote": "Customer requested a partial refund.",
> "failureReason": null,
> "status": "SUCCEEDED",
> "order": { "id": "ORD-20260715123000-A1B2C" }
> }
> ```
**Required Parameters:**
* `orderId` : The ID of an order owned by your organization.
**Optional Parameters:**
* `amountUnits` : Amount to refund in the currency's minor unit (e.g. cents for USD). Takes precedence over `amount` when both are provided.
* `amount` : Amount to refund in the currency's major unit (e.g. dollars for USD). Used only when `amountUnits` is omitted.
* `reason` : One of `"DUPLICATE"`, `"FRAUDULENT"`, `"REQUESTED_BY_CUSTOMER"`, `"OTHER"`. Defaults to `"REQUESTED_BY_CUSTOMER"`.
* `internalNote` : An internal note associated with the refund.
Omit both `amountUnits` and `amount` to refund the order's entire remaining refundable balance. The amount must be positive and cannot exceed the order's remaining refundable balance — the SDK throws `InvalidRequestError` if `orderId` is missing, and the API rejects invalid amounts.
**Returns:** A `RefundResponse` (TypeScript Interface) with `id`, `reason`, `amountUnits`, `amount`, `internalNote`, `failureReason`, `status`, and the complete serialized `order`.
### Retrieve a refund
```typescript theme={null}
const refund = await client.refunds.retrieve({
refundId: "7c2f3a91-2d4e-4a8b-9b1c-6f0a2e5d9c11",
});
```
**Required Parameters:**
* `refundId` : The UUID of the refund.
**Returns:** A `RefundResponse` (TypeScript Interface), as above.
***
## Payment Methods
The `paymentMethods` module lists saved payment methods for your organization. You can access this operation via the `paymentMethods` attribute on an initialized `Kelviq` client instance.
### List payment methods
```typescript theme={null}
const page = await client.paymentMethods.list({
customerId: "geo-jacob",
pageSize: 10,
});
```
> **Response**
>
> ```json theme={null}
> {
> "count": 1,
> "next": null,
> "previous": null,
> "results": [
> {
> "id": 482,
> "customer": {
> "customerId": "geo-jacob",
> "name": "Geo Jacob",
> "email": "geojacob@example.com",
> "billingAddress": { "country": "US" }
> },
> "methodType": "card",
> "methodData": {
> "brand": "visa",
> "last4": "4242",
> "expMonth": 12,
> "expYear": 2029,
> "funding": "credit",
> "country": "US"
> },
> "isDefault": true,
> "status": "succeeded",
> "createdOn": "2026-04-02T06:12:04.123456Z",
> "modifiedOn": "2026-04-02T06:12:04.123456Z"
> }
> ]
> }
> ```
**Optional Parameters:**
* `customerId` : Filter to payment methods belonging to the customer with this client-provided `customerId`.
* `customerEmail` : Filter to payment methods belonging to the customer with this email address.
* `page` : Page number to return.
* `pageSize` : Number of results per page. Defaults to 10, maximum 100.
Only payment methods with status `"succeeded"` are included in results.
**Returns:** A `PaymentMethodListResponse` (TypeScript Interface) with `count`, `next`, `previous`, and `results: PaymentMethodResponse[]`. Each `methodData` object is payment-provider-specific; for `card` it includes `brand`, `last4`, `expMonth`, `expYear`, `funding`, and `country`.
***
## Transactions
The `transactions` module lists financial transactions for your organization, ordered newest first. You can access this operation via the `transactions` attribute on an initialized `Kelviq` client instance.
### List transactions
```typescript theme={null}
const page = await client.transactions.list({
status: "success",
startDate: "2026-07-01",
page: 1,
pageSize: 25,
});
console.log(page.results[0].amountTotal, page.results[0].morFeeBreakdown);
```
> **Response**
>
> ```json theme={null}
> {
> "count": 1,
> "next": null,
> "previous": null,
> "results": [
> {
> "id": "5b274952-0e54-48f6-aec6-e8a3f202b12e",
> "order": { "id": "ORD-20260715123000-A1B2C" },
> "createdOn": "2026-07-28T10:30:00Z",
> "currency": "USD",
> "currencySymbol": "$",
> "amountTotal": 100,
> "morFee": 4,
> "morFeeBreakdown": [
> { "name": "fixed_fee", "percentage": null, "valueUnits": 40, "currency": "USD" },
> { "name": "base_fee", "percentage": "2.9", "valueUnits": 290, "currency": "USD" },
> { "name": "subscription_fee", "percentage": "0.7", "valueUnits": 70, "currency": "USD" }
> ],
> "collectedTax": 0,
> "netPayout": 96,
> "status": "success",
> "transactionType": "CHARGE",
> "paymentMethodType": "card",
> "availableOn": "2026-08-04",
> "isSettled": false,
> "paymentMethodName": "visa",
> "last4": "4242"
> }
> ]
> }
> ```
**Optional Parameters:**
* `search` : Search by payment-provider IDs, customer name, email or ID, product name, or payment method type.
* `status` : Filter by transaction status. One of `"success"`, `"failure"`, `"pending"`.
* `startDate` : Include transactions created on or after this date (`YYYY-MM-DD`).
* `endDate` : Include transactions created on or before this date (`YYYY-MM-DD`).
* `page` : Page number to return.
* `pageSize` : Number of results per page. Defaults to 10, maximum 100.
**Returns:** A `TransactionListResponse` (TypeScript Interface) with `count`, `next`, `previous`, and `results: TransactionResponse[]`. Each transaction includes an itemized `morFeeBreakdown` (Merchant of Record fee components) when applicable — empty when no such fee applies.
***
## License
The `license` module allows you to activate, deactivate, and validate software licenses. You can access these operations via the `license` attribute on an initialized `Kelviq` client instance.
***
### Activate a license key
Creates a new license instance for a given license key, optionally associating it with a customer.
```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);
```
> **Response**
>
> ```json theme={null}
> {
> "instanceId": "8f3e2b1a-5c6d-4e9f-8a0b-1c2d3e4f5g6h",
> "activatedAt": "2026-01-27T10:33:00Z",
> "expiresOn": "2027-01-27T10:33:00Z",
> "license": {
> "id": "7be7a5b6-7a0e-4178-a085-8ef48f0649ab",
> "licenseKey": "LIC-A8D5F89E-B46B-472C-9BED-66EE2545B85E",
> "activatedOn": "2026-01-22T03:26:56Z",
> "expiresOn": "2026-01-23T03:26:56Z",
> "activationUsage": 1,
> "activationLimit": 4,
> "enabled": true,
> "customer": {
> "customerId": "cfa95cf8c8d24e8b9d3d2d1ada98a94d",
> "name": "Geo",
> "email": "geo@kelviq.com"
> },
> "plan": {
> "identifier": "pro-yearly",
> "name": "Pro Yearly",
> "description": "Pro plan billed yearly",
> "product": {
> "id": "d970ab2d-834f-4c52-8742-2e6ed7e8a31f",
> "identifier": "kelviq-engine",
> "name": "Kelviq Engine",
> "taxCode": "software",
> "createdOn": "2025-01-01T00:00:00Z",
> "modifiedOn": "2025-06-01T00:00:00Z"
> },
> "version": 1,
> "isLatest": true
> },
> "subscription": {
> "id": "09d706ca-58f3-4fb2-818f-5b8623d67e6e",
> "recurrence": "1 month",
> "billingPeriodStartTime": "2026-01-22T03:26:56Z",
> "billingPeriodEndTime": "2026-02-22T03:26:56Z",
> "startDate": "2026-01-22",
> "endDate": null,
> "status": "active",
> "amount": "49.00",
> "currency": "usd",
> "trialDaysRemaining": 0,
> "billingType": "SUBSCRIPTION",
> "recurrenceUnit": 1,
> "recurrenceType": "MONTH"
> }
> }
> }
> ```
**Required Parameters:**
* `licenseKey` : The license key string to activate.
**Optional Parameters:**
* `customerId` : The ID of the customer this instance is associated with.
* `instanceName` : A human-readable name for this instance (e.g. device name).
* `metadata` : An object of custom key-value pairs to attach to the instance.
**Returns:**
An instance of `LicenseActivateResponse`, which includes:
* `instanceId` (string): The unique ID of the newly created license instance.
* `activatedAt` (string): ISO 8601 timestamp of when the instance was activated.
* `expiresOn` (string | null): ISO 8601 timestamp of when the instance expires, if applicable.
* `license` (`LicenseDetails`): The full license object. Key fields:
* `id` : Server-generated UUID for the license.
* `licenseKey` : The license key string.
* `activatedOn` (string | null): ISO 8601 timestamp of when the license was first activated.
* `expiresOn` (string | null): ISO 8601 timestamp of when the license expires.
* `activationUsage` : Number of currently active instances.
* `activationLimit` : Maximum number of allowed concurrent activations.
* `enabled` : Whether the license is active.
* `customer` (object | null): The associated customer, if any. Fields: `customerId`, `name`, `email`.
* `plan` (object | null): The plan associated with this license, if any. Fields: `identifier`, `name`, `description`, `version`, `isLatest`, and `product` (object with `id`, `identifier`, `name`, `taxCode`, `createdOn`, `modifiedOn`).
* `subscription` (object | null): Subscription details, if any. Fields: `id`, `recurrence`, `billingPeriodStartTime`, `billingPeriodEndTime`, `startDate`, `endDate`, `status`, `amount`, `currency`, `trialDaysRemaining`, `billingType`, `recurrenceUnit`, `recurrenceType`.
***
### Deactivate a license instance
Deactivates a specific license instance by its instance ID.
```typescript theme={null}
try {
const response = await client.license.deactivate({
licenseKey: "LIC-XXXX-YYYY-ZZZZ",
instanceId: "8f3e2b1a-5c6d-4e9f-8a0b-1c2d3e4f5a6b"
});
console.log(response.message);
} catch (e) {
console.error(`Error deactivating license: ${e}`);
}
```
> **Response**
>
> ```json theme={null}
> {
> "message": "License instance deactivated successfully.",
> "deactivatedAt": "2026-01-27T10:35:00Z"
> }
> ```
**Required Parameters:**
* `licenseKey` : The license key string.
* `instanceId` : The unique ID of the instance to deactivate.
**Returns:**
An instance of `LicenseDeactivateResponse`, which includes:
* `message` (string): Confirmation message.
* `deactivatedAt` (string): ISO 8601 timestamp of when the instance was deactivated.
***
### Validate a license key
Checks whether a license key (and optionally a specific instance) is valid.
```typescript theme={null}
const response = await client.license.validate({
licenseKey: "LIC-XXXX-YYYY-ZZZZ",
instanceId: "8f3e2b1a-5c6d-4e9f-8a0b-1c2d3e4f5a6b" // optional
});
if (response.valid) {
console.log("License is valid:", response.code);
} else {
console.log("License invalid:", response.detail);
}
```
> **Response**
>
> ```json theme={null}
> {
> "valid": true,
> "code": "VALID",
> "detail": "Instance ID valid for this license.",
> "metadata": {
> "os": "macOS",
> "arch": "arm64"
> },
> "license": {
> "id": "7be7a5b6-7a0e-4178-a085-8ef48f0649ab",
> "licenseKey": "LIC-A8D5F89E-B46B-472C-9BED-66EE2545B85E",
> "activatedOn": "2026-01-22T03:26:56Z",
> "expiresOn": "2026-01-23T03:26:56Z",
> "activationUsage": 1,
> "activationLimit": 4,
> "enabled": true,
> "customer": {
> "customerId": "cfa95cf8c8d24e8b9d3d2d1ada98a94d",
> "name": "Geo",
> "email": "geo@kelviq.com"
> },
> "plan": {
> "identifier": "pro-yearly",
> "name": "Pro Yearly",
> "product": {
> "id": "d970ab2d-834f-4c52-8742-2e6ed7e8a31f",
> "identifier": "kelviq-engine",
> "name": "Kelviq Engine",
> "taxCode": "software"
> }
> },
> "subscription": {
> "id": "09d706ca-58f3-4fb2-818f-5b8623d67e6e",
> "recurrence": "1 month",
> "billingPeriodStartTime": "2026-01-22T03:26:56Z",
> "billingPeriodEndTime": "2026-02-22T03:26:56Z",
> "startDate": "2026-01-22",
> "endDate": null,
> "status": "active",
> "amount": "49.00",
> "currency": "usd",
> "trialDaysRemaining": 0,
> "billingType": "SUBSCRIPTION",
> "recurrenceUnit": 1,
> "recurrenceType": "MONTH"
> }
> }
> }
> ```
**Required Parameters:**
* `licenseKey` : The license key string to validate.
**Optional Parameters:**
* `instanceId` : If provided, also validates that this specific instance is active for the given license key.
**Returns:**
An instance of `LicenseValidateResponse`, which includes:
* `valid` (boolean): Whether the license (and instance, if provided) is valid.
* `code` (string): A short status code (e.g. `"VALID"`, `"INVALID"`, `"EXPIRED"`).
* `detail` (string): A human-readable description of the validation result.
* `metadata` (object | null): Any metadata attached to the instance, if applicable.
* `license` (`LicenseDetails | null`): The full license object (same structure as in `activate`), or `null` if not found.
***
## Portal
The portal module allows you to create authenticated customer portal sessions. Once a session is created, you can redirect your customer directly to their self-serve customer portal without requiring them to log in manually. You can access these operations via the `portal` attribute on an initialized `Kelviq` client instance.
### Creates a new customer portal session
```typescript theme={null}
const session = await client.portal.createSession({
customerId: "cust_789",
});
// Build the signed portal link — customerPortalUrl alone does not
// authenticate; the token must be appended as a query parameter.
const portalLink = `${session.customerPortalUrl}?token=${session.token}`;
// Redirect your customer to the portal
res.redirect(portalLink);
```
> **Response**
>
> ```json theme={null}
> {
> "token": "cpt-4f7c5dc4-da9f-447b-d447-6206d835ed6d:23fca530-ajeb-4685-b3d5-9918h6d38275",
> "email": "geo@example.com",
> "customerPortalUrl": "https://www.kelviq.com/portal/acme/"
> }
> ```
**Required Parameters:**
* `customerId` : The unique identifier of the customer for whom the portal session is being created.
**Returns:**
An instance of `CreatePortalSessionResponse` (TypeScript Interface), which includes:
* `token` : The session token that authenticates the customer portal session.
* `email` : The email address of the customer.
* `customerPortalUrl` : The customer's billing portal base URL. **This URL alone does not authenticate the session** — append the `token` as a query parameter (`${customerPortalUrl}?token=${token}`) to produce the signed link you share with the customer. The token expires, so create a fresh session per visit.
### Deep-linking to add a payment method
You can send a customer directly to the **add payment method** form and have them redirected back to your application when they're done. Append these query parameters to the signed portal link, targeting the `billing` page:
| Parameter | Required | Description |
| - | - | - |
| `intent` | Yes | Set to `add_payment_method` to auto-open the add payment method form. |
| `returnUrl` | No | URL-encoded `https` URL of your application. After the payment method is saved, the customer is redirected here with `?kelviq_setup=success` appended. `http` is only allowed for `localhost` during development. |
| `customerId` | No | Opens the form for this specific customer account. If omitted, the customer's first account is used. |
```typescript theme={null}
const session = await client.portal.createSession({
customerId: "cust_789",
});
const returnUrl = encodeURIComponent("https://your-app.com/billing/done");
const addPaymentMethodLink =
`${session.customerPortalUrl}billing` +
`?token=${session.token}` +
`&intent=add_payment_method` +
`&customerId=cust_789` +
`&returnUrl=${returnUrl}`;
res.redirect(addPaymentMethodLink);
```
After the customer saves their payment method, the portal redirects them to `https://your-app.com/billing/done?kelviq_setup=success`.
***
## Webhooks
Kelviq can send real-time webhook notifications to your server when events occur in your account — for example, when a subscription is created or an invoice is paid.
### Verifying webhook signatures
Every webhook request includes three headers that you must use to verify the request is genuinely from Kelviq:
| Header | Description |
| - | - |
| `webhook-id` | Unique identifier for the event |
| `webhook-timestamp` | Unix timestamp of when the event was sent |
| `webhook-signature` | HMAC-SHA256 signature, prefixed with `v1,` |
Use the `validateEvent` helper to verify the signature and parse the event in one step. It throws `WebhookVerificationError` if the signature is invalid.
```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,
'',
);
// Process the event
console.log('Event type:', event.type);
res.sendStatus(202);
} catch (err) {
if (err instanceof WebhookVerificationError) {
return res.sendStatus(403);
}
throw err;
}
});
```
> [!WARNING]
>
> Use `express.raw()` (not `express.json()`) so that the raw request body is preserved for signature verification. Parsing the body as JSON before verification will break the signature check.
On a framework built on the Fetch API — Next.js route handlers, Hono, Remix, SvelteKit, Cloudflare Workers — `request.headers` is a `Headers` object, not a plain object. Convert it with `Object.fromEntries`, and read the body with `request.text()` so it stays unparsed:
```typescript theme={null}
import { validateEvent, WebhookVerificationError } from '@kelviq/node-sdk';
export async function POST(request: Request) {
const body = await request.text();
try {
const event = validateEvent(
body,
Object.fromEntries(request.headers),
process.env.KELVIQ_WEBHOOK_SECRET!,
);
console.log('Event type:', event.type);
return new Response(null, { status: 202 });
} catch (err) {
if (err instanceof WebhookVerificationError) {
return new Response(null, { status: 403 });
}
throw err;
}
}
```
> [!WARNING]
>
> Passing a `Headers` object straight to `validateEvent` fails with `Missing required webhook headers`, because the helper reads headers as plain object entries.
### Webhook event types
See [Webhook event types](/guides/webhooks#event-types) for the complete list of supported events and their descriptions.
### Best practices
* **Return quickly** — respond with a `2xx` status before doing any heavy processing. Kelviq waits 10 seconds for a response. It retries up to 3 times at 60-second intervals on a timeout, connection failure, `5xx` or `429`; other non-`2xx` responses are not retried.
* **Use idempotency** — use the `webhook-id` header (or the event `id` field) to detect and skip duplicate deliveries.
* **Validate every request** — always verify the signature before trusting the payload.
***
## Using the Sandbox Environment
For testing and development, you can configure the client to use the sandbox environment. This ensures that no production data is affected. To do this, set the environment option to 'sandbox' during client initialization.
```typescript theme={null}
import { Kelviq } from '@kelviq/node-sdk';
const client = new Kelviq({
accessToken: 'YOUR_SANDBOX_ACCESS_TOKEN',
environment: 'sandbox'
});
```
To pick the environment from the `KELVIQ_ENV` variable that the Kelviq CLI and MCP server also read, use `environmentFromEnv()`: it returns `'sandbox'` when `KELVIQ_ENV` is unset or empty, `'production'` or `'sandbox'` when it is set to one of those, and throws on any other value.
```typescript theme={null}
import { Kelviq, environmentFromEnv } from '@kelviq/node-sdk';
const client = new Kelviq({
accessToken: process.env.KELVIQ_SERVER_API_KEY!,
environment: environmentFromEnv(), // reads KELVIQ_ENV: sandbox when unset
});
```
> [!NOTE]
>
> The sandbox environment is completely separate from production. You will need to use a different set of API keys, and any data created (customers, subscriptions, etc.) will only exist in the sandbox.
# Python SDK
Source: https://docs.kelviq.com/backend-integration/python-sdk
Documentation for the Kelviq Python SDK
## Overview
The Kelviq Python SDK provides a convenient way to interact with the Kelviq REST APIs from your Python application. The SDK supports both synchronous and asynchronous operations.
## Installation
To install the Kelviq SDK in your environment, run the following command:
```shell theme={null}
pip install kelviq-sdk
```
## Prerequisites
Before you can initialize the client and use the SDK methods, you need a **Server API Key**.
You can obtain this key from the Kelviq application:
1. Navigate to Settings.
2. Go to the [API keys](https://app.kelviq.com/settings/api-keys) section.
3. Copy the **Server API Key**.
> [!WARNING]
>
> **Security Warning:** The Server API Key should never be exposed in client-side code. Use it only in your backend services. For client-side applications, use the Client API Key with the [React SDK](/frontend-integration/react-sdk) or [JavaScript SDK](/frontend-integration/js-sdk).
Once copied, add this key to your environment variables (recommended for security):
```python theme={null}
import os
# Example of setting it in your code (environment variables recommended)
ACCESS_TOKEN = os.environ.get("KELVIQ_SERVER_API_KEY")
```
## Configuring the Client
The SDK supports both synchronous and asynchronous clients. The primary difference lies in how you create the client instance. Once created, both client types can invoke the same methods to communicate with the Kelviq application.
#### How to Create a Synchronous Client
Use the synchronous client for traditional, blocking I/O operations.
```python theme={null}
from kelviq_sdk import Kelviq
client = Kelviq.create_sync_client(access_token=ACCESS_TOKEN)
```
#### How to Create an Asynchronous Client
Use the asynchronous client for non-blocking I/O operations, suitable for applications using `asyncio`.
```python theme={null}
from kelviq_sdk import Kelviq
async_client = Kelviq.create_async_client(access_token=ACCESS_TOKEN)
```
## Caching and offline resilience
Entitlement caching is enabled by default for both synchronous and asynchronous clients. The SDK keeps a per-process in-memory cache with a 60-second freshness window. Reads use a fresh memory entry first, then an optional distributed cache, and then the Entitlement API.
If the API is unreachable or returns a server error, the SDK returns the last known cached entitlement response when one exists. If the customer or feature has never been cached, the original request error is raised.
```python theme={null}
client = Kelviq.create_sync_client(
access_token=ACCESS_TOKEN,
enable_cache=True, # default: True
cache_ttl=60.0, # default: 60 seconds
)
```
Set `enable_cache=False` to disable caching.
To drop a customer's cached entitlements before the window expires, for example in a `subscription.paused`, `subscription.resumed`, or `subscription.cancelled` webhook handler:
```python theme={null}
client.entitlements.invalidate_cache(customerId=customer_id)
```
### Share the cache across workers
The in-memory cache and offline reporting queue belong to one process and disappear when it restarts. Use the optional Redis store when multiple workers, processes, or containers need one shared, durable cache and queue.
```shell theme={null}
pip install "kelviq-sdk[redis]"
```
```python theme={null}
from kelviq_sdk import Kelviq, RedisStore
store = RedisStore(
url="redis://localhost:6379/0",
prefix="kelviq:production",
)
client = Kelviq.create_sync_client(
access_token=ACCESS_TOKEN,
cache_store=store,
)
```
For an asynchronous client, use `AsyncRedisStore`. Use the same prefix for every worker that should share state.
### Replay queued usage
When a usage or event report fails because of a network problem, the SDK queues it and retries automatically on the next report call. You can also flush the queue explicitly:
```python theme={null}
remaining = client.reporting.flush()
# Async: remaining = await async_client.reporting.flush()
```
`remaining` is the number of reports still queued. Without Redis, the queue exists only in the current process.
See [When the Entitlement API is unavailable](/product-catalog/entitlements#when-the-entitlement-api-is-unavailable) for access-control fallback guidance.
#### Enum Helpers
The SDK provides `ChargePeriod` and `CancellationType` enum classes to prevent typos when passing required string parameters:
```python theme={null}
from kelviq_sdk import ChargePeriod, CancellationType
# Use enums instead of raw strings
ChargePeriod.MONTHLY # "MONTHLY"
CancellationType.IMMEDIATE # "IMMEDIATE"
```
## Supported Functionalities
The SDK currently supports the following operations:
1. [Customers](/backend-integration/python-sdk#customers)
2. [Checkout](/backend-integration/python-sdk#checkout)
3. [Entitlements](/backend-integration/python-sdk#entitlements)
4. [Reporting](/backend-integration/python-sdk#reporting)
5. [Subscriptions](/backend-integration/python-sdk#subscriptions)
6. [Charges](/backend-integration/python-sdk#charges)
7. [License](/backend-integration/python-sdk#license)
8. [Portal](/backend-integration/python-sdk#portal)
9. [Refunds](/backend-integration/python-sdk#refunds)
10. [Payment Methods](/backend-integration/python-sdk#payment-methods)
11. [Transactions](/backend-integration/python-sdk#transactions)
***
## Customers
The `customers` module allows you to manage customer records within Kelviq. You can access these operations via the `customers` attribute on an initialized `Kelviq` client instance.
All methods are available in both synchronous and asynchronous versions, depending on how the `Kelviq` client was instantiated.
***
### Creates a new customer
**Synchronous**:
```python theme={null}
new_customer = client.customers.create(
customerId="unique-customer-id-123",
email="new.customer@example.com",
name="John Doe",
metadata={"source": "sdk_import", "priority": "high"}
)
print(f"Customer Created: ID = {new_customer.id}, Customer Client ID = {new_customer.customerId}")
```
**Asynchronous**:
```python theme={null}
new_customer = await async_client.customers.create(
customerId="unique-customer-id-456",
email="another.new.customer@example.com",
name="Jane Roe"
)
print(f"Customer Created (Async): ID = {new_customer.id}, Customer Client ID = {new_customer.customerId}")
```
> **Response**
>
> ```json theme={null}
> {
> "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
> "customerId": "unique-customer-id-123",
> "name": "John Doe",
> "email": "new.customer@example.com",
> "details": {},
> "metadata": {
> "source": "sdk_import",
> "priority": "high"
> },
> "createdOn": "2025-06-04T06:03:30.195790Z",
> "modifiedOn": "2025-06-04T06:03:30.195831Z"
> }
> ```
**Required Parameters:**
* `customerId` (str): A unique identifier for the customer that you define. This ID will be used to reference the customer in subsequent API calls.
* `email` (str): The email address of the customer. Must be a valid email format.
**Optional Parameters:**
* `name` (str): The name of the customer.
* `metadata` (Dict\[str, str]): A dictionary of custom key-value pairs to store additional information about the customer.
**Returns:**
An instance of `CustomerResponse` (Pydantic model), representing the newly created customer record. Key attributes include:
* `id` (str): The server-generated unique UUID for the customer record.
* `customerId` (str): The client-provided customer identifier.
* `name` (str): The customer's name.
* `email` (str): The customer's email.
* `details` (\[Dict\[str, Any]]): Any server-added details about the customer (typically read-only).
* `metadata` (\[Dict\[str, str]]): The metadata associated with the customer.
* `createdOn` (string) : ISO 8601 timestamp of when the customer was created.
* `modifiedOn` (string) : ISO 8601 timestamp of when the customer was last modified.
### Updates an existing customer
This operation performs a partial update (PATCH), so you only need to provide the fields you want to change.
**Synchronous**:
```python theme={null}
updated_customer = client.customers.update(
customerId="unique-customer-id-123",
name="Johnathan Doe",
metadata={"source": "sdk_import", "priority": "very_high", "status": "active"}
)
print(f"Customer Updated: {updated_customer.name}")
```
**Asynchronous**:
```python theme={null}
updated_customer = await async_client.customers.update(
customerId="unique-customer-id-456",
email="jane.roe.updated@example.com"
)
print(f"Customer Updated (Async): {updated_customer.email}")
```
> **Response**
>
> ```json theme={null}
> {
> "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
> "customerId": "unique-customer-id-123",
> "name": "Johnathan Doe",
> "email": "new.customer@example.com",
> "details": {},
> "metadata": {
> "source": "sdk_import",
> "priority": "high",
> "status": "active"
> },
> "createdOn": "2025-06-04T06:03:30.195790Z",
> "modifiedOn": "2025-06-04T06:03:30.195831Z"
> }
> ```
**Parameters:**
**Required Parameters:**
* `customerId` (str): A unique identifier for the customer that you define. This ID will be used to reference the customer in subsequent API calls.
**Optional Parameters:**
* `email` (str): The email address of the customer. Must be a valid email format.
* `name` (str): The name of the customer.
* `metadata` (Dict\[str, str]): A dictionary of custom key-value pairs to store additional information about the customer.
**Returns:**
An instance of `CustomerResponse` (Pydantic model), representing the **updated** customer record. Key attributes include:
* `id` (str): The server-generated unique UUID for the customer record.
* `customerId` (str): The client-provided customer identifier.
* `name` (str): The customer's name.
* `email` (str): The customer's email.
* `details` (\[Dict\[str, Any]]): Any server-added details about the customer (typically read-only).
* `metadata` (\[Dict\[str, Any]]): The metadata associated with the customer.
* `createdOn` (string) : ISO 8601 timestamp of when the customer was created.
* `modifiedOn` (string) : ISO 8601 timestamp of when the customer was last modified.
***
## Checkout
The checkout module provides functionalities for creating and managing checkout sessions. You can access these operations via the checkout attribute on an initialized Kelviq client instance.
`create_session(...)`
This operation creates a new checkout session for a customer, allowing them to proceed with a purchase or subscription.
**Synchronous**:
```python theme={null}
response = client.checkout.create_session(
planIdentifier="plan-pro-monthly",
chargePeriod="MONTHLY",
customerId="cust_789", # Recommended, otherwise it will create a new customer
successUrl="https://kelviq.com/checkout/success",
trialPeriod=14,
)
print(f"Checkout Session URL: {response.checkoutUrl}")
```
**Asynchronous**:
```python theme={null}
response = await async_client.checkout.create_session(
planIdentifier="plan-pro-monthly",
chargePeriod="MONTHLY",
customerId="cust_789",
successUrl="https://kelviq.com/checkout/success",
offeringId="96d3a293-34ac-48a5-b73d-976b15781afd",
pricingTableId="6db2717a-4e43-4ae1-a293-6c0a775f6264",
ruleId="f2ebc23f-3f79-4cf4-b17e-fe8e30a42ab2",
features=[{'identifier':"seats", 'quantity': 5}],
ipAddress="103.154.35.20",
)
print(f"Checkout Session URL: {response.checkoutUrl}")
```
> **Response**
>
> ```json theme={null}
> {
> "checkoutUrl": "https://kelviq.com/checkout/cs_eeCGdGayc5oTMdGNV5XocfRk6o87K0vlwwCuKeiE7BLU9/",
> "checkoutSessionId": "cs_eeCGdGayc5oTMdGNV5XocfRk6o87K0vlwwCuKeiE7BLU9"
> }
> ```
**Required Parameters:**
* `planIdentifier` (str): The identifier of the specific plan the customer is checking out with. planIdentifier is mandatory.
* `successUrl` (str): The URL to which the user will be redirected after a successful checkout.
* `chargePeriod` (str | ChargePeriod): Required. The billing cycle for the subscription. You can use the `ChargePeriod` enum (e.g., `ChargePeriod.MONTHLY`) or a raw string. Must be one of:
* `"ONE_TIME"` / `ChargePeriod.ONE_TIME`
* `"MONTHLY"` / `ChargePeriod.MONTHLY`
* `"YEARLY"` / `ChargePeriod.YEARLY`
* `"WEEKLY"` / `ChargePeriod.WEEKLY`
* `"DAILY"` / `ChargePeriod.DAILY`
* `"TWENTY_EIGHT_DAYS"` / `ChargePeriod.TWENTY_EIGHT_DAYS`
* `"THREE_MONTHS"` / `ChargePeriod.THREE_MONTHS`
* `"SIX_MONTHS"` / `ChargePeriod.SIX_MONTHS`
**Optional Parameters:**
* `offeringId` (str): The ID (uuid) of the offering the customer is checking out with.
* `pricingTableId` (str): The id (uuid) of the pricingTable being used for this checkout. (Considered only if `offeringId` is not provided)
* `ruleId` (str): The id (uuid) of the pricing rule being applied. (Considered only if `offeringId` is not provided)
* `customerId` (str): The ID of the customer initiating the checkout. If you omit it, Kelviq creates a new customer.
* `features` (List\[Dict\[str, Union\[str, int]]]): Optional. A list of dictionaries, where each dictionary represents a feature and its desired quantity. Each dictionary **must** have two keys:
* `"identifier"` (str): The unique identifier for the feature.
* `"quantity"` (int): The desired quantity for this feature.
* Example: `[{"identifier": "seats", "quantity": 10}, {"identifier": "api-calls-tier1", "quantity": 5000}]`
* `ipAddress` (str): The IP Address of the customer, used for location based pricing.
* `trialPeriod` (int): Number of trial days to apply. Must be greater than or equal to `1` and overrides the selected plan's configured trial period.
* `currencyCode` (str): The currency code for the checkout session (e.g. `"USD"`). Required when `customAmount` is provided.
* `customAmount` (float): A custom amount to charge for this checkout session. When provided, `taxBehavior` and `currencyCode` are also required.
* `taxBehavior` (str): Specifies how taxes are applied to the `customAmount`. Required when `customAmount` is provided. Must be one of:
* `"INCLUSIVE"` — Tax is included in the custom amount.
* `"EXCLUSIVE"` — Tax is added on top of the custom amount.
* `discountsEnabled` (bool): Whether the discount/coupon code field is shown on the checkout page. Defaults to `True`.
* `lockEmail` (bool): When `True`, the email field is pre-filled and locked so the customer cannot change it. Defaults to `False`.
* `defaultBillingCountry` (str): ISO 3166-1 alpha-2 country code (e.g. `"US"`, `"GB"`) used to pre-fill the billing address country on the checkout page.
* `metadata` (dict): Arbitrary key-value pairs to attach to the checkout session. The keys are preserved exactly as supplied and the object is returned unchanged on the `checkout.completed` webhook's `metadata` field. Example: `{"order_ref": "ABC-123", "source": "pricing_page"}`.
**Returns**:
An instance of CreateCheckoutSessionResponse (Pydantic model), which includes:
* `checkoutSessionId` (str): The unique ID for the created checkout session.
* `checkoutUrl` (str): The URL that the customer should be redirected to in order to complete the payment and activate the subscription/purchase.
***
## Entitlements
The entitlements module allows you to check and retrieve customer entitlements for various features. These operations target a specific edge API endpoint ( `https://edge.api.kelviq.com` by default) and use the GET HTTP method with query parameters.
### Checks if a specific customer has access to a particular feature.
This method directly returns a boolean indicating access status.
**Synchronous**:
```python theme={null}
has_feature_access = client.entitlements.has_access(
customerId="cust_123",
featureId="premium-reporting"
)
```
**Asynchronous**:
```python theme={null}
has_feature_access = await async_client.entitlements.has_access(
customerId="cust_456",
featureId="advanced-analytics"
)
```
> **Response**
>
> ```shell theme={null}
> True
> ```
**Required Parameters**:
* `customerId` (str): The unique identifier for the customer.
* `featureId` (str): The unique identifier for the feature whose access is being checked.
**Returns**:
* `bool`: True if the customer has access to the specified feature (considering feature type, limits, etc.), False otherwise or if the feature is not found in their entitlements.
### Retrieves the detailed entitlement information for a specific feature for a given customer.
**Synchronous**:
```python theme={null}
entitlement_response = client.entitlements.get_entitlement(
customerId="cust_123",
featureId="premium-reporting"
)
```
**Asynchronous**:
```python theme={null}
entitlement_response = await async_client.entitlements.get_entitlement(
customerId="cust_456",
featureId="advanced-analytics"
)
```
> **Response**
>
> ```python theme={null}
> Entitlement(
> featureId="advanced-analytics",
> featureType="METER",
> hasAccess=True,
> hardLimit=False,
> usageLimit=3,
> currentUsage=0,
> remaining=3,
> items=[
> EntitlementDetail(featureId="advanced-analytics", featureType="METER", hasAccess=True, resetAt="2025-05-22 08:27:45", hardLimit=False, usageLimit=3, currentUsage=0, remaining=3)
> ]
> )
> ```
**Required Parameters**:
* `customerId` (str): The unique identifier for the customer.
* `featureId` (str): The unique identifier for the feature whose access is being checked.
**Returns**:
An `Entitlement` model (Pydantic), or `None` if the feature is not found. If the customer has multiple subscriptions, there can be multiple raw entries for the same featureId. The SDK aggregates these into a single model and keeps the raw entries accessible via the `items` attribute.
The aggregated `Entitlement` model includes:
* `featureId` (str): The feature ID.
* `featureType` (str): The type of the feature — `"METER"`, `"BOOLEAN"`, or `"CUSTOMIZABLE"`.
* `hasAccess` (bool): For **METER** features, computed as `remaining is None or remaining > 0`. For **BOOLEAN** and **CUSTOMIZABLE** features, `True` if any raw entry grants access.
* `hardLimit` (Optional\[bool]): For **METER**, `True` if any entry has a hard limit. For **BOOLEAN**, always `False`.
* `usageLimit` (Optional\[int]): For **METER**, summed across all items. For **CUSTOMIZABLE**, taken from the first item. For **BOOLEAN**, always `None`.
* `currentUsage` (Optional\[int]): For **METER**, summed across all items. For **CUSTOMIZABLE**, taken from the first item. For **BOOLEAN**, always `0`.
* `remaining` (Optional\[int]): For **METER**, computed as `usageLimit - currentUsage`. For **CUSTOMIZABLE**, taken from the first item. For **BOOLEAN**, always `None`.
* `items` (List\[EntitlementDetail]): The raw entitlement entries from the API. Each item retains its own `resetAt` value, since reset dates can differ across subscriptions.
### Retrieves all aggregated entitlements for a given customer.
**Synchronous**:
```python theme={null}
entitlements = client.entitlements.get_entitlements(
customerId="cust_123"
)
# Access by feature ID
if "premium-reporting" in entitlements:
ent = entitlements["premium-reporting"]
print(f"hasAccess={ent.hasAccess}, items={len(ent.items)}")
```
**Asynchronous**:
```python theme={null}
entitlements = await async_client.entitlements.get_entitlements(
customerId="cust_456"
)
for feature_id, ent in entitlements.items():
print(f"{feature_id}: hasAccess={ent.hasAccess}, items={len(ent.items)}")
```
**Required Parameters**:
* `customerId` (str): The unique identifier for the customer.
**Returns**:
A `Dict[str, Entitlement]` keyed by `featureId`, where each value is an aggregated `Entitlement` model with an `.items` list containing the raw entries. See `get_entitlement` above for the full model structure.
### Retrieves the raw entitlement response for a specific feature (no aggregation).
**Synchronous**:
```python theme={null}
raw_response = client.entitlements.get_raw_entitlement(
customerId="cust_123",
featureId="premium-reporting"
)
print(f"Customer: {raw_response.customerId}")
for detail in raw_response.entitlements:
print(f" {detail.featureId}: {detail.hasAccess}")
```
**Asynchronous**:
```python theme={null}
raw_response = await async_client.entitlements.get_raw_entitlement(
customerId="cust_456",
featureId="advanced-analytics"
)
```
**Required Parameters**:
* `customerId` (str): The unique identifier for the customer.
* `featureId` (str): The unique identifier for the feature.
**Returns**:
An instance of `CheckEntitlementsResponse` (Pydantic model) containing the raw API response without aggregation:
* `customerId` (str): The customer's ID.
* `entitlements` (List\[EntitlementDetail]): The raw list of entitlement entries.
### Retrieves all raw entitlements for a given customer (no aggregation).
**Synchronous**:
```python theme={null}
raw_response = client.entitlements.get_raw_entitlements(
customerId="cust_123"
)
```
**Asynchronous**:
```python theme={null}
raw_response = await async_client.entitlements.get_raw_entitlements(
customerId="cust_456"
)
```
> **Response**
>
> ```json theme={null}
> {
> "customerId": "cust_456",
> "entitlements": [
> {
> "featureId": "advanced-analytics",
> "featureType": "METER",
> "hasAccess": true,
> "resetAt": "2025-05-22 08:27:45",
> "hardLimit": false,
> "usageLimit": 3,
> "currentUsage": 0,
> "remaining": 3
> }
> ]
> }
> ```
**Required Parameters**:
* `customerId` (str): The unique identifier for the customer.
**Returns**:
An instance of `CheckEntitlementsResponse` (Pydantic model) containing the raw API response without aggregation:
* `customerId` (str): The customer's ID.
* `entitlements` (List\[EntitlementDetail]): The raw list of entitlement entries. Each `EntitlementDetail` has fields like:
* `featureId`: str
* `hasAccess`: bool
* `featureType`: str
* `resetAt`: str
* `hardLimit`: Optional\[bool]
* `usageLimit`: Optional\[int]
* `currentUsage`: Optional\[int]
* `remaining`: Optional\[int]
***
## Reporting
### Reporting pre-aggregated usages for customer
This endpoint is used for reporting the pre-aggregated feature usage from your application (client-level) to the Kelviq application. It allows you to update the usage count for a specific feature associated with a customer.
**Synchronous**:
```python theme={null}
# Assuming 'client' is your synchronous Kelviq client
response = client.reporting.report_usage(
value=150,
customerId="customer_001",
featureId="seats",
behaviour="SET" # Or 'DELTA'
)
```
**Asynchronous**:
```python theme={null}
# Assuming 'async_client' is your asynchronous Kelviq client
response = await async_client.reporting.report_usage(
value=75,
customerId="customer_001",
featureId="seats",
behaviour="DELTA"
)
```
> **Response**
>
> ```json theme={null}
> {
> "value": 150,
> "customerId": "customer_001",
> "featureId": "seats",
> "behaviour": "SET",
> "orgId": "1",
> "eventName": "aggregated.usage",
> "idempotencyKey": "597ee95063c744ed9bcc9b1cf5676a8a",
> "timestamp": "2025-05-22 08:27:45.430732"
> }
> ```
**Required Parameters**:
* `value` (int): The usage value being reported.
* `customerId` (str): The unique identifier for the customer associated with this usage.
* `featureId` (str): The unique identifier for the feature for which usage is being reported.
* `behaviour` parameter dictates how the usage is updated:
* `SET`: This will replace the current usage value for the feature with the new `value` provided.
* `DELTA`: This will increment the existing usage value for the feature by the amount specified in the `value` parameter
**Returns**:
An instance of ReportUsageResponse (Pydantic model), which includes:
* `value` (int): The usage value that was recorded.
* `customerId` (str): The customer ID associated with the usage.
* `featureId` (str): The feature Identifier for which usage was recorded.
* `behaviour` (str): The behaviour type ("SET" or "DELTA") that was processed.
* `orgId` (str): The organization ID associated with this record, as determined by the server.
* `eventName` (str): An internal event name generated by the server for this usage report (e.g., "aggregated.usage").
* `idempotencyKey` (str): A unique idempotency key generated by the server for this specific usage report instance.
* `timestamp` (str): The server-generated UTC timestamp (str format) indicating when the usage report was processed.
## Subscriptions
The subscriptions module allows you to list, retrieve, create, preview updates, update, and cancel customer subscriptions. You can access these operations via the `subscriptions` attribute on an initialized Kelviq client instance.
### List subscriptions
Retrieves a paginated list of subscriptions for a customer.
**Synchronous:**
```python theme={null}
page = client.subscriptions.list(
customerId="cust_789",
page=1,
page_size=20,
)
for subscription in page.results:
print(subscription.id, subscription.status)
```
**Asynchronous:**
```python theme={null}
page = await async_client.subscriptions.list(
customerId="cust_789",
page=1,
page_size=20,
)
for subscription in page.results:
print(subscription.id, subscription.status)
```
> **Response**
>
> ```json theme={null}
> {
> "count": 1,
> "next": null,
> "previous": null,
> "results": [
> {
> "id": "09d706ca-58f3-4fb2-818f-5b8623d67e6e",
> "startDate": "2025-07-28",
> "endDate": null,
> "status": "active",
> "trialDaysRemaining": 0,
> "customerId": "cust_789",
> "billingType": "SUBSCRIPTION",
> "files": [],
> "links": [],
> "license": []
> }
> ]
> }
> ```
**Required Parameters:**
* `customerId` (str): The client-defined identifier of the customer whose subscriptions will be returned.
**Optional Parameters:**
* `page` (int): Page number to retrieve.
* `page_size` (int): Number of subscriptions to return per page.
* `modifiedOnAfter` (str): Only return subscriptions modified on or after this ISO 8601 timestamp.
* `modifiedOnBefore` (str): Only return subscriptions modified on or before this ISO 8601 timestamp.
**Returns:**
A `PaginatedSubscriptionResponse` containing `count`, `next`, `previous`, and a `results` list of `SubscriptionData` models.
### Retrieve a subscription
Retrieves one subscription using its Kelviq subscription ID.
**Synchronous:**
```python theme={null}
subscription = client.subscriptions.retrieve(
subscriptionId="09d706ca-58f3-4fb2-818f-5b8623d67e6e",
)
print(subscription.id, subscription.status)
for file in subscription.files:
print(file.name, file.downloadUrl)
for link in subscription.links:
print(link.name, link.url)
for license in subscription.license:
print(license.licenseKey, license.activationLimit)
```
**Asynchronous:**
```python theme={null}
subscription = await async_client.subscriptions.retrieve(
subscriptionId="09d706ca-58f3-4fb2-818f-5b8623d67e6e",
)
print(subscription.id, subscription.status)
```
> **Response**
>
> ```json theme={null}
> {
> "id": "09d706ca-58f3-4fb2-818f-5b8623d67e6e",
> "externalSubscriptionId": "sub_1RpoZoSBstCzyocS7iPmith3",
> "startDate": "2025-07-28",
> "endDate": null,
> "billingPeriodStartTime": "2025-07-28T10:46:34Z",
> "billingPeriodEndTime": "2025-08-28T10:46:34Z",
> "amount": "261.00",
> "recurrence": "month",
> "currency": "usd",
> "status": "active",
> "trialDaysRemaining": 0,
> "customerId": "cust_789",
> "billingType": "SUBSCRIPTION",
> "recurrenceUnit": 1,
> "recurrenceType": "MONTH",
> "files": [],
> "links": [],
> "license": []
> }
> ```
**Required Parameters:**
* `subscriptionId` (str): The Kelviq UUID of the subscription.
**Returns:**
A `SubscriptionData` model. A missing subscription raises `NotFoundError`.
### Create a subscription
Creates a subscription directly for a customer without requiring a checkout session.
**Synchronous:**
```python theme={null}
from kelviq_sdk import ChargePeriod
subscription = client.subscriptions.create(
planIdentifier="plan-pro-monthly",
chargePeriod=ChargePeriod.MONTHLY,
customerId="cust_789",
successUrl="https://example.com/subscription/success",
features=[{"identifier": "seats", "quantity": 5}],
ipAddress="103.154.35.20",
)
print(subscription.id, subscription.status)
```
**Asynchronous:**
```python theme={null}
subscription = await async_client.subscriptions.create(
planIdentifier="plan-pro-monthly",
chargePeriod="MONTHLY",
customerId="cust_789",
successUrl="https://example.com/subscription/success",
features=[{"identifier": "seats", "quantity": 5}],
ipAddress="103.154.35.20",
)
print(subscription.id, subscription.status)
```
**Required Parameters:**
* `planIdentifier` (str): Identifier of the plan to subscribe to.
* `chargePeriod` (str | ChargePeriod): Billing period such as `MONTHLY`, `YEARLY`, `WEEKLY`, `DAILY`, or `TWENTY_EIGHT_DAYS`.
* `customerId` (str): Client-defined identifier of the customer.
**Optional Parameters:**
* `successUrl` (str): URL to redirect to after successful subscription creation.
* `features` (List\[Dict\[str, Any]]): Feature identifiers and requested quantities.
* `ipAddress` (str): Customer IP address used for location-based pricing.
**Returns:**
A `CreateSubscriptionResponse` model containing the created subscription, including its plan files, links, and issued licenses when available.
### Updates an existing subscription to a new plan
**Synchronous**:
```python theme={null}
from kelviq_sdk import ProrationBehavior
try:
response = client.subscriptions.update(
subscriptionId="78058918-9746-4280-9b9b-1bd5115eec6e",
planIdentifier="premium-plan",
chargePeriod="MONTHLY",
prorationBehavior=ProrationBehavior.PRORATE_NEXT_INVOICE,
)
print(response.subscriptionId)
except Exception as e:
print(f"Error updating subscription: {e}")
```
**Asynchronous**:
```python theme={null}
# Assuming 'async_client' is your asynchronous Kelviq client
try:
response = await async_client.subscriptions.update(
subscriptionId="78058918-9746-4280-9b9b-1bd5115eec6e",
planIdentifier="new_plan_enterprise",
chargePeriod="YEARLY",
prorationBehavior=ProrationBehavior.PRORATE_NEXT_INVOICE,
offeringId="offer_abc",
pricingTableId="pw_def",
ruleId="rule_ghi",
ipAddress="103.154.35.20",
features=[{'identifier': "seats", 'quantity': 10}],
trialEnd="2027-12-31 23:59:59", # or "now" to end the trial immediately
paymentBehavior="activate_on_payment",
)
print(response.subscriptionId)
except Exception as e:
print(f"Error updating subscription: {e}")
```
> **Response**
>
> ```json theme={null}
> {
> "subscriptionId": "dffaf07e-4517-47db-ba3a-59a05aa2d465"
> }
> ```
**Required Parameters**:
* `subscriptionId` (str): Required. The unique identifier of the subscription to be updated.
* `planIdentifier` (str): Required. The identifier of the new plan.
* `chargePeriod` (str | ChargePeriod): Required. The new charging period for the subscription. You can use the `ChargePeriod` enum (e.g., `ChargePeriod.MONTHLY`) or a raw string. Must be one of:
* `"ONE_TIME"` / `ChargePeriod.ONE_TIME`
* `"MONTHLY"` / `ChargePeriod.MONTHLY`
* `"YEARLY"` / `ChargePeriod.YEARLY`
* `"WEEKLY"` / `ChargePeriod.WEEKLY`
* `"DAILY"` / `ChargePeriod.DAILY`
* `"TWENTY_EIGHT_DAYS"` / `ChargePeriod.TWENTY_EIGHT_DAYS`
* `"THREE_MONTHS"` / `ChargePeriod.THREE_MONTHS`
* `"SIX_MONTHS"` / `ChargePeriod.SIX_MONTHS`
**Optional Parameters:**
* `offeringId` (str): Optional. The ID of the new offering, if applicable.
* `pricingTableId` (str): Optional. The ID of the new pricingTable, if applicable.
* `ruleId` (str): Optional. The ID of the new pricing rule, if applicable.
* `ipAddress` (str): The IP Address of the customer, used for location based pricing.
* `features` (\[List\[Dict\[str, Any]]]): Optional. A list of dictionaries, where each dictionary represents a feature and its desired quantity to update for the subscription. Each dictionary **must** have two keys:
* `"identifier"` (str): The unique identifier for the feature.
* `"quantity"` (int): The desired quantity for this feature.
* Example: `[{'identifier': 'seats', 'quantity': 10}, {'identifier': 'projects', 'quantity': 5}]`
* `trialEnd` (str): Optional. Controls the trial period for the updated subscription. Accepts either:
* The literal string `"now"` to end any active trial immediately.
* An ISO 8601 datetime string (e.g. `"2027-12-31 23:59:59"`) to set a new trial end date. The datetime must be in the future. Naive datetimes are interpreted as UTC.
* If omitted, the existing trial behavior on the subscription is preserved.
* `paymentBehavior` (`Literal["activate_on_payment"]`): Optional. Set to `"activate_on_payment"` to keep the current subscription active while payment is pending. The new plan and features are applied only after payment succeeds. If the pending update expires, the current subscription remains unchanged. Omit this field to use the default immediate-update behavior.
* `prorationBehavior` (`ProrationBehavior`): Controls how a mid-period change is billed. Use `ProrationBehavior.IMMEDIATE_CHARGE`, `ProrationBehavior.PRORATE_NEXT_INVOICE`, or `ProrationBehavior.NO_PRORATION`. When omitted, the organization's default is used.
**Returns**:
An instance of `UpdateSubscriptionResponse`, which includes:
* `subscriptionId` (str): UUID of the updated subscription.
### Preview a subscription update
Calculates the financial effect of an update without changing the subscription, creating an invoice, or charging the customer.
**Synchronous:**
```python theme={null}
from kelviq_sdk import ProrationBehavior
preview = client.subscriptions.preview_update(
subscriptionId="78058918-9746-4280-9b9b-1bd5115eec6e",
planIdentifier="premium-plan",
chargePeriod="YEARLY",
prorationBehavior=ProrationBehavior.PRORATE_NEXT_INVOICE,
features=[{"identifier": "seats", "quantity": 10}],
)
print(preview.recurringAmount, preview.amountChargedImmediately)
print(preview.nextInvoice.lineItems if preview.nextInvoice else [])
```
**Asynchronous:**
```python theme={null}
preview = await async_client.subscriptions.preview_update(
subscriptionId="78058918-9746-4280-9b9b-1bd5115eec6e",
planIdentifier="premium-plan",
chargePeriod="YEARLY",
prorationBehavior=ProrationBehavior.NO_PRORATION,
)
```
Required parameters are `subscriptionId`, `planIdentifier`, and `chargePeriod`. Optional parameters are `prorationBehavior`, `features`, `ipAddress`, and `trialEnd`.
**Returns:** A `SubscriptionUpdatePreviewResponse` Pydantic model with recurring and immediate amounts in major and minor units, plus an optional next invoice and its line items.
### Cancel an active subscription for a customer.
**Synchronous**:
```python theme={null}
# Assuming 'client' is your synchronous Kelviq client
try:
response = client.subscriptions.cancel(
subscriptionId="78058918-9746-4280-9b9b-1bd5115eec6e",
cancellationType="CURRENT_PERIOD_ENDS" # Or "IMMEDIATE", "SPECIFIC_DATE"
# cancellationDate="2025-12-31" # Required if cancellationType is "SPECIFIC_DATE"
)
print(response.message)
except Exception as e:
print(f"Error cancelling subscription: {e}")
```
**Asynchronous**:
```python theme={null}
# Assuming 'async_client' is your asynchronous Kelviq client
try:
response = await async_client.subscriptions.cancel(
subscriptionId="78058918-9746-4280-9b9b-1bd5115eec6e",
cancellationType="IMMEDIATE"
)
print(response.message)
except Exception as e:
print(f"Error cancelling subscription: {e}")
```
> **Response**
>
> ```json theme={null}
> {
> "message": "Subscription cancellation processed successfully."
> }
> ```
**Required Parameters**:
* `subscriptionId` (str): The unique identifier of the subscription to be cancelled.
* `cancellationType` (str | CancellationType): The type of cancellation to perform. You can use the `CancellationType` enum (e.g., `CancellationType.CURRENT_PERIOD_ENDS`) or a raw string. Must be one of:
* `"IMMEDIATE"` / `CancellationType.IMMEDIATE`: The subscription is cancelled immediately.
* `"CURRENT_PERIOD_ENDS"` / `CancellationType.CURRENT_PERIOD_ENDS`: The subscription will remain active until the end of the current billing period and then cancel.
* `"SPECIFIC_DATE"` / `CancellationType.SPECIFIC_DATE`: The subscription will be cancelled on the specified `cancellationDate`.
* `cancellationDate` (str): The specific date for cancellation if `cancellationType` is `"SPECIFIC_DATE"`. Must be in `YYYY-MM-DD` format. This parameter is **required** if `cancellationType` is `"SPECIFIC_DATE"`.
**Returns**:
An instance of `CancelSubscriptionResponse`, which includes:
* `message` (str): A confirmation message indicating the result of the cancellation request.
***
## Charges
The synchronous and asynchronous `charges` clients immediately charge a customer's saved payment method for a one-time plan. Use sandbox mode when testing this operation.
**Synchronous:**
```python theme={null}
charge = client.charges.create(
planIdentifier="lifetime-access",
chargePeriod="ONE_TIME",
customerId="cust_789",
currencyCode="USD",
customAmount=49.99,
taxBehavior="INCLUSIVE",
features=[{"identifier": "seats", "quantity": 5}],
)
print(charge.id, charge.status, charge.amount)
```
**Asynchronous:**
```python theme={null}
charge = await async_client.charges.create(
planIdentifier="lifetime-access",
chargePeriod="ONE_TIME",
customerId="cust_789",
currencyCode="USD",
customAmount=49.99,
taxBehavior="EXCLUSIVE",
)
```
`planIdentifier`, `chargePeriod="ONE_TIME"`, and `customerId` are required. `customAmount` must be positive and requires `currencyCode`. `taxBehavior` accepts `INCLUSIVE` or `EXCLUSIVE` and defaults to `EXCLUSIVE` on the API. Optional fields also include `features` and `ipAddress`.
**Returns:** A `CreateChargeResponse` Pydantic model containing the charge record, amount, currency, status, product, plan, features, and customer details.
***
## Refunds
The synchronous and asynchronous `refunds` clients let you list, create, and retrieve refunds for orders owned by your organization.
### List refunds
**Synchronous:**
```python theme={null}
page = client.refunds.list(
status="SUCCEEDED",
start_date="2026-07-01",
page=1,
page_size=10,
)
```
**Asynchronous:**
```python theme={null}
page = await async_client.refunds.list(status="SUCCEEDED", page=1, page_size=10)
```
> **Response**
>
> ```json theme={null}
> {
> "count": 1,
> "next": null,
> "previous": null,
> "results": [
> {
> "id": "7c2f3a91-2d4e-4a8b-9b1c-6f0a2e5d9c11",
> "reason": "REQUESTED_BY_CUSTOMER",
> "amountUnits": 2500,
> "amount": 25,
> "internalNote": "Customer requested a partial refund.",
> "failureReason": null,
> "status": "SUCCEEDED",
> "order": { "id": "ORD-20260715123000-A1B2C" }
> }
> ]
> }
> ```
**Optional Parameters:**
* `search` : Search by refund ID, order ID, customer name, email, or ID.
* `status` : Filter by refund status. One of `"PENDING"`, `"PROCESSING"`, `"SUCCEEDED"`, `"FAILED"`, `"CANCELED"`.
* `start_date` : Include refunds created on or after this date (`YYYY-MM-DD`).
* `end_date` : Include refunds created on or before this date (`YYYY-MM-DD`).
* `page` : Page number to return.
* `page_size` : Number of results per page. Defaults to 10, maximum 100.
**Returns:** A `RefundListResponse` Pydantic model with `count`, `next`, `previous`, and `results: List[RefundResponse]`.
### Create a refund
**Synchronous:**
```python theme={null}
refund = client.refunds.create(
orderId="ORD-20260715123000-A1B2C",
amountUnits=2500,
reason="REQUESTED_BY_CUSTOMER",
internalNote="Customer requested a partial refund.",
)
```
**Asynchronous:**
```python theme={null}
refund = await async_client.refunds.create(
orderId="ORD-20260715123000-A1B2C",
amountUnits=2500,
)
```
> **Response**
>
> ```json theme={null}
> {
> "id": "7c2f3a91-2d4e-4a8b-9b1c-6f0a2e5d9c11",
> "reason": "REQUESTED_BY_CUSTOMER",
> "amountUnits": 2500,
> "amount": 25,
> "internalNote": "Customer requested a partial refund.",
> "failureReason": null,
> "status": "SUCCEEDED",
> "order": { "id": "ORD-20260715123000-A1B2C" }
> }
> ```
**Required Parameters:**
* `orderId` : The ID of an order owned by your organization.
**Optional Parameters:**
* `amountUnits` : Amount to refund in the currency's minor unit (e.g. cents for USD). Takes precedence over `amount` when both are provided.
* `amount` : Amount to refund in the currency's major unit (e.g. dollars for USD). Used only when `amountUnits` is omitted.
* `reason` : One of `"DUPLICATE"`, `"FRAUDULENT"`, `"REQUESTED_BY_CUSTOMER"`, `"OTHER"`. Defaults to `"REQUESTED_BY_CUSTOMER"`.
* `internalNote` : An internal note associated with the refund.
Omit both `amountUnits` and `amount` to refund the order's entire remaining refundable balance. The amount must be positive and cannot exceed the order's remaining refundable balance — the SDK raises `InvalidRequestError` if `orderId` is missing, and the API rejects invalid amounts.
**Returns:** A `RefundResponse` Pydantic model with `id`, `reason`, `amountUnits`, `amount`, `internalNote`, `failureReason`, `status`, and the complete serialized `order`.
### Retrieve a refund
**Synchronous:**
```python theme={null}
refund = client.refunds.retrieve("7c2f3a91-2d4e-4a8b-9b1c-6f0a2e5d9c11")
```
**Asynchronous:**
```python theme={null}
refund = await async_client.refunds.retrieve("7c2f3a91-2d4e-4a8b-9b1c-6f0a2e5d9c11")
```
**Required Parameters:**
* `refundId` : The UUID of the refund.
**Returns:** A `RefundResponse` Pydantic model, as above.
***
## Payment Methods
The synchronous and asynchronous `payment_methods` clients list saved payment methods for your organization.
### List payment methods
**Synchronous:**
```python theme={null}
page = client.payment_methods.list(customer_id="geo-jacob", page_size=10)
```
**Asynchronous:**
```python theme={null}
page = await async_client.payment_methods.list(customer_email="geojacob@example.com")
```
> **Response**
>
> ```json theme={null}
> {
> "count": 1,
> "next": null,
> "previous": null,
> "results": [
> {
> "id": 482,
> "customer": {
> "customerId": "geo-jacob",
> "name": "Geo Jacob",
> "email": "geojacob@example.com",
> "billingAddress": { "country": "US" }
> },
> "methodType": "card",
> "methodData": {
> "brand": "visa",
> "last4": "4242",
> "expMonth": 12,
> "expYear": 2029,
> "funding": "credit",
> "country": "US"
> },
> "isDefault": true,
> "status": "succeeded",
> "createdOn": "2026-04-02T06:12:04.123456Z",
> "modifiedOn": "2026-04-02T06:12:04.123456Z"
> }
> ]
> }
> ```
**Optional Parameters:**
* `customer_id` : Filter to payment methods belonging to the customer with this client-provided `customerId`.
* `customer_email` : Filter to payment methods belonging to the customer with this email address.
* `page` : Page number to return.
* `page_size` : Number of results per page. Defaults to 10, maximum 100.
Only payment methods with status `"succeeded"` are included in results.
**Returns:** A `PaymentMethodListResponse` Pydantic model with `count`, `next`, `previous`, and `results: List[PaymentMethodResponse]`. Each `methodData` dict is payment-provider-specific; for `card` it includes `brand`, `last4`, `expMonth`, `expYear`, `funding`, and `country`.
***
## Transactions
The synchronous and asynchronous `transactions` clients list financial transactions for your organization, ordered newest first.
### List transactions
**Synchronous:**
```python theme={null}
page = client.transactions.list(
status="success",
start_date="2026-07-01",
page=1,
page_size=25,
)
print(page.results[0].amountTotal, page.results[0].morFeeBreakdown)
```
**Asynchronous:**
```python theme={null}
page = await async_client.transactions.list(status="success")
```
> **Response**
>
> ```json theme={null}
> {
> "count": 1,
> "next": null,
> "previous": null,
> "results": [
> {
> "id": "5b274952-0e54-48f6-aec6-e8a3f202b12e",
> "order": { "id": "ORD-20260715123000-A1B2C" },
> "createdOn": "2026-07-28T10:30:00Z",
> "currency": "USD",
> "currencySymbol": "$",
> "amountTotal": 100,
> "morFee": 4,
> "morFeeBreakdown": [
> { "name": "fixed_fee", "percentage": null, "valueUnits": 40, "currency": "USD" },
> { "name": "base_fee", "percentage": "2.9", "valueUnits": 290, "currency": "USD" },
> { "name": "subscription_fee", "percentage": "0.7", "valueUnits": 70, "currency": "USD" }
> ],
> "collectedTax": 0,
> "netPayout": 96,
> "status": "success",
> "transactionType": "CHARGE",
> "paymentMethodType": "card",
> "availableOn": "2026-08-04",
> "isSettled": false,
> "paymentMethodName": "visa",
> "last4": "4242"
> }
> ]
> }
> ```
**Optional Parameters:**
* `search` : Search by payment-provider IDs, customer name, email or ID, product name, or payment method type.
* `status` : Filter by transaction status. One of `"success"`, `"failure"`, `"pending"`.
* `start_date` : Include transactions created on or after this date (`YYYY-MM-DD`).
* `end_date` : Include transactions created on or before this date (`YYYY-MM-DD`).
* `page` : Page number to return.
* `page_size` : Number of results per page. Defaults to 10, maximum 100.
**Returns:** A `TransactionListResponse` Pydantic model with `count`, `next`, `previous`, and `results: List[TransactionResponse]`. Each transaction includes an itemized `morFeeBreakdown` (Merchant of Record fee components) when applicable — empty when no such fee applies.
***
## License
The `license` module allows you to activate, deactivate, and validate software licenses. You can access these operations via the `license` attribute on an initialized `Kelviq` client instance.
All methods are available in both synchronous and asynchronous versions, depending on how the `Kelviq` client was instantiated.
***
### Activate a license key
Creates a new license instance for a given license key, optionally associating it with a customer.
**Synchronous**:
```python theme={null}
response = client.license.activate(
licenseKey="LIC-XXXX-YYYY-ZZZZ",
customerId="cust_789",
instanceName="My MacBook Pro",
metadata={"os": "macOS", "arch": "arm64"}
)
print(f"Instance ID: {response.instanceId}")
print(f"License key: {response.license.licenseKey}")
```
**Asynchronous**:
```python theme={null}
response = await async_client.license.activate(
licenseKey="LIC-XXXX-YYYY-ZZZZ",
customerId="cust_789",
instanceName="My MacBook Pro",
)
print(f"Instance ID: {response.instanceId}")
```
> **Response**
>
> ```json theme={null}
> {
> "instanceId": "8f3e2b1a-5c6d-4e9f-8a0b-1c2d3e4f5g6h",
> "activatedAt": "2026-01-27T10:33:00Z",
> "expiresOn": "2027-01-27T10:33:00Z",
> "license": {
> "id": "7be7a5b6-7a0e-4178-a085-8ef48f0649ab",
> "licenseKey": "LIC-A8D5F89E-B46B-472C-9BED-66EE2545B85E",
> "activatedOn": "2026-01-22T03:26:56Z",
> "expiresOn": "2026-01-23T03:26:56Z",
> "activationUsage": 1,
> "activationLimit": 4,
> "enabled": true,
> "customer": {
> "customerId": "cfa95cf8c8d24e8b9d3d2d1ada98a94d",
> "name": "Geo",
> "email": "geo@kelviq.com"
> },
> "plan": {
> "identifier": "pro-yearly",
> "name": "Pro Yearly",
> "description": "Pro plan billed yearly",
> "product": {
> "id": "d970ab2d-834f-4c52-8742-2e6ed7e8a31f",
> "identifier": "kelviq-engine",
> "name": "Kelviq Engine",
> "taxCode": "software",
> "createdOn": "2025-01-01T00:00:00Z",
> "modifiedOn": "2025-06-01T00:00:00Z"
> },
> "version": 1,
> "isLatest": true
> },
> "subscription": {
> "id": "09d706ca-58f3-4fb2-818f-5b8623d67e6e",
> "recurrence": "1 month",
> "billingPeriodStartTime": "2026-01-22T03:26:56Z",
> "billingPeriodEndTime": "2026-02-22T03:26:56Z",
> "startDate": "2026-01-22",
> "endDate": null,
> "status": "active",
> "amount": "49.00",
> "currency": "usd",
> "trialDaysRemaining": 0,
> "billingType": "SUBSCRIPTION",
> "recurrenceUnit": 1,
> "recurrenceType": "MONTH"
> }
> }
> }
> ```
**Required Parameters:**
* `licenseKey` (str): The license key string to activate.
**Optional Parameters:**
* `customerId` (str): The ID of the customer this instance is associated with.
* `instanceName` (str): A human-readable name for this instance (e.g. device name).
* `metadata` (Dict\[str, Any]): A dictionary of custom key-value pairs to attach to the instance.
**Returns:**
An instance of `LicenseActivateResponse` (Pydantic model), which includes:
* `instanceId` (str): The unique ID of the newly created license instance.
* `activatedAt` (str): ISO 8601 timestamp of when the instance was activated.
* `expiresOn` (Optional\[str]): ISO 8601 timestamp of when the instance expires, if applicable.
* `license` (`LicenseDetails`): The full license object. Key fields:
* `id` (str): Server-generated UUID for the license.
* `licenseKey` (str): The license key string.
* `activatedOn` (Optional\[str]): ISO 8601 timestamp of when the license was first activated.
* `expiresOn` (Optional\[str]): ISO 8601 timestamp of when the license expires.
* `activationUsage` (int): Number of currently active instances.
* `activationLimit` (int): Maximum number of allowed concurrent activations.
* `enabled` (bool): Whether the license is active.
* `customer` (Optional\[object]): The associated customer, if any. Fields: `customerId`, `name`, `email`.
* `plan` (Optional\[object]): The plan associated with this license, if any. Fields: `identifier`, `name`, `description`, `version`, `isLatest`, and `product` (object with `id`, `identifier`, `name`, `taxCode`, `createdOn`, `modifiedOn`).
* `subscription` (Optional\[object]): Subscription details, if any. Fields: `id`, `recurrence`, `billingPeriodStartTime`, `billingPeriodEndTime`, `startDate`, `endDate`, `status`, `amount`, `currency`, `trialDaysRemaining`, `billingType`, `recurrenceUnit`, `recurrenceType`.
***
### Deactivate a license instance
Deactivates a specific license instance by its instance ID.
**Synchronous**:
```python theme={null}
try:
response = client.license.deactivate(
licenseKey="LIC-XXXX-YYYY-ZZZZ",
instanceId="8f3e2b1a-5c6d-4e9f-8a0b-1c2d3e4f5a6b"
)
print(response.message)
except Exception as e:
print(f"Error deactivating license: {e}")
```
**Asynchronous**:
```python theme={null}
try:
response = await async_client.license.deactivate(
licenseKey="LIC-XXXX-YYYY-ZZZZ",
instanceId="8f3e2b1a-5c6d-4e9f-8a0b-1c2d3e4f5a6b"
)
print(response.message)
except Exception as e:
print(f"Error deactivating license: {e}")
```
> **Response**
>
> ```json theme={null}
> {
> "message": "License instance deactivated successfully.",
> "deactivatedAt": "2026-01-27T10:35:00Z"
> }
> ```
**Required Parameters:**
* `licenseKey` (str): The license key string.
* `instanceId` (str): The unique ID of the instance to deactivate.
**Returns:**
An instance of `LicenseDeactivateResponse` (Pydantic model), which includes:
* `message` (str): Confirmation message.
* `deactivatedAt` (str): ISO 8601 timestamp of when the instance was deactivated.
***
### Validate a license key
Checks whether a license key (and optionally a specific instance) is valid.
**Synchronous**:
```python theme={null}
response = client.license.validate(
licenseKey="LIC-XXXX-YYYY-ZZZZ",
instanceId="8f3e2b1a-5c6d-4e9f-8a0b-1c2d3e4f5a6b" # optional
)
if response.valid:
print(f"License is valid: {response.code}")
else:
print(f"License invalid: {response.detail}")
```
**Asynchronous**:
```python theme={null}
response = await async_client.license.validate(
licenseKey="LIC-XXXX-YYYY-ZZZZ"
)
print(f"Valid: {response.valid}, Code: {response.code}")
```
> **Response**
>
> ```json theme={null}
> {
> "valid": true,
> "code": "VALID",
> "detail": "Instance ID valid for this license.",
> "metadata": {
> "os": "macOS",
> "arch": "arm64"
> },
> "license": {
> "id": "7be7a5b6-7a0e-4178-a085-8ef48f0649ab",
> "licenseKey": "LIC-A8D5F89E-B46B-472C-9BED-66EE2545B85E",
> "activatedOn": "2026-01-22T03:26:56Z",
> "expiresOn": "2026-01-23T03:26:56Z",
> "activationUsage": 1,
> "activationLimit": 4,
> "enabled": true,
> "customer": {
> "customerId": "cfa95cf8c8d24e8b9d3d2d1ada98a94d",
> "name": "Geo",
> "email": "geo@kelviq.com"
> },
> "plan": {
> "identifier": "pro-yearly",
> "name": "Pro Yearly",
> "product": {
> "id": "d970ab2d-834f-4c52-8742-2e6ed7e8a31f",
> "identifier": "kelviq-engine",
> "name": "Kelviq Engine",
> "taxCode": "software"
> }
> },
> "subscription": {
> "id": "09d706ca-58f3-4fb2-818f-5b8623d67e6e",
> "recurrence": "1 month",
> "billingPeriodStartTime": "2026-01-22T03:26:56Z",
> "billingPeriodEndTime": "2026-02-22T03:26:56Z",
> "startDate": "2026-01-22",
> "endDate": null,
> "status": "active",
> "amount": "49.00",
> "currency": "usd",
> "trialDaysRemaining": 0,
> "billingType": "SUBSCRIPTION",
> "recurrenceUnit": 1,
> "recurrenceType": "MONTH"
> }
> }
> }
> ```
**Required Parameters:**
* `licenseKey` (str): The license key string to validate.
**Optional Parameters:**
* `instanceId` (str): If provided, also validates that this specific instance is active for the given license key.
**Returns:**
An instance of `LicenseValidateResponse` (Pydantic model), which includes:
* `valid` (bool): Whether the license (and instance, if provided) is valid.
* `code` (str): A short status code (e.g. `"VALID"`, `"INVALID"`, `"EXPIRED"`).
* `detail` (str): A human-readable description of the validation result.
* `metadata` (Optional\[Dict\[str, Any]]): Any metadata attached to the instance, if applicable.
* `license` (Optional\[`LicenseDetails`]): The full license object (same structure as in `activate`), or `None` if not found.
***
## Portal
The portal module allows you to create authenticated customer portal sessions. Once a session is created, you can redirect your customer directly to their self-serve customer portal without requiring them to log in manually. You can access these operations via the `portal` attribute on an initialized `Kelviq` client instance.
All methods are available in both synchronous and asynchronous versions, depending on how the `Kelviq` client was instantiated.
### Creates a new customer portal session
`create_session(...)`
This operation creates a new portal session for a customer. Combine the returned `customerPortalUrl` and `token` into a signed link (`{customerPortalUrl}?token={token}`) and redirect the customer there.
**Synchronous**:
```python theme={null}
session = client.portal.create_session(
customerId="cust_789",
)
# Build the signed portal link — customerPortalUrl alone does not
# authenticate; the token must be appended as a query parameter.
portal_link = f"{session.customerPortalUrl}?token={session.token}"
print(f"Portal URL: {portal_link}")
```
**Asynchronous**:
```python theme={null}
session = await async_client.portal.create_session(
customerId="cust_789",
)
portal_link = f"{session.customerPortalUrl}?token={session.token}"
print(f"Portal URL: {portal_link}")
```
> **Response**
>
> ```json theme={null}
> {
> "token": "cpt-4f7c5dc4-da9f-447b-d447-6266dc35ed6d:23fca530-adeb-4685-b3d5-9928c6d38275",
> "email": "customer@example.com",
> "customerPortalUrl": "https://www.kelviq.com/portal/aphelo/"
> }
> ```
**Required Parameters:**
* `customerId` (str): The unique identifier of the customer for whom the portal session is being created.
**Returns:**
An instance of `CreatePortalSessionResponse` (Pydantic model), which includes:
* `token` (str): The session token that authenticates the customer portal session.
* `email` (str): The email address of the customer.
* `customerPortalUrl` (str): The customer's billing portal base URL. **This URL alone does not authenticate the session** — append the `token` as a query parameter (`{customerPortalUrl}?token={token}`) to produce the signed link you share with the customer. The token expires, so create a fresh session per visit.
### Deep-linking to add a payment method
You can send a customer directly to the **add payment method** form and have them redirected back to your application when they're done. Append these query parameters to the signed portal link, targeting the `billing` page:
| Parameter | Required | Description |
| - | - | - |
| `intent` | Yes | Set to `add_payment_method` to auto-open the add payment method form. |
| `returnUrl` | No | URL-encoded `https` URL of your application. After the payment method is saved, the customer is redirected here with `?kelviq_setup=success` appended. `http` is only allowed for `localhost` during development. |
| `customerId` | No | Opens the form for this specific customer account. If omitted, the customer's first account is used. |
```python theme={null}
from urllib.parse import quote
session = client.portal.create_session(
customerId="cust_789",
)
return_url = quote("https://your-app.com/billing/done", safe="")
add_payment_method_link = (
f"{session.customerPortalUrl}billing"
f"?token={session.token}"
f"&intent=add_payment_method"
f"&customerId=cust_789"
f"&returnUrl={return_url}"
)
```
After the customer saves their payment method, the portal redirects them to `https://your-app.com/billing/done?kelviq_setup=success`.
***
## Webhooks
Kelviq can send real-time webhook notifications to your server when events occur in your account — for example, when a subscription is created or an invoice is paid.
### Verifying webhook signatures
Every webhook request includes three headers that you must use to verify the request is genuinely from Kelviq:
| Header | Description |
| - | - |
| `webhook-id` | Unique identifier for the event |
| `webhook-timestamp` | Unix timestamp of when the event was sent |
| `webhook-signature` | HMAC-SHA256 signature, prefixed with `v1,` |
Use the `validate_event` helper to verify the signature and parse the event in one step. It raises `WebhookVerificationError` if the signature is invalid.
```python theme={null}
from flask import Flask, request
from kelviq_sdk import validate_event, WebhookVerificationError
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
try:
event = validate_event(
payload=request.data,
headers=request.headers,
secret='',
)
# Process the event
print('Event type:', event.get('type'))
return "", 202
except WebhookVerificationError as e:
return "", 403
```
> [!WARNING]
>
> Always pass the **raw** request body (e.g. `request.data` in Flask) to `validate_event`. Parsing the body as JSON before verification will break the signature check.
### Webhook event types
See [Webhook event types](/guides/webhooks#event-types) for the complete list of supported events and their descriptions.
### Best practices
* **Return quickly** — respond with a `2xx` status before doing any heavy processing. Kelviq waits 10 seconds for a response. It retries up to 3 times at 60-second intervals on a timeout, connection failure, `5xx` or `429`; other non-`2xx` responses are not retried.
* **Use idempotency** — use the `webhook-id` header (or the event `id` field) to detect and skip duplicate deliveries.
* **Validate every request** — always verify the signature before trusting the payload.
***
## Using the Sandbox Environment
The SDK can connect to the Kelviq sandbox environment, which is perfect for **testing and development**. All operations performed in the sandbox are isolated and will not affect your live production data.
To use the sandbox, simply pass the argument `environment="sandbox"` when creating the client.
### How to Create a Synchronous Client (Sandbox)
```python theme={null}
from kelviq_sdk import Kelviq
# Note the 'environment' parameter
client = Kelviq.create_sync_client(
access_token=ACCESS_TOKEN,
environment="sandbox"
)
```
### Reading the environment from `KELVIQ_ENV`
`environment_from_env()` turns the `KELVIQ_ENV` convention shared with the Kelviq CLI and MCP server into the `environment` argument: `"sandbox"` when the variable is unset or empty, `"production"` or `"sandbox"` when it is set to one of those, and a `ValueError` naming any other value.
```python theme={null}
import os
from kelviq_sdk import Kelviq, environment_from_env
client = Kelviq.create_sync_client(
access_token=os.environ["KELVIQ_SERVER_API_KEY"],
environment=environment_from_env(), # reads KELVIQ_ENV: sandbox when unset
)
```
`environment="production"` and `environment="prod"` mean the same thing.
> [!NOTE]
>
> The sandbox environment is completely separate from production. You will need to use a different set of API keys, and any data created (customers, subscriptions, etc.) will only exist in the sandbox.
# Changelog
Source: https://docs.kelviq.com/changelog
Every Kelviq release across the dashboard, API, SDKs, and developer tools
All
**v1.24**
### New Features
#### Retention offers in the API
Two new endpoints let you build the cancellation flow, with retention offers, in your own app instead of the customer portal:
* [Get retention offers](/api-reference/subscriptions/get-retention-offers) returns every cancellation reason with the offer configured for it (discount, pause or support) and whether the subscription is eligible for it right now.
* [Accept a retention offer](/api-reference/subscriptions/accept-a-retention-offer) applies the offer the customer chose: attaches the discount, pauses the subscription, or returns the support link.
```json theme={null}
{
"offerType": "PAUSE",
"cancellationFeedback": "UNUSED",
"pauseMonths": 3
}
```
Offers are configured in the dashboard retention settings. See [Retention offers](/product-catalog/subscriptions#retention-offers) for the flow.
**v1.23**
## Custom fields
Ask customers your own questions at checkout, such as a company name for the invoice, a purchase order number, or agreement to your terms.

Create a field in **Settings → Custom fields** and pick its type: text, number, yes/no, email, dropdown, URL, or date. Make it required, add help text and a placeholder, and set limits such as length, a pattern, or a date range. Customers see your error message if an answer doesn't pass.
Add the field to a checkout link from **Get Link** on the product page, or pass it in `customFields` when you create a checkout session. Answers are saved with the order and subscription, and sent in your webhooks.
Read [Custom fields](/checkout/custom-fields) to set one up.
**v1.22**
## Retention offers
Customers who cancel in the customer portal can now get an offer to stay. Kelviq asks why they're leaving, then shows the offer you picked for that reason.

Set up three kinds of offer in **Settings → Retention**:
* **Discount:** a percentage off the next few payments.
* **Pause:** 1, 3, or 6 months without billing.
* **Support:** a link to a booking page, live chat, or your help site.
Then match each cancellation reason to an offer, such as a discount for "Too expensive" or a pause for "Not using it enough". You can also ask one optional question before the customer confirms, and choose whether an offer can be accepted more than once.
Read [Retention offers](/customer-portal/retention) to set it up.
## Pause and resume subscriptions
Customers can now pause their subscription from the customer portal, when a retention offer includes a pause. A paused subscription isn't billed and its entitlements are off until it resumes. Choose in **Settings → Retention → Advanced settings** whether resuming charges a full period on the spot or waits for the original billing day.
Read [Pause a subscription](/product-catalog/subscriptions#pause-a-subscription).
## A discount in the final abandoned cart reminder
The last [abandoned cart](/sales/abandoned-cart-recovery) reminder can now offer a percentage off the checkout total. Turn it on in **Settings → Customer emails** and set the percentage. It isn't offered when another discount already applies to the product.
## Settings, regrouped
Settings now has four groups so related pages sit together: **Account** (Profile), **Organization** (Set up with AI, General, Members, Business details, Tax settings), **Checkout & subscriptions** (Checkout design, Custom fields, Subscription proration, Customer emails, Retention), and **Developers** (API keys, Webhooks, Integrations). **Notifications** is now **Customer emails**. Nothing moved out of Settings and no setting changed.
**v1.9.0**
### New Features
#### Webhooks
`subscription.paused` and `subscription.resumed`, sent whichever surface the change came from. Renewals skipped during a pause send no events.
#### Retention settings
`GET` and `PATCH /api/v1/organizations/retention-settings/` read and update the offers, the reason-to-offer map, written feedback, and the resume and repeat policies. The customer portal API gains `…/retention/` to load the offers for a subscription and `…/retention/accept/` to accept one. The cancel endpoint accepts an optional `offerType` to record an offer the customer declined.
#### SDKs
`entitlements.invalidateCache` (Node) and `entitlements.invalidate_cache` (Python) drop a customer's cached entitlements, for use in `subscription.paused` and `subscription.cancelled` handlers.
See [Pause a subscription](/product-catalog/subscriptions#pause-a-subscription) and [Retention offers](/customer-portal/retention).
**v1.21**
## Set up with AI
Billing is now something you can hand to your coding agent. The new **Settings → Set up with AI** page writes a prompt for Claude Code, Cursor, Codex, Lovable, Replit, Bolt, or any agent that can edit files and run commands, and you paste it in as your first message.

Pick one of two paths. **Integrate with my app** connects the plans you have already published: the agent adds checkout, the billing portal, feature checks, and a verified webhook route, and registers the webhook when your app has a public URL. **Set up everything** starts earlier. The agent reads your app, proposes plans, prices, limits, license keys and discount codes, and only creates them in your sandbox after you approve.
Describe what you want in plain language, or add a common request such as a monthly and yearly toggle or a 7-day free trial, and the prompt includes it.
Your keys never go in the chat. The page gives you a block to paste into your project's `.env.local`, and the agent reads them from the file. Build and test with sandbox keys, or set it up directly in production. Business verification and your keys stay with you.
## Checkout design
Make checkout look like the rest of your product. **Settings → Checkout design** has your logo, colors, and corner radius in one place, with a live preview next to the controls.

Change any color for light and dark mode, set how rounded cards and buttons are, and choose whether checkout opens light, dark, or matching the buyer's device. You can also hide parts of checkout you don't need, such as product images or the discount code field. Read [Checkout design](/checkout/checkout-design) for every setting.
## Fixes
* Amounts show two decimal places everywhere in the dashboard.
* In promotion settings, the security options are now off for new promotions.
**v1.20**
## New checkout
Every checkout link and checkout session now opens in a redesigned page.

Checkout follows the customer's device setting for light or dark mode, and a switch in the corner lets them change it. Plans, billing periods, included features, promotion codes, tax, and business purchases are all on the one page.
See every variation on the [Checkout examples](/checkout/checkout-examples) page, and change the look in [Checkout design](/checkout/checkout-design).
**v1.19**
## Choose how plan changes are billed
When a customer moves between plans mid-period, you decide what happens to the partial period. **Settings → Subscription proration** has two defaults:

* **Plan upgrades and downgrades** defaults to **Immediate charge**: Kelviq prorates the difference and invoices it right away.
* **Plan version migrations** defaults to **Prorate on next invoice**: the change applies now and the difference lands on the next renewal.
Both accept a third option, **No proration**, which changes the plan without crediting or charging for the partial period. Switching between monthly and yearly billing always invoices immediately, whatever you choose here. API requests that pass `prorationBehavior` override these defaults.
[Subscriptions](/product-catalog/subscriptions#control-how-a-change-is-billed) explains each option with an example.
**v1.18**
## Abandoned cart recovery
Kelviq now follows up with customers who start a checkout but do not pay. When a checkout sits untouched for an hour, Kelviq marks it as abandoned and sends up to three reminder emails, one each day for three days. Each email carries a **Return to checkout** link that takes the customer back to their session. The checkout expires four days after it was created.
You do not need to set anything up. Recovery emails are on by default for every organization, and you can turn them off with the **Abandoned cart recovery** toggle in **Settings → Notifications**. Reminders are only sent when there is a customer email to send them to, and a customer is never emailed twice for the same step. A customer who starts several checkouts gets one set of reminders, not one per checkout.
Read [Abandoned cart recovery](/sales/abandoned-cart-recovery) for the full schedule, the abandonment rules, and what happens at expiration.
**v1.8.3**
### Improvements
#### Custom field responses in webhooks
The `checkout.completed`, `order.created`, `order.updated`, and `order.refunded` webhook payloads now include `custom_fields_data` in `data.object`: the customer's responses to the custom fields collected during checkout, keyed by field identifier. The object is empty when no custom fields were collected.
```json theme={null}
"custom_fields_data": {
"company-name": "Acme Inc",
"purchase-order-number": "PO-4821"
}
```
See [Webhooks](/guides/webhooks#custom-field-responses) for details.
**v1.8.2**
### New Features
#### Custom fields in checkout sessions
The [Create a checkout session](/api-reference/checkout/create-a-checkout-session) endpoint now documents the `customFields` request property. Pass an array of custom field identifiers to display those fields during checkout and collect validated responses from the customer:
```json theme={null}
{
"planIdentifier": "pro-plan",
"chargePeriod": "MONTHLY",
"successUrl": "https://example.com/success",
"customFields": ["company-name", "purchase-order-number"]
}
```
Only active custom fields configured for the authenticated organization are included, and they appear in their configured sort order.
**v1.8.1**
### Improvements
#### Webhook retries on server errors and rate limits
Kelviq now retries a webhook delivery when your endpoint returns a `5xx` status code or `429 Too Many Requests`. Previously, only network failures and timeouts were retried, so an endpoint that was briefly unavailable or rate limiting lost the delivery after one attempt.
Retries follow the existing schedule: up to `3` retries, `60` seconds apart. Each attempt appears in the delivery logs with its status code. Other `4xx` and `3xx` responses are still treated as final and are not retried.
See [Webhooks](/guides/webhooks#how-it-works) for details.
**v1.8.0**
### New Features
#### Per-organization rate limits
Requests to endpoints authenticated with a Server or Client API Key are now rate limited per organization: `500 requests` / minute in production, `100 requests` / minute in sandbox. A Server and Client API Key belonging to the same organization share one quota.
Exceeding the limit returns a `429 Too Many Requests` response with a `Retry-After` header naming how many seconds to wait before retrying:
```json theme={null}
{
"detail": "Request was throttled. Expected available in 42 seconds."
}
```
The limit only applies to requests authenticated with your own API keys.
See [Rate limits](/api-reference/introduction#rate-limits) for details.
**v0.4.2**
### Fixes
* `kelviq pull` no longer fails on a plan that grants unlimited usage of a feature. The dashboard stores an empty `value` for those entitlements, which the config format rejected. Pull now writes them as `hasUnlimitedUsage: true` with no `value`.
**v0.4.1**
### Fixes
* `kelviq discount create --product ` now finds the product. It looks the identifier up through the product list, since the product endpoint itself takes only a UUID.
**v1.2.1**
### Fixes
* Product and feature identifiers now resolve correctly in every tool that accepts them. 1.2.0 looked them up through the retrieve endpoints, which take only a UUID, so any identifier failed with a 404.
* `product_retrieve`, `product_update`, `product_archive`, the product settings and image tools, `feature_retrieve` and `feature_archive` accept an identifier as well as a UUID.
**v0.4.0**
### New Features
#### Env files, a live key check, and the live catalog
* Every command reads `KELVIQ_*` variables from `.env.local` and then `.env` in the working directory, so an app's env file is enough. A variable set in the shell always wins; `--env-file ` reads a specific file and `--no-env-file` reads none.
* `kelviq env --check` makes one authenticated read per configured key and reports `ok`, `rejected` or `error`, exiting non-zero when a key fails. Nothing from the key is printed.
* `kelviq status` shows the live catalog: products, features, and each plan's latest version with its price and entitlement counts, visibility and license-key state. `--json` for scripts.
#### Webhooks and discounts from the terminal
* `kelviq webhook add --events … --write-secret .env.local` registers the app's endpoint, reusing one that already has the same URL, and writes the signing secret into the env file as `KELVIQ_WEBHOOK_SECRET` without printing it. `kelviq webhook list` shows endpoints with secrets hidden.
* `kelviq discount create` creates a code after restating it for confirmation, with `--product` taking the product's identifier. `kelviq discount list` lists codes.
#### Starter templates
`kelviq init --template saas|usage|license` writes a complete catalog that pushes as written: a SaaS product with free and Pro plans, a metered product with an included quantity and tiered overage, or a plan that issues license keys.
### Improvements
* `kelviq push` compares prices only on the fields your config sets. Leaving out `freeTrial`, `taxBehavior`, or a charge's `paymentType` and `reset` no longer shows a difference on every run.
* `kelviq push` and `kelviq promote` stop before writing when a `METER` feature would be created without `featureDetails`, naming the feature and the fields to add.
**v1.2.0**
### New Features
#### `connection_check`
A read-only tool that makes one authenticated read with the server key and reports the environment, both API hosts and whether the key is `ok`, `missing` or `rejected`, without ever including the key. Ask for it first when a tool returns 401 or 403.
### Improvements
* `plan_create`, `plan_entitlements_add`, `discount_create`'s `appliesTo` and `offering_get_product` accept a product or feature **identifier** as well as a UUID. The server resolves the identifier before calling the API, so the assistant no longer has to look up UUIDs first.
* The bundled documentation is refreshed with every release, so `docs_search` and `docs_read` match the current docs.
**v2.9.0**
### New Features
#### `environment_from_env()`
Resolves the client's `environment` argument from `KELVIQ_ENV`, the variable the CLI and MCP server share: `"sandbox"` when unset, `"production"` or `"sandbox"` when set to one of those, and a `ValueError` naming any other value.
```python theme={null}
from kelviq_sdk import Kelviq, environment_from_env
client = Kelviq.create_sync_client(
access_token=os.environ["KELVIQ_SERVER_API_KEY"],
environment=environment_from_env(),
)
```
### Improvements
* `environment="production"` is accepted alongside `"prod"`, so a value read straight from `KELVIQ_ENV` works.
* An environment name the SDK doesn't know now logs a warning. It still falls back to the production hosts.
**v2.9.0**
### New Features
#### `environmentFromEnv()`
Turns the `KELVIQ_ENV` convention shared with the CLI and MCP server into a typed `environment` option: `sandbox` when unset, `production` or `sandbox` when set to one of those, and an error naming any other value.
```typescript theme={null}
import { Kelviq, environmentFromEnv } from '@kelviq/node-sdk';
const client = new Kelviq({
accessToken: process.env.KELVIQ_SERVER_API_KEY!,
environment: environmentFromEnv(),
});
```
**v0.3.1**
### Improvements
#### Cleaner `kelviq pull` output
A pulled `kelviq.config.ts` now writes charge prices the way you'd author them. A charge's `feature` is the feature's identifier:
```typescript theme={null}
chargeCatalogPrice: [
{
feature: 'seats',
priceModel: 'FLAT',
...
},
]
```
Previously that field held the whole feature object the API returns — its UUID, timestamps and other ids — and each charge carried its own server-generated ids. Those fields are now left out, so a pulled catalog is around 20% smaller, holds no UUIDs or other ids, and reads as something you can edit by hand and review in a pull request.
Existing pulled files keep working: `kelviq push` accepts the identifier, the UUID and the embedded object alike.
**v1.1.0**
### New Features
#### Discount and webhook tools
11 new tools bring the server to 56:
* Discounts: `discount_list`, `discount_create`, `discount_retrieve`, `discount_update` to enable or disable a code, and `discount_archive`
* Webhooks: `webhook_endpoint_list`, `webhook_endpoint_create`, `webhook_endpoint_retrieve`, `webhook_endpoint_update`, `webhook_endpoint_delete`, and `webhook_log_list` for delivery attempts from the last 30 days
Webhook signing secrets are hidden in tool results so they don't end up in your AI conversation. Pass `revealSecret: true` when you need the value.
### Improvements
* `checkout_create_session` accepts `trialPeriod`, `billingPeriodsEnabled` and `email`.
* `subscription_update` accepts `prorationBehavior`, and `subscription_cancel` accepts `cancellationFeedback` and `cancellationComment`.
**v0.3.0**
### Breaking Changes
#### `KELVIQ_SERVER_API_KEY` follows `KELVIQ_ENV`
`KELVIQ_SERVER_API_KEY` now belongs to the environment named by `KELVIQ_ENV`, and `KELVIQ_ENV` defaults to `sandbox` — the same rules as the [MCP server](/guides/mcp-server). It previously always meant your production key.
If you keep a production key in `KELVIQ_SERVER_API_KEY`, add `KELVIQ_ENV=production` or rename the variable to `KELVIQ_PRODUCTION_SERVER_API_KEY`. Keys saved with `kelviq login` are unaffected, and commands still target production only when you pass `--prod`.
### New Features
#### Environment-named keys
`KELVIQ_SANDBOX_SERVER_API_KEY` and `KELVIQ_PRODUCTION_SERVER_API_KEY` always apply to their own environment and take priority over `KELVIQ_SERVER_API_KEY`, so one shell or CI job can hold both keys for `kelviq promote`.
### Improvements
* `kelviq env` shows which environment `KELVIQ_ENV` points your unprefixed variables at.
* When a key is missing, the error names every way to provide it, and tells you if `KELVIQ_SERVER_API_KEY` is set for the other environment.
* An invalid `KELVIQ_ENV` value stops the command instead of guessing.
### Fixes
* `kelviq push` now works with a globally installed CLI in projects that don't install `@kelviq/cli` locally. Previously it failed with `Cannot find package '@kelviq/cli'`.
**v1.7.9**
### New Features
#### Discounts API
Discount codes can now be managed directly through the API, authenticated with your server API key:
* `GET /discount/` — list discounts
* `POST /discount/` — create a discount (Merchant of Record organizations only)
* `GET /discount/{id}/` — retrieve a discount
* `PATCH /discount/{id}/` — enable or disable a discount
* `DELETE /discount/{id}/` — archive a discount
```json theme={null}
{
"discountType": "PERCENTAGE",
"percentageOff": 20,
"name": "Summer Sale",
"code": "SUMMER20",
"duration": "ONCE"
}
```
See the [Discounts API reference](/api-reference/introduction) for the full request/response shape, including restricting a discount to specific products or plans with `appliesTo`.
#### Manage webhook endpoints via API
`WebhookEndpoint` resources — previously dashboard-only — are now fully manageable through the API:
* `GET /webhook/endpoints/` — list webhook endpoints
* `POST /webhook/endpoints/` — create a webhook endpoint
* `GET /webhook/endpoints/{id}/` — retrieve a webhook endpoint
* `PATCH /webhook/endpoints/{id}/` — update the URL, subscribed events, or enabled state
* `DELETE /webhook/endpoints/{id}/` — delete a webhook endpoint
```json theme={null}
{
"url": "https://example.com/webhooks/kelviq",
"events": ["invoice.paid", "subscription.cancelled"],
"enabled": true
}
```
The signing secret used to verify the `webhook-signature` header is generated automatically and returned in the create response.
**v1.0.0**
### Breaking Changes
#### Sandbox is now the default environment
When `KELVIQ_ENV` isn't set, the server now uses your **sandbox** environment instead of production, so an AI client can't change live data unless you choose production explicitly.
If you use the MCP server with a production key, add `KELVIQ_ENV=production` to its environment. Without it, requests go to the sandbox host and return 403 errors — nothing is written.
### Improvements
* The startup log shows which environment the server is using, and warns when `KELVIQ_ENV` isn't set.
* The server tells the connected AI client which environment its tools act on.
* 401 and 403 errors now include a hint: when `KELVIQ_ENV` isn't set, they explain the sandbox default and how to use a production key; otherwise they name the active environment and link its API keys page.
* Setup examples now run `npx -y @kelviq/mcp-server@latest`, so clients pick up new releases instead of a cached copy.
### Fixes
* If your MCP client passes an unexpanded placeholder such as `${KELVIQ_ENV}` instead of a value, the server now treats that variable as unset and warns at startup. Previously the server failed to start, or sent the placeholder text as your API key.
**v1.7.8**
### New Features
#### Limit checkout to selected billing periods
`POST /checkout/` now accepts `billingPeriodsEnabled`, a comma-separated list of billing periods to display and allow during checkout. Values are case-insensitive and normalized to uppercase. A checkout attempt using a billing period outside this list is rejected.
```json theme={null}
{
"planIdentifier": "premium-plan",
"chargePeriod": "MONTHLY",
"billingPeriodsEnabled": "MONTHLY,YEARLY",
"successUrl": "https://example.com/checkout/success"
}
```
`GET /monetization/product-offering/{product_id}/` also accepts `billing_periods_enabled` to return only the requested billing periods. Static checkout links support the same snake-case parameter.
#### Prefill the checkout email address
`POST /checkout/` now accepts `email` to prefill the checkout page. Combine it with `lockEmail: true` when the customer must not edit the address.
```json theme={null}
{
"planIdentifier": "premium-plan",
"chargePeriod": "MONTHLY",
"email": "customer@example.com",
"lockEmail": true,
"successUrl": "https://example.com/checkout/success"
}
```
Static checkout links can prefill the same value with the `email` query parameter.
**v2.8.0**
### New Features
#### Refunds
A new `client.refunds` module lets you list, create, and retrieve refunds.
```typescript theme={null}
const refund = await client.refunds.create({
orderId: "ORD-20260715123000-A1B2C",
amountUnits: 2500,
reason: "REQUESTED_BY_CUSTOMER",
internalNote: "Customer requested a partial refund.",
});
const page = await client.refunds.list({ status: "SUCCEEDED", page: 1, pageSize: 10 });
const retrieved = await client.refunds.retrieve({ refundId: refund.id });
```
`client.refunds.create()` accepts either `amountUnits` (minor currency unit) or `amount` (major currency unit) for a partial refund; omit both to refund the order's entire remaining refundable balance. `client.refunds.list()` supports `search`, `status`, `startDate`, `endDate`, `page`, and `pageSize` filters.
#### Payment Methods
A new `client.paymentMethods` module lists saved payment methods, optionally filtered by `customerId` or `customerEmail`.
```typescript theme={null}
const methods = await client.paymentMethods.list({ customerId: "cust_789" });
console.log(methods.results[0].methodData.brand, methods.results[0].methodData.last4);
```
Only payment methods with status `succeeded` are returned.
#### Transactions
A new `client.transactions` module lists financial transactions, including the itemized Merchant of Record fee breakdown when applicable.
```typescript theme={null}
const transactions = await client.transactions.list({ status: "success", startDate: "2026-07-01" });
console.log(transactions.results[0].amountTotal, transactions.results[0].morFeeBreakdown);
```
`client.transactions.list()` supports `search`, `status`, `startDate`, `endDate`, `page`, and `pageSize` filters.
**v2.8.0**
### New Features
#### Refunds
The synchronous and asynchronous clients now provide a `refunds` module for listing, creating, and retrieving refunds.
```python theme={null}
refund = client.refunds.create(
orderId="ORD-20260715123000-A1B2C",
amountUnits=2500,
reason="REQUESTED_BY_CUSTOMER",
internalNote="Customer requested a partial refund.",
)
page = client.refunds.list(status="SUCCEEDED", page=1, page_size=10)
retrieved = client.refunds.retrieve(refund.id)
```
**Async variant:**
```python theme={null}
refund = await async_client.refunds.create(
orderId="ORD-20260715123000-A1B2C",
amountUnits=2500,
)
```
`client.refunds.create()` accepts either `amountUnits` (minor currency unit) or `amount` (major currency unit) for a partial refund; omit both to refund the order's entire remaining refundable balance. `client.refunds.list()` supports `search`, `status`, `start_date`, `end_date`, `page`, and `page_size` filters. Pydantic models validate parameters and provide typed refund records.
#### Payment Methods
The synchronous and asynchronous clients now provide a `payment_methods` module that lists saved payment methods, optionally filtered by `customer_id` or `customer_email`.
```python theme={null}
methods = client.payment_methods.list(customer_id="cust_789")
print(methods.results[0].methodData["brand"], methods.results[0].methodData["last4"])
```
Only payment methods with status `succeeded` are returned.
#### Transactions
The synchronous and asynchronous clients now provide a `transactions` module that lists financial transactions, including the itemized Merchant of Record fee breakdown when applicable.
```python theme={null}
transactions = client.transactions.list(status="success", start_date="2026-07-01")
print(transactions.results[0].amountTotal, transactions.results[0].morFeeBreakdown)
```
`client.transactions.list()` supports `search`, `status`, `start_date`, `end_date`, `page`, and `page_size` filters.
**v2.7.4**
### New Features
#### Control How Subscription Updates Are Billed
`client.subscriptions.update()` now supports `prorationBehavior`, using the exported `PRORATION_BEHAVIORS` constant. It controls when the customer is billed for a mid-period change: `IMMEDIATE_CHARGE` invoices the difference right now, `PRORATE_NEXT_INVOICE` bills it on the next invoice, and `NO_PRORATION` changes the plan without crediting or charging for the partial period.
```typescript theme={null}
import { CHARGE_PERIOD_CHOICES, PRORATION_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,
prorationBehavior: PRORATION_BEHAVIORS.PRORATE_NEXT_INVOICE,
});
```
The SDK serializes the option as `proration_behavior`. Omit it to use your organization's default.
> [!WARNING]
>
> Changing `chargePeriod` to a different billing interval resets the billing cycle and invoices the new period immediately, whichever value you pass — with `NO_PRORATION`, without any credit for unused time on the old plan.
#### Preview Subscription Updates
`client.subscriptions.previewUpdate()` calculates the financial effect of a proposed subscription update without changing the subscription, creating an invoice, or charging the customer. It accepts the same plan, charge period, proration behavior, features, IP address, and trial-end options used by an update.
```typescript theme={null}
const preview = await client.subscriptions.previewUpdate({
subscriptionId: "78058918-9746-4280-9b9b-1bd5115eec6e",
planIdentifier: "premium-plan",
chargePeriod: CHARGE_PERIOD_CHOICES.YEARLY,
prorationBehavior: PRORATION_BEHAVIORS.PRORATE_NEXT_INVOICE,
features: [{ identifier: "seats", quantity: 10 }],
});
console.log(preview.recurringAmount, preview.amountChargedImmediately);
```
The response includes typed major- and minor-unit amounts, the next payment attempt, and preview invoice line items.
#### Set a Custom Trial Period at Checkout
`client.checkout.createSession()` now accepts `trialPeriod`. Pass an integer greater than or equal to `1` to override the selected plan's configured trial period. Invalid values are rejected by the SDK before a request is sent.
```typescript theme={null}
const session = await client.checkout.createSession({
planIdentifier: "premium-plan",
chargePeriod: CHARGE_PERIOD_CHOICES.MONTHLY,
successUrl: "https://example.com/checkout/success",
trialPeriod: 14,
});
```
#### Use a Custom Amount for One-Time Charges
`client.charges.create()` now accepts `customAmount` and `taxBehavior`. `customAmount` must be greater than zero and requires `currencyCode`; `taxBehavior` accepts `INCLUSIVE` or `EXCLUSIVE`.
```typescript theme={null}
const charge = await client.charges.create({
planIdentifier: "lifetime-access",
chargePeriod: "ONE_TIME",
customerId: "cust_789",
currencyCode: "USD",
customAmount: 49.99,
taxBehavior: "INCLUSIVE",
});
```
### Deprecations
`probationBehaviour` and `PROBATION_BEHAVIOURS` are deprecated in favour of `prorationBehavior` and `PRORATION_BEHAVIORS` (`probation` was a typo for `proration`). The old field still works.
`PROBATION_BEHAVIOURS.NEXT_BILLING_CYCLE` and `PROBATION_BEHAVIOURS.PRO_RATA` were never supported by the API and are rejected by it. Use `PRORATION_BEHAVIORS.NO_PRORATION` and `PRORATION_BEHAVIORS.PRORATE_NEXT_INVOICE` instead. `IMMEDIATE_CHARGE` is unchanged and now works.
**v2.7.4**
### New Features
#### Control How Subscription Updates Are Billed
The synchronous and asynchronous `client.subscriptions.update()` methods now support `prorationBehavior`, with a `ProrationBehavior` enum exported from the package. It controls when the customer is billed for a mid-period change: `IMMEDIATE_CHARGE` invoices the difference right now, `PRORATE_NEXT_INVOICE` bills it on the next invoice, and `NO_PRORATION` changes the plan without crediting or charging for the partial period.
```python theme={null}
from kelviq_sdk import ProrationBehavior
subscription = client.subscriptions.update(
subscriptionId="78058918-9746-4280-9b9b-1bd5115eec6e",
planIdentifier="premium-plan",
chargePeriod="MONTHLY",
prorationBehavior=ProrationBehavior.PRORATE_NEXT_INVOICE,
)
```
The SDK validates the value and serializes it as `proration_behavior`. Omit it to use your organization's default.
> [!WARNING]
>
> Changing `chargePeriod` to a different billing interval resets the billing cycle and invoices the new period immediately, whichever value you pass — with `NO_PRORATION`, without any credit for unused time on the old plan.
#### Preview Subscription Updates
The synchronous and asynchronous subscription clients now provide `preview_update()`. It calculates the financial effect of a proposed update without changing the subscription, creating an invoice, or charging the customer.
```python theme={null}
preview = client.subscriptions.preview_update(
subscriptionId="78058918-9746-4280-9b9b-1bd5115eec6e",
planIdentifier="premium-plan",
chargePeriod="YEARLY",
prorationBehavior=ProrationBehavior.PRORATE_NEXT_INVOICE,
features=[{"identifier": "seats", "quantity": 10}],
)
print(preview.recurringAmount, preview.amountChargedImmediately)
```
The returned Pydantic models include major- and minor-unit amounts, the next payment attempt, and preview invoice line items.
#### Set a Custom Trial Period at Checkout
The synchronous and asynchronous `checkout.create_session()` methods now accept `trialPeriod`. Pass an integer greater than or equal to `1` to override the selected plan's configured trial period.
```python theme={null}
session = client.checkout.create_session(
planIdentifier="premium-plan",
chargePeriod="MONTHLY",
successUrl="https://example.com/checkout/success",
trialPeriod=14,
)
```
#### Use a Custom Amount for One-Time Charges
The synchronous and asynchronous `charges.create()` methods now accept `customAmount` and `taxBehavior`. `customAmount` must be greater than zero and requires `currencyCode`; `taxBehavior` accepts `INCLUSIVE` or `EXCLUSIVE`.
```python theme={null}
charge = client.charges.create(
planIdentifier="lifetime-access",
chargePeriod="ONE_TIME",
customerId="cust_789",
currencyCode="USD",
customAmount=49.99,
taxBehavior="INCLUSIVE",
)
```
### Changes
`successUrl` is now required when calling the synchronous or asynchronous `checkout.create_session()` method, matching the API contract.
### Deprecations
`probationBehaviour` is deprecated in favour of `prorationBehavior` (`probation` was a typo for `proration`). It is still accepted and continues to serialize as `probation_behaviour`.
**v1.7.7**
### New Features
#### Set a custom trial period at checkout
`POST /checkout/` now accepts `trialPeriod`, allowing you to set the number of trial days for the subscription created by a checkout session. When provided, it overrides the trial period configured on the selected plan.
```json theme={null}
{
"planIdentifier": "premium-plan",
"chargePeriod": "MONTHLY",
"trialPeriod": 14,
"successUrl": "https://example.com/checkout/success"
}
```
### Changes
#### Success URL is required
`successUrl` is mandatory when calling `POST /checkout/`. Requests that omit it are rejected with a `400` response.
**v1.17**
## Find transactions faster
Search on the **Payouts → Transactions** page now works properly, and filtering is more precise:
* A status filter narrows the list to successful, failed, or pending transactions.
* The search field matches payment method types. Search for `upi`, `card`, `link`, `amazon_pay`, or `sepa_debit` to see payments made through that method.
## Migrate subscriptions to the latest plan version
When you republish a plan with new pricing or entitlements, existing subscribers stay on the version they bought. You can now move them forward. The migrate action on a subscription updates it to the latest version of its plan, and it is available in two places: the dashboard, and the customer portal, where customers can migrate their own subscription.
Custom-priced subscriptions cannot be migrated. Developers can run the same migration with `POST /subscriptions/{subscriptionId}/migrate/` and control how the change is billed with the organization's proration defaults. The [API changelog](/changelog#api) has the details.
**v1.7.6**
### New Features
#### Organization-level proration defaults
`GET /organizations/settings/` and `PATCH /organizations/settings/` now expose two settings for controlling how subscription changes are prorated:
| Setting | Default | Used when |
| - | - | - |
| `updateSubscriptionProrationBehavior` | `IMMEDIATE_CHARGE` | A subscription is upgraded or downgraded without a `prorationBehavior` in the update request. |
| `migrateSubscriptionProrationBehavior` | `PRORATE_NEXT_INVOICE` | A subscription is migrated to the latest version of its plan. |
Both settings accept `IMMEDIATE_CHARGE`, `PRORATE_NEXT_INVOICE`, or `NO_PRORATION`.
```json theme={null}
{
"updateSubscriptionProrationBehavior": "IMMEDIATE_CHARGE",
"migrateSubscriptionProrationBehavior": "PRORATE_NEXT_INVOICE"
}
```
#### Organization checkout customization
Checkout branding and appearance can now be stored as organization defaults. Use `GET /organizations/checkout-settings/` to retrieve the settings and `PATCH /organizations/checkout-settings/` to update the brand name, light and dark logos, favicon, background image, and checkout customization object.
The hosted checkout session response now includes these settings so the organization's branding can be applied consistently across checkout sessions.
#### Migrate individual subscriptions
`POST /subscriptions/{subscriptionId}/migrate/` migrates an existing subscription to the latest version of its current plan. Set at least one of `updateFeatures` or `updatePricing` to `true`:
```json theme={null}
{
"updateFeatures": true,
"updatePricing": true
}
```
The migration action is now available from both the Kelviq dashboard and the customer portal. Customer-portal migrations are restricted to the signed-in customer's own subscription. Custom-priced subscriptions cannot be migrated.
### Changes
#### Identify custom-priced subscriptions
`GET /subscriptions/` and `GET /subscriptions/{subscriptionId}/` now include `isCustomPricing`. It is `true` when the subscription uses a custom price instead of the plan's configured price, and `false` otherwise.
```json theme={null}
{
"id": "09d706ca-58f3-4fb2-818f-5b8623d67e6e",
"isCustomPricing": true
}
```
**v1.7.5**
### New Features
#### Control how subscription updates are billed
`POST /subscriptions/{subscriptionId}/update/` and its `/preview/` counterpart now accept `prorationBehavior`, which controls when the customer is billed for a mid-period change:
| Value | Effect |
| - | - |
| `IMMEDIATE_CHARGE` | Prorates and invoices the difference right now. |
| `PRORATE_NEXT_INVOICE` | Prorates now, but bills it on the next invoice. |
| `NO_PRORATION` | Changes the plan immediately without crediting or charging for the partial period; the next invoice bills the full new price. |
If omitted, your organization's existing default is used, so existing integrations are unaffected.
```json theme={null}
{
"planIdentifier": "premium-plan",
"chargePeriod": "MONTHLY",
"prorationBehavior": "PRORATE_NEXT_INVOICE"
}
```
Invalid values are now rejected with a clear `400` naming the accepted values, instead of a generic error.
> [!WARNING]
>
> Changing `chargePeriod` to a different billing interval (for example `MONTHLY` to `YEARLY`) resets the billing cycle and invoices the new period immediately, whichever `prorationBehavior` you pass. With `NO_PRORATION` the customer is charged for a full new period with no credit for unused time on the old plan. Call the preview endpoint first to see the exact amount.
### Improvements
`amountChargedImmediately` on the update preview endpoint is now correct for billing-interval changes. It previously reported `0` for a `MONTHLY` to `YEARLY` change unless `prorationBehavior` was `IMMEDIATE_CHARGE`, even though the customer was charged right away.
### Deprecations
The undocumented `probation_behaviour` request field is deprecated in favour of `prorationBehavior` (`probation` was a typo for `proration`). It is still accepted and continues to work unchanged.
**v1.7.4**
### New Features
#### Preview subscription updates
Use `POST /subscriptions/{subscriptionId}/update/preview/` to see the financial effect of a plan, billing-period, or feature change before applying it. Send the same `planIdentifier`, `chargePeriod`, and `features` payload used to update a subscription.
The response includes the new recurring amount, any amount that would be charged immediately, and the previewed next invoice with its line items. Amounts are returned in both major currency units and integer minor units. The preview does not update the subscription, create an invoice, or charge the customer.
```json theme={null}
{
"currency": "USD",
"recurringAmount": 50,
"recurringAmountUnits": 5000,
"amountChargedImmediately": 39.5,
"amountChargedImmediatelyUnits": 3950,
"nextInvoice": {
"amountDue": 39.5,
"amountDueUnits": 3950,
"nextPaymentAttempt": "2026-09-01T00:00:00Z",
"lineItems": [
{
"description": "Remaining time on Enterprise after 12 Aug 2026",
"amount": 39.5,
"amountUnits": 3950
}
]
}
}
```
#### Custom amounts for one-time charges
`POST /charges/` now accepts `customAmount` to override a plan's configured one-time price. Pass the amount in the currency's major unit and include `currencyCode`. You can also set `taxBehavior` to `INCLUSIVE` or `EXCLUSIVE`; it defaults to `EXCLUSIVE`.
```json theme={null}
{
"planIdentifier": "lifetime-access",
"chargePeriod": "ONE_TIME",
"customerId": "cust_789",
"currencyCode": "USD",
"customAmount": 49.99,
"taxBehavior": "INCLUSIVE"
}
```
**v1.7.3**
### New Features
#### Trial ending webhook
You can now subscribe to `subscription.trial_will_end`. Kelviq sends this a few days before a subscription's trial ends. It's skipped if the subscription has already converted to `active` or has no trial days left by the time the notice would go out. `data.object` uses the same subscription payload shape as `subscription.created` and `subscription.updated`.
**v1.7.2**
### New Features
#### Invoices API
You can now list and retrieve invoices directly, rather than only receiving them through webhooks:
* `GET /invoices/` — a paginated list of invoices for your organization, filterable by `subscription_id` and `customer_id`.
* `GET /invoices/{invoiceId}/` — a single invoice by its Kelviq invoice ID.
The invoice object also has three new fields, matching what's already sent in invoice webhooks:
* `attemptCount` — number of payment attempts made against the invoice.
* `nextPaymentAttempt` — when will next retry collection, if a retry is scheduled.
* `failureDetails` — `code`, `message`, and `paymentMethodType` for the most recent failed payment attempt. `null` once the invoice is paid.
#### Filter subscriptions by modification date
`GET /subscriptions/` now accepts `modified_on_after` / `modified_on_before` query parameters, for filtering by when the subscription was last modified.
```
GET /subscriptions/?modified_on_after=2026-08-01T00:00:00Z
```
#### Subscription timestamps
`GET /subscriptions/` and `GET /subscriptions/{subscriptionId}/` now return `createdOn` and `modifiedOn` on the subscription object.
**v1.16**
## Clearer subscription cancellation dates
Two fixes for cancelled subscriptions:
* The customer details page now shows when a subscription was cancelled, alongside its status.
* Cancelling a subscription from the dashboard now selects the correct cancellation date.
On the API side, subscription responses now include cancellation metadata: when the subscription was cancelled, whether the customer or the merchant initiated it, and any feedback the customer gave. See the [API changelog](/changelog#v1-7-0) entry for v1.7.0.
**v1.7.1**
### New Features
#### Payment links in invoice webhooks
The invoice object sent by `invoice.created`, `invoice.paid`, and `invoice.payment_failed` now includes `payment_link`. This URL takes the customer directly to the invoice payment page, so integrations can surface payment recovery without constructing the link themselves.
`payment_link` is present when the invoice is open or payment has failed. If the invoice is already paid when it is created, `payment_link` is `null` because no payment action is required.
Example:
```json theme={null}
{
"paymentLink": "https://portal.kelviq.com/acme-inc/pay/aW5fMVJwb1pvU0JzdEN6eW9jUzdpUG1pdGgz"
}
```
### Changes
#### Portal sessions require a customer email
`POST /portal/session/` now returns `400 Bad Request` when the selected customer does not have an email address. Portal session tokens are tied to the customer's email, so add an email to the customer before creating a session.
**v1.7.0**
### New Features
#### Subscription cancellation details
`GET /subscriptions/` and `GET /subscriptions/{subscriptionId}/` now return cancellation metadata:
* `canceledAt` — when the cancellation was recorded.
* `cancellationReason` — whether it was initiated by the customer, merchant, or provider.
* `cancellationFeedback` — structured feedback such as `TOO_EXPENSIVE`, `MISSING_FEATURES`, or `UNUSED`.
* `cancellationComment` — an optional free-form comment.
`POST /subscriptions/{subscriptionId}/cancel/` now accepts the optional `cancellationFeedback` and `cancellationComment` fields. The cancellation endpoint records the merchant as the initiator; cancellations submitted through the customer portal record the customer as the initiator.
#### Failed invoice webhook
You can now subscribe to `invoice.payment_failed`. Kelviq sends this event when a subscription invoice payment fails or requires customer action. Its `data.object` uses the same invoice payload shape as `invoice.created` and `invoice.paid`.
#### Refund webhook context
The `refund.created` and `refund.updated` webhook objects now include:
* `customer` — the associated customer's ID, name, email, customer identifier, and billing address.
* `subscription_id` — the associated subscription ID, or `null` for an order without a subscription.
#### Refunded order total
Order responses from `GET /orders/` and `GET /orders/{id}/` now include `refundedTotalUnits`, the total amount refunded against the order in the sale currency's minor units.
**v1.15**
## Kelviq now covers your payout fees
Payout fees are on us. When your cleared Kelviq balance is sent to your bank account or eligible debit card, Kelviq bears the provider's payout costs. You keep the full amount of your balance, and transaction fees are unchanged.
See [Fees](/getstarted/fees) for what a transaction costs and [Payouts](/payouts/overview) for how balances reach your bank.
## More billing periods
Plans now support a six-month billing period alongside monthly, quarterly, and yearly.
Plans can also bill on a fixed four-week cycle. Renewals happen on the same weekday every 28 days, and metered features can reset usage on the same 28-day schedule instead of following calendar months.
> [!NOTE]
>
> The 28-day period, and other custom periods such as weekly or daily, must be enabled for your organization first. Contact [hi@kelviq.com](mailto:hi@kelviq.com) to turn them on.
## Trials now default to 7 days
New free trials default to 7 days instead of 30. You can still set any duration from 1 to 365 days.
**v1.6.3**
### Fixes
#### `subscription.updated` now fires when a cancellation is scheduled for period-end
Scheduling a subscription to cancel at the end of the current billing period (rather than immediately) previously sent no webhook at all — status stays `active` until the period actually ends, so nothing was detected as changed. This now correctly sends `subscription.updated` (with the new `end_date` and a `previous_attributes.end_date` of `null`) at the moment the cancellation is scheduled. `subscription.cancelled` still only fires once the subscription actually reaches cancelled status — for a period-end cancellation, that's later, when the period ends.
**v1.6.2**
### New Features
#### Checkout sessions and checkout session events
Three endpoints are now part of the public API:
* `GET /checkout/sessions/` — paginated list of checkout sessions for the authenticated organization.
* `GET /checkout/sessions/{session_id}/` — a single checkout session, including the order, subscription, and payment method it produced (if any).
* `GET /checkout/sessions/{session_id}/events/` — the session's timeline events (session created, customer details updated, payment attempted, payment completed, etc.).
**v1.6.1**
### New Features
#### Orders, order events, and webhook delivery logs
Four endpoints are now part of the public API:
* `GET /orders/` — paginated list of orders. `PENDING` orders (payment never attempted/completed) are never included. Accepts `status`, `billing_type` (`ONE_TIME`/`SUBSCRIPTION`), and `is_renewal` (`true`/`false`) filters.
* `GET /orders/{id}/` — a single order.
* `GET /orders/{id}/events/` — the order's timeline events (order created, order completed, order receipt sent, invoice paid, etc.). This returns events for every order tied to the same underlying subscription as `{id}`, not just that single order record.
* `GET /webhook/logs/` — one row per webhook delivery attempt, including retries.
`GET /webhook/logs/` also accepts `start_date`/`end_date` query params. Results are always limited to the trailing 30 days — these params can narrow that window but can't widen it beyond 30 days back. They filter against `lastAttempt` (when that specific delivery attempt happened), not `createdOn` (when the underlying event was first created) — the two can differ for retried deliveries.
```
GET /webhook/logs/?start_date=2026-07-20&end_date=2026-07-25
```
```
GET /orders/?billing_type=SUBSCRIPTION&is_renewal=false
```
**v1.6.0**
### New Features
#### Filter subscriptions by status
`GET /subscriptions/` now accepts a `status` query parameter. Pass a comma-separated list to match any of several statuses, e.g. `?status=active,trialing`.
```
GET /subscriptions/?status=active,trialing
```
This changes the shape of the response compared to the default (no `status`) call:
* **Without `status`**: exactly one row per subscription — its most recent state, with expired (`end_date` in the past) and superseded records excluded.
* **With `status`**: that deduplication and exclusion is skipped, and matching records are returned directly. A subscription that has passed through the requested status more than once, or whose only matching record has since expired or been superseded, can appear more than once or in a state you didn't expect.
Use `status` for point-in-time queries ("show me everything currently `active`"); omit it for the current, deduplicated view of each subscription.
### Fixes
#### HTTPS pagination links
Paginated list responses' `next` and `previous` links now correctly use `https://` when the request was made over HTTPS. Previously they were always rendered as `http://`, regardless of the request's actual scheme.
**v1.5.0**
### Changes
#### Itemized Merchant of Record fees
Transaction responses from `GET /transaction/` now include `morFeeBreakdown`, an array that itemizes the Merchant of Record fee charged for the transaction.
Each component includes its `name`, percentage rate, amount in minor units (`valueUnits`), and currency. Fixed fees return `null` for `percentage`. Transactions without an applicable Merchant of Record fee return an empty array.
```json theme={null}
{
"morFee": 4,
"morFeeBreakdown": [
{
"name": "fixed_fee",
"percentage": null,
"valueUnits": 40,
"currency": "USD"
},
{
"name": "base_fee",
"percentage": "2.9",
"valueUnits": 290,
"currency": "USD"
},
{
"name": "subscription_fee",
"percentage": "0.7",
"valueUnits": 70,
"currency": "USD"
}
]
}
```
Possible components are `base_fee`, `fixed_fee`, `international_fee`, `subscription_fee`, and `conversion_fee`. A `fee_adjustment` component (also `null` percentage) is included when the calculated fee is raised to match a processor-fee floor.
**v1.14**
## Meet the Kelviq CLI (beta)
You can now manage your pricing catalog as code. The Kelviq CLI (`@kelviq/cli`) turns your products, features, plans, entitlements, and prices into one typed TypeScript file, `kelviq.config.ts`, that lives in your repository like the rest of your code.
The loop works in both directions:
* `kelviq pull` writes your live catalog to a config file, so a catalog you built in the dashboard is one command away from version control.
* `kelviq push` deploys the file back. It shows a diff, asks for confirmation, and writes drafts first. Nothing a customer sees changes until you pass `--publish`, which asks again.
* `kelviq promote` moves your whole sandbox catalog to production with the same preview and gates.
Your pricing model decides what customers are billed and which features they can reach, but dashboard edits leave no trail in your engineering workflow. With the catalog in git, every pricing change is a commit with an author, a diff, and a pull-request review. Drift between sandbox and production stops being invisible too: pull both environments and `diff` the two files.
Because the config is plain TypeScript, your editor autocompletes it and the compiler catches mistakes. A misspelled field or an invalid enum fails with a "did you mean" suggestion before Kelviq ever sees it. It also gives AI coding assistants something they are good at working with: ask one to "add a Team plan between Pro and Enterprise with 25 seats" and the types keep it honest.
Install it with `npm install -g @kelviq/cli`, run `kelviq login`, then `kelviq pull`. The CLI is in beta. It is safe by design, with previews and confirmations before every write, but commands and flags may still change between releases, so pin an exact version in CI. Start with the [CLI overview](/cli/overview).
## A bigger, safer MCP server
Two releases of `@kelviq/mcp-server` shipped this week for teams driving Kelviq from Claude, Cursor, or any other MCP client.
**v0.3.0** adds sandbox support. Set `KELVIQ_ENV=sandbox` and the server targets your sandbox API hosts automatically; an invalid value fails fast at startup instead of silently hitting production. `portal_session_create` now returns a ready-to-share `signedPortalUrl` with the session token already appended, since the bare portal URL does not authenticate on its own.
**v0.4.0** completes the checkout surface. `checkout_create_session` now accepts every field the API supports, including `customAmount` for custom-priced sessions, `discountCode`, and `metadata`. Fields the server did not recognize were previously dropped before the request was sent; a spec-conformance suite now guards every request schema against the OpenAPI spec so that cannot happen again.
All 45 tools now declare MCP safety annotations, so clients can auto-approve read-only calls while asking for confirmation on destructive ones like `subscription_cancel`. Three guided prompts encode common workflows end to end: `setup_product`, `launch_plan`, and `diagnose_customer`. In Claude Code they appear as slash commands, such as `/kelviq:setup_product`.
Six write tools were removed in v0.3.0, including bulk price editing, which now belongs to the CLI's reviewed `kelviq push` flow. The full list of changes is in the [MCP server changelog](/changelog#mcp-server), and the [MCP server guide](/guides/mcp-server) covers setup.
**v0.4.0**
### New Features
#### Full checkout surface
`checkout_create_session` now accepts every field the API supports — including `customAmount` (with `taxBehavior` and `currencyCode`) for custom-priced sessions, `discountCode` to pre-apply a coupon, `metadata` (returned verbatim on the `checkout.completed` webhook), `discountsEnabled`, `lockEmail`, and `defaultBillingCountry`. Previously these were silently dropped before the request was sent.
#### Subscription update options
`subscription_update` now supports `trialEnd` (`"now"` or an ISO 8601 datetime) and `paymentBehavior: "activate_on_payment"` to keep the current subscription active until payment for the update succeeds.
#### Safety annotations on every tool
All 45 tools now declare [MCP tool annotations](https://modelcontextprotocol.io/docs/concepts/tools#tool-annotations) — `readOnlyHint`, `destructiveHint`, `idempotentHint` — so MCP clients can auto-approve reads while asking for confirmation on destructive operations like `plan_archive` or `subscription_cancel`.
#### Guided prompts
Three built-in MCP prompts encode the golden-path workflows: `setup_product` (catalog bootstrap end-to-end), `launch_plan` (add a plan to an existing product), and `diagnose_customer` (entitlement/subscription cross-check). In Claude Code they appear as slash commands, e.g. `/kelviq:setup_product`.
### Fixes
* The server now reports its real version to MCP clients (it was pinned to an old value).
* An automated spec-conformance suite now guards every request schema against the OpenAPI spec, so new API fields can no longer be silently dropped.
**v0.3.0**
### New Features
#### Sandbox environment support
Set `KELVIQ_ENV=sandbox` to target your sandbox environment — the server switches both API hosts (`sandboxapi.kelviq.com` and `edge.sandboxapi.kelviq.com`) automatically. Keys only work against their own environment, and an invalid `KELVIQ_ENV` value fails fast at startup instead of silently hitting production.
#### Signed portal links
`portal_session_create` now returns a ready-to-share `signedPortalUrl` — the portal URL with the session token already appended. The bare `customerPortalUrl` does not authenticate on its own; always share the signed link.
### Breaking Changes
Six tools were removed: `media_presigned_upload`, the four `partner_org_*` tools, and `plan_prices_bulk_set`. Plan pricing is managed in the dashboard or with the [Kelviq CLI](/cli/overview) (`kelviq push`), which previews and confirms every price change; `plan_prices_list` (read) remains available.
### Improvements
* `subscription_create` now documents that it requires off-session charging to be enabled for your organization, with `checkout_create_session` as the fallback.
* The missing-key error links directly to the API-keys page in the right dashboard mode for your environment.
**v2.7.3**
### 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.
**v2.7.3**
### New Features
#### Activate Subscription Updates After Payment
The synchronous and asynchronous `client.subscriptions.update()` methods now support `paymentBehavior="activate_on_payment"`. This keeps the current subscription active while payment for the update is pending.
```python theme={null}
subscription = client.subscriptions.update(
subscriptionId="78058918-9746-4280-9b9b-1bd5115eec6e",
planIdentifier="premium-plan",
chargePeriod="MONTHLY",
paymentBehavior="activate_on_payment",
)
```
The SDK validates the option and serializes it as `payment_behavior`. The new plan and features are applied only after payment succeeds; if the pending update expires, the existing subscription remains active and unchanged.
**v1.13**
## Safer discount creation
Fixed-amount discounts now include an explicit currency selector. Kelviq places the currency from your organization profile first in the selector, while still letting you review and choose the currency for the discount.
Discount validation is also more reliable:
* Kelviq shows an error when a fixed-amount discount's currency does not match the selected price.
* Product-restricted discounts now use stricter product-selection validation during creation.
* Recurring discounts are now enabled by default, matching the behavior many users expected. You can still limit a discount to a fixed number of billing cycles or let it apply forever.
* Fixed a bug that moved the cursor to the end of a discount field while typing.
## Trials now follow the selected billing model
Trials are available only when a plan has at least one recurring price. One-time-only plans no longer expose trial settings or show a trial at checkout.
If a plan starts with both recurring and one-time pricing and the final recurring price is later disabled, Kelviq now removes the trial behavior from the remaining one-time offer.
## Resend webhook deliveries from the dashboard
Webhook delivery logs now include a **Resend** action. If a delivery fails because an endpoint is unavailable or returns an error, you can fix the endpoint and retry the delivery from the Kelviq dashboard.
The new attempt appears in the delivery logs, where you can inspect its status and response without waiting for another event.
> [!NOTE]
>
> A resend can deliver the same event more than once. Webhook handlers should use the event `id` or `webhook-id` header as an idempotency key.
## Faster feature selection
After a feature is added from the **Manage features & limits** dialog, Kelviq now clears the search field. You can immediately search for and add another feature without deleting the previous query.
## Customer portal fixes
We fixed two customer portal edge cases:
* Login code input is normalized to uppercase as the customer types.
* The cancel action remains available for an active subscription when an associated order has been refunded.
**v1.4.0**
### New Features
#### 28-day subscription billing
Checkout and subscription creation now support a four-week billing period. Pass `TWENTY_EIGHT_DAYS` as `chargePeriod` when creating an eligible checkout session or subscription.
This is useful for products sold in fixed four-week cycles: renewals happen on the same weekday every four weeks, and metered usage can reset on the same schedule instead of following calendar months.
```json theme={null}
{
"planIdentifier": "four-week-pro",
"chargePeriod": "TWENTY_EIGHT_DAYS",
"customerId": "customer_123",
"successUrl": "https://app.example.com/billing/success"
}
```
Metered features that follow the same cadence can use `EVERY_28_DAYS` as their usage reset period.
The new billing period works across renewals, orders, invoices, emails, and subscription reporting. Organizations must enable the 28-day period before using it on a plan.
#### Subscription end dates in webhook payloads
Subscription webhook payloads now include `end_date` inside `data.object`. The field contains an ISO 8601 date when the subscription has a known end date and `null` when no end date is set.
```json theme={null}
{
"type": "subscription.cancelled",
"data": {
"object": {
"id": "sub_123",
"end_date": "2026-08-17"
}
}
}
```
Merchants can use this value to schedule access removal, customer notifications, and account cleanup without making a separate request for the latest subscription.
### Fixes
#### Reliable `invoice.paid` webhook delivery
Kelviq now emits `invoice.paid` when an invoice is created with a `PAID` status. Previously, these invoices could emit only `invoice.created`, which meant integrations waiting for `invoice.paid` did not receive confirmation that payment had been collected.
The event payload has not changed. Continue to use `data.object.status`, `data.object.paid_at`, and `data.object.subscription_id` when processing the event.
**v1.3.1**
### Changes
#### Multiple customers can use the same email
More than one customer record can now use the same email address. This removes the previous one-customer-per-email restriction.
Each record must still have a unique `customerId`. Archiving a customer does not release that ID, so it cannot be assigned to a new record.
**v1.3.0**
### New Features
#### List, Create, and Retrieve Refunds
The Refunds API now supports the complete refund workflow:
* **`GET /refunds/` — List refunds**: Returns a paginated list of refunds for the authenticated organization, with search, status, date-range, and pagination filters.
* **`POST /refunds/` — Create a refund**: Creates a full or partial refund for an order. Provide `amountUnits` or `amount` for a partial refund, or omit both to refund the remaining balance.
* **`GET /refunds/{refundId}/` — Retrieve a refund**: Returns a refund by its Kelviq UUID.
Create requests use `orderId` to identify the order. List results, newly created refunds, and retrieved refunds share the same response format, including the refund amount, reason, status, failure details, internal note, and associated order.
**v2.7.2**
### 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.
**v2.7.2**
### New Features
#### List, Retrieve, and Create Subscriptions
The synchronous and asynchronous subscription clients now support 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.
```python theme={null}
page = client.subscriptions.list(
customerId="cust_789",
page=1,
page_size=20,
)
subscription = client.subscriptions.retrieve(
subscriptionId=page.results[0].id,
)
created = client.subscriptions.create(
planIdentifier="plan-pro-monthly",
chargePeriod="MONTHLY",
customerId="cust_789",
)
```
All three methods are also available on `async_client` and return validated Pydantic models. New models cover paginated results, create payloads, product and plan details, features, files, links, and issued licenses.
**v1.2.0**
### New Features
#### Subscription List, Create, and Retrieve APIs
The Subscriptions API now includes three new endpoints for reading and creating subscriptions:
* **`GET /subscriptions/` — List subscriptions**: Retrieves a paginated list for a customer. The `customer_id` query parameter is required.
* **`POST /subscriptions/create/` — Create a subscription**: Creates a subscription directly for a customer without requiring a checkout session.
* **`GET /subscriptions/{subscriptionId}/` — Retrieve a subscription**: Retrieves a subscription by its Kelviq UUID.
Subscription list, create, and retrieve responses now include:
* `files` — downloadable files attached to the subscription's plan.
* `links` — external links attached to the plan.
* `license` — issued licenses associated with the subscription.
#### One-Time Charges
The new `POST /charges/` endpoint immediately charges a customer's saved payment method without creating a checkout session. The `chargePeriod` must be `ONE_TIME`.
**v1.12**
## Customer deliverables in one place
Customer detail pages now include the files, links, and license keys delivered through a customer's plans. Support teams can review a customer's access and manage license keys without opening the related order first.
This makes common support requests faster, including:
* Finding a purchased download
* Opening a private resource link
* Reviewing an issued license key
* Managing a customer's license access
## Better feature and usage configuration
We improved the feature-management experience for metered and configurable entitlements:
* Numeric values now use number inputs where appropriate.
* Credit rollover appears before usage alerts, matching the order in which these settings are configured.
* Feature names can be edited from the feature row's action menu.
* Regional prices now carry over the base price's usage reset, usage alert, and credit rollover settings.
These changes reduce repetitive setup and keep entitlement behavior consistent across base and regional prices.
## Business details are easier to find
Business verification is now available from **Settings → Business details** as well as the onboarding flow. Kelviq also uses the country from the organization profile to make the relevant country option easier to find during verification.
## Cleaner catalog, settings, and sandbox navigation
We simplified several dashboard workflows:
* Product identifiers are no longer shown or requested in the product interface. Kelviq uses the product's internal UUID.
* The sandbox no longer shows a **Start selling** action, because live selling is completed from the production environment.
* Settings navigation has clearer visual separation between groups.
**v2.7.0-beta.2**
### 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.
**v2.7.1**
### New Features
#### Create One-Time Charges
The synchronous and asynchronous clients now provide `client.charges.create()` for immediately charging a customer's saved payment method without creating a checkout session.
```python theme={null}
charge = client.charges.create(
planIdentifier="lifetime-access",
chargePeriod="ONE_TIME",
customerId="cust_789",
currencyCode="USD",
features=[{"identifier": "seats", "quantity": 5}],
ipAddress="103.154.35.20",
)
print(charge.id, charge.status, charge.amount)
```
**Async variant:**
```python theme={null}
charge = await async_client.charges.create(
planIdentifier="lifetime-access",
chargePeriod="ONE_TIME",
customerId="cust_789",
)
```
The customer must already have a usable payment method on file. `chargePeriod` is restricted to `"ONE_TIME"`; use the subscriptions API for recurring billing. Pydantic request and response models validate parameters and provide typed charge records.
**v2.7.0**
### 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`.
**v2.7.0**
### 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 / gunicorn workers / containers, and survive restarts.
Read-through order is **L1 → L2 → API**, with write-through to both on every successful fetch.
```python theme={null}
client = Kelviq.create_sync_client(
access_token=ACCESS_TOKEN,
enable_cache=True, # default True
cache_ttl=60.0, # freshness window in seconds (default 60)
)
```
Set `enable_cache=False` to disable caching entirely. Both the sync and async clients are supported.
#### 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.
```python theme={null}
remaining = client.reporting.flush() # returns count still queued
# async: remaining = await async_client.reporting.flush()
```
#### Distributed Cache — `RedisStore` / `AsyncRedisStore`
Ready-to-use Redis-backed stores can be shared across multiple processes / gunicorn workers / containers so they use one cache and one durable usage queue. Redis is an optional extra (`pip install "kelviq-sdk[redis]"`).
```python theme={null}
from kelviq_sdk import Kelviq, RedisStore
store = RedisStore(url="redis://localhost:6379/0", prefix="kelviq:prod")
client = Kelviq.create_sync_client(access_token=ACCESS_TOKEN, cache_store=store)
```
```python theme={null}
# Async
from kelviq_sdk import Kelviq, AsyncRedisStore
store = AsyncRedisStore(url="redis://localhost:6379/0", prefix="kelviq:prod")
async_client = Kelviq.create_async_client(access_token=ACCESS_TOKEN, cache_store=store)
```
You can also bring your own backend by implementing the public `CacheStore` interface and passing it as `cache_store`. All workers/tasks that should share a cache must use the same `prefix`.
**v1.11**

## Quarterly billing periods
Kelviq now supports quarterly billing periods. Sellers can now offer plans that renew every 3 months.
Example pricing:
```text theme={null}
Monthly: $29 per month
Quarterly: $79 per quarter
Annual: $299 per year
```
Quarterly billing is useful when monthly feels too short and annual feels too expensive. It works well for:
* AI tools
* Developer tools
* Creator products
* Small business SaaS
* B2B products with longer buying cycles
## Pay as you go improvements
We improved pay as you go support across the product. This makes it easier to set up and review usage based billing.
## Usage based pricing improvements
We improved the usage based pricing experience in the UI. This helps sellers create usage based plans with less confusion. Supported pricing patterns include:
* Per unit pricing
* Package pricing
* Included usage with overage
* Pay as you go
* Subscription plus usage
* Credit based usage
Example:
```text theme={null}
Starter plan: $19 per month
Includes: 100 AI credits
Extra credits: $10 per 100 credits
```
## Checkout pricing improvements
Checkout now handles more pricing cases across custom amounts, usage based plans, and tax behavior. This helps keep pricing consistent across:
* Pricing page
* Checkout page
* Customer portal
* Transaction details
* Invoices
## Node SDK webhook improvements
The Node SDK includes more webhook improvements for safer event handling. This helps developers build reliable flows for:
* License generation
* Subscription access
* Plan changes
* Customer portal links
* Usage updates
* Billing state sync
## Final fixes and cleanup
We also shipped smaller fixes and cleanup across checkout, pricing, product setup, and plan setup. This includes improvements for:
* Checkout plan switching
* Package pricing labels
* Product setup flows
* Plan setup flows
* Checkout management
* Dashboard clarity
**v1.10**

## Organization settings page
Kelviq now has an Organization settings page. This gives teams a central place to manage organization level settings. The goal is to keep account level setup separate from product level setup.
This can include:
* Organization name
* Business details
* Team settings
* Billing settings
* Product defaults
## Payout tab is visible before business verification
The payout tab is now visible before business verification is complete. This avoids confusion for new sellers. Before this, some users were not sure where payout setup would appear. Now sellers can see the payout area earlier, even if live payouts are not enabled yet.
## Zapier integration
Kelviq now supports Zapier integration. This helps sellers connect Kelviq with other tools without custom code.
Examples:
* Send a Slack message when a new order is created
* Add a new customer to HubSpot
* Create a Google Sheets row after checkout
* Send an onboarding email after payment
* Notify support when a refund happens
* Add a buyer to a course platform
## Plans removed from the sidebar
We removed Plans from the sidebar because it was confusing. Plans are now managed inside products. This makes the dashboard easier to understand. This matches how sellers usually think about setup:
1. Create a product
2. Add plans to the product
3. Create checkout links for those plans
## Email removed from checkout list
We removed the separate email field from the checkout list because the customer column already shows the email. This makes the table cleaner and reduces repeated information.
## Frontend improvements
We shipped several dashboard improvements focused on clarity and setup speed. This includes:
* Cleaner product navigation
* Better table layout
* Better empty states
* Easier checkout link access
* Clearer plan setup
* Better plan publishing flow
## Custom checkout pricing improvements
We improved the custom checkout pricing flow. This is useful for sales led deals and private pricing. You can create a custom checkout for that customer without adding a public plan.
**v0.2.0 – v0.2.3**
Initial public release of `@kelviq/mcp-server`: the full Kelviq API as typed MCP tools plus the entire documentation as searchable resources, followed by packaging fixes and consolidation onto a single edge host.
**v1.9**

## Subscription data in license validation
License checks can return subscription context along with the license result. This is important for desktop software, private model tooling, and downloadable products where access is tied to both a license key and a live subscription.
Example response:
```json theme={null}
{
"license": {
"key": "LIC-123",
"status": "active"
},
"subscription": {
"status": "active",
"plan": "Pro",
"trialDaysRemaining": 0
...
}
}
```
## License API support in the Node SDK
The Node SDK now supports the License API. You can validate license keys and connect license status directly to your app. Backend services using `@kelviq/node-sdk` can validate licenses without making manual API requests. For example, a CLI product can validate a license key on launch, confirm the associated subscription is active, and decide whether to unlock premium features.
**Example:**
```js theme={null}
const result = await client.license.validate({
licenseKey: "LIC-123"
});
if (result.valid) {
// Unlock product access
}
```
This is useful for products such as:
* macOS apps
* WordPress plugins
* VS Code extensions
* Figma plugins
* Desktop developer tools
* Paid templates
## Trial end support for subscription updates
The Node SDK now supports `trialEnd` when updating subscriptions. This gives developers more control over trials. This helps teams extend a trial after sales approval or shorten a trial when a customer upgrades early.
You can now handle flows like:
* Extend a trial by 7 days
* End a trial immediately
* Set a custom trial end date
* Move a customer from trial to paid
* Give a sales led customer a longer trial
```js theme={null}
await client.subscriptions.update({
subscriptionId: "sub_123",
trialEnd: "2026-06-30T23:59:59Z"
});
```
## Portal session support in the Node SDK
The Node SDK now supports customer portal sessions. You can create a signed portal link from your backend and send customers to the portal.
Example:
```js theme={null}
const session = await client.portal.createSession({
customerId: "cust_789",
});
// Redirect your customer to the portal
res.redirect(session.customerPortalUrl);
```
Customers can use the portal to:
* View invoices
* Update payment method
* Cancel subscription
* Manage billing details
* Access downloads
* View license keys
## Signed customer portal link from order details
You can now generate and send a signed customer portal link directly from the order details page. This is especially useful for support workflows.
**Example:** A customer asks:
```text theme={null}
Can you send me my invoice?
I need to update my payment method.
I need access to my license details.
```
You can open the order, generate a secure customer portal link, and send it to the customer. Customers can then access invoices, update their payment method, manage subscriptions, and view license details on their own. No custom engineering is required for this support workflow.
## Webhooks in the dashboard
We added webhook support in the dashboard. This gives developers a clear place to manage webhook related setup. Webhooks are useful when your app needs to react to Kelviq events.
Examples:
* Create a license after checkout
* Unlock a paid feature
* Update a subscription
* Sync customer state
* Send an onboarding email
* Disable access after cancellation
## Webhook improvements in the Node SDK
The Node SDK now includes webhook improvements. This helps developers verify and handle webhook events more safely.
Example events:
```text theme={null}
checkout.completed
order.created
subscription.created
subscription.updated
subscription.cancelled
plan.changed
```
## Webhook logs
Kelviq now includes webhook logs. This helps developers debug webhook delivery issues. This is useful when your endpoint is down, slow, or returning an error. You can inspect:
* Event type
* Delivery status
* Response code
* Timestamp
* Error response
## Plan changed webhook
We added a plan changed webhook. This helps your app react when a customer changes plans.
Example: A customer moves from `Starter` to `Pro`.
Your app can listen for the event and update:
* Feature access
* Usage limits
* Seat limits
* Credit balance
* Internal customer state
## Publish plan UI and UX update
We updated the publish plan UI and UX. The new flow makes it clearer when a plan is still a draft and when it is ready for customers. This helps reduce mistakes before sharing checkout links.
## Plan description in plan detail header
Plan detail pages now show the plan description in the header. This makes it easier to identify similar plans.
Example:
```text theme={null}
Pro
For growing AI teams with higher usage limits
```
## Free plan switch disabled for published plans
Kelviq now disables free plan switching for published plans when switching is not possible. This prevents users from selecting an action that cannot be completed.
**v1.8**

## Usage based pricing in the dashboard
Merchants can configure metered pricing without leaving the dashboard. A plan can include a base subscription, included usage, and overage pricing for usage beyond the included amount.
## Pay as you go
Kelviq now supports pay as you go pricing. Your customers can continue using a metered product after included usage runs out, then pay for additional consumption. This fits LLM tools, API platforms, transcription products, and image generation products where hard cutoffs create poor customer experiences.
Example: A customer uses the following in one month:
```text theme={null}
120,000 tokens
40 image generations
500 API calls
```
Kelviq will calculate the bill based on the pricing rules you set.
## Enable pay as you go from the dashboard
You can now enable pay as you go from the dashboard. This helps sellers launch usage based pricing without building their own billing system. A simple AI product can now charge like this:
```text theme={null}
Base plan: $20 per month
Included usage: 100,000 tokens
Overage: $2 per 10,000 tokens
```
## Usage alerts
Kelviq now supports usage alerts. Usage alerts help you know when a customer reaches an important usage threshold. Merchants can notify customers when metered usage crosses a configured limit. This is especially useful for AI products where usage can grow quickly across tokens, image generations, API calls, embeddings, storage, or compute minutes.
Examples:
* Customer used 80% of included credits
* Customer crossed 10,000 API calls
* Customer used more than `$100` in AI model cost
* Customer is close to the monthly usage limit
* Customer is likely to need an upgrade soon
This helps sellers avoid surprise bills and give customers a better experience.
## Usage alerts for status entitlements
Usage alerts now work with status entitlements. This helps connect product access with usage state.
Example: A customer has `100` credits. Different features can consume credits differently:
```text theme={null}
GPT 5.1 request: 2 credits
Claude Sonnet request: 3 credits
Page crawl: 5 credits
Long report generation: 20 credits
```
Kelviq can help track when the customer is close to the limit and trigger an alert.
## Negative usage values
Usage reporting now supports negative values. This is useful when you need to correct usage after it was reported. If an app accidentally reports `125,000` tokens instead of `120,000`, it can send `-5,000` to bring the meter back in line.
Examples:
* Remove duplicate API usage
* Reverse usage after a failed job
* Credit back usage after a refund
* Fix a metering mistake
* Adjust token usage after an LLM call failed
* Reserve tokens up front and return the unused amount once the action completes
This also enables a reserve-and-settle pattern. You can block a number of tokens in advance, then report a negative value to return whatever wasn't used once the action finishes. For example, reserve `1,000` tokens before an LLM call, then send `-400` if only `600` were consumed.
```js theme={null}
import { BEHAVIOUR_CHOICES } from '@kelviq/node-sdk';
// Reserve tokens before the action
await client.reporting.reportUsage({
value: 1000,
customerId: "cus_123",
featureId: "tokens",
behaviour: BEHAVIOUR_CHOICES.DELTA
});
// Return the unused tokens after it completes
await client.reporting.reportUsage({
value: -400,
customerId: "cus_123",
featureId: "tokens",
behaviour: BEHAVIOUR_CHOICES.DELTA
});
```
## Checkout API supports locked email and default billing country
The checkout API now supports locked email and default billing country. This helps create a cleaner checkout flow when your app already knows the customer.
Example:
```js theme={null}
await client.checkout.createSession({
planIdentifier: "plan-pro",
customerId: "cust_789",
lockEmail: true,
defaultBillingCountry: "US"
});
```
This prevents customers from using the wrong email during checkout.
## Prefill customer email in checkout links
Checkout links can now prefill the customer email when available. This is useful when checkout starts from your app.
Example: A logged in customer clicks upgrade. Checkout opens with their email already filled in.
```text theme={null}
alex@company.com
```
This reduces checkout mistakes and support issues.
## Lock email using checkout config
Checkout links now support locking the email field using config. This means customers can see the email but cannot edit it.
This is useful for:
* License keys
* Account based access
* Customer portal access
* Subscription upgrades
* Team plans
## SEPA payment method
Checkout now supports SEPA. This helps sellers serve more European customers, especially B2B customers who prefer bank debit. SEPA is useful for:
* SaaS subscriptions
* B2B tools
* Annual contracts
* European customers
* Larger invoice amounts
**v2.6.0**
### 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.
**v2.6.0**
### New Features
#### Checkout Session — `metadata` Parameter
`checkout.create_session()` now accepts an optional `metadata` parameter 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.
```python theme={null}
response = client.checkout.create_session(
planIdentifier="plan-pro-monthly",
chargePeriod="MONTHLY",
customerId="cust_789",
successUrl="https://example.com/success",
metadata={
"orderId": "order_12345",
"referralCode": "SUMMER2026",
},
)
```
**Async variant:**
```python theme={null}
response = await async_client.checkout.create_session(
planIdentifier="plan-pro-monthly",
chargePeriod="MONTHLY",
customerId="cust_789",
successUrl="https://example.com/success",
metadata={"orderId": "order_12345"},
)
```
The keys inside `metadata` are preserved exactly as provided — they are not transformed on the way to the API, so they survive the round-trip back through webhooks unchanged.
**v1.7**

## Tax inclusive pricing settings
Kelviq now supports tax inclusive pricing settings. This lets you decide whether the price shown to the customer already includes tax.
Example: A customer sees:
```text theme={null}
$20 per month
```
If tax inclusive pricing is enabled, the customer still pays \$20 per month.
Kelviq calculates the tax portion from that amount. This is useful in markets where customers expect the displayed price to be the final price.
## Tax inclusive behavior is shown in checkout
Checkout now respects tax inclusive settings so the buyer sees the same pricing behavior that was configured for the plan. This avoids the common confusion where a pricing page says `$49` but checkout unexpectedly adds tax on top.
## Tax type and tax percentage in checkout
Checkout links now support tax type (such as VAT, GST, Sales Tax, or any other configured tax type) and the tax percentage. This gives customers a clearer view of what they are paying.
Example with tax added on top:
```text theme={null}
Product price: $100
Tax: 10%
Total: $110
```
Example with tax included:
```text theme={null}
Total: €20
Includes tax: €3.33
```
## Tax behavior in plan details
Plan detail pages now show tax behavior. Teams can confirm that a `Pro EU` plan includes VAT, while a `Pro US` plan remains tax exclusive. You can see whether a plan is tax inclusive or tax exclusive before sharing it with customers, helping you avoid setup mistakes.
## Tax code category on products
You can now set a tax code category when creating or editing a product. This helps Kelviq apply the right tax treatment based on what you sell.
Examples:
* SaaS subscription
* API access
* AI credits
* Digital download
* License key
* Course
* Template
* Plugin
This matters because different product types can have different tax rules.
## Tax information in transactions
Transaction views now show tax details so support and finance teams can answer customer questions without leaving the dashboard.
You can use this to answer questions like:
* What was the product price?
* How much tax was charged?
* Was tax included in the price?
* What did the customer pay in total?
## Custom checkout pricing
Kelviq now supports custom checkout pricing. This lets you create a checkout with a custom amount instead of always using the fixed public plan price.
This is useful for:
* Custom quotes
* One off payments
* Sales led plans
* Manual upgrades
* Private deals
* Customer specific pricing
Example: A customer agrees to a custom price of `$349 per month`.
You can create a checkout session for that amount without creating a public plan for every custom price.
## Custom amount on checkout page
Checkout can now show and charge a custom amount. This gives sellers more flexibility when dealing with larger customers or special pricing cases.
Example:
```text theme={null}
Custom plan
$349 per month
```
## Regional pricing minimum value validation
We added validation for regional pricing minimum values. This helps prevent a regional price from becoming too low to process.
Example: A plan is priced at `$5`.
A very large regional discount could reduce the price below the minimum amount allowed by the payment method. Kelviq now catches this earlier so sellers can fix it before customers reach checkout.
## Fixes
**Canada postal code** —
Fixed an issue with Canadian postal codes during checkout. Canadian postal codes such as `M5V 2T6` are now handled correctly.
**Checkout plan switching** —
Fixed an issue where checkout could fail when customers switched between plans. This improves checkout reliability when customers compare plans before paying.
**Package pricing label** —
Fixed the label for package based pricing. This makes package pricing easier for customers to understand at checkout.
Example:
```text theme={null}
$10 per 1,000 API calls
```
**One time payment cancellation** —
Fixed an issue where cancel subscription was visible for one time payments. One time payments do not have a subscription to cancel, so that action is now hidden.
**v1.6**

## Promotions are now faster and more reliable
We moved Promotions storage to DynamoDB. This gives promotion rules
a durable home across the dashboard, APIs, and hosted checkout.
Example: A merchant selling an AI support agent can create `STARTUP20`,
limit it to the Pro plan, and keep the rule available when checkout
links are regenerated.
## Promotions are now available in the dashboard
We added Promotions to the Kelviq dashboard. Teams can now create,
review, and manage offers without asking engineering to touch config.
The flow is built around what a billing owner actually needs to know:
which plan is discounted, whether the promotion is active, and where
the SDK docs are when you need to wire it up from code.
A common flow looks like this:
1. Create a product
2. Create a plan
3. Add a promotion
4. Copy the checkout link
5. Share the link or add it to your pricing page
## API and SDK docs are linked from Promotions
The dashboard now connects promotion setup to the integration docs a developer needs when creating checkout sessions from the backend. This helps developers move from setup to integration faster.
## Checkout link empty screen is clearer
The empty screen now points users toward creating or copying the right checkout link instead of leaving the page feeling broken.
## Checkout links are easier to access
You can now get checkout links from product and plan listing pages. This saves time when you are setting up a product and want to quickly copy a checkout link.
## Product cards are now fully clickable
Instead of clicking only on a small button or title, you can click anywhere on the product card to open it. This makes navigation easier when you have multiple products.
## Developers tab is now API Key
We renamed the Developers tab to API Key. Most people go there to find their API key, so the label now matches the destination.
Before: **Developers**
Now: **API Key**
## Chart theming support
We added theming options for charts, including branded and dark mode. Charts now stay consistent with the rest of your dashboard without manual workarounds.
## Analytics style update
We updated the analytics UI style to make revenue, customers,
transactions, and product data easier to scan.
This is helpful when you want to quickly answer questions like:
* How much did we sell today?
* Which plan is performing better?
* Which product has the most transactions?
* Are refunds increasing?
* Are more customers choosing annual plans?
## Docs copy cleanup
We updated docs copy from `archive` to `delete` in more places.
This makes the docs match the real product action.
## Fixes
**Duplicate file upload issue** —
Fixed an issue where duplicate files could be created during
multiple file uploads.
**Plan dialog issue** —
Fixed an issue where the add plan dialog closed even when the
user selected the option to create more plans. Now users can
create multiple plans without reopening the dialog each time.
**Feature delete issue** —
Fixed an issue where a feature was not removed correctly from
the list after deletion.
**v1.5**
### Payments & Billing
* **UPI Payments Support** — You can now accept payments via UPI. Customers can pay seamlessly using their preferred UPI apps (like Google Pay, PhonePe, or Paytm).
* **Usage-Based Billing** — You can now bill customers based on their actual consumption. This provides maximum flexibility for products using credits, seats, or data metrics.
### Customer Experience
* **Signed Customer Portal Links** — Added the option to generate and send a signed customer portal link directly from the order details page for secure, instant access.
### Tax & Compliance
* **Tax Information** — Tax detail is added in transaction.
* **Tax Categories** — You can now define **Tax Categories** (B2B, B2C, Ebook, Online Course, etc.) during product creation.
* **Business Tax ID Support** — Control how valid business tax IDs are handled for tax-inclusive pricing. When enabled, Kelviq automatically removes the included tax amount from the checkout total if a customer provides a valid business tax ID during checkout. When disabled, customers continue paying the listed tax-inclusive price with no tax deduction applied.
**v2.5.0**
### 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.
**v2.5.0**
### New Features
#### Subscription Update — `trialEnd` Parameter
`subscriptions.update()` now accepts an optional `trialEnd` parameter 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-31 23:59:59"`) to set a new trial end date. The datetime must be in the future. Naive datetimes are interpreted as UTC.
* Omit the parameter to preserve the existing trial behavior on the subscription.
```python theme={null}
# End the trial now
response = client.subscriptions.update(
subscriptionId="78058918-9746-4280-9b9b-1bd5115eec6e",
planIdentifier="premium-plan",
chargePeriod="MONTHLY",
trialEnd="now",
)
# Extend the trial to a specific future date
response = client.subscriptions.update(
subscriptionId="78058918-9746-4280-9b9b-1bd5115eec6e",
planIdentifier="premium-plan",
chargePeriod="MONTHLY",
trialEnd="2025-12-31 23:59:59",
)
```
**Async variant:**
```python theme={null}
response = await async_client.subscriptions.update(
subscriptionId="78058918-9746-4280-9b9b-1bd5115eec6e",
planIdentifier="premium-plan",
chargePeriod="MONTHLY",
trialEnd="2025-12-31 23:59:59",
)
```
The SDK validates `trialEnd` client-side — invalid datetime strings or past datetimes raise `InvalidRequestError` before the request is sent.
**v2.1.0**
### New Features
#### Product Offerings — Pricing API Support
The SDK can now fetch and display product pricing data. Pass a `productId` to the client to enable pricing methods.
```ts theme={null}
const client = kelviqSDK({
productId: 'your-product-id',
accessToken: 'your-access-token',
});
```
If `customerId` is also set, it is forwarded to the pricing API so the response reflects that customer's subscription state.
***
##### `fetchPricing(forceRefresh?)` — Fetch and Cache Pricing
Fetches product offering data for the configured `productId`. Subsequent calls return the cached result unless `forceRefresh` is `true`.
```ts theme={null}
const pricing = await client.fetchPricing();
console.log(pricing.plans, pricing.currencyCode, pricing.billingPeriods);
await client.fetchPricing(true); // bypass cache
```
##### `getPricing()` — Read from Cache
Returns cached pricing data synchronously, or `null` if not yet fetched.
```ts theme={null}
const pricing = client.getPricing();
```
##### `getPlan(identifier)` — Look Up a Single Plan
Finds an enabled plan by identifier from the pricing cache. Returns `null` if not found or pricing hasn't been fetched.
```ts theme={null}
const plan = client.getPlan('pro-plan');
console.log(plan?.displayName, plan?.price.priceType);
```
##### `isPricingLoading()` — Loading State
Returns `true` while a pricing fetch is in progress.
```ts theme={null}
if (client.isPricingLoading()) { /* show spinner */ }
```
##### `getLastPricingError()` — Error State
Returns the last `Error` from a failed pricing fetch, or `null`.
```ts theme={null}
const err = client.getLastPricingError();
```
##### `clearPricingCache()` — Reset Cache
Clears cached pricing data and resets loading and error states.
```ts theme={null}
client.clearPricingCache();
```
***
##### `renderPricing(options?)` — DOM Binding
Scans the DOM for elements with `data-kq-price` attributes and populates them with localized formatted prices. Requires pricing data to be fetched first.
```html theme={null}
Loading...
```
```ts theme={null}
await client.fetchPricing();
client.renderPricing();
// Scope to a container
client.renderPricing({ container: document.getElementById('pricing-section') });
// With format options
client.renderPricing({ formatOptions: { compact: true } });
```
| Attribute | Required | Description |
| - | - | - |
| `data-kq-price` | Yes | Plan identifier |
| `data-kq-period` | No | Billing period (e.g. `MONTHLY`, `YEARLY`). Falls back to the first available charge. |
***
##### `kqFormatPrice(amount, currencySymbol, options?)` — Price Formatter
Standalone named export for formatting a numeric amount with a currency symbol.
```ts theme={null}
import { kqFormatPrice } from '@kelviq/js-sdk';
kqFormatPrice(9.99, '$', { pricingLocale: 'en-US' }); // "$9.99"
kqFormatPrice(1200, '€', { compact: true, pricingLocale: 'de-DE' }); // "€1,2K"
kqFormatPrice(9.99, '$', { includeCurrencySymbol: false }); // "9.99"
```
| 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 and Exports
```ts theme={null}
export { kqFormatPrice } from './formatPrice';
export type { KQFormatPriceOptions } from './formatPrice';
export type {
RawPricingFeature,
RawPricingCharge,
RawPricingPrice,
RawPricingPlan,
RawPricingBillingPeriod,
RawPricingApiResponse,
} from './types';
```
**v2.1.1**
### 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}
```
**`plansEnabled`** — a comma-separated list of plan identifiers to include in the response. When omitted, all active plans are returned:
```tsx theme={null}
```
Both props are reactive — updating either value will trigger a new pricing fetch automatically.
**v2.1.0**
### 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}
```
##### `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);
```
##### `` — Render-Prop Price Component
Displays localized pricing for a plan and billing period:
```tsx theme={null}
}
fallback={
{formattedPrice}
{hasFreeTrial && Try free for {trialPeriod} days}
)}
```
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 |
##### `` — Render-Prop Feature Component
Iterates over enabled features for a plan. Supports optional filtering by feature type:
```tsx theme={null}
}
fallback={
No features available
}
>
{({ feature, plan, index }) => (
{feature.displayName}
)}
```
| 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';
```
**v0.0.4**
### Changed
#### Close Icon Update
The close icon across all promotion UI components has been updated for better visibility and brand alignment. This change ensures a more consistent user experience when dismissing active promotions.
**v1.1.0**
### New Features
#### Enhanced Checkout Configuration
You can now pass additional configuration parameters when [creating a checkout session via the API](https://docs.kelviq.com/api-reference/checkout/create-a-checkout-session). These new fields allow for tighter control over the customer experience:
* **`lockEmail`**: When set to `true`, the email address field on the checkout page is disabled, preventing customers from changing the email associated with the session.
* **`discountsEnabled`**: A boolean to explicitly allow or disallow discount code application on the checkout page.
* **`defaultBillingCountry`**: Pre-fills the billing country field (e.g., `"IN"`).
**Example Payload:**
```json theme={null}
{
"planIdentifier": "base",
"successUrl": "[https://kelviq.com/checkout/success](https://kelviq.com/checkout/success)",
"chargePeriod": "MONTHLY",
"customerId": "sachin",
"lockEmail": true,
"discountsEnabled": false,
"defaultBillingCountry": "IN"
}
```
**v1.0.0**
### New Features
#### Custom Dynamic Amount Support
Added support for custom dynamic amounts on the checkout page. You can now pass a specific `custom_amount` directly via the checkout API, which is ideal for usage-based, variable, or custom-quoted pricing models.
* **`custom_amount`**: The specific numeric amount to be charged to the customer. This overrides static plan pricing and is ideal for usage-based, variable, or custom-quoted pricing models.
* **`tax_behavior`**: Specifies how taxes should be applied to the custom amount. For example, setting it to "INCLUSIVE" means the tax is already factored into the custom\_amount, while "EXCLUSIVE" would add the tax on top of the base amount.
* **`currency_code`**: The three-letter ISO currency code (e.g., "INR", "USD") that defines the currency in which the custom\_amount should be processed.
```json theme={null}
{
"planIdentifier": "automatic-pricing",
"successUrl": "[https://www.kelviq.com/](https://www.kelviq.com/)",
"chargePeriod": "MONTHLY",
"customerId": "sam-altman0010",
"custom_amount": 1200,
"tax_behavior": "INCLUSIVE",
"currency_code": "INR"
}
```
**v1.4**
### Platform Improvements
* **Webhook Enhancements** — New webhook options in settings, including **Webhook Logs** for easier debugging and a new **Plan Changed** webhook event.
* **Analytics Refresh** — The analytics dashboard has a fresh new look for better data visualization and clarity.
* **Early Payout Access** — The Payouts tab is now enabled even before business verification is complete, allowing you to prepare your financial setup easily.
### Fixes
* **File Uploads** — Resolved an issue that caused duplicate files to be created during multiple file uploads.
**v2.4.0**
### 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",
});
```
**v2.4.0**
### New Features
#### Checkout Session — New Optional Parameters
Three new optional parameters on `checkout.create_session()` give you more control over the hosted checkout page.
**`discounts_enabled`** — controls whether the coupon/discount code field is shown. Defaults to `True` on the server.
**`lock_email`** — when `True`, the email field is pre-filled and locked so the customer cannot change it. Defaults to `False`.
**`default_billing_country`** — ISO 3166-1 alpha-2 country code (e.g. `"US"`, `"GB"`) used to pre-fill the billing address country field.
```python theme={null}
response = client.checkout.create_session(
planIdentifier="plan-pro-monthly",
chargePeriod="MONTHLY",
customerId="cust_789",
successUrl="https://example.com/success",
discountsEnabled=False,
lockEmail=True,
defaultBillingCountry="US",
)
```
**Async variant:**
```python theme={null}
response = await async_client.checkout.create_session(
planIdentifier="plan-pro-monthly",
chargePeriod="MONTHLY",
customerId="cust_789",
successUrl="https://example.com/success",
discountsEnabled=False,
lockEmail=True,
defaultBillingCountry="US",
)
```
**v2.3.0**
### 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,
'',
);
// 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`. 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`.
**v2.3.0**
### New Features
#### Webhook Verification
A new `validate_event` 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 dictionary.
```python theme={null}
from flask import Flask, request
from kelviq_sdk import validate_event, WebhookVerificationError
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
try:
event = validate_event(
payload=request.data,
headers=request.headers,
secret='',
)
# Process the event
print('Event type:', event.get('type'))
return "", 202
except WebhookVerificationError as e:
return "", 403
```
**`validate_event(payload, headers, secret)`**
* `payload` — Raw request body as `bytes` or `str`. Must be the unparsed body — do not pass a pre-parsed dictionary.
* `headers` — Any mapping of header name to value (e.g. `request.headers` in Flask). Header lookup is case-insensitive.
* `secret` — Your webhook signing secret (`kq_whsec_...`) from the Kelviq dashboard.
Returns the parsed event as `Dict[str, Any]`. Raises `WebhookVerificationError` if a required header is missing, the signature format is invalid, or the signature does not match.
#### `WebhookVerificationError`
New exception class raised by `validate_event` when verification fails. Extends `Exception`.
**v1.3**
### New Features
* **Promotions** — A new framework for managing promotional offers and campaigns directly within the dashboard.
* **Tax Information in Transactions** — Detailed tax breakdowns are now included in transaction records, providing full transparency for accounting.
* **Zapier Integration** — Connect Kelviq with thousands of other apps. Automate your workflow by triggering actions in external tools whenever a subscription or payment event occurs.
**v2.2.0**
### 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`.
**v2.2.0**
### New Features
#### License Management Module
A new `client.license` module provides full lifecycle management for software licenses. All methods are available in both synchronous and asynchronous clients.
**`license.activate(licenseKey, customerId?, instanceName?, metadata?)`**
Activates a license key and creates a new instance. Returns a `LicenseActivateResponse` (Pydantic model) containing the `instanceId`, `activatedAt`, `expiresOn`, and the full `LicenseDetails` object.
```python theme={null}
response = client.license.activate(
licenseKey="LIC-XXXX-YYYY-ZZZZ",
customerId="cust_789",
instanceName="My MacBook Pro",
metadata={"os": "macOS", "arch": "arm64"},
)
print(response.instanceId)
print(response.license.activationUsage) # e.g. 1
print(response.license.plan.product.name) # e.g. "Kelviq Engine"
print(response.license.subscription.billingType) # "SUBSCRIPTION"
```
**`license.deactivate(licenseKey, instanceId)`**
Deactivates a specific license instance. Returns a `LicenseDeactivateResponse` with `message` and `deactivatedAt`.
```python theme={null}
response = client.license.deactivate(
licenseKey="LIC-XXXX-YYYY-ZZZZ",
instanceId="8f3e2b1a-5c6d-4e9f-8a0b-1c2d3e4f5g6h",
)
print(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`.
```python theme={null}
response = client.license.validate(
licenseKey="LIC-XXXX-YYYY-ZZZZ",
instanceId="8f3e2b1a-5c6d-4e9f-8a0b-1c2d3e4f5g6h",
)
if response.valid:
print(f"Valid — code: {response.code}")
else:
print(f"Invalid — {response.detail}")
```
#### New Pydantic Models
* **`LicenseDetails`** — Full license object with `id`, `licenseKey`, `activatedOn`, `expiresOn`, `activationUsage`, `activationLimit`, `enabled`, `customer`, `plan`, and `subscription`.
* **`LicenseCustomer`** — `customerId`, `name`, `email` nested within `LicenseDetails`.
* **`LicensePlan`** — Expanded with `description`, `version`, `isLatest`, and `product` (nested `LicensePlanProduct` model).
* **`LicensePlanProduct`** — `id`, `identifier`, `name`, `taxCode`, `createdOn`, `modifiedOn`.
* **`LicenseActivateResponse`**, **`LicenseDeactivateResponse`**, **`LicenseValidateResponse`** — Typed Pydantic response models 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 `None`.
* **`recurrenceType`** — Recurrence period in uppercase (e.g. `"MONTH"`), or `None`.
**v1.2**
### Improvements
* **Webhooks Foundation** — Added initial webhook configuration options within the settings menu to prepare for advanced automation.
* **Product UI Refinement** — The entire product card is now clickable for faster navigation, and we've added field validation for regional pricing minimum values.
**v2.1.0**
### 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.
**v2.1.0**
### 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. Available on both synchronous and asynchronous clients.
**`portal.create_session(customerId)`**
```python theme={null}
session = client.portal.create_session(customerId="cust_789")
# Redirect your customer — no login required
print(session.customerPortalUrl)
```
**Async variant:**
```python theme={null}
session = await async_client.portal.create_session(customerId="cust_789")
```
Returns `CreatePortalSessionResponse` (Pydantic model) with:
* `token` (str) — Session token authenticating the portal session.
* `email` (str) — The customer's email address.
* `customerPortalUrl` (str) — Pre-authenticated URL to redirect the customer to.
**v1.1**
### New Features
* **Multiple subscriptions per customer** — Kelviq now supports customers having multiple active subscriptions. Use this to set up add-ons, credit packs, or top-ups alongside a base plan. Our SDKs automatically aggregate entitlements across all subscriptions, so your app always sees the correct combined usage and limits.
* **New Subscriptions page** — A dedicated page to view and manage all subscriptions in one place.
* **New Payout Details page** — View detailed breakdowns for individual payouts.
* **Transactions & Details page under Payouts** — Browse all payout transactions with a detail view for each entry.
* **Discount coupons on checkout URLs** — You can now attach discount coupons directly to checkout URLs, allowing you to share pre-discounted links with customers.
* **Block size 1 for package pricing** — Package pricing now supports a block size of 1, giving you more granular control over package-based plans.
* **Updated Publish modal UI** — The publish modal has been redesigned for a cleaner, more intuitive experience.
### Improvements
* **Flat & package pricing for one-time charges** — Flat-rate and package pricing models are now available for one-time products, not just recurring plans.
* **Copy charge period on hover** — Hover over a charge period in the pricing section to quickly copy it to your clipboard.
* **Plan details page UI improvements** — The plan details page has been refreshed with a cleaner layout and better readability.
* **Improved refund error messages** — Refund errors now include clearer descriptions to help you understand what went wrong.
* **Improved Analytics UI** — The analytics dashboard has been updated with a more polished interface.
* **Download invoice in subscription detail page** — You can now download invoices directly from any subscription's detail page.
* **Cancel subscription from customer detail page** — Added the ability to cancel a customer's subscription directly from their detail page without navigating away.
### Fixes
* **Entitlements override indicator** — Fixed an issue where entitlements were incorrectly shown as "overridden" in the UI when the values hadn't actually changed during feature copying.
* **Refund button disabled when not applicable** — The refund payment button is now correctly disabled when a payment has already been refunded or the subscription is in a trial period.
* **Billing info badge for one-time orders** — Fixed the billing info badge not appearing for one-time orders in the order list and detail pages.
**v2.0.0**
### New Features
#### Hybrid Aggregation Engine
If a customer has multiple subscriptions (e.g., a base plan + a top-up), the API may return duplicate `featureId` entries. The SDK now automatically aggregates them into a single `Entitlement` object per feature — no manual merging required.
**Aggregation rules:**
| Feature Type | `hasAccess` | `currentUsage` | `usageLimit` | `hardLimit` |
| - | - | - | - | - |
| **METER** | `true` if `remaining > 0` or unlimited | Summed | Summed | `true` if any item sets it |
| **BOOLEAN** | `true` if any item grants access | `0` | `null` | `false` |
| **CUSTOMIZABLE** | `true` if any item grants access | First item's value | First item's value | First item's value |
Per-item details (like `resetAt` and individual `hardLimit` values) are available on `entitlement.items[]`.
#### `getEntitlements()` — Fetch All Entitlements
Returns all aggregated entitlements as a map keyed by `featureId`. Replaces the old `getAllEntitlements()` method.
```typescript theme={null}
const entitlements = kq.getEntitlements();
// { "email-sends": Entitlement, "api-calls": Entitlement, ... }
```
#### `getRawEntitlement(featureId)` — Raw Data for a Single Feature
Returns the un-aggregated raw API items for a specific `featureId`. Useful when you need per-subscription details.
```typescript theme={null}
const rawItems = kq.getRawEntitlement("email-sends");
// [{ featureId: 'email-sends', featureType: 'METER', hasAccess: true, ... }, ...]
```
#### `getRawEntitlements()` — Full Raw API Response
Returns the complete raw API response including the `customerId` wrapper, before any aggregation.
```typescript theme={null}
const raw = kq.getRawEntitlements();
// { customerId: 'cust_123', entitlements: [...] }
```
#### `ready()` — Wait for Initial Fetch
Returns a promise that resolves once the initial entitlement fetch completes. If `initializeAndFetch` was not set, resolves immediately.
```typescript theme={null}
const kq = kelviqSDK({
customerId: "cust_123",
accessToken: "your-access-token",
});
await kq.ready();
// Entitlements are now available
const emails = kq.getEntitlement("email-sends");
console.log(emails.hasAccess, emails.remaining);
```
#### New Type Exports
`RawEntitlement` and `RawEntitlementsApiResponse` are now exported for consumers who need to type the raw API objects in their own code.
***
### Breaking Changes
#### Unified `Entitlement` Interface
The separate `BooleanEntitlement`, `ConfigEntitlement`, and `MeteredEntitlement` types have been replaced with a single, unified `Entitlement` interface:
```typescript theme={null}
interface Entitlement {
featureId: string;
featureType: "METER" | "BOOLEAN" | "CUSTOMIZABLE";
hasAccess: boolean; // true if at least one item grants access
hardLimit: boolean; // true if any item has hardLimit set
currentUsage: number; // SUM of all items' currentUsage (METER only)
usageLimit: number | null; // SUM of all items' usageLimit (METER only)
remaining: number | null; // usageLimit - currentUsage
items: RawEntitlement[]; // Original API objects for this featureId
}
```
**Migration:** Replace all references to `BooleanEntitlement`, `ConfigEntitlement`, `MeteredEntitlement`, and `AnyEntitlement` with `Entitlement`.
#### `getEntitlement()` No Longer Accepts a Type Argument
The generic type parameter and second `type` argument have been removed. The SDK now determines the feature type automatically.
```typescript theme={null}
// Before
const emailQuota = kq.getEntitlement("email-sends", "metered");
console.log(emailQuota.used, emailQuota.limit);
// After
const emailQuota = kq.getEntitlement("email-sends");
console.log(emailQuota.currentUsage, emailQuota.usageLimit);
```
#### Renamed Fields
| Before | After | Notes |
| - | - | - |
| `used` | `currentUsage` | Aligned with the raw API field name |
| `limit` | `usageLimit` | Aligned with the raw API field name |
| `type` | `featureType` | Now uses uppercase API values (`"METER"`, `"BOOLEAN"`, `"CUSTOMIZABLE"`) |
| `configuration` | *(removed)* | No longer surfaced at the top level |
| `resetAt` | *(removed)* | Access via `entitlement.items[].resetAt` (may differ per subscription) |
#### Removed Exports
The following types are no longer exported:
* `BooleanEntitlement`, `ConfigEntitlement`, `MeteredEntitlement`, `AnyEntitlement`
* `EntitlementMap` is now `Record` (no union type)
#### `getAllEntitlements()` Removed
Use `getEntitlements()` instead, which returns the aggregated map.
***
### Changed
* **`fetchAllEntitlements()` deduplicates concurrent calls** — Instead of rejecting with "already in progress", it returns the same in-flight promise. This makes `initializeAndFetch: true` safely composable with `await fetchAllEntitlements()`.
* **`fetchAllEntitlements()` stores raw data** — Now stores both the full raw API response and the aggregated cache internally.
* **`clearCache()` clears raw data** — Now also clears the stored raw API response.
***
### Fixed
* **`initializeAndFetch: false` was ignored** — The factory used `||` instead of `??`, so passing `false` had no effect and entitlements were always fetched on initialization. Fixed to use nullish coalescing (`??`).
* **Options table in documentation** — Filled in missing type values for `accessToken` (`string`) and `onError` (`(error: Error) => void`).
**v2.0.0**
### 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
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>`.
#### `environment` Prop on `KelviqProvider`
Supports `'production'` (default) and `'sandbox'`, which selects the appropriate default API URL:
```tsx theme={null}
```
***
### 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
// After
```
#### `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 ;
console.log(data?.used);
// After
const entitlement = useMeteredEntitlement("my-feature");
const { isLoading, error } = useKelviq();
if (isLoading) return ;
console.log(entitlement?.currentUsage);
```
#### `getEntitlement()` Simplified
The generic type parameter and second argument have been removed:
```ts theme={null}
// Before
getEntitlement("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
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
**v2.0.0**
### 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
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>`.
#### `environment` Prop on `KelviqProvider`
Supports `'production'` (default) and `'sandbox'`, which selects the appropriate default API URL:
```tsx theme={null}
```
***
### 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
// After
```
#### `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 ;
console.log(data?.used);
// After
const entitlement = useMeteredEntitlement("my-feature");
const { isLoading, error } = useKelviq();
if (isLoading) return ;
console.log(entitlement?.currentUsage);
```
#### `getEntitlement()` Simplified
The generic type parameter and second argument have been removed:
```ts theme={null}
// Before
getEntitlement("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
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
**v2.0.0**
### 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` — 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.
**v2.0.0**
### New Features
#### Entitlements Aggregation Engine
When a customer has multiple active subscriptions, the API can return duplicate `featureId` entries. The SDK now automatically aggregates them into a single `Entitlement` model while preserving the raw data.
```python theme={null}
entitlement = client.entitlements.get_entitlement("cust_123", "email-sends")
# Aggregated values across all subscriptions
print(entitlement.hasAccess) # True
print(entitlement.usageLimit) # 1500 (e.g., 1000 from base + 500 from top-up)
print(entitlement.currentUsage) # 200
print(entitlement.remaining) # 1300
# Per-subscription details
for item in entitlement.items:
print(item.usageLimit, item.resetAt)
```
**Aggregation rules:**
| Field | Aggregation Strategy |
| - | - |
| `hasAccess` | `True` if **any** raw entry grants access |
| `usageLimit` | **Summed** across all entries (for `METER` feature types) |
| `currentUsage` | **Summed** across all entries (for `METER` feature types) |
| `remaining` | Computed as `usageLimit - currentUsage` |
| `hardLimit` | `True` if **any** entry has a hard limit |
> **Note:** `resetAt` is intentionally **not** aggregated at the top level because each subscription item can have a different reset date. Access individual reset dates via `entitlement.items[i].resetAt`.
#### New `Entitlement` Pydantic Model
A new model representing an aggregated entitlement:
* `featureId`, `featureType`, `hasAccess`, `hardLimit`, `usageLimit`, `currentUsage`, `remaining`
* `items: List[EntitlementDetail]` — the raw entries that were aggregated into this model
#### `get_entitlements(customerId)` — All Entitlements as a Map
Returns a `Dict[str, Entitlement]` keyed by `featureId`. Each value is an aggregated model with an `.items` list containing the original raw entries.
```python theme={null}
entitlements = client.entitlements.get_entitlements("cust_123")
for feature_id, entitlement in entitlements.items():
print(f"{feature_id}: hasAccess={entitlement.hasAccess}, remaining={entitlement.remaining}")
```
#### `get_raw_entitlement(customerId, featureId)` — Raw API Response for a Feature
Returns the raw API response (`CheckEntitlementsResponse`) for a specific feature without any aggregation.
```python theme={null}
raw = client.entitlements.get_raw_entitlement("cust_123", "email-sends")
```
#### `get_raw_entitlements(customerId)` — Full Raw API Response
Returns the raw API response for all entitlements without any aggregation.
```python theme={null}
raw = client.entitlements.get_raw_entitlements("cust_123")
```
#### Enum Helpers for IDE Autocompletion
New `str`-based `Enum` classes:
* **`ChargePeriod`** — `ONE_TIME`, `MONTHLY`, `YEARLY`, `WEEKLY`, `DAILY`, `THREE_MONTHS`, `SIX_MONTHS`
* **`CancellationType`** — `IMMEDIATE`, `CURRENT_PERIOD_ENDS`, `SPECIFIC_DATE`
```python theme={null}
from kelviq_sdk import ChargePeriod, CancellationType
response = client.checkout.create_session(
planIdentifier="pro-plan",
chargePeriod=ChargePeriod.MONTHLY,
successUrl="https://example.com/success"
)
```
Raw string values (e.g., `"MONTHLY"`) continue to work everywhere — fully backward-compatible.
***
### Changed
* **`get_entitlement(customerId, featureId)`** now returns `Optional[Entitlement]` (aggregated) instead of raw `CheckEntitlementsResponse`. Use `get_raw_entitlement()` for the original response.
* **`get_all_entitlements` removed** — Use `get_entitlements()` instead.
* **`resetAt` removed from top-level `Entitlement`** — Access individual reset dates via the `.items` list.
**v1.1.0**
### New Features
#### Entitlements Aggregation Engine
When a customer has multiple active subscriptions, the API can return duplicate `featureId` entries. The SDK now automatically aggregates them into a single `Entitlement` model while preserving the raw data.
```python theme={null}
entitlement = client.entitlements.get_entitlement("cust_123", "email-sends")
# Aggregated values across all subscriptions
print(entitlement.has_access) # True
print(entitlement.usage_limit) # 1500 (e.g., 1000 from base + 500 from top-up)
print(entitlement.current_usage) # 200
print(entitlement.remaining) # 1300
# Per-subscription details
for item in entitlement.items:
print(item.usage_limit, item.reset_at)
```
**Aggregation rules:**
| Field | Aggregation Strategy |
| - | - |
| `hasAccess` | `True` if **any** raw entry grants access |
| `usageLimit` | **Summed** across all entries (for `METER` feature types) |
| `currentUsage` | **Summed** across all entries (for `METER` feature types) |
| `remaining` | Computed as `usageLimit - currentUsage` |
| `hardLimit` | `True` if **any** entry has a hard limit |
> **Note:** `resetAt` is intentionally **not** aggregated at the top level because each subscription item can have a different reset date. Access individual reset dates via `entitlement.items[i].resetAt`.
#### New `Entitlement` Pydantic Model
A new model representing an aggregated entitlement:
* `featureId`, `featureType`, `hasAccess`, `hardLimit`, `usageLimit`, `currentUsage`, `remaining`
* `items: List[EntitlementDetail]` — the raw entries that were aggregated into this model
#### `get_entitlements(customerId)` — All Entitlements as a Map
Returns a `Dict[str, Entitlement]` keyed by `featureId`. Each value is an aggregated model with an `.items` list containing the original raw entries.
```python theme={null}
entitlements = client.entitlements.get_entitlements("cust_123")
for feature_id, entitlement in entitlements.items():
print(f"{feature_id}: hasAccess={entitlement.has_access}, remaining={entitlement.remaining}")
```
#### `get_raw_entitlement(customerId, featureId)` — Raw API Response for a Feature
Returns the raw API response (`CheckEntitlementsResponse`) for a specific feature without any aggregation. Includes the `customerId` wrapper.
```python theme={null}
raw = client.entitlements.get_raw_entitlement("cust_123", "email-sends")
# CheckEntitlementsResponse with customerId wrapper
```
#### `get_raw_entitlements(customerId)` — Full Raw API Response
Returns the raw API response for all entitlements without any aggregation.
```python theme={null}
raw = client.entitlements.get_raw_entitlements("cust_123")
# CheckEntitlementsResponse with customerId wrapper and all raw entries
```
#### Enum Helpers for IDE Autocompletion
New `str`-based `Enum` classes that improve developer experience with IDE autocompletion and prevent typos:
* **`ChargePeriod`** — `ONE_TIME`, `MONTHLY`, `YEARLY`, `WEEKLY`, `DAILY`, `THREE_MONTHS`, `SIX_MONTHS`
* **`CancellationType`** — `IMMEDIATE`, `CURRENT_PERIOD_ENDS`, `SPECIFIC_DATE`
```python theme={null}
from kelviq_sdk import ChargePeriod, CancellationType
# With enums — IDE autocompletion and typo prevention
response = client.checkout.create_session(
planIdentifier="pro-plan",
chargePeriod=ChargePeriod.MONTHLY,
successUrl="https://example.com/success"
)
# Cancel with enum
client.subscriptions.cancel(
subscriptionId="sub_123",
cancellationType=CancellationType.CURRENT_PERIOD_ENDS,
)
```
Raw string values (e.g., `"MONTHLY"`) continue to work everywhere — the enums are fully backward-compatible.
***
### Changed
* **`get_entitlement(customerId, featureId)`** now returns `Optional[Entitlement]` (aggregated) instead of raw `CheckEntitlementsResponse`. Use `get_raw_entitlement()` if you need the original response.
* **`get_all_entitlements` removed** — Use `get_entitlements()` instead, which returns a `Dict[str, Entitlement]` keyed by `featureId`.
* **`resetAt` removed from top-level `Entitlement`** — Access individual reset dates via the `.items` list.
***
### Fixed
* Removed invalid JavaScript-style comments (`// Server-generated UUID`) from JSON response blocks in documentation.
* Fixed `//` comments to `#` comments in Python code snippets.
* Fixed trailing comma in Checkout response JSON block.
* Fixed typo: "newly created customer" → "**updated** customer" in `customers.update` return description.
* Removed placeholder text from "Supported Functionalities" section.
* Updated entitlements description to explain multi-subscription aggregation and the `items` attribute.
# Apply a discount code at checkout
Source: https://docs.kelviq.com/checkout/apply-a-discount-code
Let customers enter a discount code or pre-apply one to a Kelviq checkout
Kelviq can show a discount-code field at checkout or apply a known code before the checkout page opens. Use the first option for general promotions and the second for targeted campaigns, partner offers, or customer-specific links.
## Before you start
Create the code in **Product Catalog → Discounts** and check that it:
* is active and has not expired;
* has not reached its redemption limit;
* applies to the selected product and plan; and
* uses the same currency as the price when it is a fixed-amount discount.
[Learn how to create and restrict a discount →](/product-catalog/discounts)
## Let the customer enter a code
When creating a checkout session through the API, set `discountsEnabled` to `true`. This shows the discount-code field so the customer can enter an eligible code.
```json theme={null}
{
"planIdentifier": "pro",
"chargePeriod": "MONTHLY",
"customerId": "cust_789",
"successUrl": "https://example.com/checkout/success",
"discountsEnabled": true
}
```
`discountsEnabled` defaults to `true`. Set it to `false` only when you want to hide discount-code entry for that session.
## Pre-apply a code with the Checkout API
Pass the code as `discountCode` in the JSON request body. Kelviq validates it while creating the checkout session. If the code is inactive, expired, or not valid for the selected plan, the request fails instead of creating a session with the wrong price.
```bash cURL theme={null}
curl --request POST \
--url https://sandboxapi.kelviq.com/api/v1/checkout/ \
--header "Authorization: Bearer $KELVIQ_SANDBOX_SERVER_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"planIdentifier": "pro",
"chargePeriod": "MONTHLY",
"customerId": "cust_789",
"successUrl": "https://example.com/checkout/success",
"discountsEnabled": true,
"discountCode": "LAUNCH20"
}'
```
```javascript Node.js theme={null}
const response = await fetch("https://sandboxapi.kelviq.com/api/v1/checkout/", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.KELVIQ_SANDBOX_SERVER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
planIdentifier: "pro",
chargePeriod: "MONTHLY",
customerId: "cust_789",
successUrl: "https://example.com/checkout/success",
discountsEnabled: true,
discountCode: "LAUNCH20",
}),
});
if (!response.ok) {
throw new Error(await response.text());
}
const { checkoutUrl } = await response.json();
```
```python Python theme={null}
import os
import requests
response = requests.post(
"https://sandboxapi.kelviq.com/api/v1/checkout/",
headers={
"Authorization": f"Bearer {os.environ['KELVIQ_SANDBOX_SERVER_API_KEY']}",
"Content-Type": "application/json",
},
json={
"planIdentifier": "pro",
"chargePeriod": "MONTHLY",
"customerId": "cust_789",
"successUrl": "https://example.com/checkout/success",
"discountsEnabled": True,
"discountCode": "LAUNCH20",
},
)
response.raise_for_status()
checkout_url = response.json()["checkoutUrl"]
```
A successful request returns the hosted checkout URL:
```json theme={null}
{
"checkoutUrl": "https://kelviq.com/checkout/cs_eeCGdGayc5oTMdGNV5XocfRk6o87K0vlwwCuKeiE7BLU9/",
"checkoutSessionId": "cs_eeCGdGayc5oTMdGNV5XocfRk6o87K0vlwwCuKeiE7BLU9"
}
```
Redirect the customer to `checkoutUrl`. Before completing payment, confirm that checkout shows the code, adjusted price, currency, and billing period you expect.
## Pre-apply a code in a static checkout link
For a static product checkout link, use the snake-case `discount_code` query parameter:
```text theme={null}
https://www.kelviq.com/buy/123e4567-e89b-12d3-a456-426614174000/?plan_identifier=pro&charge_period=MONTHLY&discounts_enabled=true&discount_code=LAUNCH20
```
Use the product UUID shown in the Kelviq dashboard. URL-encode the code if it contains spaces or reserved characters.
> [!NOTE]
>
> The names are different by design: use `discountCode` in an API JSON body and `discount_code` in a static checkout URL.
[See all static checkout URL parameters →](/checkout/static-checkout-url)
## Test the integration
Test the complete flow in sandbox before using a production code:
1. Create a sandbox discount and an eligible product price.
2. Create the checkout session with the matching sandbox API key.
3. Open the returned checkout URL.
4. Confirm the original price, discount, tax, and final total.
5. Complete payment with a sandbox payment method and check the resulting order.
Do not test a small live charge on your own card. Repeated self-payments can cause the card to be blocked. Use sandbox, or use a valid 100% discount when you must inspect the production checkout without collecting a real payment.
## Troubleshooting
| Problem | Check |
| - | - |
| Checkout creation fails | Confirm the code is active, unexpired, within its redemption limit, and valid for the selected plan. |
| Fixed discount is rejected | Confirm the discount currency matches the selected price currency. |
| The code field is missing | Set `discountsEnabled` or `discounts_enabled` to `true`, depending on how you create checkout. |
| The code is not pre-applied | Use `discountCode` for the API or `discount_code` for a static link. |
| Sandbox code is not found in production | Recreate the discount in production; the two environments do not share data. |
For the complete request and response schema, see [Create a checkout session API reference](/api-reference/checkout/create-a-checkout-session).
# Configuration & Customization
Source: https://docs.kelviq.com/checkout/checkout-configuration
Configure and customize checkout experiences for your customers
Checkout in Kelviq allows you to collect payments from customers. You can generate checkout links for your products, customize the checkout experience, and support multiple payment options and billing periods.
## Overview
Kelviq's checkout system:
* **Powers your sales** - Collect payments and create customer subscriptions
* **Many payment methods** - Cards, wallets, bank payments, and local methods
* **Highly configurable** - Control plans, pricing, trial periods, and more
* **Multi-currency ready** - Support customers worldwide
* **Easy to embed** - Generate links for any product and plans
## Generating Checkout Links
Checkout links are generated at the **product level**. Each link can include one or more plans.

### Quick Start
1. **Go to Products** → Select your product
2. **Click "Get Link"** → Opens the link builder
3. **Select plans** → Choose which plans to include
4. **Configure options** → Set defaults and features
5. **Copy link** → Share with customers
### Link Format
A production checkout link starts with your product UUID:
```
https://www.kelviq.com/buy/{PRODUCT_UUID}/
```
Sandbox links use `https://www.kelviq.com/sandbox/buy/{PRODUCT_UUID}/`. Copy the link from the dashboard so the environment and product UUID are correct.
Add query parameters to control what the customer sees:
| Parameter | What it controls | Example |
| - | - | - |
| `enabled` | Comma-separated plan identifiers shown at checkout | `basic,pro` |
| `plan_identifier` | Plan selected when checkout opens | `pro` |
| `charge_period` | Billing period selected when checkout opens | `MONTHLY` |
| `discounts_enabled` | Whether the discount-code field is available | `true` |
| `discount_code` | A valid discount code to apply automatically | `LAUNCH20` |
| `customer_id` | Customer record associated with checkout | `cust_789` |
| `success_url` | Where to send the customer after a successful checkout | `https://example.com/checkout/success` |
For example:
```text theme={null}
https://www.kelviq.com/buy/123e4567-e89b-12d3-a456-426614174000/?enabled=basic,pro&plan_identifier=pro&charge_period=MONTHLY&discounts_enabled=true&discount_code=LAUNCH20&customer_id=cust_789&success_url=https%3A%2F%2Fexample.com%2Fcheckout%2Fsuccess
```
URL-encode `success_url` and any other parameter value that contains reserved characters.
[See every supported static checkout parameter and complete examples →](/checkout/static-checkout-url)
## Multi-Plan Checkout
Include multiple plans in a single checkout to let customers choose their tier.
### Configuration
> **Plans Configuration**
>
> * Select which plans are enabled (shown to customer)
> * Choose a recommended plan (highlighted by default)
> * Cannot disable all plans
> * Disabled plans don't appear in checkout
### Example: Multi-Plan Pricing
Your product has 3 plans: Free, Pro, Enterprise
```
Plan 1: Free
├─ Enabled: Yes
├─ Price: $0/month
└─ Show in checkout: Yes
Plan 2: Pro (Recommended)
├─ Enabled: Yes
├─ Price: $29/month
└─ Show in checkout: Yes (selected by default)
Plan 3: Enterprise
├─ Enabled: Yes
├─ Price: Custom quote
└─ Show in checkout: Yes
```
Generated checkout URL:
```
?enabled=free,pro,enterprise&plan_identifier=pro
```
## Billing Periods & Pricing
### Supported Billing Periods
Plans in Kelviq can support multiple billing periods:
| Period | Duration | Use Case |
| - | - | - |
| **Monthly** | 1 month | Recurring subscriptions |
| **3 Months** | 3 months | Quarterly billing |
| **6 Months** | 6 months | Biannual billing |
| **Yearly** | 12 months | Annual billing, often with discounts |
| **One Time** | One-off payment | Digital products, courses, lifetime access |
> [!TIP]
>
> We can also add custom billing periods such as weekly, daily, or 28 days upon request. Contact [hi@kelviq.com](mailto:hi@kelviq.com) to enable these options.
### Setting Default Billing Period
Configure which billing period is pre-selected for each plan:
```
Plan: Pro
├─ Monthly: $29/month (default for checkout)
├─ Yearly: $290/year (saves $58)
└─ Checkout pre-selects: Monthly
```
In your checkout link:
```
&charge_period=MONTHLY
```
### Pricing by Period
Different plans can have different pricing for the same period:
```
Plan: Basic
├─ Monthly: $9/month
└─ Yearly: $90/year
Plan: Pro
├─ Monthly: $29/month
└─ Yearly: $290/year (17% discount vs monthly)
Plan: Enterprise
├─ Monthly: $99/month
└─ Yearly: $999/year (16% discount vs monthly)
```
Customers see all options and can choose their preferred period at checkout.
## Free Trials
Offer free trial periods before charging customers.
### Configuring Trials
1. **Open your plan** → Go to the plan's pricing section
2. **Enable Free Trial** → Toggle the trial option
3. **Set duration** → 1-365 days (default 7 days)
4. **Save plan**
### Trial Behavior
When a customer starts a trial:
```
Timeline:
Day 0: Customer signs up, trial begins
Days 1-30: Customer has full access, no charge
Day 30: Trial ends, first billing occurs
Day 31+: Regular subscription
```
### Trial Configuration Example
```
Plan: Professional
├─ Regular price: $29/month
├─ Free trial: 30 days
├─ After trial: Full charge of $29
└─ Customer can cancel anytime during trial
```
> [!TIP]
>
> Trials reduce payment friction. Customers are more likely to convert from trial to paid if they've experienced value.
## Advanced Checkout Configuration
### Success and Cancellation URLs
The success URL is optional when you link to or embed Kelviq Checkout directly. Add it only when you want Kelviq to redirect the customer to your own confirmation or onboarding page after checkout succeeds:
```
Customer completes purchase
↓
Kelviq redirects to your success page
↓
You can:
- Show confirmation
- Deliver files/licenses
- Set up their account
- Continue onboarding
```
For a static checkout link, add the URL-encoded `success_url` parameter when you want this redirect:
```text theme={null}
&success_url=https%3A%2F%2Fyourdomain.com%2Fcheckout-success
```
When creating a checkout session through the API, the `successUrl` field is required. You can also optionally provide a `cancelUrl` to redirect the customer if they cancel payment.
### Checkout Session Options
`POST /api/v1/checkout/` accepts options that let you tailor a checkout session without changing the underlying plan.
| Field | What it does |
| - | - |
| `successUrl` | Redirects the customer after a successful checkout |
| `cancelUrl` | Redirects the customer if payment is cancelled |
| `plansEnabled` | Limits checkout to a comma-separated list of plan identifiers |
| `discountsEnabled` | Shows or hides discount-code entry; defaults to `true` |
| `discountCode` | Applies a valid discount code before checkout opens |
| `lockEmail` | Prevents the customer from changing the pre-filled email address |
| `defaultBillingCountry` | Pre-fills the billing country using a two-letter ISO country code |
| `currencyCode`, `customAmount`, `taxBehavior` | Sets a custom charge and its tax treatment |
| `metadata` | Attaches your own key-value data to the checkout session |
For a complete request, webhook example, and implementation guidance, see [Add metadata to checkout](/checkout/checkout-metadata).
```javascript theme={null}
const response = await fetch("https://api.kelviq.com/api/v1/checkout/", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.KELVIQ_SERVER_API_KEY}`,
},
body: JSON.stringify({
planIdentifier: "pro",
chargePeriod: "MONTHLY",
customerId: "customer-123",
successUrl: "https://example.com/checkout/success",
cancelUrl: "https://example.com/pricing",
plansEnabled: "basic,pro",
discountsEnabled: true,
discountCode: "LAUNCH20",
lockEmail: true,
defaultBillingCountry: "US",
}),
});
const { checkoutUrl } = await response.json();
```
### Discount Code Integration
Allow customers to apply discount codes at checkout:

```
Customer at checkout:
1. Select plan and period
2. (Optional) Enter discount code
3. See adjusted price
4. Complete payment
```
Discount codes are applied automatically if the customer enters a valid code.
[See API and static-link examples for applying a discount code →](/checkout/apply-a-discount-code)
### Tax Collection
Kelviq can collect and handle taxes:
**Tax ID Collection:**
* Customers can enter their tax ID (VAT number, EIN, etc.)
* Used to determine applicable taxes
* Required for some jurisdictions
**Configuration:**
* Enable/disable tax collection per product
* Taxes are calculated based on customer location and tax ID
### Country Restrictions
Restrict plans to specific countries:
```
Plan: Premium Tier
├─ Available countries: US, UK, CA, AU
├─ Restricted countries: China, Russia
└─ Checkout effect: Plan hidden for customers in restricted countries
```
## Checkout session lifecycle
Each checkout attempt is a session with one of five statuses: **Open**, **Processing**, **Success**, **Closed**, or **Expired**. An unpaid session expires four days after it's created. [Checkout sessions](/sales/checkout-sessions) explains each status and how to read a session's timeline in the dashboard.
### Create Checkout Sessions via API
You can programmatically create checkout sessions using the SDK:
**Python**
```python theme={null}
from kelviq_sdk import Kelviq
client = Kelviq.create_sync_client(access_token="your_server_api_key")
# Create a checkout session
response = client.checkout.create_session(
planIdentifier="plan-pro-monthly",
chargePeriod="MONTHLY",
customerId="cust_123",
successUrl="https://yoursite.com/checkout/success"
)
print(f"Checkout URL: {response.checkoutUrl}")
print(f"Session ID: {response.checkoutSessionId}")
```
**Node.js**
```typescript theme={null}
import { Kelviq, CHARGE_PERIOD_CHOICES } from '@kelviq/node-sdk';
const client = new Kelviq({ accessToken: 'your_server_api_key' });
// Create a checkout session
const response = await client.checkout.createSession({
planIdentifier: "plan-pro-monthly",
chargePeriod: CHARGE_PERIOD_CHOICES.MONTHLY,
customerId: "cust_123",
successUrl: "https://yoursite.com/checkout/success"
});
console.log(`Checkout URL: ${response.checkoutUrl}`);
console.log(`Session ID: ${response.checkoutSessionId}`);
```
## Checkout Flow Examples
### Example 1: Simple Product
One plan, one price:
```
Product: My Course
├─ Plan: Full Access ($49)
└─ Billing: One-time payment
Link: ?enabled=full_access&plan_identifier=full_access&charge_period=ONE_TIME
```
### Example 2: Tiered Plans
Multiple plans, multiple billing periods:
```
Product: Pro Templates
├─ Plan 1: Starter ($9/month or $90/year)
├─ Plan 2: Professional ($29/month or $290/year)
└─ Plan 3: Enterprise (Custom pricing)
Link: ?enabled=starter,pro,enterprise
&plan_identifier=pro
&charge_period=MONTHLY
```
### Example 3: Free + Paid
Offer free plan with paid upgrades:
```
Product: Productivity App
├─ Plan 1: Free (no payment)
└─ Plan 2: Premium ($9.99/month with 30-day trial)
Link: ?enabled=free,premium
&plan_identifier=free
&charge_period=MONTHLY
```
## Payment Methods
Checkout shows the payment methods available for the customer's location and currency: cards, wallets such as Apple Pay and Google Pay, bank payments, and local methods. Cards work everywhere. See [Payment methods](/checkout/payment-methods) for the full list.
### Does Kelviq support UPI AutoPay?
Yes. Kelviq supports UPI AutoPay for recurring subscriptions. Eligible customers in India can authorize the recurring payment at checkout. Availability depends on support from the customer's bank and UPI app.
## Currency & Localization
### Multi-Currency Support
Prices are stored per currency. When customers checkout:
```
Customer from EU viewing checkout:
- Product prices shown in EUR
- Customer sees "29,99 EUR"
Customer from US viewing same product:
- Product prices shown in USD
- Customer sees "$29.99"
```
### Pricing Configuration
Set prices for each currency you support:
```
Plan: Pro
├─ USD: $29/month
├─ EUR: €27/month
├─ GBP: £25/month
└─ AUD: A$45/month
```
### Regional Pricing
Offer different prices based on customer location (PPP - Purchasing Power Parity):
```
Plan: Professional ($29 in US)
├─ India: ₹599 (auto-adjusted for PPP)
├─ Brazil: R$99
└─ US: $29
```
## Testing Checkout
### Sandbox Testing
Test checkouts without processing real payments:
1. **Use a test card** → Use 4242 4242 4242 4242
2. **Test various scenarios** → Successful payment, declined card, etc.
3. **Monitor test checkouts** → View in your dashboard
### Test Card Examples
| Card | Result |
| - | - |
| 4242 4242 4242 4242 | Successful payment |
| 4000 0000 0000 0002 | Card declined |
| 4000 0025 0000 3155 | Requires authentication |
# Checkout design
Source: https://docs.kelviq.com/checkout/checkout-design
Match checkout to your brand with your logo, colors, and the sections you want customers to see
**Settings → Checkout design** controls how every checkout for your business looks. The preview on the right updates as you make changes, and the changes apply to all checkout links and checkout sessions when you save.

## Branding
| Setting | What it changes |
| - | - |
| Business name | The name shown at the top of checkout |
| Logo | Shown at the top of checkout. Upload a **Dark mode logo** if your logo doesn't read well on a dark background |
| Logo height | Size of the logo, in pixels |
| Favicon | The icon in the browser tab |
| Background image | An image behind the checkout page |
## Theme and colors
Checkout starts on the **Kelviq** theme. Change any color and it becomes **Custom**.
Light and dark mode each have their own set of colors. Under **Editing**, pick which one you're changing. This only affects the editor, not what customers see.


| Group | Colors |
| - | - |
| Brand | Primary (buttons and the selected plan), Primary text (text on buttons) |
| Backgrounds | Product panel, Payment panel, Raised surface, Selected plan, Subtle fill, Billing period track |
| Inputs | Input background, Focus ring, Focus glow |
| Text | Text, Muted text, Placeholder text, Disabled text |
| Borders | Input border, Dividers, Plan card border |
| Status | Error, Success, Warning |
> [!TIP]
>
> Keep enough contrast between **Primary** and **Primary text**. The button label sits on the primary color.
## Shape
Set how rounded cards, buttons, and inputs are. Tick **Link card, button, and input radius** to use one value for all three, or untick it to set **Corner radius**, **Button radius**, and **Input radius** separately.
## Default mode
Choose how checkout opens: **Match buyer's device**, **Light**, or **Dark**. Customers can still switch modes themselves from the corner of the page.
## Choose what customers see
Under **Visibility**, turn sections of checkout on or off. Everything is on by default except **Metered features**.

| Section | Shows |
| - | - |
| Logo | Your logo at the top |
| Heading | The product name |
| Product description | The text under the product image |
| Product images | The images you added to the product |
| Plan selector | The list of plans and the billing-period tabs |
| Metered features | Usage included in the plan and its usage rates, such as credits and the price per 1,000 after that |
| Included features | The plan's features and limits |
| Discount code field | The **Add promotion code** field |
| Price breakdown | Subtotal, discount, and tax lines above the total |
> [!NOTE]
>
> Hiding the plan selector only hides the list. Checkout still uses the plan from the link. To hide the discount field for one link rather than every checkout, add `discounts_enabled=false` to the [checkout URL](/checkout/static-checkout-url).
## Related documentation
* [Checkout examples](/checkout/checkout-examples)
* [Customize a static checkout URL](/checkout/static-checkout-url)
* [Products](/product-catalog/products)
# Checkout examples
Source: https://docs.kelviq.com/checkout/checkout-examples
See how checkout looks for subscriptions, free plans, trials, one-time payments, usage-based plans, and discounts
Every example is a live sandbox checkout. To try one, pay with the test card `4242 4242 4242 4242`, any future expiry date, and any CVC.
The examples use the default look. You can change the logo, colors, and which sections appear in [Checkout design](/checkout/checkout-design).
| Example | What changes at checkout | Button |
| - | - | - |
| [Plan picker](#plan-picker) | Customer chooses from several plans | **Subscribe** |
| [Single plan](#single-plan) | Price shown at the top, no plan list | **Subscribe** |
| [Free plan](#free-plan) | Name and email only, no payment fields | **Get it for Free** |
| [Free trial](#free-trial) | Shows the first charge date, nothing due today | **Start 7-day Trial** |
| [More billing periods](#more-billing-periods) | Extra tabs for 3 and 6 months | **Subscribe** |
| [One-time payment](#one-time-payment) | No billing period, one charge | **Pay Now** |
| [Included usage with overage](#included-usage-with-overage) | Base fee today, overage billed later | **Subscribe** |
| [Pay as you go](#pay-as-you-go) | \$0 today, usage billed monthly | **Subscribe** |
| [Prepaid credit pack](#prepaid-credit-pack) | One-time top-up of usage | **Pay Now** |
| [Discount applied](#discount-applied) | Code filled in and deducted | **Subscribe** |
| [Discount field hidden](#discount-field-hidden) | No promotion code entry | **Subscribe** |
| [Per-seat plan](#per-seat-plan) | Seat count and total for the team | **Start 14-day Trial** |
| [Prefilled customer](#prefilled-customer) | Name and email already filled in | **Subscribe** |
| [Business purchase](#business-purchase) | Company name, address, and tax ID fields | **Subscribe** |
## Light and dark mode
By default, checkout follows the light or dark setting on the customer's device, and the customer can switch it from the bottom-left corner. To always show light or dark, change the mode in [Checkout design](/checkout/checkout-design#default-mode). The other screenshots on this page match the theme you're reading in.
**Light**

**Dark**

## Plan picker
List several plans in the link and checkout shows them as a list the customer can choose from. You also pick which plan is selected when the page opens. This is the usual link behind a **Buy** button on a pricing page.

[See example →](https://www.kelviq.com/sandbox/buy/f60cea73-579b-4932-8d85-02b79710e9dc/?enabled=voice-starter,voice-creator,voice-pro,voice-scale,voice-business\&plan_identifier=voice-creator\&charge_period=MONTHLY)
## Single plan
Put only one plan in the link and checkout shows just that plan, already selected. Use this when the customer has picked a plan on your site.

[See example →](https://www.kelviq.com/sandbox/buy/f60cea73-579b-4932-8d85-02b79710e9dc/?enabled=voice-starter\&plan_identifier=voice-starter\&charge_period=MONTHLY)
## Free plan
A [free plan](/product-catalog/free-and-trials#free-plans) asks for a name and email only. There are no card or address fields, and the button reads **Get it for Free**. The customer still gets a subscription and the plan's entitlements.

[See example →](https://www.kelviq.com/sandbox/buy/574a07ca-b9ef-4700-9907-deb64bd7f7e3/?enabled=ctx-free\&plan_identifier=ctx-free\&charge_period=MONTHLY)
## Free trial
When the plan has a [trial period](/product-catalog/free-and-trials), checkout collects the card but charges nothing today. The summary shows **Total after trial** and **Total due today**, and the text under the button gives the date of the first charge.

[See example →](https://www.kelviq.com/sandbox/buy/dfb9c6d9-d195-4176-8bf3-036e42345c3d/?enabled=sp-premium\&plan_identifier=sp-premium\&charge_period=MONTHLY)
The trial comes from the plan settings, not from the link.
The price is in rupees because this plan uses [location-based pricing](/product-catalog/location-based-pricing) and the screenshot was taken from India.
## More billing periods
A plan can be sold monthly, every 3 months, every 6 months, and yearly. Checkout shows a tab for each period the plan has a price for, and the link decides which tab opens first.

[See example →](https://www.kelviq.com/sandbox/buy/dfb9c6d9-d195-4176-8bf3-036e42345c3d/?enabled=sp-premium\&plan_identifier=sp-premium\&charge_period=THREE_MONTHS)
## One-time payment
For a [one-time](/product-catalog/one-time-payments) plan there's no billing-period toggle, the summary says **One-time payment**, and the button reads **Pay Now**.

[See example →](https://www.kelviq.com/sandbox/buy/83cd7be6-8459-4099-bee7-47bbf74c6813/?enabled=pg-hobby\&plan_identifier=pg-hobby\&charge_period=ONE_TIME)
The PixelGallery plans issue a [license key](/product-catalog/license-keys) with each order. For an ebook or course, attach [digital downloads](/product-catalog/digital-downloads) to the plan instead.
You can offer several one-time plans on one page, the same way as subscriptions. This example offers an ebook or a bundle, with the bundle selected:

[See example →](https://www.kelviq.com/sandbox/buy/9b6bc03c-8899-4bfc-b435-fbf7ca855741/?enabled=ip-ebook,ip-complete\&plan_identifier=ip-complete\&charge_period=ONE_TIME)
## Included usage with overage
The Context API Developer plan costs \$19 a month, includes 7,500 credits, and charges \$2.20 per 1,000 credits after that. Checkout charges the \$19 base fee. Overage is [billed at the end of each monthly period](/product-catalog/usage-based-billing) from the usage your backend reports.

[See example →](https://www.kelviq.com/sandbox/buy/574a07ca-b9ef-4700-9907-deb64bd7f7e3/?enabled=ctx-developer\&plan_identifier=ctx-developer\&charge_period=MONTHLY)
## Pay as you go
A [pay-as-you-go](/product-catalog/pay-as-you-go) plan with no base price opens at \$0 a month. Nothing is charged at checkout. The token rates are listed under what's included, and Kelviq bills the recorded usage at the end of each month.

[See example →](https://www.kelviq.com/sandbox/buy/47236d49-8477-4483-9b8e-a1b35f1886e2/?enabled=tf-payg\&plan_identifier=tf-payg\&charge_period=MONTHLY)
## Prepaid credit pack
Sell usage upfront with a one-time plan that grants a fixed amount of a Meter feature. This pack adds 5M tokens for \$10.

[See example →](https://www.kelviq.com/sandbox/buy/47236d49-8477-4483-9b8e-a1b35f1886e2/?enabled=tf-pack-10\&plan_identifier=tf-pack-10\&charge_period=ONE_TIME)
## Discount applied
A link can carry a discount code, so the customer doesn't have to type it. Checkout fills in the code and shows the discount and the reduced total. Here the link adds `discount_code=VOICE50`, which takes 50% off the first month of Creator. Use it in campaign emails and partner links.

[See example →](https://www.kelviq.com/sandbox/buy/f60cea73-579b-4932-8d85-02b79710e9dc/?enabled=voice-creator\&plan_identifier=voice-creator\&charge_period=MONTHLY\&discounts_enabled=true\&discount_code=VOICE50)
The code must be active and valid for the plan and currency. [How checkout discounts work →](/checkout/apply-a-discount-code)
## Discount field hidden
You can also remove the **Add promotion code** field, so customers aren't tempted to leave and search for a code. This link adds `discounts_enabled=false`.

[See example →](https://www.kelviq.com/sandbox/buy/f60cea73-579b-4932-8d85-02b79710e9dc/?enabled=voice-creator\&plan_identifier=voice-creator\&charge_period=MONTHLY\&discounts_enabled=false)
## Per-seat plan
A per-seat plan charges for each seat the customer buys. The seat count comes from your app, so this checkout is created with the [Checkout API](/api-reference/checkout/create-a-checkout-session) rather than a static link. This example offers TeamBoard Basic and Business for 5 seats, with Business selected: \$16 a seat, \$80 a month, after a 14-day trial.

```json theme={null}
{
"planIdentifier": "tb-business",
"plansEnabled": "tb-basic,tb-business",
"chargePeriod": "MONTHLY",
"features": [{ "identifier": "tb-seats", "quantity": 5 }],
"successUrl": "https://example.com/welcome"
}
```
The response has a `checkoutUrl` to send the customer to. See [Seat-based pricing](/product-catalog/seat-based-pricing) for how to set up the plan.
## Prefilled customer
Add `customer_id` to the link for a customer your app already knows, and checkout fills in their name and email. The purchase is attached to that customer record.

[See example →](https://www.kelviq.com/sandbox/buy/f60cea73-579b-4932-8d85-02b79710e9dc/?enabled=voice-pro\&plan_identifier=voice-pro\&charge_period=MONTHLY\&customer_id=cust_demo_jane)
## Business purchase
Every checkout has an **I'm purchasing as a business** box. Ticking it adds fields for the company name, address, and tax ID, which then appear on the invoice.

[See example →](https://www.kelviq.com/sandbox/buy/f60cea73-579b-4932-8d85-02b79710e9dc/?enabled=voice-pro\&plan_identifier=voice-pro\&charge_period=MONTHLY)
## Redirect after payment
To send the customer back to your app after they pay, add `success_url` with the address of your page. Checkout itself looks the same. This example sends the customer to `https://example.com/welcome`.
[See example →](https://www.kelviq.com/sandbox/buy/f60cea73-579b-4932-8d85-02b79710e9dc/?enabled=voice-pro\&plan_identifier=voice-pro\&charge_period=MONTHLY\&success_url=https%3A%2F%2Fexample.com%2Fwelcome)
## Related documentation
* [Customize a static checkout URL](/checkout/static-checkout-url)
* [Checkout configuration](/checkout/checkout-configuration)
* [Free plans and trials](/product-catalog/free-and-trials)
* [Create a checkout session with the API](/api-reference/checkout/create-a-checkout-session)
# Add metadata to checkout
Source: https://docs.kelviq.com/checkout/checkout-metadata
Attach your own IDs and context to a checkout session, then read them from the checkout.completed webhook
Metadata lets you carry context from your application through a Kelviq checkout. Add an object when you create the session, and Kelviq returns the same object in the `checkout.completed` webhook.
A common example is an order reference:
```json theme={null}
{
"metadata": {
"order_ref": "ABC-123",
"workspace_id": "ws_acme",
"source": "pricing_page"
}
}
```
Use metadata for values that help your backend connect a completed checkout to its own records, such as an internal order ID, workspace ID, campaign, or signup source.
> [!WARNING]
>
> Do not put API keys, passwords, card details, or other sensitive information in metadata. Metadata is returned in webhook payloads and may appear in operational logs.
## Before you start
You need:
* a server API key from **Settings → API keys**;
* a plan identifier and supported charge period;
* a backend endpoint that creates checkout sessions.
A webhook endpoint is optional. Subscribe it to `checkout.completed` if you want to receive the metadata when checkout finishes.
Keep the server API key in your backend. Never expose it in browser or mobile code.
## Add metadata when creating the session
Pass `metadata` at the top level of the Checkout API request body. Kelviq preserves the keys exactly as supplied, so `order_ref` and `orderRef` are different keys.
```bash cURL theme={null}
curl --request POST \
--url https://sandboxapi.kelviq.com/api/v1/checkout/ \
--header "Authorization: Bearer $KELVIQ_SANDBOX_SERVER_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"planIdentifier": "pro",
"chargePeriod": "MONTHLY",
"customerId": "cust_789",
"successUrl": "https://example.com/checkout/success",
"metadata": {
"order_ref": "ABC-123",
"workspace_id": "ws_acme",
"source": "pricing_page"
}
}'
```
```typescript Node.js SDK theme={null}
import { Kelviq, CHARGE_PERIOD_CHOICES } from "@kelviq/node-sdk";
const client = new Kelviq({
accessToken: process.env.KELVIQ_SANDBOX_SERVER_API_KEY,
});
const session = await client.checkout.createSession({
planIdentifier: "pro",
chargePeriod: CHARGE_PERIOD_CHOICES.MONTHLY,
customerId: "cust_789",
successUrl: "https://example.com/checkout/success",
metadata: {
order_ref: "ABC-123",
workspace_id: "ws_acme",
source: "pricing_page",
},
});
```
```python Python SDK theme={null}
import os
from kelviq_sdk import Kelviq
client = Kelviq.create_sync_client(
access_token=os.environ["KELVIQ_SANDBOX_SERVER_API_KEY"]
)
session = client.checkout.create_session(
planIdentifier="pro",
chargePeriod="MONTHLY",
customerId="cust_789",
successUrl="https://example.com/checkout/success",
metadata={
"order_ref": "ABC-123",
"workspace_id": "ws_acme",
"source": "pricing_page",
},
)
```
The response contains the hosted checkout URL and the Kelviq session ID:
```json theme={null}
{
"checkoutUrl": "https://kelviq.com/checkout/cs_eeCGdGayc5oTMdGNV5XocfRk6o87K0vlwwCuKeiE7BLU9/",
"checkoutSessionId": "cs_eeCGdGayc5oTMdGNV5XocfRk6o87K0vlwwCuKeiE7BLU9"
}
```
Redirect the customer to `checkoutUrl`. The create response does not echo the metadata, so save `checkoutSessionId` alongside your internal record if you also need to look up the Kelviq session later.
> [!NOTE]
>
> This guide uses the sandbox host and a sandbox server API key. Switch both to production together when you are ready to accept live payments.
## Read metadata after checkout
When payment succeeds, the `checkout.completed` event includes the metadata at `data.object.metadata`:
```json theme={null}
{
"id": "1619afec-caab-40c7-827c-6af4f3acc9a3",
"type": "checkout.completed",
"data": {
"object": {
"id": "cs_jBy6ViggULoyDMW9ikaw4ydur1NUa6dCORT6vqKPbPPW8",
"metadata": {
"order_ref": "ABC-123",
"workspace_id": "ws_acme",
"source": "pricing_page"
}
}
}
}
```
Verify the webhook signature before reading or acting on the payload. After verification, you can use the metadata to find the matching record in your system:
```typescript theme={null}
import { validateEvent } from "@kelviq/node-sdk";
import express from "express";
const app = express();
app.post(
"/webhooks/kelviq",
express.raw({ type: "application/json" }),
async (req, res) => {
let event;
try {
event = validateEvent(
req.body,
req.headers,
process.env.KELVIQ_WEBHOOK_SECRET,
);
} catch {
return res.status(400).send("Invalid webhook signature");
}
if (event.type === "checkout.completed") {
const metadata = event.data.object.metadata ?? {};
// Replace this with your queue or application service.
await queueCheckoutProvisioning({
eventId: event.id,
orderRef: metadata.order_ref,
checkoutSessionId: event.data.object.id,
});
}
return res.sendStatus(202);
},
);
```
Use the webhook event `id`, not a metadata value, as your idempotency key. Kelviq can deliver the same event more than once during automatic retries or a manual resend.
[Learn how to register an endpoint and verify signatures →](/guides/webhooks)
## Choose useful metadata
Keep the object small and focused on correlation. A stable naming convention makes webhook handlers easier to maintain.
| Value | Example | Why keep it |
| - | - | - |
| Internal order reference | `order_ref: "ABC-123"` | Finds the pending order in your database |
| Account or workspace ID | `workspace_id: "ws_acme"` | Connects the purchase to the right tenant |
| Signup source | `source: "pricing_page"` | Records where the checkout started |
| Campaign reference | `campaign: "launch_2026"` | Attributes the checkout to a campaign |
Metadata is helpful context, but it is not proof of payment or authorization. Confirm `event.type`, verify the signature, and use Kelviq's resource IDs when calling Kelviq APIs.
### Checkout metadata and customer metadata are different
Checkout metadata describes one checkout session. Customer metadata describes the customer record and can be managed through the Customers API. Adding metadata to a checkout session does not replace or update customer metadata.
## Test the complete flow
1. Create a session with a sandbox server API key and recognizable test metadata.
2. Open the returned `checkoutUrl` and complete a sandbox payment.
3. Open **Settings → Webhooks**, then inspect the `checkout.completed` delivery.
4. Confirm that `data.object.metadata` contains the same keys and values.
5. Resend the delivery and confirm that your idempotency check prevents duplicate work.
## Troubleshooting
| Problem | Check |
| - | - |
| Metadata is missing from the webhook | Make sure `metadata` is at the top level of the checkout request and that you are inspecting `checkout.completed`. |
| A handler cannot find a key | Key names are preserved verbatim. Check spelling, capitalization, and underscores. |
| The request returns `401` | Use a server API key for the same environment as the API host. |
| Signature verification fails | Verify the raw request body before JSON parsing. In Express, use `express.raw()` on the webhook route. |
| Provisioning runs twice | Deduplicate by the event `id` or `webhook-id`, including deliveries triggered by **Resend**. |
## Related documentation
* [Checkout configuration](/checkout/checkout-configuration)
* [Checkout API reference](/api-reference/checkout/create-a-checkout-session)
* [Webhooks](/guides/webhooks)
* [Node SDK](/backend-integration/node-sdk)
* [Python SDK](/backend-integration/python-sdk)
# Custom fields
Source: https://docs.kelviq.com/checkout/custom-fields
Ask customers for extra information at checkout, such as a company name, PO number, or use case
Custom fields add your own questions to checkout. Use them for anything Kelviq doesn't already collect, such as a company name for the invoice, a purchase order number, a team size, or agreement to your terms.
You create a field once in Settings, then choose which checkout links or checkout sessions should ask it.
## Create a field
1. Open **Settings → Custom fields** and click **Create custom field**.
2. Pick a **Field type** and decide whether the field is **required**.
3. Enter the **Field label**, the question customers see above the input.
4. Add optional **Help text**, shown below the input, and a **Placeholder** that shows the expected format.
5. Set the validation for the type you picked, and the **Validation error message** customers see when their answer doesn't pass.
6. Click **Create field**.

## Field types
| Type | What the customer enters | Validation you can set |
| - | - | - |
| **String** | A single line of text | Minimum and maximum length, and a regex pattern the answer must match |
| **Number** | A number | Minimum and maximum value |
| **Boolean** | Yes or no, with a checkbox | Required or not |
| **Email** | An email address | Checked as a valid email |
| **Dropdown** | One option from a list you define. Each option has a value and a label. | The answer must be one of your options |
| **URL** | A web address | Checked as a valid URL |
| **Date** | A date, in `YYYY-MM-DD` format | Earliest and latest date |
Use a required **Boolean** field for "I agree to the terms" style checkboxes.
## Add a field to checkout
**Checkout links:** on the product page, click **Get Link**. Under **Assign custom field**, choose the field, then copy the checkout link. Customers who open that link see the question.
**Checkout sessions from the API:** pass the field identifiers in `customFields` when you [create a checkout session](/api-reference/checkout/create-a-checkout-session):
```json theme={null}
{
"planIdentifier": "pro-plan",
"chargePeriod": "MONTHLY",
"successUrl": "https://example.com/welcome",
"customFields": ["company-name", "purchase-order-number"]
}
```
Fields appear in the order they're sorted in Settings. Each answer is checked against the field's type and validation before payment. Identifiers that don't exist or belong to archived fields are ignored.
Editing a field changes it on every checkout link that already uses it.
## Where the answers go
Answers are saved with the [checkout session](/sales/checkout-sessions) and the [order](/sales/orders) and subscription it creates. Through the API and webhooks they arrive keyed by field identifier:
* `customFieldsData` on orders, checkout sessions, and invoices in the API.
* `custom_fields_data` in the `checkout.completed`, `order.created`, `order.updated`, and `order.refunded` [webhooks](/guides/webhooks#custom-field-responses).
# Payment methods
Source: https://docs.kelviq.com/checkout/payment-methods
Cards, Apple Pay, Google Pay, UPI, PayPal, Klarna, iDEAL, SEPA Direct Debit, Pix, and the other ways customers can pay at Kelviq checkout
Kelviq checkout accepts cards, wallets, bank payments, buy now pay later, and local payment methods from around the world. You don't need to turn anything on or sign up with each provider. Kelviq is the merchant of record, so we handle the payment provider agreements, currency conversion, and payouts to you.
## Which methods a customer sees
Checkout shows the methods that fit each purchase, based on:
* **The customer's country.** A buyer in the Netherlands sees iDEAL, a buyer in India sees UPI, a buyer in Brazil sees Pix.
* **The checkout currency.** Most local methods only work in their own currency. UPI needs INR, Pix needs BRL, iDEAL needs EUR. Set local prices with [location-based pricing](/product-catalog/location-based-pricing) so these methods can appear.
* **One-time or recurring.** Some methods can be charged again automatically for subscription renewals. Others can only be used for one-time payments, so checkout hides them for subscriptions.
Cards, Apple Pay, and Google Pay work almost everywhere and for both one-time payments and subscriptions.
## Cards
| Method | Where | Subscriptions |
| - | - | - |
| Visa, Mastercard, American Express | Worldwide | Yes |
| Discover, Diners Club, JCB, UnionPay | Worldwide | Yes |
| Cartes Bancaires | France | Yes |
| eftpos | Australia | Yes |
| Korean local cards | South Korea | Yes |
## Wallets
| Method | Where | Subscriptions |
| - | - | - |
| Apple Pay | Wherever Apple Pay is available | Yes |
| Google Pay | Wherever Google Pay is available | Yes |
| Link | Most countries | Yes |
| PayPal | Worldwide | Yes |
| Amazon Pay | Worldwide (USD) | Yes |
| Revolut Pay | UK and EU | Yes |
| Cash App Pay | United States | Yes |
| Kakao Pay, Naver Pay | South Korea | Yes |
| Alipay, WeChat Pay | Customers in China | One-time only |
| MobilePay | Denmark, Finland | One-time only |
| MB WAY | Portugal | One-time only |
| GrabPay | Malaysia, Singapore | One-time only |
| PayPay | Japan | One-time only |
## Bank redirects
The customer is sent to their bank or banking app to approve the payment, then back to checkout.
| Method | Where | Currency | Subscriptions |
| - | - | - | - |
| iDEAL (Wero) | Netherlands | EUR | Yes, renewals are charged by SEPA Direct Debit |
| Bancontact | Belgium | EUR | Yes, renewals are charged by SEPA Direct Debit |
| BLIK | Poland | PLN | Yes |
| TWINT | Switzerland | CHF | Yes |
| EPS | Austria | EUR | One-time only |
| Przelewy24 | Poland | EUR, PLN | One-time only |
| FPX | Malaysia | MYR | One-time only |
## Bank debits
The customer enters their bank account details once and is debited directly. Good for B2B customers and high-value subscriptions.
| Method | Where | Currency | Subscriptions |
| - | - | - | - |
| SEPA Direct Debit | Euro area and SEPA countries | EUR | Yes |
| ACH Direct Debit | United States | USD | Yes |
| Bacs Direct Debit | United Kingdom | GBP | Yes |
| BECS Direct Debit | Australia, New Zealand | AUD, NZD | Yes |
## Buy now, pay later
The customer splits the payment into instalments. The provider pays the full amount at checkout.
| Method | Where | Subscriptions |
| - | - | - |
| Klarna | US, Canada, UK, most of Europe, Australia, New Zealand | Yes, in some countries |
| Afterpay / Clearpay | US, Canada, UK, Australia, New Zealand | One-time only |
| Affirm | US, Canada | One-time only |
| Zip | Australia | One-time only |
## Real-time payments
| Method | Where | Currency | Subscriptions |
| - | - | - | - |
| UPI | India | INR | Yes, with UPI AutoPay |
| Pix | Brazil | BRL | Yes, with Pix Automático |
| PayNow | Singapore | SGD | One-time only |
| PromptPay | Thailand | THB | One-time only |
| Pay by Bank | United Kingdom | GBP | One-time only |
## Vouchers
The customer gets a voucher and pays it in cash or online at a store, bank, or ATM. The order completes when the payment arrives, which can take a day or more.
| Method | Where | Currency | Subscriptions |
| - | - | - | - |
| Boleto | Brazil | BRL | Yes, a new voucher for each renewal |
| OXXO | Mexico | MXN | One-time only |
| Konbini | Japan | JPY | One-time only |
| Multibanco | Portugal | EUR | One-time only |
## UPI
UPI is the most used way to pay online in India. Customers pay from any UPI app, such as Google Pay, PhonePe, Paytm, or their bank's app, by scanning a QR code.
* **Who can use it:** customers in India, when the checkout is priced in INR.
* **One-time payments:** from ₹1 up to ₹1,00,000 per payment.
* **Subscriptions:** the customer approves a UPI AutoPay mandate in their UPI app at checkout. Kelviq then charges each renewal automatically, up to ₹15,000 per charge.
* **QR codes** expire after 5 minutes.
* **Refunds** can be issued for up to 60 days after the payment and take up to 7 business days to reach the customer.
To offer UPI, give your plan an INR price with [location-based pricing](/product-catalog/location-based-pricing). Customers in India then see the INR price and UPI at checkout.
## Related
* [Checkout examples](/checkout/checkout-examples) shows checkout for each pricing model.
* [Location-based pricing](/product-catalog/location-based-pricing) sets prices in local currencies.
* [Managing payment methods](/customer-portal/managing-payment-methods) explains how customers update the method used for renewals.
# Customize a static checkout URL
Source: https://docs.kelviq.com/checkout/static-checkout-url
Choose plans, set a billing period, attach a customer, apply a discount, and control the success redirect in a Kelviq checkout link
A static checkout URL is a shareable link for one published product. You can use the link exactly as Kelviq generates it, or add query parameters to control what the customer sees when checkout opens.
Static links work well on pricing pages, in emails, and behind a purchase button. If the checkout needs data that only your backend knows, use the [Checkout API](/api-reference/checkout/create-a-checkout-session) instead.
## Copy the checkout URL
1. Open **Product Catalog → Products**.
2. Select a published product.
3. Click **Get Link**, then copy the link.
4. Choose the plans and default billing period.
5. Copy the generated URL.
Production links use this format:
```text theme={null}
https://www.kelviq.com/buy/{PRODUCT_UUID}/
```
Sandbox links include `/sandbox/`:
```text theme={null}
https://www.kelviq.com/sandbox/buy/{PRODUCT_UUID}/
```
The product UUID is part of the copied link.
## Available query parameters
The dashboard adds the plan, billing-period, and discount options you select. You can add `customer_id` and `success_url` when your integration needs them.
| Parameter | Value | What it changes |
| - | - | - |
| `enabled` | Comma-separated plan identifiers | Limits checkout to the listed plans |
| `plan_identifier` | One plan identifier | Selects the plan shown first |
| `charge_period` | A supported billing-period value | Selects the initial billing period |
| `discounts_enabled` | `true` or `false` | Shows or hides discount-code entry |
| `discount_code` | An active discount code | Applies the code when checkout opens |
| `customer_id` | Your unique customer ID | Links checkout to a specific customer record |
| `success_url` | A URL-encoded URL | Redirects the customer after successful checkout |
Use these values for `charge_period`:
| Value | Billing period |
| - | - |
| `ONE_TIME` | One-time payment |
| `DAILY` | Daily |
| `WEEKLY` | Weekly |
| `TWENTY_EIGHT_DAYS` | Every 28 days |
| `MONTHLY` | Monthly |
| `THREE_MONTHS` | Every three months |
| `SIX_MONTHS` | Every six months |
| `YEARLY` | Yearly |
The selected plan must support the selected period. If you use `enabled`, include the value from `plan_identifier` in that list.
## Build a link safely
Use `?` before the first query parameter and `&` before each additional parameter:
```text theme={null}
https://www.kelviq.com/buy/{PRODUCT_UUID}/?enabled=starter,pro&plan_identifier=pro&charge_period=MONTHLY
```
Using `URL` and `URLSearchParams` is safer than joining strings by hand. It handles characters that must be encoded:
```javascript theme={null}
const checkoutUrl = new URL(
"https://www.kelviq.com/buy/{PRODUCT_UUID}/",
);
checkoutUrl.searchParams.set("enabled", "starter,pro");
checkoutUrl.searchParams.set("plan_identifier", "pro");
checkoutUrl.searchParams.set("charge_period", "MONTHLY");
checkoutUrl.searchParams.set("discounts_enabled", "true");
checkoutUrl.searchParams.set("discount_code", "LAUNCH20");
checkoutUrl.searchParams.set("customer_id", "cust_789");
checkoutUrl.searchParams.set(
"success_url",
"https://example.com/checkout/success",
);
console.log(checkoutUrl.toString());
```
The resulting URL is equivalent to:
```text theme={null}
https://www.kelviq.com/buy/{PRODUCT_UUID}/?enabled=starter%2Cpro&plan_identifier=pro&charge_period=MONTHLY&discounts_enabled=true&discount_code=LAUNCH20&customer_id=cust_789&success_url=https%3A%2F%2Fexample.com%2Fcheckout%2Fsuccess
```
## Common configurations
### Select a plan and billing period
Show the Starter and Pro plans, with Pro monthly selected when checkout opens:
```text theme={null}
?enabled=starter,pro&plan_identifier=pro&charge_period=MONTHLY
```
### Hide discount-code entry
```text theme={null}
?discounts_enabled=false
```
`discounts_enabled` defaults to `true`.
### Apply a discount code
```text theme={null}
?discounts_enabled=true&discount_code=LAUNCH20
```
The discount must be active and valid for the selected product, plan, currency, and date. [Learn how checkout discounts work →](/checkout/apply-a-discount-code)
### Link checkout to a customer
```text theme={null}
?customer_id=cust_789
```
Use the stable, unique customer ID from your application. When the customer record already has an email address, Kelviq can prefill it at checkout.
> [!WARNING]
>
> A customer-specific URL should not be published as a general checkout link. Query parameters can appear in browser history, analytics, and server logs. Use an opaque customer ID and never put an email address, API key, or secret in the URL.
### Redirect after successful checkout
```text theme={null}
?success_url=https%3A%2F%2Fexample.com%2Fcheckout%2Fsuccess
```
Encode the full destination URL. In JavaScript, `URLSearchParams` does this for you. In other environments, use the standard URL-encoding function provided by the language or framework.
> [!WARNING]
>
> A visit to `success_url` is not proof of payment. Customers can open that page directly. Confirm payment through `checkout.completed`, the resulting subscription or order, or a current entitlement check.
## Static URL names and API names
Static checkout links use snake\_case query parameters. The Checkout API and backend SDKs use camelCase request fields.
| Static URL | Checkout API or SDK |
| - | - |
| `enabled` | `plansEnabled` |
| `plan_identifier` | `planIdentifier` |
| `charge_period` | `chargePeriod` |
| `discounts_enabled` | `discountsEnabled` |
| `discount_code` | `discountCode` |
| `customer_id` | `customerId` |
| `success_url` | `successUrl` |
Do not put camelCase names into the URL. For example, `successUrl` belongs in an API request; `success_url` belongs in a static checkout URL.
## When to use the Checkout API
Use the Checkout API or a backend SDK when you need per-session options that are not part of the static URL contract, including:
* a cancellation URL;
* feature quantities;
* location-based pricing through the customer's IP address;
* custom amounts, currency, or tax behavior;
* a locked email or default billing country; or
* checkout metadata for webhook correlation.
The API creates a short-lived checkout session and returns `checkoutUrl` and `checkoutSessionId`. Keep the server API key on your backend.
[Create a checkout session with the API →](/api-reference/checkout/create-a-checkout-session)
## Test the link
1. Enter sandbox mode and copy a sandbox checkout link.
2. Add one customization at a time.
3. Open the link and confirm the visible plans, selected period, discount behavior, and customer details.
4. Complete a sandbox checkout.
5. Confirm the success redirect and the `checkout.completed` webhook.
Keep sandbox products, customers, discounts, and API credentials together. A live customer ID or discount code will not work against a sandbox checkout.
## Troubleshooting
| Problem | Check |
| - | - |
| A parameter is ignored | Match the snake\_case name exactly. Query parameter names are case-sensitive. |
| The wrong billing period opens | Use an uppercase value such as `MONTHLY`, and confirm that the selected plan supports it. |
| The default plan is not available | Add that plan identifier to `enabled`, or remove `enabled` to show every available plan. |
| The success URL breaks the checkout link | URL-encode the complete value instead of inserting an unescaped URL. |
| A discount does not apply | Check its status, dates, redemption limit, product and plan restrictions, and currency. |
| The wrong customer is selected | Check `customer_id` and make sure the customer exists in the same environment as the checkout link. |
| A sandbox link cannot find live data | Create or select the matching record in sandbox. Sandbox and production data are separate. |
## Related documentation
* [Checkout examples](/checkout/checkout-examples)
* [Checkout configuration](/checkout/checkout-configuration)
* [Apply a discount code at checkout](/checkout/apply-a-discount-code)
* [Add metadata to checkout](/checkout/checkout-metadata)
* [Webhooks](/guides/webhooks)
# Command reference
Source: https://docs.kelviq.com/cli/commands
Every Kelviq CLI command, flag by flag.
All commands support `--help`. The CLI targets your **sandbox** environment unless you pass `--prod` — see [Environments & authentication](/cli/environments).
Every command reads `KELVIQ_*` variables from `.env.local` and then `.env` in the working directory before it runs, so a project's env file works without exporting anything. A variable already set in your shell always wins. Two global flags control this:
| Flag | Description |
| - | - |
| `--env-file ` | Read this dotenv file instead of `.env.local`/`.env`. Repeat it for several files; a missing file is an error. |
| `--no-env-file` | Don't read any env file. |
Output is colorized on interactive terminals (green creates, yellow updates, red archives) and automatically plain when piped. Disable colors explicitly with the global `--no-color` flag or the `NO_COLOR` environment variable.
***
## kelviq init
Writes a starter `kelviq.config.ts` to the current directory.
```shell theme={null}
kelviq init # the saas template
kelviq init --template usage # metered usage with an overage price
kelviq init --template license # a plan that issues license keys
kelviq init -o pricing/kelviq.config.ts
kelviq init --force
```
| Flag | Description |
| - | - |
| `--template ` | Which starter catalog to write: `saas` (default), `usage`, or `license`. |
| `-o, --output ` | Where to write the file. Defaults to `kelviq.config.ts` in the current directory. |
| `--force` | Overwrite the file if it already exists. Without it, `init` refuses rather than clobbering your edits. |
Each template is a complete catalog that `kelviq push` accepts as written, using the typed builders from `@kelviq/cli/config` — see the [format reference](/cli/kelviq-config):
* **`saas`** — a product with a free plan and a Pro plan billed monthly or yearly, gating a Boolean feature and a Customizable limit.
* **`usage`** — a `METER` feature with the details the API requires, a free plan with a hard monthly allowance, and a Pro plan with an included quantity plus a tiered pay-as-you-go overage price.
* **`license`** — a Pro plan whose orders issue license keys valid on up to three devices.
***
## kelviq login
Stores a Server API key for the CLI to use.
```shell theme={null}
kelviq login # store a sandbox key (default)
kelviq login --prod # store a production key
```
| Flag | Description |
| - | - |
| `--prod` | Store the key for the production environment instead of sandbox. |
What it does:
1. Prints the dashboard's [API keys page](https://app.kelviq.com/settings/api-keys) URL and tries to open it in your default browser — already switched to the environment you're logging into (sandbox by default, production with `--prod`), so the key on screen is the key the CLI expects.
2. Prompts you to paste the key. In an interactive terminal each pasted character shows as `*` — so you can see the paste landed — and a `Received a N-character key.` line confirms the length. The key itself is never echoed or logged.
3. Stores it in `~/.config/kelviq/config.json` with owner-only permissions (`0600`).
> [!WARNING]
>
> Paste a **Server API Key**, not a Client API Key. Server keys have full API access — treat the config file like any other credential store, and never commit it anywhere.
`login` does not check the key. Run `kelviq env --check` afterwards to confirm the API accepts it. Kelviq has no key-introspection endpoint yet, so the CLI can't show which organization the key belongs to.
For CI and other non-interactive contexts, skip `login` entirely and set an environment variable instead — see [Environments & authentication](/cli/environments#environment-variables).
***
## kelviq logout
Removes stored API keys.
```shell theme={null}
kelviq logout # remove the stored sandbox key
kelviq logout --prod # remove the stored production key
kelviq logout --all # remove both
```
| Flag | Description |
| - | - |
| `--prod` | Remove the production key instead of sandbox. |
| `--all` | Remove both sandbox and production keys. |
`logout` only touches the stored config file. Keys supplied via environment variables are unaffected — unset those in your shell or CI configuration.
> [!NOTE]
>
> Removing a key locally does not revoke it. To revoke a key, delete it from the [API keys page](https://app.kelviq.com/settings/api-keys) in the dashboard.
***
## kelviq env
Shows which environment and keys the CLI is configured with, and which env files it read.
```shell theme={null}
kelviq env
kelviq env --check
```
```text theme={null}
Environment : sandbox (default — use --prod to target production)
KELVIQ_ENV : unset (sandbox) — unprefixed KELVIQ_* vars apply to sandbox
Sandbox URL : https://sandboxapi.kelviq.com/api/v1
Prod URL : https://api.kelviq.com/api/v1
Sandbox key : set
Prod key : missing
Org : unknown — Kelviq has no key-introspection endpoint yet
Env files : /path/to/app/.env.local (KELVIQ_SERVER_API_KEY, KELVIQ_ENV)
```
| Flag | Description |
| - | - |
| `--check` | Make one authenticated read per configured key and report whether the API accepted it. |
| `--json` | Machine-readable output. Never includes any part of a key's value. |
Without `--check`, key rows show `set` or `missing` only and nothing is sent to the API. With `--check`, each configured key gets one `GET /catalog/products/` call against its own environment's host and a result line:
```text theme={null}
Sandbox check : ok
Prod check : skipped (no key configured)
```
`ok` means the key was accepted. `rejected` (HTTP 401 or 403) almost always means the key belongs to the other environment. The command exits non-zero when any configured key fails, or when no key is configured, so a script or agent can gate on it. `--check --json` adds `sandboxCheck` and `prodCheck` objects (`{ ok, status, httpStatus?, message? }`) to the JSON document.
***
## kelviq pull
Fetches your remote catalog and writes it as a `kelviq.config.ts`.
```shell theme={null}
kelviq pull # pull the sandbox catalog
kelviq pull --prod # pull the production catalog
kelviq pull -o pricing.config.ts
kelviq pull --force # overwrite an existing file
```
| Flag | Description |
| - | - |
| `--prod` | Pull from the production environment instead of sandbox. |
| `-o, --output ` | Where to write the config. Defaults to `kelviq.config.ts` in the current directory. |
| `--force` | Overwrite the output file if it already exists. Without it, `pull` refuses rather than overwriting local edits. |
What it fetches: all products, all features, the **latest version** of each plan, and each plan's prices and entitlements. Every cross-reference in the generated file uses `identifier` slugs, never UUIDs — the file is stable across environments that share identifiers.
On success, `pull` prints the path it wrote and a **warnings list** for anything it couldn't represent in the config format (for example, plan fields like `links`, `countries`, and `files` are dropped, with one warning each; a plan's `license` settings are pulled into the file). Warnings are informational — the file it wrote is still valid.
On failure — missing key, network error, unexpected API response — `pull` writes `pull failed: ` to stderr and exits non-zero without touching the output file.
***
## kelviq push
Deploys a `kelviq.config.ts` to a Kelviq environment: fetches the current remote catalog, computes exactly what would change, shows you the diff, and applies it only after confirmation.
```shell theme={null}
kelviq push --dry-run # preview only — never writes anything
kelviq push # diff, confirm, apply to sandbox
kelviq push --prod # same, against production
kelviq push --publish # also publish the plans this run changed
kelviq push pricing.config.ts # explicit config path
```
| Flag | Description |
| - | - |
| `[config]` | Path to the config file. Defaults to `kelviq.config.ts`. |
| `--prod` | Target production instead of sandbox. |
| `--dry-run` | Print the diff and exit. Guaranteed zero writes. |
| `--yes` | Skip the confirmation prompts (required in non-interactive sessions, e.g. CI). |
| `--publish` | Publish the plans this run created, updated, or price-replaced — with its own confirmation. |
| `--migrate-features` | With `--publish`: migrate existing customers to the published plan's entitlements. |
| `--migrate-pricing` | With `--publish`: migrate existing customers to the published plan's pricing. |
| `--prune` | Archive remote-only plans/features that the CLI created (see below). |
| `--allow-prod-prune` | Required in place of the interactive typed confirmation to `--prune` production non-interactively. |
| `--verbose` | Expand aggregated warnings (e.g. list every stripped server-owned price field individually). |
| `--json` | With `--dry-run` only: emit the machine-readable plan-of-record as JSON on stdout. |
The diff shows **what actually changes**, value to value — `~ update plan "pro": name "Pro" → "Pro Plan"` — so the confirmation gate is reviewable at a glance.
### Scripting with `--json`
`kelviq push --dry-run --json` prints exactly one JSON document: `{environment, direction, changes, summary, operations, prune, publish, featureDrift, errors, warnings}`. The `changes` boolean makes drift checks a one-liner in CI:
```shell theme={null}
kelviq push --dry-run --json | jq -e '.changes | not' || echo "catalog has drifted"
```
`--json` is valid only with `--dry-run` (JSON mode never mixes with prompts or writes), always exits 0 on a successful render, and emits `{"error": …}` documents on failures. The shape is a documented, versioned contract — see the repo README.
### What push writes — and in what order
Push converges the environment toward your config in dependency order: products → features → plans → entitlements → prices → publish → prune. Cross-references you wrote as identifiers are resolved to UUIDs automatically, including references to resources created earlier in the same run.
**Everything push writes is a draft by default.** Plan creates land in draft; updating a published plan creates a new draft version. Nothing a customer sees changes until a plan is explicitly published. On a mid-run failure, push stops immediately, reports exactly what was applied and what wasn't, and exits non-zero — there's no rollback, but because the writes were drafts, a half-applied push has no customer impact.
Push stamps a `managed-by: kelviq-cli` entry into the `metadata` of every feature and plan it creates (merged with any metadata you declare). That marker is how `--prune` later distinguishes CLI-managed resources from everything else — you'll see it in the dashboard, and it's safe to leave alone.
### Prices are all-or-nothing per plan
A plan **with** a `prices` array in config always pushes that complete array, replacing the plan's entire remote price set. A plan **without** a `prices` key never touches remote prices at all. Omitting `prices` means "leave pricing alone," not "this plan is free."
Prices are compared on the fields your config sets. A field you omit — `freeTrial`, `taxBehavior`, a charge's `paymentType` or `reset` — is left to the server's default and never counts as a difference, so a hand-authored price diffs clean after its first push.
### METER features are checked before anything is written
The API rejects a `METER` feature created without `featureDetails` (`featureSubType` and `units`). Push checks for this while building the plan and stops before any write, naming the feature and the shape to add — see [feature()](/cli/kelviq-config#feature).
### Features can't be updated — by design
The Kelviq API deliberately treats feature attributes as immutable; renaming a feature is a dashboard operation. If a feature in your config differs from its remote counterpart, push stops **before writing anything** and lists the drifted fields. A feature's identifier and type can't change: to change them, create a new feature, then run `kelviq pull` and push again. Edit the name in the [dashboard](https://app.kelviq.com).
### Publishing
`--publish` publishes the plans the run just changed, after a second, separate confirmation listing them. A bare `--publish` never migrates existing customers — they stay on their current entitlements and pricing until you deliberately pass `--migrate-features` and/or `--migrate-pricing`, each of which spells out its consequence before you confirm. `--yes` skips prompts; it never substitutes for passing a migration flag explicitly.
### Pruning
Remote-only resources — things that exist in the environment but not in your config — are normally just listed as warnings. `--prune` archives them, under strict conditions:
* Only plans and features **carrying the `managed-by: kelviq-cli` marker** are ever archived. Anything created in the dashboard, by another tool, or before marker support is listed as *skipped* with the reason — never touched.
* **Products are never pruned**, ever.
* Archiving is the platform's soft-delete — nothing is destroyed.
* Pruning production requires typing the environment name at a dedicated prompt (or `--allow-prod-prune` in CI). `--yes` alone never unlocks it.
***
## kelviq promote
Moves your sandbox catalog to production in one command — the same engine and safety rails as `push`, with sandbox as the source instead of a local file.
```shell theme={null}
kelviq promote --dry-run # preview what production would change — read-only
kelviq promote # diff, confirm, apply drafts to production
kelviq promote --publish # and publish the changed plans
```
| Flag | Description |
| - | - |
| `--dry-run` | Print the sandbox→production diff and exit. Guaranteed zero writes. |
| `--yes` | Skip the confirmation prompts. |
| `--publish` / `--migrate-features` / `--migrate-pricing` | Same semantics as `push`. |
| `--prune` | Archive marker-carrying production plans/features absent from sandbox. Always crosses the production-prune gate. |
| `--allow-prod-prune` | Non-interactive stand-in for the typed production confirmation. |
| `--verbose` / `--json` | Same semantics as `push` (`--json` requires `--dry-run`). |
Promote reads and writes **no local file** — it fetches the sandbox catalog in memory and reconciles production toward it, matching resources by identifier (which is why the config format bans per-environment UUIDs). It needs both keys configured: sandbox to read, production to write.
Everything you know about push applies: drafts by default, a confirmation showing the full diff first, feature drift between the environments stops the run with dashboard guidance, and prices replace whole-set per plan. The intended workflow is always `--dry-run` first:
```shell theme={null}
kelviq promote --dry-run # read what would change
kelviq promote # then do it
```
Promotion is one-way — sandbox to production. There is no reverse mode.
***
## kelviq status
Shows the live catalog of an environment without writing a config file: products, features, and the latest version of every plan with its price and entitlement counts. Read-only — the check to run after a push to confirm what landed.
```shell theme={null}
kelviq status
kelviq status --prod
kelviq status --json
```
```text theme={null}
Environment : sandbox
Products (1)
my-app My App
Features (2)
api-calls METER API calls
history-retention CUSTOMIZABLE History retention (days)
Plans (2)
free Free product my-app · v1 · 1 price · 1 entitlement · visible
pro Pro product my-app · v2 · 1 price · 2 entitlements · visible · license keys
```
| Flag | Description |
| - | - |
| `--prod` | Read production instead of sandbox. |
| `--json` | Emit `{ environment, products, features, plans }`, each plan as `{ identifier, name, product, version, isLatest, isVisible, prices, entitlements, licenseKeys }`. |
A plan with no prices is flagged, since it can't be bought. The API reports a plan's version number rather than a draft/published state, so that's what `status` shows.
***
## kelviq webhook
Registers the app's webhook endpoint and gets the signing secret into the app's env file without it ever appearing on screen.
```shell theme={null}
kelviq webhook list
kelviq webhook add https://app.example.com/api/kelviq/webhooks \
--events checkout.completed,subscription.created,subscription.updated,subscription.cancelled,invoice.payment_failed \
--write-secret .env.local
```
### kelviq webhook add
| Flag | Description |
| - | - |
| `` | The public https URL that receives events. Localhost and private addresses are rejected by the API. |
| `--events ` | Comma-separated event types — see the [event list](/guides/webhooks#event-types). Required. |
| `--write-secret ` | Write the signing secret into this dotenv file, replacing or appending a `KELVIQ_WEBHOOK_SECRET=` line. The value is never printed. |
| `--variable ` | Variable name to write instead of `KELVIQ_WEBHOOK_SECRET`. |
| `--print-secret` | Print the secret to stdout instead. Off by default. |
| `--prod` | Target production instead of sandbox. |
| `--yes` | Skip the confirmation prompt (required in non-interactive sessions). |
| `--json` | Machine-readable output. The endpoint's `secret` is always `[hidden]` unless `--print-secret` is passed. |
`add` is idempotent on the URL: an endpoint that already exists for the same URL is reused instead of registered twice, and its events are extended when the ones you pass aren't all covered. With neither `--write-secret` nor `--print-secret`, the secret stays hidden and the output tells you how to get it.
### kelviq webhook list
Lists endpoints with their URL, state, id and events. Secrets are never shown. `--prod` and `--json` as above.
***
## kelviq discount
Creates and lists discount codes. Only enabling or disabling a code can change after creation, so `create` restates everything and asks before writing.
```shell theme={null}
kelviq discount list
kelviq discount create --name "Launch offer" --code LAUNCH20 --percent 20 \
--duration repeating --months 3 --product my-app --plans pro-monthly
kelviq discount create --name "Ten off" --code TENOFF --amount 10 --currency USD
```
### kelviq discount create
| Flag | Description |
| - | - |
| `--name ` | Shown on the customer's invoice. Required. |
| `--code ` | What customers enter at checkout: uppercase letters and digits. Required. |
| `--percent ` | Percentage off, 1–100. Exactly one of `--percent` or `--amount`. |
| `--amount ` | Fixed amount off in normal currency units; needs `--currency`. |
| `--currency ` | ISO currency code for `--amount`. |
| `--duration ` | How long the discount applies. Default `once`. |
| `--months ` | With `--duration repeating`: how many **billing periods** it applies for. Not calendar months — `3` on a yearly plan is three years, so scope a "first 3 months" code to monthly plans. |
| `--product ` | Limit the code to one product, by identifier or UUID. The CLI resolves the identifier to the UUID the API wants. |
| `--plans ` | With `--product`: comma-separated plan identifiers. Omit to cover every plan of the product. |
| `--max-redemptions ` | Total number of uses across all customers. |
| `--redeem-by ` | ISO 8601 date/time after which the code stops working. |
| `--prod` / `--yes` / `--json` | As for `webhook add`. |
Discount codes need a Merchant of Record organization.
### kelviq discount list
Lists codes with amount, duration and state. `--search ` filters by name or code; `--prod` and `--json` as above.
# Environments & authentication
Source: https://docs.kelviq.com/cli/environments
How the CLI targets sandbox vs. production, where keys live, and how to authenticate in CI.
Every Kelviq organization has a **sandbox** environment alongside production — see the [sandbox guide](/guides/sandbox). The CLI is sandbox-first: every command targets sandbox unless you explicitly pass `--prod`.
```shell theme={null}
kelviq pull # sandbox
kelviq pull --prod # production
kelviq push --prod # production, after a diff + confirmation
```
There is deliberately no configuration that makes an unqualified command touch production — `--prod` is always explicit, per command. The one command that inherently targets production, `kelviq promote`, says so in its name, shows you the full diff of what production would change, and requires confirmation before writing (plus a typed production confirmation before any `--prune` archive). Promote needs **both** keys configured: sandbox to read, production to write.
Each environment has its own API host and its own keys — the CLI selects both together:
| Environment | API host | Key |
| - | - | - |
| Sandbox (default) | `https://sandboxapi.kelviq.com/api/v1` | Sandbox Server API Key |
| Production (`--prod`) | `https://api.kelviq.com/api/v1` | Production Server API Key |
Always pair the key with its own environment's host — a key from one environment doesn't belong on the other's host, and the CLI never mixes them.
***
## Getting keys
The CLI authenticates with **Server API Keys** from the dashboard:
1. Sign in at [app.kelviq.com](https://app.kelviq.com).
2. Go to **Settings → [API keys](https://app.kelviq.com/settings/api-keys)**.
3. Copy the Server API Key for the environment you want (sandbox and production have separate keys).
> [!WARNING]
>
> Server API Keys have full API access. Never expose them in client-side code, commit them to version control, or paste them into shared chats. The CLI never echoes or logs a key.
***
## Key storage
`kelviq login` stores keys in a per-user config file:
```text theme={null}
~/.config/kelviq/config.json
```
* The file is created with owner-only permissions (`0600`), inside a `0700` directory.
* `XDG_CONFIG_HOME` is respected if set (the file lives at `$XDG_CONFIG_HOME/kelviq/config.json`).
* Sandbox and production keys are stored as separate entries; `kelviq logout` removes them individually (`--prod`) or together (`--all`).
***
## Environment variables
Environment variables **override** the stored config file — useful for CI, containers, and one-off overrides without touching your stored keys. They can come from the shell or from the project's env file: before every command, the CLI reads `KELVIQ_*` variables from `.env.local` and then `.env` in the working directory, never overriding a variable the shell already sets. Pass `--env-file ` to read a specific file instead, or `--no-env-file` to read none. `kelviq env` lists the files it read. `KELVIQ_SERVER_API_KEY` and `KELVIQ_ENV` work the same way as in the [MCP server](/guides/mcp-server); the environment-named keys are CLI-only.
| Variable | Purpose |
| - | - |
| `KELVIQ_SERVER_API_KEY` | Server key for the environment named by `KELVIQ_ENV`. |
| `KELVIQ_ENV` | `sandbox` (the default when unset, as in the MCP server) or `production`. |
| `KELVIQ_SANDBOX_SERVER_API_KEY` | Server key that always applies to sandbox. |
| `KELVIQ_PRODUCTION_SERVER_API_KEY` | Server key that always applies to production. |
| `KELVIQ_BASE_URL`, `KELVIQ_EDGE_URL` | Override the API or edge URL for the `KELVIQ_ENV` environment. You almost never need these. |
* A variable named for an environment wins over `KELVIQ_SERVER_API_KEY`. Use `KELVIQ_SANDBOX_SERVER_API_KEY` and `KELVIQ_PRODUCTION_SERVER_API_KEY` when one shell needs both keys, for example for `kelviq promote`.
* `KELVIQ_ENV` only decides which environment `KELVIQ_SERVER_API_KEY` belongs to. It never makes a command target production — only `--prod` does.
* Any value other than `sandbox` or `production` makes commands fail rather than guess.
A typical CI setup skips `kelviq login` entirely:
```yaml theme={null}
# e.g. GitHub Actions
env:
KELVIQ_SERVER_API_KEY: ${{ secrets.KELVIQ_SANDBOX_SERVER_API_KEY }}
KELVIQ_ENV: sandbox
```
```shell theme={null}
kelviq pull --force # authenticates from the environment variables
```
For `kelviq promote` in CI, set both `KELVIQ_SANDBOX_SERVER_API_KEY` and `KELVIQ_PRODUCTION_SERVER_API_KEY` instead.
If a command needs a key and finds none — neither in the environment nor stored — it fails with a message naming the variables to set and where to get a key. If `KELVIQ_SERVER_API_KEY` is set for the other environment, the message says so.
***
## Checking your setup
`kelviq env` prints the full picture without calling the API, and `kelviq env --check` adds one authenticated read per configured key:
```text theme={null}
Environment : sandbox (default — use --prod to target production)
KELVIQ_ENV : unset (sandbox) — unprefixed KELVIQ_* vars apply to sandbox
Sandbox URL : https://sandboxapi.kelviq.com/api/v1
Prod URL : https://api.kelviq.com/api/v1
Sandbox key : set
Prod key : missing
Org : unknown — Kelviq has no key-introspection endpoint yet
```
> [!NOTE]
>
> `set` means a key is configured (stored, from an env file, or from the shell) — not that it's valid. `kelviq env --check` verifies each key with one read against its own environment and exits non-zero if any configured key is rejected. Kelviq has no key-introspection endpoint yet, so the CLI can't display your organization name.
# kelviq.config.ts reference
Source: https://docs.kelviq.com/cli/kelviq-config
The pricing-as-code file format: builders, fields, and the rules that keep it portable.
`kelviq.config.ts` is a plain TypeScript file that declares your pricing catalog. You author it by hand (starting from `kelviq init`) or generate it from a live environment with `kelviq pull` — the two are interchangeable starting points.
The format follows one design idea: the file describes the **desired state** of your catalog, and nothing else. Anything that's an artifact of a particular deployment — UUIDs, draft/published status, server-generated timestamps — stays out, which is what makes the same file portable across sandbox and production and meaningful to diff over time. It's TypeScript rather than YAML or JSON for one reason: **types**. Your editor autocompletes every field, invalid enums fail to compile, and a config that type-checks is a config the CLI can read.
A config file imports three **builders** from `@kelviq/cli/config` and exports the results:
```ts theme={null}
import { product, feature, plan } from '@kelviq/cli/config';
export const app = product({
identifier: 'my-app',
name: 'My App',
taxCode: 'saas',
});
export const apiCalls = feature({
identifier: 'api-calls',
name: 'API calls',
type: 'METER',
featureDetails: {
featureSubType: 'PRE_AGGREGATED_USAGE',
units: { singular: 'call', plural: 'calls' },
},
});
export const historyRetention = feature({
identifier: 'history-retention',
name: 'History retention (days)',
type: 'CUSTOMIZABLE',
});
export const pro = plan({
identifier: 'pro',
name: 'Pro',
product: 'my-app',
entitlements: [
{ feature: 'history-retention', value: 30 },
{ feature: 'api-calls', value: 10000, reset: 'EVERY_MONTH', hardLimit: false },
],
prices: [
{
priceType: 'PAID',
currency: 'USD',
freeTrial: false,
trialPeriod: 0,
enabled: true,
taxBehavior: 'EXCLUSIVE',
chargeCatalogPrice: [
{
feature: null,
priceModel: 'FLAT',
paymentType: 'ADVANCE_COMMITMENT',
reset: 'NEVER',
charges: [
{ chargePeriod: 'MONTHLY', priceData: { amount: 29 } },
{ chargePeriod: 'YEARLY', priceData: { amount: 290 } },
],
},
],
},
],
});
```
The builders are typed: your editor autocompletes every field, and a misspelled or wrongly-typed field is a **compile-time error** with a "did you mean" suggestion — validation happens before any API call.
How the CLI reads the file: every export created by `product()` / `feature()` / `plan()` is collected, regardless of its export name. Other exports (helper constants, functions) are ignored, so you can organize the file however you like — split values, comment freely, generate plans in a loop. Duplicate identifiers within a kind are a hard error.
***
## The identity rule
Every cross-reference in the format — `plan.product`, `entitlements[].feature` — is an **identifier slug, never a UUID**. Identifiers are lowercase URL-safe slugs matching `^[a-z0-9][a-z0-9-_]*$` (for example `pro`, `api-calls`, `seats_v2`).
The Kelviq API sometimes wants UUIDs internally; the CLI resolves identifiers to UUIDs at sync time so your config file stays portable — the same file works against sandbox and production, where the same catalog has different UUIDs.
The plan lifecycle (draft → published) is also deliberately absent: the config describes **desired state**, not deployment state. Lifecycle is handled at sync time.
***
## product()
| Field | Type | Required | Description |
| - | - | - | - |
| `identifier` | slug | yes | Stable identifier, the join key across environments. |
| `name` | string | yes | Display name. |
| `taxCode` | enum | yes | One of: `saas`, `saas_business`, `software`, `videocontent`, `informationservice`, `ebook`, `digitalgraphic`, `videogame`, `eservice`, `training`. |
| `description` | string | no | Free-text description. |
***
## feature()
| Field | Type | Required | Description |
| - | - | - | - |
| `identifier` | slug | yes | Stable identifier, referenced from plan entitlements. |
| `name` | string | yes | Display name. |
| `type` | enum | yes | `BOOLEAN` (on/off), `CUSTOMIZABLE` (a setting the plan configures, such as days of history retention), or `METER` (a counted quantity — usage that resets, or seats a customer buys). Seats are a `METER`, not `CUSTOMIZABLE`: only a meter can carry a per-unit price and take a quantity at checkout — see [seat-based pricing](/product-catalog/seat-based-pricing). |
| `description` | string | no | Free-text description. |
| `featureDetails` | object | for `METER` | Type-specific detail payload, passed through to the API. A `METER` feature needs `featureSubType` (`PRE_AGGREGATED_USAGE`) and `units` (`{ singular, plural }`): `kelviq push` refuses to create one without them, before writing anything, because the API rejects it. See [Create a feature](/product-catalog/create-a-feature). |
| `meter` | object | no | Meter configuration for `METER` features, passed through to the API. |
| `metadata` | object | no | Arbitrary key-value metadata. |
***
## plan()
| Field | Type | Required | Description |
| - | - | - | - |
| `identifier` | slug | yes | Stable identifier. |
| `name` | string | yes | Display name. |
| `product` | slug | yes | Identifier of the parent product. |
| `description` | string | no | Free-text description. |
| `isVisible` | boolean | no | Whether the plan is publicly visible. |
| `license` | object | no | License-key issuance settings for orders on this plan — see below. |
| `entitlements` | array | no | What the plan grants — see below. |
| `prices` | array | no | The plan's price set — see below. |
| `metadata` | object | no | Arbitrary key-value metadata. |
### Entitlements
Each entry grants a feature to the plan:
| Field | Type | Required | Description |
| - | - | - | - |
| `feature` | slug | yes | Identifier of the granted feature. |
| `value` | boolean or number | no | The grant: `true`/`false` for `BOOLEAN` features, a number for limits. |
| `hasUnlimitedUsage` | boolean | no | Grant unlimited usage instead of a numeric value. Omit `value` when this is `true`; `kelviq pull` writes unlimited entitlements this way. |
| `reset` | enum | no | Usage reset cadence: `NEVER`, `EVERY_DAY`, `EVERY_28_DAYS`, `EVERY_MONTH`, `EVERY_QUARTER`, `EVERY_YEAR`. |
| `hardLimit` | boolean | no | Whether usage is blocked at the limit. |
| `usageAlerts` | object | no | Alert configuration, passed through to the API. |
Entitlements accept additional keys beyond these — the shape varies by feature type, and unrecognized keys are passed through to the API rather than rejected.
> [!NOTE]
>
> Some enum values (for example `EVERY_28_DAYS`) are only available when enabled for your account. The CLI's enum fields autocomplete the standard values but accept any string: values it doesn't recognize are passed through verbatim, and the API validates them when you push. This means new server-side values work without a CLI update.
### License keys
When [license keys](/product-catalog/license-keys) are enabled on a plan, every order generates a key. The `license` object holds the plan-level issuance settings — the same controls as the dashboard's **License keys** section:
```ts theme={null}
export const team = plan({
identifier: 'team',
name: 'Team',
product: 'my-app',
license: {
enabled: true,
activationLimitEnabled: true,
activationLimit: 5, // up to 5 devices per key
hasExpiry: true,
durationValue: 12,
durationUnit: 'MONTH', // keys expire 12 months after purchase
},
});
```
| Field | Type | Required | Description |
| - | - | - | - |
| `enabled` | boolean | no | Whether orders for this plan issue license keys. |
| `activationLimitEnabled` | boolean | no | Cap how many devices one key can be activated on. When `false`, activations are unlimited. |
| `activationLimit` | integer ≥ 1 | no | The cap, when `activationLimitEnabled` is `true`. |
| `hasExpiry` | boolean | no | Whether keys expire. When `false`, keys never expire. |
| `durationValue` | integer ≥ 1 | no | How long a key stays valid after purchase, in `durationUnit`s. |
| `durationUnit` | enum | no | `DAY`, `MONTH`, or `YEAR`. |
The object is compared and sent as one unit: `kelviq push` shows the old and new license config side by side and sends the whole object when anything in it differs. A plan that **omits** `license` leaves the remote license settings untouched, so a config file that doesn't mention license keys never disables them. Additional keys are passed through to the API.
Individual issued keys — activating, validating, or deactivating a customer's key — are per-customer runtime operations, not catalog state. They live in the API, the SDKs, and the [MCP server](/guides/mcp-server), not in this file.
### Prices
The price envelope is validated; the charge structures inside it are deliberately loose and passed through to the API as-is:
| Field | Type | Required | Description |
| - | - | - | - |
| `priceType` | enum | yes | `FREE`, `PAID`, or `CUSTOM`. |
| `currency` | string | no | ISO currency code, e.g. `USD`. |
| `freeTrial` | boolean | no | Whether the price includes a free trial. |
| `trialPeriod` | number | no | Trial length in days. |
| `taxBehavior` | enum | no | `INCLUSIVE`, `EXCLUSIVE`, or `UNSPECIFIED`. |
| `enabled` | boolean | no | Whether the price is available for purchase. |
| `chargeCatalogPrice` | array of objects | no | The full charge structure (price models, tiers, billing periods). Passed through verbatim — see the plan-prices endpoints in the [API Reference](/api-reference/introduction) for the shape. |
> [!NOTE]
>
> A pulled config reproduces your remote prices in full, including nested charge structures. To author one by hand, start from `kelviq init --template saas|usage|license` or pull an existing plan and adapt it. Fields you leave out — `freeTrial`, `taxBehavior`, a charge's `paymentType` or `reset` — take the server's defaults, and `kelviq push` compares prices only on the fields you set, so leaving them out never shows up as a change.
***
## Schemas as values
Everything the builders validate against is exported from `@kelviq/cli/config` if you want to reuse it — `productDefSchema`, `featureDefSchema`, `planDefSchema`, `entitlementDefSchema`, `priceDefSchema`, `licenseDefSchema`, `kelviqConfigSchema`, the enums (`taxCodeSchema`, `featureTypeSchema`, `priceTypeSchema`, `resetSchema`, `taxBehaviorSchema`, `durationUnitSchema`, `identifierSchema`), and their TypeScript types (`ProductDef`, `FeatureDef`, `PlanDef`, `LicenseDef`, and so on). They're standard [Zod](https://zod.dev) schemas.
# Kelviq CLI
Source: https://docs.kelviq.com/cli/overview
Pricing-as-code for Kelviq — define your products, features, and plans in TypeScript and sync them with your account from the terminal.
> [!NOTE]
>
> **The Kelviq CLI is in beta.** It's fully functional — and safe by design, with previews and confirmations before every write — but commands, flags, and the `--json` output shape may still change between releases. Pin an exact version in CI and review the release notes before upgrading.
> [!TIP]
>
> Working with an AI coding agent? The [Agent Toolkit](/guides/agent-toolkit)'s `pricing-as-code` skill teaches agents this exact workflow — including the rule that agents preview diffs freely but never apply changes without your approval.
The Kelviq CLI (`@kelviq/cli`) lets you manage your pricing catalog as **code** instead of dashboard clicks. Your products, features, plans, entitlements, and prices live in a single typed TypeScript file — `kelviq.config.ts` — that you can review, diff, and version-control like the rest of your codebase.
```ts theme={null}
import { product, feature, plan } from '@kelviq/cli/config';
export const app = product({
identifier: 'my-app',
name: 'My App',
taxCode: 'saas',
});
export const historyRetention = feature({
identifier: 'history-retention',
name: 'History retention (days)',
type: 'CUSTOMIZABLE',
});
export const pro = plan({
identifier: 'pro',
name: 'Pro',
product: 'my-app',
entitlements: [{ feature: 'history-retention', value: 30 }],
});
```
The loop is complete in both directions: `kelviq pull` turns a live environment into a config file, and `kelviq push` deploys a config file back — with a diff preview, draft-first writes, and explicit gates before anything customer-facing changes. `kelviq promote` moves your whole sandbox catalog to production the same way.
***
## Why pricing-as-code?
Your pricing model decides what customers are billed, what features they can reach, and what limits they hit. It is production infrastructure. But when it lives only in a dashboard, it's managed unlike any other production infrastructure you run:
* **Changes are invisible to your engineering workflow.** A plan edit doesn't show up in a pull request, doesn't get a review, and doesn't appear in `git log`. The first your team hears of it may be a support ticket.
* **There's no meaningful diff.** "What exactly changed in our Pro plan last quarter?" has no good answer when the only record is the current dashboard state.
* **Environments drift silently.** Sandbox and production start identical and slowly diverge — a limit tweaked in one, a price fixed in the other — until "it worked in sandbox" stops meaning anything.
* **The model lives in people's heads.** Understanding your own packaging means clicking through screens, plan by plan, feature by feature.
A `kelviq.config.ts` under version control changes each of those:
| Without | With pricing-as-code |
| - | - |
| Dashboard edits leave no trail | Every change is a commit — author, date, diff, and the PR discussion that approved it |
| Pricing changes skip review | Pricing changes go through the same pull-request review as any other production change |
| Sandbox/production drift is invisible | Pull both environments and `diff` them — drift is a one-command check |
| The catalog is spread across dashboard screens | The whole monetization model is one readable file — onboarding is "read this" |
| Mistakes surface at runtime, via the API | The file is **typed**: a misspelled field or invalid enum is a compile-time error with a "did you mean" suggestion, before Kelviq ever sees it |
The concepts in the file map one-to-one to Kelviq's own — products, features, plans, entitlements, prices. If you know the [core concepts](/quickstart/core-concepts), you already know the format.
***
## What you can do today
### Put your catalog under version control
If you've already built your pricing in the dashboard, one command gets it into git:
```shell theme={null}
kelviq pull
git add kelviq.config.ts && git commit -m "Snapshot pricing catalog"
```
From here on, re-running `kelviq pull --force` and committing captures every catalog change as a diff. Run it on a schedule in CI and dashboard edits stop being invisible — they become commits your team can read.
### Catch drift between sandbox and production
Pull both environments and compare:
```shell theme={null}
kelviq pull -o sandbox.config.ts --force
kelviq pull --prod -o production.config.ts --force
diff sandbox.config.ts production.config.ts
```
Because the config format references everything by stable `identifier` slugs — never per-environment UUIDs — the two files are directly comparable. A clean diff means your environments genuinely match; anything else shows you exactly where they don't. Run it before a launch, after a migration, or whenever "it worked in sandbox" needs verifying.
### Review pricing changes like code — then deploy them
Propose a pricing change by editing `kelviq.config.ts` in a pull request — the reviewer sees exactly what changes: the new limit, the removed entitlement, the price that swapped. After the merge, deploying is one command with a preview built in:
```shell theme={null}
kelviq push --dry-run # exactly what would change, zero writes
kelviq push # apply as drafts, after showing the diff and asking
kelviq push --publish # and publish the changed plans, after a second confirmation
```
Everything push writes is a **draft** by default — nothing a customer sees changes until you publish, and migrating existing customers to new entitlements or pricing always requires its own explicit flags. Pricing changes get the full GitOps treatment: PR → review → merge → push, with the dashboard as a viewer rather than the editor.
### Promote sandbox to production
Build and test your pricing in sandbox, then move it to production in one reviewed step:
```shell theme={null}
kelviq promote --dry-run # read-only: exactly what production would change
kelviq promote # apply it, drafts first, confirmation required
```
Promote matches resources across environments by identifier — no files, no UUID bookkeeping — and inherits every one of push's safety rails.
### Give your AI tools your pricing model
A typed, self-contained TypeScript file is something coding assistants are genuinely good at working with — ask one to "add a Team plan between Pro and Enterprise with 25 seats" and the builders' types keep it honest; anything invalid fails to compile. Pair the CLI with the [Kelviq MCP server](/guides/mcp-server) and your assistant can read the config for context *and* apply catalog changes through the API today.
### Start a new project from a template
```shell theme={null}
kelviq init
```
Writes a complete starter `kelviq.config.ts` to edit by hand — useful when you're designing pricing for a new product and want to sketch the model in code (with autocomplete and validation) before anything exists in Kelviq. `--template usage` and `--template license` start from a metered or a license-key catalog instead of the default SaaS one.
***
## How it fits with the rest of Kelviq
Each Kelviq surface has a distinct job — the CLI doesn't replace the others:
| Tool | Use it for |
| - | - |
| **CLI** (this) | Managing the **catalog itself** — the definition of your products, features, plans, and prices — as versioned code. |
| [Dashboard](https://app.kelviq.com) | Visual management, analytics, customer operations, feature renames — and reviewing the drafts push creates. |
| [Backend](/backend-integration/node-sdk) & [frontend SDKs](/frontend-integration/react-sdk) | Your **application's** runtime integration — checkouts, entitlement checks, usage reporting inside your product. |
| [MCP server](/guides/mcp-server) | Driving the Kelviq API conversationally from Claude, Cursor, or any MCP client. |
| [REST API](/api-reference/introduction) | Everything, programmatically — the layer all of the above are built on. |
A useful rule of thumb: SDKs are how your **product** talks to Kelviq at runtime; the CLI is how your **team** manages what Kelviq is configured to sell.
***
## Installation
Requires Node.js 18 or newer.
```shell theme={null}
npm install -g @kelviq/cli
```
Verify the install:
```shell theme={null}
kelviq --version
```
***
## Quickstart
### Step 1: Authenticate
```shell theme={null}
kelviq login
```
`login` opens the dashboard's [API keys page](https://app.kelviq.com/settings/api-keys) in your browser and prompts you to paste a **Server API Key**. Input is masked and the key is stored locally with owner-only file permissions. If the key is already in your project's `.env.local` or `.env` as `KELVIQ_SERVER_API_KEY`, skip this step: the CLI reads it from there.
The CLI targets your **sandbox** environment by default. To store a production key too, run `kelviq login --prod`. See [Environments & authentication](/cli/environments) for how keys are stored and how to use environment variables instead (for example, in CI).
### Step 2: Pull your catalog
```shell theme={null}
kelviq pull
```
`pull` fetches your sandbox catalog — products, features, plans, entitlements, and prices — and writes it as a `kelviq.config.ts` in the current directory. If you've already set up pricing in the dashboard, this is the fastest way to get it under version control.
### Step 3: Or start from scratch
```shell theme={null}
kelviq init
```
`init` writes a starter `kelviq.config.ts` you can edit by hand. The file is plain TypeScript with full autocomplete — misspell a field and the compiler tells you before Kelviq ever sees it.
### Step 4: Check where you're pointed
```shell theme={null}
kelviq env --check
```
Prints which environment the CLI targets, which keys are configured and which env files were read, and with `--check` confirms each key is accepted by the API — useful before any command that touches your account. Keys can also live in the project's `.env.local` or `.env`: every command reads `KELVIQ_*` variables from there, so an app's existing env file is enough.
### Step 5: Make a change and deploy it
Edit the config — bump a limit, add a plan — then:
```shell theme={null}
kelviq push --dry-run # see exactly what would change
kelviq push # apply it as drafts, after confirmation
```
When you're happy, `kelviq push --publish` makes the changed plans live, and `kelviq promote` carries the whole catalog from sandbox to production.
***
## The commands
| Command | What it does |
| - | - |
| [`kelviq init`](/cli/commands#kelviq-init) | Write a starter `kelviq.config.ts`. |
| [`kelviq login`](/cli/commands#kelviq-login) | Paste and store a Server API key. |
| [`kelviq logout`](/cli/commands#kelviq-logout) | Remove stored API keys. |
| [`kelviq env`](/cli/commands#kelviq-env) | Show the configured environment and key status. |
| [`kelviq pull`](/cli/commands#kelviq-pull) | Write your remote catalog to `kelviq.config.ts`. |
| [`kelviq push`](/cli/commands#kelviq-push) | Deploy a `kelviq.config.ts`: diff, confirm, draft-first writes, optional publish and prune. |
| [`kelviq promote`](/cli/commands#kelviq-promote) | Move the sandbox catalog to production with the same preview and gates. |
| [`kelviq status`](/cli/commands#kelviq-status) | Show the live catalog: products, features, each plan's latest version with price and entitlement counts. |
| [`kelviq webhook`](/cli/commands#kelviq-webhook) | Register the app's webhook endpoint and write its signing secret into an env file without printing it. |
| [`kelviq discount`](/cli/commands#kelviq-discount) | Create and list discount codes, with the product given by identifier. |
See the full [command reference](/cli/commands) for every flag, and the [`kelviq.config.ts` reference](/cli/kelviq-config) for the file format.
***
## What's coming
* **Browser-based login** — a pairing flow that mints a scoped key from your dashboard session, replacing paste-a-key.
# Managing payment methods
Source: https://docs.kelviq.com/customer-portal/managing-payment-methods
How your customers add, update, and remove payment methods
## Multiple customer records with one email
More than one customer record can use the same email address. Each record has a unique customer ID and keeps its own subscriptions, payment methods, and billing details.
If a customer sees more than one record in the portal, they should select the record connected to the subscription they want to manage.
## How Kelviq chooses a payment method
When a subscription renews, Kelviq first charges the payment method selected when the subscription was created.
If that payment method fails because it has expired, has insufficient funds, or cannot be processed, Kelviq uses the saved default payment method as a fallback.
## How customers pay a failed card invoice
If an automatic card payment fails, your customer gets an email with a link to pay the invoice. They can also open **Billing settings** in the customer portal sidebar to add or update a payment method.
Paying an invoice manually does not turn off automatic payments. The customer's payment authorization remains active for future renewals.
See [Failed subscription payments](/product-catalog/subscriptions#failed-subscription-payments) for the card retry schedule.
## How customers manage payment methods
### Add a payment method
1. The customer opens **Settings** from the main menu and selects **Billing**.
2. They select the customer record they want to update.
3. They click **Add Payment Method**.
4. They enter the payment details and click **Save**.
The new payment method appears under **Saved payment methods**.
### Set the default payment method
The default payment method is used as a fallback if the payment method attached to a subscription fails.
1. The customer opens **Billing settings** in the customer portal sidebar.
2. They find the payment method they want to use as the default.
3. They click the star icon next to it.
The portal adds a **Default** badge to the selected payment method.
### Delete a payment method
Customers cannot delete their current default payment method. They must set another payment method as the default first.
1. The customer opens **Billing settings** in the customer portal sidebar.
2. They find the payment method they want to remove.
3. They click the trash icon next to it.
# Customer portal
Source: https://docs.kelviq.com/customer-portal/overview
A self-serve hub for your customers to manage subscriptions and access content.
Kelviq provides a hosted customer portal.
This is a self-serve environment. Your customers manage their own subscriptions, access files, and handle billing details. You don't need to build this. It ensures you don't have to manually answer support tickets for basic tasks like upgrading plans or downloading invoices.
## Access and login
The portal uses passwordless, secure access.
**1. Purchase and invite**
When a customer completes a purchase, they get access immediately.
**2. Login**
Customers enter their email address on your portal login page.
**3. Secure entry**
We send a one-time access code to their email. They enter this code to log in. No passwords to remember.

## My orders
This is the home screen. It lists everything the customer owns. They can click on a specific product to see details.

### Product details
Inside a specific product, customers find their deliverables.
* **Digital downloads**
Access files, e-books, or software binaries.
* **External links**
Private links to Discord servers or Notion templates.
* **License management**
View license keys. If you enforce seat limits, customers can see active devices and revoke licenses to free up seats for new team members.

## Subscription management
Customers have control over their lifecycle without contacting you.
* **Change plans**
Upgrade to a higher tier or downgrade.
* **Pause and cancel**
Pause a subscription temporarily or stop it entirely. With [retention offers](/customer-portal/retention) on, customers see an offer before they cancel.
* **Payment methods**
Update credit card details or switch methods for future renewals.

## Billing settings
This section handles compliance and admin tasks.
### Invoices and receipts
Customers can view their full payment history. They can download PDF invoices and receipts for their own accounting.

### Account details
This is where they manage the legal side of the account.
* **Tax IDs**
Update billing addresses and VAT/Tax IDs.
* **Refund status**
Track the status of partial or full refunds.
* **Account transfer**
Transfer ownership of the account and subscription to a different email address.

# Retention offers
Source: https://docs.kelviq.com/customer-portal/retention
Ask customers why they're cancelling and make an offer to keep them
When a customer cancels a subscription in the [customer portal](/customer-portal/overview), Kelviq asks why, then shows the offer you picked for that reason: a discount, a pause, or a link to your support team. If the customer says no, they continue to cancel.
Set it up in **Settings → Retention** and turn on **Enable retention offer**.
## What the customer sees
**1. They pick a reason**

**2. They see your offer**
The offer depends on the reason. Here, **Not using it enough** leads to a pause.

**3. They confirm or keep the subscription**
A customer who declines the offer chooses when access ends: at the end of the billing period, immediately, or on a date they pick. If you've turned on written feedback, your question appears here too.

## Set up your offers
Set each offer up once. Then choose which reasons show it.

| Offer | What you set | What happens if the customer accepts |
| - | - | - |
| Discount | A percentage off and how many billing cycles it lasts | The plan stays the same and the next payments are discounted. After that, the price goes back to normal |
| Pause | Which lengths to offer: 1, 3, or 6 months | Billing and access stop for the period they choose, then resume. See [Pause a subscription](/product-catalog/subscriptions#pause-a-subscription) |
| Support | A link to a booking page, live chat, or your help site | They go to that link. Nothing changes on the subscription |
Use the eye icon next to an offer to preview it.

## Match offers to reasons
Under **Offers by cancellation reason**, pick an offer for each reason, or **No offer** to let the customer go straight to the final step.

| Reason | Example offer |
| - | - |
| Too expensive | Discount |
| Not using it enough | Pause |
| Missing features | Support |
| Switching to another product | No offer |
| Technical issues | Support |
| Poor support experience | Support |
| Business or project ended | No offer |
| Other | No offer |
> [!TIP]
>
> Offer what fits the reason. A customer who isn't using the product won't stay for a discount, but might come back after a pause.
## Ask for written feedback
Turn on **Written feedback** to ask one open question before the customer confirms, such as "What could we do better?". Answering is optional. The answer is saved as the cancellation comment on the subscription.
## When an offer isn't available
An offer the customer can't use still appears, greyed out, with the reason. A customer sees an offer only once per subscription unless you change the repeat policy below, and offers aren't available on free or manually billed subscriptions.
A pause has to cover at least one full billing period, so a 1-month pause isn't offered on a yearly plan, and a yearly plan usually gets no pause offer at all. Only an active subscription can be paused: not one on trial, past due, or already scheduled to cancel.
## Advanced settings
| Setting | Options |
| - | - |
| Resume billing behavior | **Reset billing cycle** (default) charges a full period on the day the subscription resumes and makes that the new billing day. **Keep billing day** charges nothing on resume and waits for the original billing day. A customer who resumes inside a period they've already paid for is never charged again, whichever you choose |
| Offer repeat policy | **Once per subscription** (default), **After previous offer ends**, or **Unlimited**. Controls whether a customer who accepted an offer can accept it again on a later cancellation. A plan change doesn't reset this |
## Related documentation
* [Customer portal](/customer-portal/overview)
* [Subscriptions](/product-catalog/subscriptions)
* [Discounts](/product-catalog/discounts)
# Overview
Source: https://docs.kelviq.com/customers-management/overview
View and manage customer details, subscriptions, and entitlements.
Managing customers sits at the center of Kelviq. Each customer connects directly to their subscriptions, the plans behind them, and the features they own. You need to understand these relationships to handle the full customer lifecycle.
## View customer list
Select **Customers** in the main navigation to see your list.
You can do a few things here.
* **Search** by name, email, or identifier.
* **Add a new customer** with **New customer** in the top right.
* **Manage a customer** by clicking their row to see details.

## Add a new customer
You can create records manually.
**1. Open the modal**
Click **+ New customer** in the top right of the list page.
**2. Fill in details**
You need to provide a few key pieces of info.
* **Customer name** is required. Use the person's full name or company name.
* **Identifier** is the unique ID the API uses. We auto-generate this based on the name, but you can edit it. Linking this to the ID in your own database is usually a good idea.
* **Email address** is optional.
**3. Create**
Click **Create** to finish.
If you are adding a batch of people, toggle on **Create more**. This keeps the window open so you can add the next one immediately.

> [!NOTE]
>
> More than one customer record can use the same email address, but every record must have a unique **Identifier**. The API calls this value `customerId`.
>
> When recreating a test customer, you can reuse the email address, but you must provide a new Identifier. An Identifier cannot be reused after its customer is archived. If you receive a `customer already exists` error, check the Identifier first.
## Manage existing customers
You can edit details or revoke access from the list view.
### Edit a customer
Click the **three dots** on the right side of the customer row and select **Edit**.

You can update the **Customer name** and **Email address**. The Identifier is fixed after creation and cannot be changed.
### Archive a customer
Archiving makes a customer inactive and revokes their access. Click the **three dots** and select **Archive**.

You will see a confirmation warning. Click **Confirm** to proceed.
Kelviq archives customers; it does not hard-delete them. The archived record keeps its Identifier, so that value cannot be reused. To create another customer with the same email address, use a new Identifier.
> [!WARNING]
>
> You cannot archive a customer with an active subscription. You must cancel their subscription first.
## Customer details
Click on any customer row to open their full profile.
This page holds the source of truth for that user. You can see their unique Billing ID and timestamps for when they joined or were last updated.

### Subscriptions
This section lists active and past plans. It shows the status, such as Active or Canceled, and the relevant dates. For a canceled subscription, the date it was canceled appears alongside the status.
### Files, links, and license keys
The customer detail page shows the deliverables the customer received through their plans:
* **Files** — View and download digital files available to the customer.
* **Links** — Open private or external resource links attached to the plan.
* **License keys** — View issued license keys and use the available license-management controls.
These sections let support teams confirm access without opening each order separately.
For catalog setup, see [Digital Downloads](/product-catalog/digital-downloads), [Links](/product-catalog/links), and [License Key Management](/product-catalog/license-keys).
### Entitlements
This view shows exactly what the customer can do right now based on their plan.
**Overrides**
You often need to make exceptions for specific users. You can do that here without changing the underlying plan.
* Change the value of a specific feature.
* Update the usage limit for a Meter feature.
Changes made here apply only to this specific customer.

# JavaScript SDK
Source: https://docs.kelviq.com/frontend-integration/js-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 [React SDK](/frontend-integration/react-sdk) instead.
### 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}
```
### 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.
```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 | Default | 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.
### 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 | 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`
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>`
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`
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}
Loading...Loading...Loading...
```
```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"`, `"YEARLY"`)
* 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", "YEARLY"
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 (
{canUseAdvancedSearch && }
);
```
### 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(config: ApiRequestConfig): Promise> {
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
Set `environment: "sandbox"` when you call `kelviqSDK()`, and use a sandbox Client API Key.
```typescript theme={null}
const kq = kelviqSDK({
customerId: "your-customer-id",
accessToken: "your-access-token",
environment: "sandbox"
});
```
# Promotions UI JS SDK
Source: https://docs.kelviq.com/frontend-integration/promotions-ui-js
Add purchasing power parity discounts and a localised banner to any website
The Promotions UI JS SDK automatically applies location-based discounts to prices on your page and renders a customisable banner that tells visitors about their local deal. It works with plain HTML — no framework required.
> [!NOTE]
>
> This SDK is separate from the [entitlements JS SDK](/frontend-integration/js-sdk). Use this SDK when you need PPP / country-based discount banners and price display.
***
## Installation
### 1. Add the Script
Paste this snippet just before the closing `` tag on your pricing page:
```html theme={null}
```
### 2. Initialize the SDK
Copy your promotion ID from the promotion detail page and call `init()`:

```javascript theme={null}
KQPromotionUI.init({
// Replace with your promotion ID
promotionId: "YOUR_PROMOTION_ID",
// Access token can be found in the Settings → API keys section
accessToken: "YOUR_CLIENT_ACCESS_TOKEN",
showBanner: true,
});
```
This will automatically display a discount banner if the visitor is eligible for a discount.
> [!WARNING]
>
> **Never expose your Server API Key in the browser.** Use the **Client API Key**, which is safe for client-side code. You can find both keys under **Settings → API keys** in the [Kelviq dashboard](https://app.kelviq.com/settings/api-keys).
***
## `init()` options
| Option | Type | Default | Description |
| - | - | - | - |
| `promotionId` | `string` | — | Promotion ID from the Kelviq dashboard |
| `accessToken` | `string` | — | Client API Key (Settings → API keys) |
| `showBanner` | `boolean` | `true` | Render the localised discount banner automatically |
| `environment` | `"production" \| "sandbox"` | `"production"` | Target environment |
| `baseCurrencyCode` | `string` | `"USD"` | ISO 4217 code for prices displayed on the page |
| `baseCurrencySymbol` | `string` | `"$"` | Symbol written to `data-kq-currency-symbol` elements |
| `currencyDisplay` | `"symbol" \| "code" \| "name"` | `"symbol"` | How the currency is formatted via `Intl.NumberFormat` |
| `showDecimal` | `boolean` | `false` | Include the decimal portion in formatted prices |
| `minimumDecimalDigits` | `number` | `2` | Minimum fraction digits (0–2) |
| `maximumDecimalDigits` | `number` | `2` | Maximum fraction digits (0–2) |
| `banner.*` | object | — | Customization for the discount banner |
| `banner.unStyled` | boolean | false | if true, no styling will be applied to the banner |
| `banner.addCloseIcon` | boolean | true | Shows or hides the close button. Works only when the close button is enabled in the promotion's banner settings in Kelviq. |
| `banner.placement` | `top\|bottom` | 'top' | The placement of the discount banner |
| `banner.container` | string | null | The container for the discount banner |
| `banner.backgroundColor` | string | '#f0f0f0' | The background color of the discount banner |
| `banner.fontColor` | string | '#333333' | The font color of the discount banner |
| `banner.borderRadius` | string | '0px' | The border radius of the discount banner |
| `banner.fontSize` | string | '14px' | The font size of the discount banner |
***
## Auto price update
After `init()` resolves, every element with `data-kq-price` on the page is automatically updated with the discounted price for the visitor's country.
### Basic usage
```html theme={null}
```
> [!TIP]
>
> The original-price element is hidden by default. The SDK shows it automatically when a discount is active and hides it when the visitor's country has no discount.
### Price container attributes
| Attribute | Value | Description |
| - | - | - |
| `data-kq-price` | `number` | Base price — the discounted value is computed and written to child elements |
| `data-kq-original-price-display` | `number` | Original (pre-discount) price; hidden when there is no active discount |
| `data-kq-rel` | `string` | Links a price element and its original-price element so they update together |
| `data-kq-show-decimal` | `"true" \| "false"` | Per-element decimal override (falls back to `showDecimal` from `init()`) |
| `data-kq-currency-display` | `"symbol" \| "code" \| "name"` | Per-element currency format |
| `data-kq-minimum-decimal-digits` | `number` | Per-element minimum fraction digits (0–2) |
| `data-kq-maximum-decimal-digits` | `number` | Per-element maximum fraction digits (0–2) |
### Sub-element attributes
Place these inside a price container to receive individual parts of the formatted price:
| Attribute | Example output |
| - | - |
| `data-kq-currency-symbol` | `$` |
| `data-kq-price-integer` | `49` |
| `data-kq-price-decimal` | `99` |
| `data-kq-price-decimal-separator` | `.` |
| `data-kq-currency-code` | `USD` |
| `data-kq-price-formatted` | `$49.99` |
***
## JS API
All methods return Promises and require `init()` to have been called first.
### `getUpdatedPrice(price, options?)`
Computes the discounted price and returns a `PriceObject` without touching the DOM.
```javascript theme={null}
const result = await KQPromotionUI.getUpdatedPrice(100, { showDecimal: true });
console.log(result.formattedPrice); // "$80.00"
console.log(result.price); // 80
```
**Parameters**
| Parameter | Type | Description |
| - | - | - |
| `price` | `number` | Base price to apply the discount to |
| `options` | `Partial` | Optional overrides (same fields as `init()`) |
**Returns — `PriceObject`**
| Field | Type | Description |
| - | - | - |
| `price` | `number` | Final price after discount |
| `formattedPrice` | `string` | Fully formatted string, e.g. `$80.00` |
| `integerPart` | `string` | Integer portion only, e.g. `80` |
| `decimalPart` | `string` | Decimal digits only, e.g. `00` |
| `decimalSeparator` | `string` | Locale-specific separator, e.g. `.` |
| `currencySymbol` | `string` | Currency symbol, e.g. `$` |
| `currencyCode` | `string` | ISO 4217 code, e.g. `USD` |
| `apiResponse` | `PricingData` | Full API response including country, percentage, and widgets |
***
### `updatePriceElement(element, options?)`
Updates a specific DOM element with the computed discounted price. The element **must** have a `data-kq-price` attribute. All elements linked to it via `data-kq-rel` are updated as well.
```javascript theme={null}
const el = document.getElementById("pro-price");
const result = await KQPromotionUI.updatePriceElement(el, { showDecimal: true });
console.log(result.formattedPrice);
```
**Parameters**
| Parameter | Type | Description |
| - | - | - |
| `element` | `HTMLElement` | Must have `data-kq-price` attribute |
| `options` | `Partial` | Optional overrides |
Returns a `PriceObject` (same shape as `getUpdatedPrice`).
***
### `updatePrice(configs[])`
Batch-update multiple elements, optionally rendering each with an HTML template.
```javascript theme={null}
await KQPromotionUI.updatePrice([
{
element: document.getElementById("monthly"),
price: 29,
options: { showDecimal: false },
template: "
",
},
]);
```
**Config object fields**
| Field | Type | Description |
| - | - | - |
| `element` | `HTMLElement` | Target element — its `innerHTML` is replaced with the rendered template |
| `price` | `number` | Base price |
| `options` | `Partial` | Optional overrides |
| `template` | `string` | HTML string with `{{key}}` placeholders |
**Available template variables** (any field from `PriceObject`):
`{{price}}` · `{{formattedPrice}}` · `{{integerPart}}` · `{{decimalPart}}` · `{{decimalSeparator}}` · `{{currencySymbol}}` · `{{currencyCode}}`
Returns `Promise` — one entry per config.
***
## CSS hooks
The SDK adds classes to `document.body` that you can target in your stylesheets:
| Class | Added when |
| - | - |
| `kq-discount-applied` | An active discount > 0% exists for the visitor's country |
| `kq-has-parity-banner` | The discount banner has been injected into the page |
```css theme={null}
/* Show a sale badge only when a discount is active */
.sale-badge { display: none; }
body.kq-discount-applied .sale-badge { display: inline-block; }
/* Add top padding so content isn't hidden under the banner */
body.kq-has-parity-banner main { padding-top: 60px; }
```
***
## Sandbox
Set `environment: "sandbox"` to test against the Kelviq sandbox environment.
```javascript theme={null}
KQPromotionUI.init({
promotionId: "YOUR_PROMOTION_ID",
accessToken: "YOUR_CLIENT_ACCESS_TOKEN",
environment: "sandbox",
});
```
Use sandbox API keys (available under **Settings → API keys** with the Sandbox toggle enabled) so real visitor data is never affected during testing.
# React Promotions UI
Source: https://docs.kelviq.com/frontend-integration/react-promotions-ui
Add purchasing power parity discounts and a localised banner to your React app
The `@kelviq/react-promotions-ui` SDK provides React components for displaying location-based promotional banners and dynamically discounted prices. It handles API communication, discount calculation, and currency formatting out of the box.
* A **React Context Provider** (`KQProvider`) to fetch and share promotion data
* A **banner component** (`KQBanner`) with template variables and full styling control
* **Price components** for formatted display of discounted prices
* **Granular price components** for building fully custom price layouts
> [!NOTE]
>
> This SDK is separate from the [entitlements React SDK](/frontend-integration/react-sdk) and the [React UI SDK](/frontend-integration/react-ui). Use this SDK when you need PPP / country-based discount banners and price display in a React app.
***
## Installation
```bash theme={null}
npm install @kelviq/react-promotions-ui
# or
yarn add @kelviq/react-promotions-ui
```
Requires `react` and `react-dom` as peer dependencies (v17, v18, or v19).
***
## Quick start
Wrap your app with `KQProvider` and place `KQBanner` where you want the banner to appear.
```tsx theme={null}
import { KQProvider, KQBanner } from '@kelviq/react-promotions-ui';
function App() {
return (
{/* Your app components */}
);
}
```
> [!WARNING]
>
> **Never expose your Server API Key in the browser.** Use the **Client API Key**, which is safe for client-side code. You can find both keys under **Settings → API keys** in the [Kelviq dashboard](https://app.kelviq.com/settings/api-keys).
***
## `KQProvider`
The provider initializes the SDK, fetches promotion data from the Kelviq API, and makes it available to all child components via React Context.
### Props
| Prop | Type | Default | Description |
| - | - | - | - |
| `promotionId` | `string` | `''` | The promotion ID from the Kelviq dashboard |
| `accessToken` | `string` | `''` | Client API Key (Settings → API keys) |
| `kqIdentifier` | `string` | `''` | Optional Kelviq identifier for the request |
| `environment` | `'production' \| 'sandbox'` | `'production'` | Set to `'sandbox'` to use the sandbox API |
| `baseCurrencyCode` | `string` | `'USD'` | ISO 4217 currency code used for formatting |
| `baseCurrencySymbol` | `string` | `'$'` | Currency symbol for display |
| `config` | `object` | `{}` | Advanced configuration (see below) |
### Advanced config
The optional `config` prop accepts:
| Field | Type | Default | Description |
| - | - | - | - |
| `apiUrl` | `string` | — | Override the default API endpoint |
| `onError` | `(error: Error) => void` | — | Error callback for API failures |
| `maxRetries` | `number` | `3` | Maximum retry attempts for failed requests |
| `timeout` | `number` | `5000` | Request timeout in milliseconds |
### Sandbox
Set `environment` to `'sandbox'` to test against the Kelviq sandbox environment:
```tsx theme={null}
{/* ... */}
```
***
## `KQBanner`
Displays a location-aware promotional banner. The banner message is configured in the Kelviq dashboard and supports template variables that are automatically replaced with real data.
```tsx theme={null}
import { KQBanner } from '@kelviq/react-promotions-ui';
// Basic — uses styles from the dashboard
// With local style overrides
console.log('Banner closed')}
/>
```
### Props
| Prop | Type | Default | Description |
| - | - | - | - |
| `backgroundColor` | `string` | *(from API)* | Banner background color |
| `fontColor` | `string` | *(from API)* | Banner text color |
| `borderRadius` | `string` | *(from API)* | Banner border radius |
| `fontSize` | `string` | *(from API)* | Banner font size |
| `unStyled` | `boolean` | *(from API)* | If `true`, no default styles are injected |
| `addCloseIcon` | `boolean` | *(from API)* | Show a close button on the banner |
| `className` | `string` | `''` | Custom CSS class name |
| `onClose` | `() => void` | — | Callback fired when the banner is closed |
> [!TIP]
>
> Props override the corresponding values returned by the API. If you don't pass a prop, the dashboard-configured value is used.
### Template variables
The banner message (configured in the dashboard) supports these placeholders:
| Variable | Replaced with | Example |
| - | - | - |
| `{country}` | Visitor's country name | `United States` |
| `{country_flag}` | Country flag image (CDN-hosted SVG) | 🇺🇸 *(rendered as ``)* |
| `{coupon_code}` | The discount coupon code | `SAVE20` |
| `{discount_percentage}` | The discount percentage | `20` |
**Example message:**
```
Hello {country_flag} You're visiting from {country}! Use code "{coupon_code}" for {discount_percentage}% off.
```
### CSS hooks
The banner adds classes to `document.body` that you can target in your stylesheets:
| Class | Added when |
| - | - |
| `kq-has-banner` | The banner is visible on the page |
```css theme={null}
/* Add top padding so content isn't hidden behind the banner */
body.kq-has-banner main {
padding-top: 60px;
}
```
### Unstyled mode
Set `unStyled={true}` to remove all default styles and apply your own CSS from scratch. Target the banner using the `kq-banner` CSS class or the `data-testid="kq-banner"` attribute.
```tsx theme={null}
```
***
## `KQPriceFormatted`
Displays a fully formatted discounted price based on the visitor's location.
```tsx theme={null}
import { KQPriceFormatted } from '@kelviq/react-promotions-ui';
// Discounted price
// With decimals
// Currency code instead of symbol
```
### Props
| Prop | Type | Default | Description |
| - | - | - | - |
| `price` | `number` | — | **Required.** The product price |
| `displayPrice` | `number` | — | The price shown for original display (used with `isOriginalDisplay`) |
| `isOriginalDisplay` | `boolean` | `false` | Renders as strikethrough original price for comparison |
| `className` | `string` | `''` | Custom CSS class name |
| `currencyDisplay` | `'symbol' \| 'code' \| 'name'` | `'symbol'` | How the currency is displayed |
| `showDecimal` | `boolean` | `false` | Whether to show decimal digits |
| `minimumDecimalDigits` | `number` | `2` | Minimum decimal digits when `showDecimal` is `true` |
| `maximumDecimalDigits` | `number` | `2` | Maximum decimal digits (max 2) |
### Original + discounted price
Show a strikethrough original price next to the discounted price:
```tsx theme={null}
{/* Original price (strikethrough) */}
{/* Discounted price */}
```
***
## Granular price components
Use these components individually to build custom price layouts with full styling control. All components must be rendered inside a `KQProvider`.
### `KQPriceInteger`
Displays just the integer part of the discounted price.
```tsx theme={null}
import { KQPriceInteger } from '@kelviq/react-promotions-ui';
```
| Prop | Type | Default | Description |
| - | - | - | - |
| `price` | `number` | — | **Required.** The product price |
| `className` | `string` | `''` | Custom CSS class name |
### `KQPriceDecimal`
Displays just the decimal portion of the discounted price.
```tsx theme={null}
import { KQPriceDecimal } from '@kelviq/react-promotions-ui';
```
| Prop | Type | Default | Description |
| - | - | - | - |
| `price` | `number` | — | **Required.** The product price |
| `className` | `string` | `''` | Custom CSS class name |
| `minimumDecimalDigits` | `number` | `2` | Minimum decimal digits |
| `maximumDecimalDigits` | `number` | `2` | Maximum decimal digits (max 2) |
### `KQPriceDecimalSeparator`
Renders the locale-specific decimal separator (e.g. `.` or `,`).
```tsx theme={null}
import { KQPriceDecimalSeparator } from '@kelviq/react-promotions-ui';
```
| Prop | Type | Default | Description |
| - | - | - | - |
| `price` | `number` | — | **Required.** The product price |
| `className` | `string` | `''` | Custom CSS class name |
### `KQPriceCurrencySymbol`
Renders the currency symbol (e.g. `$`, `€`, `₹`).
```tsx theme={null}
import { KQPriceCurrencySymbol } from '@kelviq/react-promotions-ui';
```
| Prop | Type | Default | Description |
| - | - | - | - |
| `className` | `string` | `''` | Custom CSS class name |
### `KQPriceCurrencyCode`
Renders the currency code (e.g. `USD`, `EUR`, `INR`).
```tsx theme={null}
import { KQPriceCurrencyCode } from '@kelviq/react-promotions-ui';
```
| Prop | Type | Default | Description |
| - | - | - | - |
| `className` | `string` | `''` | Custom CSS class name |
### Custom price layout example
Combine granular components to build a fully custom price display:
```tsx theme={null}
import {
KQPriceCurrencySymbol,
KQPriceInteger,
KQPriceDecimalSeparator,
KQPriceDecimal,
KQPriceCurrencyCode,
} from '@kelviq/react-promotions-ui';
```
***
## Complete example
```tsx theme={null}
import {
KQProvider,
KQBanner,
KQPriceFormatted,
KQPriceCurrencySymbol,
KQPriceInteger,
KQPriceDecimalSeparator,
KQPriceDecimal,
} from '@kelviq/react-promotions-ui';
function PricingPage() {
return (
{/* Promotional banner */}
{/* Formatted price with original + discounted */}
{/* Custom price layout with granular components */}
);
}
```
***
## Support
For technical support and questions, reach out at [hi@kelviq.com](mailto:hi@kelviq.com).
# React SDK
Source: https://docs.kelviq.com/frontend-integration/react-sdk
Documentation for the Kelviq React SDK
The `@kelviq/react-sdk` provides a seamless way to integrate Kelviq entitlement management into your React applications.
## Overview
* A **React Context Provider** (`KelviqProvider`) to initialize and configure the SDK.
* **Hooks** to easily access entitlement status, pricing, and data within your components.
* **Conditional rendering components** to show or hide UI elements based on feature access.
* **Pricing components** (`KQPrice`, `KQFeatureList`) for displaying localized plan pricing and features.
* Automatic and manual **data fetching** and caching mechanisms.
The SDK fetches all entitlements for a given organization and customer, caches them, and makes them available throughout your application via React Context and custom hooks.
## **2. Installation**
Install the SDK using npm or yarn:
```bash theme={null}
npm install @kelviq/react-sdk
# or
yarn add @kelviq/react-sdk
```
You will also need `react` as a peer dependency.
## **3. Setup & Configuration**
### `KelviqProvider`
The core of the SDK is the `KelviqProvider`. You need to wrap your application (or the relevant part of it) with this provider. It initializes the SDK and makes entitlement data available to its children components via context.
```tsx theme={null}
// In your App.tsx or main application entry point
import React from 'react';
import { KelviqProvider } from '@kelviq/react-sdk';
const App = () => {
return (
);
};
const MyAppContent = () => {
return (
My Application
);
};
export default App;
```
### Required Props for `KelviqProvider`
* `accessToken: string | null`
* Your **Client API Key** for authenticating requests to the Kelviq API. This key is safe to expose in client-side code. You can find it in the [API keys](https://app.kelviq.com/settings/api-keys) section of your Kelviq dashboard. Never use your Server API Key in client-side code.
### Optional Props for `KelviqProvider`
**Optional Props**
* `customerId?: string`
* The ID of the customer for whom entitlements are being fetched. This will be sent as a query parameter (`customer_id`). Required for entitlements, subscriptions, and customer data. Optional when only using pricing.
* `productId?: string`
* The product identifier for fetching pricing/offering data. Required when `fetchPricingOnMount` is `true`.
* `plansEnabled?: string`
* Comma-separated list of plan identifiers to include in the pricing response. When omitted, all active plans for the product are returned. Example: `"pro,enterprise"`.
* `apiUrl: string`
* The base URL for your Kelviq API. For example, `https://edge.api.kelviq.com/api/v1`.
* `environment: 'sandbox' | 'production'`
* The environment to use. If you want to use the sandbox, you must provide this.
* `entitlementsPath: string`
* The specific path for the endpoint that returns all entitlements for a customer. This path will be appended to `apiUrl`. For example, `entitlement-check` or `user-entitlements`.
* `config?: KelviqApiBehaviorOptions`
* An optional object to configure more advanced behaviors of the SDK.
### **Configuration Object (**`config: KelviqApiBehaviorOptions`**)**
This optional object can contain the following properties:
* `onError?: (error: Error) => void`
* A callback function that will be invoked if an error occurs during the API request for entitlements.
* `maxRetries?: number`
* The maximum number of times to retry a failed API request. Defaults to `3`.
* `timeout?: number`
* Timeout for API requests in milliseconds. Defaults to `5000`.
* `backoffBaseDelay?: number`
* Base delay (in milliseconds) for the exponential backoff strategy used in retries. Defaults to `1000`.
* `fetchEntitlementsOnMount?: boolean`
* Defaults to `true`. If true, entitlements are fetched automatically when the `KelviqProvider` mounts.
* Set to `false` if you want to control the initial fetch manually using the `refreshAllEntitlements` function from the `useKelviq` hook.
* `fetchSubscriptionsOnMount?: boolean`
* Defaults to `false`. If true, the customer's subscription data is fetched automatically when the `KelviqProvider` mounts.
* When enabled, data is available via the `useSubscriptions()` hook or `subscriptions` from `useKelviq()`.
* `fetchCustomerOnMount?: boolean`
* Defaults to `false`. If true, customer data is fetched automatically when the `KelviqProvider` mounts.
* When enabled, data is available via the `useCustomer()` hook or `customer` from `useKelviq()`.
* `fetchPricingOnMount?: boolean`
* Defaults to `false`. If true, pricing/offering data is fetched automatically when the `KelviqProvider` mounts. Requires `productId` to be set on the provider.
* When enabled, data is available via the `usePricing()` hook or `pricing` from `useKelviq()`.
## **4. Fetching Entitlements**
### **Automatic Fetching on Mount**
By default (`fetchEntitlementsOnMount: true` in the `config` prop, or if `config` or this specific option is omitted), the SDK will attempt to fetch all entitlements for the specified `customerId` as soon as the `KelviqProvider` is mounted.
The `isLoading` state from `useKelviq()` will be `true` during this initial fetch.
### **Manual Fetching**
If `fetchEntitlementsOnMount` is set to `false`, or if you need to re-fetch entitlements at any point (e.g., after a user action that might change their entitlements), you can use the `refreshAllEntitlements` function.
**Code Example**
```tsx theme={null}
import { useKelviq } from '@kelviq/react-sdk';
function MyComponent() {
const { refreshAllEntitlements, isLoading, error } = useKelviq();
const handleRefresh = async () => {
try {
await refreshAllEntitlements();
console.log("Entitlements refreshed!");
} catch (e) {
console.error("Failed to refresh entitlements:", e);
}
};
return (
{error &&
Error: {error.message}
}
{/* ... rest of your component ... */}
);
}
```
## **5. Accessing Entitlement Data (Hooks)**
The SDK provides several hooks to access entitlement data and SDK state.
### `useKelviq()`
This is the primary hook to access the SDK's context.
* **Returns:** `KelviqContextValue` object containing:
* `allEntitlements: AsyncState`: The state object for all fetched entitlements.
* `data`: An `EntitlementMap` (a `Record`) where keys are `featureId`s, or `null` if not loaded or error.
* `isLoading`: Boolean indicating if the `allEntitlements` data is currently being fetched/refreshed.
* `error`: An `Error` object if the last fetch failed, otherwise `null`.
* `refreshAllEntitlements: () => Promise`: Function to manually trigger a re-fetch of all entitlements.
* `getEntitlements: () => EntitlementMap`: Returns all aggregated entitlements as a `Record` keyed by `featureId`. Returns an empty object if data has not been fetched yet.
* `getEntitlement: (featureId: string) => Entitlement | null`: Returns the aggregated entitlement for a given `featureId`, or `null` if not found or not yet loaded.
* `getRawEntitlements: () => EntitlementsResponse | null`: Returns the raw API response with `customerId` and the un-aggregated `entitlements` array (duplicate `featureId` entries are preserved). Returns `null` if data has not been fetched yet.
* `getRawEntitlement: (featureId: string) => EntitlementsResponse | null`: Returns the raw API response filtered to a specific `featureId`, with `customerId` and only the matching entries.
* `hasAccess: (featureId: string) => boolean`: A utility function to quickly check access for a feature. Returns `true` if access is granted, `false` if denied, feature not found, or data is not yet loaded.
* `updateEntitlement: (featureId: string, data: Partial<...>) => void`: Updates a cached entitlement in-place. Accepts partial data (excluding `featureId`, `featureType`, and `items`). Automatically recalculates `remaining` when `usageLimit` or `currentUsage` are updated.
* `subscriptions: AsyncState`: The state object for the customer's subscriptions. Only populated when `fetchSubscriptionsOnMount` is `true` or after calling `refreshSubscriptions()`.
* `refreshSubscriptions: () => Promise`: Function to manually trigger a re-fetch of the customer's subscriptions.
* `customer: AsyncState`: The state object for customer data. Only populated when `fetchCustomerOnMount` is `true` or after calling `refreshCustomer()`.
* `refreshCustomer: () => Promise`: Function to manually trigger a re-fetch of the customer data.
* `pricing: AsyncState`: The state object for pricing/offering data with localized currency. Only populated when `fetchPricingOnMount` is `true` or after calling `refreshPricing()`.
* `refreshPricing: () => Promise`: Function to manually trigger a re-fetch of the pricing data.
* `isLoading: boolean`: A global loading state indicating if the SDK is performing its initial configured fetch or a refresh operation.
* `error: Error | null`: A global error state reflecting any error from the last fetch operation initiated by the provider.
* `customerId: string`: The customer ID passed to the provider.
* `environment: string`: The environment (`'production'` or `'sandbox'`) passed to the provider.
* `apiUrl: string`: The resolved API URL used by the provider.
* `entitlementsPath: string`: The entitlements path passed to the provider.
***
#### `hasAccess(featureId)`
Quickly check if a customer has access to a feature.
```tsx theme={null}
const { hasAccess } = useKelviq();
const canUse = hasAccess("premium-reporting");
```
> **Response**
>
> ```json theme={null}
> true
> ```
***
#### `getEntitlement(featureId)`
Returns the aggregated entitlement for a given feature. When the API returns multiple entries for the same `featureId` (e.g. from different subscriptions), the SDK groups them and exposes aggregated totals at the top level.
```tsx theme={null}
const { getEntitlement } = useKelviq();
const entitlement = getEntitlement("api-calls-quota");
```
> **Response — Metered Entitlement**
>
> ```json theme={null}
> {
> "featureId": "api-calls-quota",
> "featureType": "METER",
> "hasAccess": true,
> "currentUsage": 150,
> "usageLimit": 1000,
> "remaining": 850,
> "hardLimit": true,
> "items": [
> {
> "featureId": "api-calls-quota",
> "featureType": "METER",
> "hasAccess": true,
> "resetAt": "2025-06-01T00:00:00Z",
> "hardLimit": false,
> "usageLimit": 500,
> "currentUsage": 100,
> "remaining": 400
> },
> {
> "featureId": "api-calls-quota",
> "featureType": "METER",
> "hasAccess": true,
> "resetAt": "2025-07-01T00:00:00Z",
> "hardLimit": true,
> "usageLimit": 500,
> "currentUsage": 50,
> "remaining": 450
> }
> ]
> }
> ```
> **Response — Boolean Entitlement**
>
> ```json theme={null}
> {
> "featureId": "enable-dark-mode",
> "featureType": "BOOLEAN",
> "hasAccess": true,
> "currentUsage": 0,
> "usageLimit": null,
> "remaining": null,
> "hardLimit": false,
> "items": [
> {
> "featureId": "enable-dark-mode",
> "featureType": "BOOLEAN",
> "hasAccess": true
> }
> ]
> }
> ```
> **Response — Customizable Entitlement**
>
> ```json theme={null}
> {
> "featureId": "max-items-per-page",
> "featureType": "CUSTOMIZABLE",
> "hasAccess": true,
> "currentUsage": 0,
> "usageLimit": 50,
> "remaining": 50,
> "hardLimit": false,
> "items": [
> {
> "featureId": "max-items-per-page",
> "featureType": "CUSTOMIZABLE",
> "hasAccess": true,
> "usageLimit": 50,
> "currentUsage": 0,
> "remaining": 50
> }
> ]
> }
> ```
***
#### `getEntitlements()`
Returns all aggregated entitlements as a `Record` keyed by `featureId`.
```tsx theme={null}
const { getEntitlements } = useKelviq();
const entitlements = getEntitlements();
```
> **Response**
>
> ```json theme={null}
> {
> "api-calls-quota": {
> "featureId": "api-calls-quota",
> "featureType": "METER",
> "hasAccess": true,
> "currentUsage": 150,
> "usageLimit": 1000,
> "remaining": 850,
> "hardLimit": true,
> "items": [
> {
> "featureId": "api-calls-quota",
> "featureType": "METER",
> "hasAccess": true,
> "resetAt": "2025-06-01T00:00:00Z",
> "hardLimit": false,
> "usageLimit": 500,
> "currentUsage": 100,
> "remaining": 400
> },
> {
> "featureId": "api-calls-quota",
> "featureType": "METER",
> "hasAccess": true,
> "resetAt": "2025-07-01T00:00:00Z",
> "hardLimit": true,
> "usageLimit": 500,
> "currentUsage": 50,
> "remaining": 450
> }
> ]
> },
> "enable-dark-mode": {
> "featureId": "enable-dark-mode",
> "featureType": "BOOLEAN",
> "hasAccess": true,
> "currentUsage": 0,
> "usageLimit": null,
> "remaining": null,
> "hardLimit": false,
> "items": [
> {
> "featureId": "enable-dark-mode",
> "featureType": "BOOLEAN",
> "hasAccess": true
> }
> ]
> }
> }
> ```
***
#### `getRawEntitlements()`
Returns the raw, un-aggregated API response with `customerId`. Duplicate `featureId` entries from multiple subscriptions are preserved as-is.
```tsx theme={null}
const { getRawEntitlements } = useKelviq();
const raw = getRawEntitlements();
```
> **Response**
>
> ```json theme={null}
> {
> "customerId": "cust_456",
> "entitlements": [
> {
> "featureId": "api-calls-quota",
> "featureType": "METER",
> "hasAccess": true,
> "resetAt": "2025-06-01T00:00:00Z",
> "hardLimit": false,
> "usageLimit": 500,
> "currentUsage": 100,
> "remaining": 400
> },
> {
> "featureId": "api-calls-quota",
> "featureType": "METER",
> "hasAccess": true,
> "resetAt": "2025-07-01T00:00:00Z",
> "hardLimit": true,
> "usageLimit": 500,
> "currentUsage": 50,
> "remaining": 450
> },
> {
> "featureId": "enable-dark-mode",
> "featureType": "BOOLEAN",
> "hasAccess": true
> }
> ]
> }
> ```
***
#### `getRawEntitlement(featureId)`
Returns the raw API response filtered to a specific `featureId`. Useful for accessing per-subscription data for a single feature.
```tsx theme={null}
const { getRawEntitlement } = useKelviq();
const raw = getRawEntitlement("api-calls-quota");
```
> **Response**
>
> ```json theme={null}
> {
> "customerId": "cust_456",
> "entitlements": [
> {
> "featureId": "api-calls-quota",
> "featureType": "METER",
> "hasAccess": true,
> "resetAt": "2025-06-01T00:00:00Z",
> "hardLimit": false,
> "usageLimit": 500,
> "currentUsage": 100,
> "remaining": 400
> },
> {
> "featureId": "api-calls-quota",
> "featureType": "METER",
> "hasAccess": true,
> "resetAt": "2025-07-01T00:00:00Z",
> "hardLimit": true,
> "usageLimit": 500,
> "currentUsage": 50,
> "remaining": 450
> }
> ]
> }
> ```
***
#### `updateEntitlement(featureId, data)`
Updates a cached entitlement in-place. Useful for optimistically updating the UI after a usage increment without waiting for a full refresh. The `remaining` field is automatically recalculated when `usageLimit` or `currentUsage` are updated.
```tsx theme={null}
const { updateEntitlement } = useKelviq();
// After the user performs an action that consumes usage
updateEntitlement("api-calls-quota", { currentUsage: 151 });
```
> **Updated Entitlement**
>
> ```json theme={null}
> {
> "featureId": "api-calls-quota",
> "featureType": "METER",
> "hasAccess": true,
> "currentUsage": 151,
> "usageLimit": 1000,
> "remaining": 849,
> "hardLimit": true,
> "items": [...]
> }
> ```
Accepts `Partial>`. You can update any combination of `hasAccess`, `currentUsage`, `usageLimit`, and `remaining`.
### `useAllEntitlements()`
A convenience hook that directly returns the `allEntitlements` state object.
* **Returns:** `AsyncState`
```tsx theme={null}
import { useAllEntitlements } from '@kelviq/react-sdk';
function EntitlementsList() {
const { data: entitlementsMap, isLoading, error } = useAllEntitlements();
if (isLoading) return
);
}
```
> [!NOTE]
>
> You must set `productId` on the provider and enable `fetchPricingOnMount: true` in the provider's `config` prop, or call `refreshPricing()` manually, for this hook to return data.
>
> When `customerId` is provided, the pricing response includes the customer's active subscription details and the `isCustomerExists` flag. Use `plansEnabled` to filter the response to specific plans (e.g., `"pro,enterprise"`).
**Example using a specific entitlement hook:**
```tsx theme={null}
import { useBooleanEntitlement, useKelviq } from '@kelviq/react-sdk';
function FeatureSpecificComponent({ featureId }: { featureId: string }) {
const { isLoading, error } = useKelviq();
const entitlement = useBooleanEntitlement(featureId);
if (isLoading) return
;
if (entitlement && entitlement.hasAccess) {
return
You have access to {featureId}!
;
} else {
return
You do not have access to {featureId}.
;
}
}
```
## **6. Conditional Rendering Components**
These components provide a declarative way to render UI based on entitlement status. They internally use the respective hooks.
**Common Props:**
* `featureId: string`: The unique identifier of the feature to check.
* `children: ReactNode | ((data: Entitlement) => ReactNode)`:
* If a `ReactNode`, it's rendered when the user is entitled and conditions are met.
* If a function (render prop), it's called with the aggregated `Entitlement` data and its return value is rendered.
* `fallback?: ReactNode`: Content to render if the user is not entitled, or if data is loading (and no `loadingComponent` is provided), or if an error occurs. Defaults to `null`.
* `loadingComponent?: ReactNode`: Specific content to render while the entitlement data is loading. Overrides `fallback` during loading.
### `ShowWhenBooleanEntitled`
Renders children if the boolean feature is enabled (`hasAccess: true`).
```tsx theme={null}
import { ShowWhenBooleanEntitled } from '@kelviq/react-sdk';
Checking theme settings...
}
fallback={
Dark mode is not available.
}
>
{(entitlementData) => (
Advanced settings for {entitlementData.featureId} are visible!
)}
```
> **Render Prop Data (meterData)**
>
> ```json theme={null}
> {
> "featureId": "api-calls-quota",
> "featureType": "METER",
> "hasAccess": true,
> "currentUsage": 150,
> "usageLimit": 1000,
> "remaining": 850,
> "hardLimit": true,
> "items": [
> {
> "featureId": "api-calls-quota",
> "featureType": "METER",
> "hasAccess": true,
> "resetAt": "2025-06-01T00:00:00Z",
> "hardLimit": false,
> "usageLimit": 500,
> "currentUsage": 100,
> "remaining": 400
> },
> {
> "featureId": "api-calls-quota",
> "featureType": "METER",
> "hasAccess": true,
> "resetAt": "2025-07-01T00:00:00Z",
> "hardLimit": true,
> "usageLimit": 500,
> "currentUsage": 50,
> "remaining": 450
> }
> ]
> }
> ```
## **7. Pricing Components**
The SDK provides render-prop components for displaying pricing data. These require `productId` on the provider and `fetchPricingOnMount: true` (or a manual `refreshPricing()` call).
### `KQPrice`
Renders pricing information for a specific plan and billing period. Provides a `formattedPrice` string with locale-aware number formatting and currency symbol.
**Props:**
* `planIdentifier: string`: The plan identifier to display pricing for.
* `billingPeriod: string`: The billing period to show (e.g. `"MONTHLY"`, `"YEARLY"`, `"ONE_TIME"`).
* `children: (data) => ReactNode`: Render function receiving price data.
* `formatOptions?: KQFormatPriceOptions`: Options for the `formattedPrice` string.
* `loadingComponent?: ReactNode`: Shown while pricing data is loading.
* `fallback?: ReactNode`: Shown when the plan is not found or pricing data is unavailable.
```tsx theme={null}
import { KQPrice } from '@kelviq/react-sdk';
Price not available}
>
{({ formattedPrice, isFree, hasFreeTrial, trialPeriod }) => (
{formattedPrice}
{hasFreeTrial &&
{trialPeriod}-day free trial
}
)}
```
> **Render Prop Data**
>
> The `children` render function receives an object with:
>
> * `plan`: The full plan object (`RawPricingPlan`).
> * `charge`: The matching charge for the billing period (`RawPricingCharge | null`).
> * `amount: number`: The raw price amount.
> * `formattedPrice: string`: Locale-formatted price string (e.g. `"$49.99"`, `"Free"`).
> * `currencySymbol: string`: The currency symbol (e.g. `"$"`, `"€"`).
> * `currencyCode: string`: The currency code (e.g. `"USD"`, `"EUR"`).
> * `pricingLocale: string`: The locale used for pricing (e.g. `"en-US"`).
> * `isFree: boolean`: Whether the plan is free.
> * `hasFreeTrial: boolean`: Whether the plan has a free trial.
> * `trialPeriod: number`: Trial period in days.
#### `KQFormatPriceOptions`
Options for customizing the `formattedPrice` string:
* `compact?: boolean`: Use compact notation (e.g. `"1.2K"` instead of `"1,200"`). Defaults to `false`.
* `locale?: string`: Locale for number formatting. Defaults to the `pricingLocale` from the API.
* `includeCurrencySymbol?: boolean`: Whether to include the currency symbol. Defaults to `true`.
### `KQFeatureList`
Iterates over features for a specific plan, calling the render function for each feature.
**Props:**
* `planIdentifier: string`: The plan identifier to display features for.
* `children: (data) => ReactNode`: Render function called for each feature.
* `featureType?: 'BOOLEAN' | 'METER'`: Optional filter to show only specific feature types.
* `loadingComponent?: ReactNode`: Shown while pricing data is loading.
* `fallback?: ReactNode`: Shown when the plan is not found or has no features.
```tsx theme={null}
import { KQFeatureList } from '@kelviq/react-sdk';
No features}
>
{({ feature }) => (
)}
```
> **Render Prop Data**
>
> The `children` render function receives an object with:
>
> * `feature`: The feature object (`RawPricingFeature`) with `id`, `displayName`, `featureType`, `value`, `enabled`.
> * `plan`: The full plan object (`RawPricingPlan`).
> * `index: number`: The index of the feature in the list.
### `kqFormatPrice(amount, currencySymbol, options?)`
A standalone utility function for formatting prices. Exported from the SDK for use outside of components.
```tsx theme={null}
import { kqFormatPrice } from '@kelviq/react-sdk';
kqFormatPrice(1299.99, '$'); // "$1,299.99"
kqFormatPrice(1299.99, '$', { compact: true }); // "$1.3K"
kqFormatPrice(49.99, '€', { locale: 'de-DE' }); // "€49,99"
kqFormatPrice(49.99, '$', { includeCurrencySymbol: false }); // "49.99"
```
## **8. Key Types**
The SDK exports several types for better integration with TypeScript. Some key ones include:
* `KelviqContextValue`: The shape of the object returned by `useKelviq()`.
* `KelviqApiBehaviorOptions`: Configuration options for the `config` prop.
* `AsyncState`: Generic type for asynchronous data states (`{ data, isLoading, error }`).
* `Entitlement`: The unified entitlement type with aggregated data and an `items` array containing the raw API objects.
* `EntitlementMap`: The structure of the cached entitlements (`Record`), keyed by `featureId`.
* `EntitlementsResponse`: The raw API response shape (`{ customerId, entitlements }`) returned by `getRawEntitlements()` and `getRawEntitlement()`.
* `RawEntitlementsApiResponse` and related `Raw...` types: Represent the structure of the direct API response.
* `RawSubscriptionData`: A single subscription object as returned by the subscriptions API, including `plan`, `product`, `features`, `status`, `amount`, `recurrence`, etc.
* `RawCustomerApiResponse`: Customer data as returned by the customer API, including `id`, `customerId`, `name`, `email`, `billingAddress`, `details`, `metadata`, `createdOn`, and `modifiedOn`.
* `RawBillingAddress`: The billing address object (`country`, `line1`, `line2`, `postalCode`, `city`, `state`).
* `RawPricingApiResponse`: The full pricing/offering API response including `plans`, `currencySymbol`, `currencyCode`, `pricingLocale`, and `billingPeriods`.
* `RawPricingPlan`: A single plan with `identifier`, `displayName`, `displayDescription`, `price`, `features`, and `enabled`.
* `RawPricingFeature`: A plan feature with `id`, `displayName`, `featureType`, `value`, and `enabled`.
* `RawPricingCharge`: A charge entry with `chargePeriod` and `priceData` (containing `amount`).
* `KQFormatPriceOptions`: Options for the `kqFormatPrice` utility (`compact`, `locale`, `includeCurrencySymbol`).
You can import these types directly from the main SDK entry:
```ts theme={null}
import type {
Entitlement,
EntitlementMap,
EntitlementsResponse,
RawSubscriptionData,
RawCustomerApiResponse,
RawBillingAddress,
RawPricingApiResponse,
RawPricingPlan,
RawPricingFeature,
RawPricingCharge,
KQFormatPriceOptions,
} from '@kelviq/react-sdk';
```
## **9. Error Handling**
* **Provider Level:** The `onError` callback in `config` (`KelviqApiBehaviorOptions`) can be used to globally handle errors that occur during the entitlement fetching process initiated by the provider.
* **Context Level:** `useKelviq()` returns a global `isLoading: boolean` and `error: Error | null` state, reflecting the provider's most recent fetch operation. Use these for loading/error UI in your components.
* **`allEntitlements` Level:** The `allEntitlements` object from `useKelviq()` or `useAllEntitlements()` also carries its own `isLoading` and `error` fields within the `AsyncState` wrapper.
## **10. Example Usage**
A comprehensive example is provided within the comments of the SDK code, demonstrating provider setup and component usage. The core setup involves:
1. Wrapping your application with `KelviqProvider` and providing the required props (`customerId`, `accessToken`)
2. Using hooks like `useBooleanEntitlement` or conditional components like `ShowWhenBooleanEntitled` in your components to control UI and behavior based on entitlements.
## Using the Sandbox
If you want to use the sandbox, you need to set the `environment` option to `sandbox` in the `KelviqProvider`.
```tsx theme={null}
{/* Your app components */}
```
# React UI
Source: https://docs.kelviq.com/frontend-integration/react-ui
Documentation for the Kelviq React UI SDK - A comprehensive pricing table and paywall solution
## 1. Overview
The **Kelviq React UI SDK** provides a complete React-based solution for integrating dynamic, customizable pricing tables and paywalls into web applications. It handles fetching offering data, displaying pricing plans with multiple billing periods, managing subscriptions, and processing checkouts with full localization support.
***
## 2. Installation
Install the SDK using npm or yarn:
```bash theme={null}
npm install @kelviq/react-ui
# or
yarn add @kelviq/react-ui
```
***
## 3. Quick Start
### Basic Setup
Note: You must import the CSS file for the components to be styled correctly.
```tsx theme={null}
import { KelviqProvider, KQPricingTable } from '@kelviq/react-ui';
import '@kelviq/react-ui/dist/kq-react-ui.css';
function App() {
return (
);
}
```
### With Custom Event Handling
```tsx theme={null}
import { KelviqProvider, KQPricingTable } from '@kelviq/react-ui';
import '@kelviq/react-ui/dist/kq-react-ui.css';
function App() {
const handleCustomPriceClick = (details) => {
console.log('Custom price plan clicked:', details);
// Handle custom pricing logic
};
return (
);
}
```
***
## 4. Core Components
### 4.1. `KelviqProvider`
The top-level provider component that initializes the SDK and manages the global state.
#### Required Props
| Prop | Type | Description |
| - | - | - |
| `accessToken` | `string` | Your Kelviq access token |
| `productId` | `string` | The unique identifier of the product to display the offering for |
#### Optional Props
| Prop | Type | Default | Description |
| - | - | - | - |
| `customerId` | `string` | `undefined` | Customer identifier for personalized pricing |
| `environment` | `sandbox\|production` | 'production' | The environment to use. If you want to use the sandbox, you must provide this. |
| `checkoutSuccessUrl` | `string` | `window.location.href` | URL to redirect after successful checkout |
| `apiConfig` | `KelviqAPIConfig` | `{}` | Advanced API configuration options |
#### API Configuration Options
```tsx theme={null}
interface KelviqAPIConfig {
onError?: (error: Error) => void;
maxRetries?: number; // Default: 3
timeout?: number; // Default: 5000ms
backoffBaseDelay?: number;
offeringApiUrl?: string;
subscriptionApiUrl?: string;
checkoutApiUrl?: string;
}
```
### 4.2. `KQPricingTable`
The main pricing table component that displays all plans and handles user interactions.
#### Props
| Prop | Type | Default | Description |
| - | - | - | - |
| `customStyles` | `PricingTableStyle` | `{}` | Style overrides (colors, typography, layout). Any field you omit falls back to the SDK defaults. |
| `pricingOptions` | `Partial` | `DEFAULT_PRICING_OPTIONS` | Price-formatting options (decimals, currency symbol/code, localization). Partial objects are merged with the defaults. |
| `customActionButton` | `CustomActionButton` | `undefined` | Custom button component |
| `customCheckIcon` | `React.ReactNode` | `undefined` | Custom check icon |
| `customUnCheckIcon` | `React.ReactNode` | `undefined` | Custom uncheck icon |
| `customTooltipIcon` | `React.ReactNode` | `undefined` | Custom tooltip icon |
| `onCustomPriceClick` | `(details: PlanClickDetails) => void` | `undefined` | Callback for custom price plans |
| `renderActionButtonAfter` | `features \| price` | `price` | Render the action button after the features or the price |
| `insertElements.*` | `InsertElements` | `{}` | Insert custom elements anywhere in the pricing table |
| `insertElements.beforePlanName` | `(plan: OfferingPlan, activePeriod: ChargePeriodEnum) => JSX.Element` | `undefined` | Insert custom elements before the plan name |
| `insertElements.afterPlanName` | `(plan: OfferingPlan, activePeriod: ChargePeriodEnum) => JSX.Element` | `undefined` | Insert custom elements after the plan name |
| `insertElements.beforePlanDescription` | `(plan: OfferingPlan, activePeriod: ChargePeriodEnum) => JSX.Element` | `undefined` | Insert custom elements before the plan description |
| `insertElements.afterPlanDescription` | `(plan: OfferingPlan, activePeriod: ChargePeriodEnum) => JSX.Element` | `undefined` | Insert custom elements after the plan description |
| `insertElements.beforePrice` | `(plan: OfferingPlan, activePeriod: ChargePeriodEnum) => JSX.Element` | `undefined` | Insert custom elements before the price |
| `insertElements.afterPrice` | `(plan: OfferingPlan, activePeriod: ChargePeriodEnum) => JSX.Element` | `undefined` | Insert custom elements after the price |
| `insertElements.beforeFeatures` | `(plan: OfferingPlan, activePeriod: ChargePeriodEnum) => JSX.Element` | `undefined` | Insert custom elements before the features |
| `insertElements.afterFeatures` | `(plan: OfferingPlan, activePeriod: ChargePeriodEnum) => JSX.Element` | `undefined` | Insert custom elements after the features |
| `insertElements.beforeActionButton` | `(plan: OfferingPlan, activePeriod: ChargePeriodEnum) => JSX.Element` | `undefined` | Insert custom elements before the action button |
| `insertElements.afterActionButton` | `(plan: OfferingPlan, activePeriod: ChargePeriodEnum) => JSX.Element` | `undefined` | Insert custom elements after the action button |
| `insertElements.beforeSwitch` | `(activePeriod: ChargePeriodEnum) => JSX.Element` | `undefined` | Insert custom elements before the switch |
| `insertElements.afterSwitch` | `(activePeriod: ChargePeriodEnum) => JSX.Element` | `undefined` | Insert custom elements after the switch |
#### Example Usage
```tsx theme={null}
{
console.log('Custom plan selected:', details.plan.displayName);
// Handle custom pricing logic
}}
customStyles={{
style: {
cardBackgroundColor: '#f8f9fa',
buttonBackgroundColor: '#28a745',
buttonColor: '#ffffff',
titleColor: '#212529'
},
typography: {
titleFontSize: 24,
titleFontWeight: '600'
},
layout: {
gap: 20,
minWidth: 280,
maxWidth: 400
}
}}
insertElements={{
afterPrice: (plan) => {
if(plan.shouldHighlight) {
return
Most popular plan
;
}
return null;
}
}}
/>
```
### 4.3. `KQPlan`
Renders a list of pricing plans with billing period switching.
#### Props
| Prop | Type | Required | Description |
| - | - | - | - |
| `plans` | `OfferingPlan[]` | Yes | Array of plans to display |
| `pricingOptions` | `PricingOptions` | Yes | Price-formatting options |
| `billingPeriods` | `BillingPeriod[]` | Yes | Available billing periods |
| `countryCode` | `string` | Yes | Country code for localization |
| `currencyCode` | `string` | Yes | Currency code (e.g., "USD") |
| `currencySymbol` | `string` | Yes | Currency symbol (e.g., "\$") |
| `currencyConversionRate` | `number` | Yes | Currency conversion rate |
| `pricingLocale` | `string` | Yes | Locale for price formatting |
| `onPlanClick` | `(details: PlanClickDetails) => void` | Yes | Plan selection callback |
| `customStyles` | `PricingTableStyle` | No | Custom styling |
| `customActionButton` | `CustomActionButton` | No | Custom button component |
| `actionButtonLoadingPlanIdentifier` | `string` | No | Loading state for specific plan |
| `noStyles` | `boolean` | No | Disable default styling |
### 4.4. `KQPlanItem`
Displays a single pricing plan card.
#### Props
| Prop | Type | Required | Description |
| - | - | - | - |
| `plan` | `OfferingPlan` | Yes | Plan data to display |
| `pricingOptions` | `PricingOptions` | Yes | Price-formatting options |
| `period` | `ChargePeriodEnum` | Yes | Current billing period |
| `countryCode` | `string` | Yes | Country code |
| `currencyCode` | `string` | Yes | Currency code |
| `currencySymbol` | `string` | Yes | Currency symbol |
| `currencyConversionRate` | `number` | Yes | Conversion rate |
| `pricingLocale` | `string` | Yes | Locale for formatting |
| `onKQPlanItemClick` | `(details: PlanClickDetails) => void` | Yes | Click handler |
| `customActionButton` | `CustomActionButton` | No | Custom button |
| `actionButtonText` | `string` | No | Button text |
| `actionButtonLoading` | `boolean` | No | Loading state |
| `actionButtonDisabled` | `boolean` | No | Disabled state |
| `isActive` | `boolean` | No | Active plan indicator |
| `successText` | `string` | No | Success message |
| `noStyles` | `boolean` | No | Disable styling |
### 4.5. `KQPrice`
Renders formatted price display with currency and period information.
#### Props
| Prop | Type | Required | Description |
| - | - | - | - |
| `price` | `number` | Yes | Price amount |
| `countryCode` | `string` | Yes | Country code |
| `currencyCode` | `string` | Yes | Currency code |
| `currencySymbol` | `string` | Yes | Currency symbol |
| `currencyConversionRate` | `number` | Yes | Conversion rate |
| `pricingLocale` | `string` | Yes | Locale for formatting |
| `period` | `ChargePeriodEnum` | Yes | Billing period |
| `showDecimal` | `boolean` | Yes | Show decimal places |
| `showCurrencySymbol` | `boolean` | Yes | Show currency symbol |
| `showCurrencyCode` | `boolean` | Yes | Show currency code |
| `minimumFractionDigits` | `number` | Yes | Minimum decimal places |
| `maximumFractionDigits` | `number` | Yes | Maximum decimal places |
### 4.6. `KQFeatures`
Renders the features list for a plan.
#### Props
| Prop | Type | Required | Description |
| - | - | - | - |
| `features` | `OfferingFeature[]` | Yes | Array of features |
| `period` | `ChargePeriodEnum` | Yes | Current billing period |
| `customCheckIcon` | `React.ReactNode` | No | Custom check icon |
| `customUnCheckIcon` | `React.ReactNode` | No | Custom uncheck icon |
| `customTooltipIcon` | `React.ReactNode` | No | Custom tooltip icon |
| `onUsageChange` | `(featureId: string, quantity: number) => void` | No | Usage change handler |
> Feature tooltips are controlled per-feature by the `displayTooltipDescription` flag on each feature in the offering data, configured in your Kelviq dashboard — there is no global tooltip prop.
***
## 5. Data Types
### Core Types
```tsx theme={null}
// Billing periods
enum ChargePeriodEnum {
ONE_TIME = 'ONE_TIME',
MONTHLY = 'MONTHLY',
YEARLY = 'YEARLY',
WEEKLY = 'WEEKLY',
DAILY = 'DAILY',
THREE_MONTHS = 'THREE_MONTHS',
SIX_MONTHS = 'SIX_MONTHS'
}
// Price types
enum PriceTypeEnum {
FREE = 'FREE',
PAID = 'PAID',
CUSTOM = 'CUSTOM'
}
// Price models
enum PriceModelEnum {
FLAT = 'FLAT',
PACKAGE = 'PACKAGE',
TIERED = 'TIERED',
VOLUME = 'VOLUME'
}
// Plan structure
interface OfferingPlan {
id: string;
name: string;
displayName: string;
displayDescription: string;
identifier: string;
isArchived: boolean;
features: OfferingFeature[];
price: OfferingPlanPrice;
shouldHighlight: boolean;
enabled: boolean;
}
// Plan click details
interface PlanClickDetails {
plan: OfferingPlan;
chargePeriod: ChargePeriodEnum;
usageMap: Record;
}
// Pricing table style
interface PricingTableStyle {
style: PricingTableColorConfig;
typography: PricingTableTypographyConfig;
layout: PricingTableLayoutConfig;
}
// Price-formatting options (passed via the `pricingOptions` prop)
interface PricingOptions {
localization: boolean; // use the offering's locale/currency formatting
showDecimal: boolean; // show decimal places
minimumDecimalDigits: number; // minimum decimal places
maximumDecimalDigits: number; // maximum decimal places
showCurrencySymbol: boolean; // show the currency symbol (e.g. $)
showCurrencyCode: boolean; // show the currency code (e.g. USD)
}
```
### Color Configuration
```tsx theme={null}
interface PricingTableColorConfig {
primaryColor: string;
primaryHoverColor: string;
onPrimaryColor: string;
secondaryColor: string;
cardBackgroundColor: string;
billingSwitchBackgroundColor: string;
billingSwitchColor: string;
billingSwitchSelectedColor: string;
titleColor: string;
descriptionColor: string;
priceColor: string;
priceUnitColor: string;
pricePeriodColor: string;
featureColor: string;
dividerColor: string;
buttonBackgroundColor: string;
buttonColor: string;
buttonHoverBackgroundColor: string;
advancedMode: boolean;
}
```
### Typography Configuration
```tsx theme={null}
interface PricingTableTypographyConfig {
titleFontSize: number;
titleFontWeight: string;
descriptionFontSize: number;
descriptionFontWeight: string;
priceFontSize: number;
priceFontWeight: string;
priceUnitFontSize: number;
priceUnitFontWeight: string;
featureFontSize: number;
featureFontWeight: string;
pricePeriodFontSize: number;
pricePeriodFontWeight: string;
}
```
### Layout Configuration
```tsx theme={null}
interface PricingTableLayoutConfig {
minWidth: number;
maxWidth: number;
gap: number;
align: 'start' | 'center' | 'end';
tierSelectionType: 'switch' | 'tabs';
}
```
***
## 6. Styling and Customization
### 6.1. CSS Variables
The SDK uses CSS variables for styling. You can override these in your CSS:
```css theme={null}
:root {
/* Card & Background */
--kq-card-bg: #ffffff;
--kq-card-border-width: 1px solid;
--kq-card-border-color: 1px solid;
/* Text Colors */
--kq-title-color: #171923;
--kq-description-color: #718096;
--kq-feature-color: #171923;
/* Price Colors */
--kq-price-color: #171923;
--kq-price-unit-color: #171923;
--kq-price-period-color: #718096;
/* Button Colors */
--kq-button-bg: #1A202C;
--kq-button-hover-bg: #1A202C;
--kq-button-color: #F7FAFC;
/* Billing Switch Colors */
--kq-billing-switch-bg: #1A202C;
--kq-billing-switch-color: #F7FAFC;
--kq-billing-switch-selected-color: #F7FAFC;
/* UI Elements */
--kq-divider-color: #E2E8F0;
--kq-tooltip-bg: #2D3748;
--kq-tooltip-color: #ffffff;
/* Typography - Title */
--kq-title-size: 24px;
--kq-title-weight: 600;
/* Typography - Description */
--kq-description-size: 16px;
--kq-description-weight: 400;
/* Typography - Price */
--kq-price-size: 28px;
--kq-price-weight: 700;
--kq-price-unit-size: 32px;
--kq-price-unit-weight: 700;
--kq-price-period-size: 16px;
--kq-price-period-weight: 400;
/* Typography - Feature */
--kq-feature-size: 14px;
--kq-feature-weight: 400;
/* Layout */
--kq-min-width: 360px;
--kq-max-width: 400px;
--kq-gap: 24px;
--kq-align: center;
}
```
### 6.2. Custom Styles Object
```tsx theme={null}
const customStyles: PricingTableStyle = {
style: {
cardBackgroundColor: '#ffffff',
buttonBackgroundColor: '#28a745',
buttonColor: '#ffffff',
titleColor: '#212529',
descriptionColor: '#6c757d',
priceColor: '#000000',
featureColor: '#495057'
},
typography: {
titleFontSize: 24,
titleFontWeight: '600',
descriptionFontSize: 14,
descriptionFontWeight: '400',
priceFontSize: 32,
priceFontWeight: '700'
},
layout: {
gap: 24,
minWidth: 300,
maxWidth: 450,
align: 'center'
}
};
```
### 6.3. Price Formatting
How prices are formatted is configured entirely in the SDK via the `pricingOptions` prop. Pass only the fields you want to change — the rest fall back to `DEFAULT_PRICING_OPTIONS`:
```tsx theme={null}
```
Defaults (`DEFAULT_PRICING_OPTIONS`):
```tsx theme={null}
{
localization: true,
showDecimal: true,
minimumDecimalDigits: 0,
maximumDecimalDigits: 2,
showCurrencySymbol: true,
showCurrencyCode: false,
}
```
### 6.4. Custom Action Button
```tsx theme={null}
const CustomButton: CustomActionButton = ({ onClick, children, plan, disabled }) => (
);
```
### 6.5. noStyles Mode
For complete styling control, use the `noStyles` prop:
```tsx theme={null}
```
### 6.6. CSS Class Reference
The Kelviq React UI components use consistent CSS class names that you can target for custom styling. All classes are prefixed with `kq-` to avoid naming conflicts.
#### CSS Class Hierarchy
```css theme={null}
.kq-plan /* Root container */
├── .kq-plan-switch /* Billing period switcher */
│ └── .kq-switch /* Switch wrapper */
│ └── .kq-switch-button /* Individual switch buttons */
│ └── .kq-promo-caption /* Discount badges (20%, etc.) */
├── .kq-plan-items /* Plans container */
└── .kq-plan-item /* Individual plan card */
├── .kq-plan-item-header /* Plan title section */
│ ├── .kq-plan-item-heading /* Plan name */
│ └── .kq-plan-item-subheading /* Plan description */
├── .kq-plan-item-price /* Price section */
│ ├── .kq-price-container /* Price wrapper */
│ │ ├── .kq-price-currency /* Currency container */
│ │ │ ├── .kq-price-currency-symbol /* $ symbol */
│ │ │ └── .kq-price-currency-code /* USD text */
│ │ ├── .kq-price-integer /* Main price number */
│ │ ├── .kq-price-decimal-separator /* Decimal point */
│ │ └── .kq-price-decimal /* Decimal digits */
│ └── .kq-plan-item-price-period /* /month text */
├── .kq-separator /* Divider line */
├── .kq-plan-item-features /* Features section */
│ └── .kq-features-list /* Features list */
│ └── .kq-features-list-item /* Individual features */
│ ├── .kq-tooltip-wrapper /* Feature tooltips */
│ │ └── .kq-tooltip /* Tooltip content */
│ ├── .kq-feature-quantity-input /* Quantity number input */
│ ├── .kq-feature-quantity-input-unit /* Input unit label */
│ └── .kq-feature-quantity-select-container /* Quantity dropdown */
│ ├── .kq-feature-quantity-select /* Select element */
│ └── .kq-feature-quantity-select-arrow /* Dropdown arrow */
└── .kq-plan-item-button /* CTA button */
```
***
## 7. Advanced Features
### 7.1. Usage-Based Pricing
The SDK supports metered billing and tiered pricing:
```tsx theme={null}
// Features with usage tracking
const features = [
{
identifier: 'api_calls',
displayName: 'API Calls',
priceModel: PriceModelEnum.VOLUME,
charges: [
{
chargePeriod: ChargePeriodEnum.MONTHLY,
tiers: [
{ flatAmount: 0, unitAmount: 0.01, upTo: 1000 },
{ flatAmount: 10, unitAmount: 0.008, upTo: 10000 },
{ flatAmount: 82, unitAmount: 0.005, upTo: null }
]
}
]
}
];
```
### 7.2. Custom Price Plans
Handle custom pricing plans with the `onCustomPriceClick` callback:
```tsx theme={null}
{
// Open contact form or custom pricing modal
openCustomPricingModal(details.plan);
}}
/>
```
### 7.3. Subscription Management
The SDK automatically handles subscription updates:
* **New Subscriptions**: Redirects to checkout
* **Plan Upgrades/Downgrades**: Updates existing subscription
* **Free Plan Upgrades**: Handles free trial to paid conversion
### 7.4. Error Handling
```tsx theme={null}
{
console.error('kelviq Error:', error);
// Show user-friendly error message
showErrorMessage('Unable to load pricing. Please try again later.');
},
maxRetries: 3,
timeout: 10000
}}
>
```
***
## 8. Complete Example
```tsx theme={null}
import React from 'react';
import {
KelviqProvider,
KQPricingTable,
PricingTableStyle
} from '@kelviq/react-ui';
const App: React.FC = () => {
const customStyles: PricingTableStyle = {
style: {
cardBackgroundColor: '#ffffff',
buttonBackgroundColor: '#007bff',
buttonColor: '#ffffff',
buttonHoverBackgroundColor: '#0056b3',
titleColor: '#212529',
descriptionColor: '#6c757d',
priceColor: '#000000',
featureColor: '#495057',
dividerColor: '#dee2e6',
billingSwitchBackgroundColor: '#f8f9fa',
billingSwitchColor: '#6c757d',
billingSwitchSelectedColor: '#007bff'
},
typography: {
titleFontSize: 24,
titleFontWeight: '600',
descriptionFontSize: 14,
descriptionFontWeight: '400',
priceFontSize: 32,
priceFontWeight: '700',
priceUnitFontSize: 16,
priceUnitFontWeight: '400',
featureFontSize: 14,
featureFontWeight: '400',
pricePeriodFontSize: 14,
pricePeriodFontWeight: '400'
},
layout: {
gap: 24,
minWidth: 300,
maxWidth: 450,
align: 'center'
}
};
const handleCustomPriceClick = (details) => {
console.log('Custom plan selected:', details.plan.displayName);
// Open contact form or pricing calculator
window.open('/contact', '_blank');
};
return (
);
};
export default App;
```
***
## 9. API Endpoints
The SDK uses the following default API endpoints:
* **Product Offering API**: `https://api.kelviq.com/api/v1/monetization/product-offering/{productId}/`
* **Checkout API**: `https://api.kelviq.com/api/v1/checkout/`
* **Subscription API**: `https://api.kelviq.com/api/v1/subscriptions`
You can override these in the `apiConfig`:
```tsx theme={null}
```
***
## 10. Troubleshooting
### Common Issues
1. **Loading State**: Ensure your `accessToken` and `productId` are correct
2. **Styling Issues**: Check CSS variable overrides and custom styles
3. **API Errors**: Verify API endpoints and network connectivity
4. **TypeScript Errors**: Ensure all required props are provided
### Debug Mode
Enable console logging for debugging:
```tsx theme={null}
{
console.log('kelviq Debug:', {
error: error.message,
productId: 'your-product-id',
timestamp: new Date().toISOString()
});
}
}}
>
```
***
## Using the Sandbox
If you want to use the sandbox, you need to set the `environment` option to `sandbox` in the `KelviqProvider`.
```tsx theme={null}
{/* Your app components */}
```
## 12. Support
For technical support and questions:
* **Documentation**: [docs.kelviq.com](https://docs.kelviq.com)
* **Email**: [hi@kelviq.com](mailto:hi@kelviq.com)
***
# Fees
Source: https://docs.kelviq.com/getstarted/fees
Understand Kelviq's transaction fees and additional charges
Kelviq operates as a [Merchant of Record](/getstarted/merchant-of-record), handling payments, tax compliance, and global selling on your behalf. You pay a transaction fee when a payment succeeds. There are no setup fees, monthly fees, or minimum transaction requirements.
## What's included
Your transaction fee covers everything you need to sell globally:
* Payment processing (cards, wallets, local payment methods)
* Sales tax, VAT, and GST calculation, collection, and remittance
* Fraud protection and chargeback handling
* Localized pricing in 135+ currencies
* Compliance with regional regulations
* Usage metering and billing
* License key generation
* Digital product and file delivery
* Feature and entitlement management
* Customer billing portal
## Transaction fees
Every successful payment starts with a base fee. The base rate changes after your organization processes \$5,000 in total sales volume. A recurring charge or international card can add another percentage to the same transaction.
| Fee component | Rate | When it applies |
| - | - | - |
| Base fee, up to \$5,000 in total volume | 2.9% + \$0.40 | Successful transactions processed before your organization reaches \$5,000 in total sales |
| Base fee, after \$5,000 in total volume | 3.5% + \$0.40 | Subsequent successful transactions after your organization reaches \$5,000 in total sales |
| Recurring billing | +0.7% | Subscription payments and renewals |
| International card | +1.5% | Cards issued outside the United States |
> [!NOTE]
>
> The recurring and international-card percentages are added to the applicable base rate. The fixed \$0.40 is charged once per successful transaction.
### How the \$5,000 threshold works
The threshold is based on your organization's cumulative processed sales volume, not the number of payments or the volume for a single month. You pay 2.9% + \$0.40 on transactions processed before reaching \$5,000. After your total processed volume reaches \$5,000, subsequent transactions use the 3.5% + \$0.40 base rate.
The rate change does not alter fees on transactions that were already processed.
## All fee combinations
The additional percentages stack when a charge is both recurring and paid with an international card.
| Transaction type | Up to \$5,000 in total volume | After \$5,000 in total volume |
| - | - | - |
| One-time payment, domestic card | 2.9% + \$0.40 | 3.5% + \$0.40 |
| One-time payment, international card | 4.4% + \$0.40 | 5.0% + \$0.40 |
| Subscription, domestic card | 3.6% + \$0.40 | 4.2% + \$0.40 |
| Subscription, international card | 5.1% + \$0.40 | 5.7% + \$0.40 |
### Example fees on a \$100 charge
This example uses a \$100 gross charge. If the payment includes tax, the gross amount is the full amount charged to the customer.
| Transaction type | Fee before the \$5,000 threshold | Fee after the \$5,000 threshold |
| - | -: | -: |
| One-time payment, domestic card | \$3.30 | \$3.90 |
| One-time payment, international card | \$4.80 | \$5.40 |
| Subscription, domestic card | \$4.00 | \$4.60 |
| Subscription, international card | \$5.50 | \$6.10 |
### Example: a sale with sales tax
A customer in the US buys a \$50 product with a US card. Sales tax is 8%, so they pay \$54. Your organization has already passed \$5,000 in sales, so the base fee is 3.5% + \$0.40. The fee is charged on the full amount the customer pays, including tax.
| | Amount |
| - | -: |
| Customer pays (\$50 + \$4 sales tax) | \$54.00 |
| Kelviq fee (3.5% of \$54 + \$0.40) | −\$2.29 |
| Sales tax, collected and remitted by Kelviq | −\$4.00 |
| **Added to your balance** | **\$47.71** |
| Payout fee | \$0.00 |
Before the \$5,000 threshold, the same sale costs 2.9% + \$0.40, a \$1.97 fee.
## How fees appear in Transactions
The **Fee** column under **Payouts → Transactions** shows the total fee deducted from a payment. It combines the applicable base rate, recurring fee, and international-card fee into one amount. Tax is shown separately.
For a successful transaction:
```text theme={null}
Net = Gross - Fee - Tax
```
[Learn how to read the Transactions page →](/payouts/transactions)
## Payout fees
There's no payout fee. Your full balance is sent to your bank account or eligible debit card. If your bank account uses a different currency, the amount is converted when the payout is sent.
[Learn how Kelviq payouts work →](/payouts/overview)
## Refunds
Here's how a refund of the \$54 sale above is paid out:
| | Full refund | Partial refund of \$20 |
| - | -: | -: |
| Customer gets back | \$54.00 | \$20.00 |
| Tax part of the refund, returned by Kelviq | \$4.00 | \$1.48 |
| Taken from your balance | \$50.00 | \$18.52 |
| **What you keep from the sale** | **−\$2.29** | **\$29.19** |
For a partial refund, the amount you enter is what the customer gets back, and the tax inside it is worked out proportionally (\$20 × \$4 / \$54 = \$1.48).
If your balance is lower than the amount taken for a refund, the refund still goes through and your balance goes negative.
## Disputes and chargebacks
When a customer disputes a charge with their bank, a **\$15 fee** applies regardless of the dispute outcome.
| Outcome | Fee | Transaction amount |
| - | - | - |
| You win | \$15 | Returned to you |
| You lose | \$15 | Refunded to customer |
### Reducing disputes
> **Clear billing descriptors**
>
> Use a recognizable business name on statements so customers remember the purchase.
> **Proactive support**
>
> Make it easy for customers to contact you before they escalate to their bank.
> **Accurate descriptions**
>
> Set clear expectations about what customers are purchasing and delivery timelines.
> **Easy cancellation**
>
> For subscriptions, provide a simple way to cancel to prevent "friendly fraud" disputes.
## FAQ
> **When are fees deducted?**
>
> Fees are deducted automatically from each transaction before the payout reaches your account.
> **Are there monthly fees or minimums?**
>
> No. You only pay when you make a sale. There are no setup fees, monthly fees, or minimum transaction requirements.
> **How is 'international' determined?**
>
> A transaction is considered international when the customer's card is issued outside the United States. Since Kelviq is the Merchant of Record registered in the US, this applies regardless of where your business is located.
> **Do fees apply to free trials?**
>
> No. Fees only apply to successful paid transactions. \$0 trial authorizations do not incur fees.
> **What about failed transactions?**
>
> No fees are charged for declined or failed transactions.
# Introduction to Kelviq
Source: https://docs.kelviq.com/getstarted/introduction-to-kelviq
What Kelviq does, how it fits into your app, and where to start.
Kelviq is a merchant of record with billing built in. You set your prices and plans, Kelviq sells them, collects the tax, and pays you out. Your app asks Kelviq what each customer can use.
> **[Set up with your coding agent](/ai/overview)**
>
> Using Claude Code, Cursor, or Codex? Your agent can set up Kelviq for you.
## What Kelviq handles
| Area | What you get |
| - | - |
| **Tax and compliance** | Kelviq is the seller of record. We calculate, collect, and file sales tax, VAT, and GST, and handle fraud and chargebacks. |
| **Checkout** | Hosted checkout with plan picker, trials, discounts, business purchases, and local payment methods. |
| **Pricing** | Flat, per seat, usage-based, tiered, volume, credits, pay as you go, one-time, or a mix. |
| **Entitlements** | Turn features on per plan, set usage limits, and check what each customer can access with one SDK call. In the dashboard, these are **Features & limits**. |
| **Usage** | Report usage as it happens. Kelviq counts it, resets it, and bills overage. |
| **Subscriptions** | Upgrades, downgrades, proration, and cancellations, plus a customer portal. |
| **Digital products** | File downloads, license keys, and links delivered after purchase. |
| **Local pricing** | Prices by country in 135+ currencies. |
Pricing changes happen in the dashboard or the CLI, not in your code. Your app checks features, not plan names, so moving a feature between plans needs no deploy.
## How it fits together
1. Create **products**, **plans**, and **features** in the dashboard or with the CLI.
2. Send customers to **checkout**. Kelviq takes the payment and the tax.
3. Your app **checks access** and **reports usage** with the SDK.
4. Kelviq bills renewals and overage, and **pays you out**.
## FAQ
> **Do I need to handle webhooks?**
>
> No. Your app can ask Kelviq for a customer's access at any time, so you don't need to keep subscription state in your database. [Webhooks](/guides/webhooks) are there if you want events in your own systems.
> **What if Kelviq can't be reached?**
>
> The Node and Python SDKs return the last known access for a customer and retry usage reports later. See [When the Entitlement API is unavailable](/product-catalog/entitlements#when-the-entitlement-api-is-unavailable).
> **Can I test without real payments?**
>
> Yes. The [sandbox](/guides/sandbox) is a separate environment with its own keys and test cards.
> **What does it cost?**
>
> A percentage of each transaction, with no monthly fee. See [Fees](/getstarted/fees).
> **Do I need a Stripe account?**
>
> No. You can sell and get paid without one, including in countries where Stripe doesn't offer its own accounts, such as India. Payouts go through a payout account you set up in Kelviq. See [Payouts](/payouts/overview#do-i-need-a-stripe-account).
> **Can I use my own Stripe account?**
>
> Yes, if you'd rather stay the merchant of record yourself. You then handle your own tax and compliance. See [Merchant of record](/getstarted/merchant-of-record).
> **[Next: set up your dashboard](/quickstart/dashboard-setup)**
>
> Create a product, a plan, and your first feature.
## Need help?
Email us at **[hi@kelviq.com](mailto:hi@kelviq.com)** or [book a demo](https://cal.com/alok-vats-fkpnc9/30min).
# Merchant of Record
Source: https://docs.kelviq.com/getstarted/merchant-of-record
Kelviq operates as your Merchant of Record (MoR). This means we are the legal reseller of your products to the end customer. We handle the taxes, compliance, and payment liability so you don’t have to.
***
## What is a Merchant of Record (MoR)?
The "Merchant of Record" is the most important concept to understand when using Kelviq. It's the engine that enables you to sell globally without handling the legal and financial complexity yourself.
A Merchant of Record is the entity that is **legally responsible for selling a product to an end customer**.
When a customer buys your product, their financial transaction is with the MoR (Kelviq), not with your company. We take on the responsibility for that sale, handling all the complex financial and legal requirements that come with it.
This model allows you to focus 100% on building your product while we handle the entire checkout, tax, and compliance backend.
***
### The Core Responsibilities of an MoR
By acting as your MoR, Kelviq takes on a massive amount of financial and legal liability. This is the "hard part" of global sales that we handle for you:
* **Global Tax & Compliance**
This is the single biggest challenge of selling online. An MoR is responsible for calculating, collecting, and remitting sales taxes like VAT (EU and UK), GST (India, Australia, Singapore, New Zealand), GST/HST (Canada), JCT (Japan), and state-level sales tax in the U.S.
* **Financial Liability & Risk**
The MoR is financially liable for every transaction. This includes bearing the cost of customer chargebacks (disputes) and all fraudulent payments.
* **Payment & Regulatory Burden**
The MoR ensures that every part of the checkout and payment process is compliant with global regulations, such as PCI-DSS and PSD2/SCA.
### How it Works: A Real-World Example
To understand exactly where the money goes, let's look at a specific transaction.
1. **The Sale:** A customer in **Germany** visits your site and buys your product. You have set the price at **€50.00**.
2. **The Calculation:** Kelviq automatically detects the customer is in Germany and adds the 19% German VAT (€9.50). The customer pays a total of **€59.50**.
3. **The Legal Transaction:** The customer's €59.50 transaction is legally with Kelviq. We are the "seller" on their invoice.
4. **Tax Remittance (The Hard Part):** At the end of the quarter, our finance team takes that **€9.50** and files the VAT return with the German authorities. You never see or touch this tax money, so you never have to worry about it.
5. **Your Payout:** We take our fee from the full **€59.50** the customer paid, send the €9.50 VAT to the German tax authority, and pay the rest out to your bank account. See [Fees](/getstarted/fees).
You get your expected revenue. We handle the legal and tax complexity of the extra €9.50.
### MoR vs. Payment Service Provider (PSP)
A PSP (like Stripe or PayPal) gives you the *tools* to process a payment, but **you** are still the seller of record.
| Feature | Payment Service Provider (PSP) | Merchant of Record (Kelviq) |
| :- | :- | :- |
| **Who is the Seller?** | **You** are the seller of record. | **Kelviq** is the seller of record. |
| **Tax Liability** | **Your responsibility.** You must register, file, and remit. | **Kelviq's responsibility.** We handle all filing and payments. |
| **Fraud Risk** | **You.** You lose the sale + fees on chargebacks. | **Kelviq.** We take the financial hit. |
| **Time to Market** | Weeks or months of setup. | Minutes. |
### Your Two Model Choices in Kelviq
Kelviq is designed to be flexible, offering two primary ways to operate:
**1. Kelviq as your Merchant of Record (Recommended)**
This is the default and recommended model. When you sign up, Kelviq acts as your MoR, handling all the responsibilities described on this page. You get the full all-in-one platform with zero compliance overhead.
**2. Bring Your Own Stripe Account (BYOA)**
For businesses that prefer to remain the Merchant of Record, you can connect your own Stripe account. In this model, **you are responsible for your own tax, fraud, and compliance**, but you still get access to all of Kelviq's monetization features—such as our usage-based pricing engine, feature flagging, and localization tools. Please contact us at [hi@kelviq.com](mailto:hi@kelviq.com) to enable this option.
# Payment security and seller funds
Source: https://docs.kelviq.com/getstarted/payment-security-and-seller-funds
How Kelviq protects card details, routes payments through Stripe, and handles payouts, account reviews, and migrations
When a customer buys through Kelviq Checkout, Kelviq is the Merchant of Record and Stripe processes the payment. The customer's raw card details go directly to Stripe through secure payment fields; they are not passed to Kelviq or your connected account. Stripe also provides the connected account used for your payouts. Here is how payment data, money, and subscription records move at each stage.
> [!NOTE]
>
> This page is an operational overview. If it conflicts with the [Seller Agreement](/legal/master-service-agreement), the agreement controls.
## How does a payment move?
**1. The customer enters their payment details**
Kelviq Checkout uses [Stripe Elements](https://docs.stripe.com/payments/elements). Stripe tokenizes sensitive payment details in the browser, so the full card number and CVC do not pass through or get stored on Kelviq's servers.
**2. Stripe processes the charge**
The charge and its PaymentIntent are created on Kelviq's Stripe platform account because Kelviq is the Merchant of Record.
**3. Kelviq calculates your share**
Kelviq accounts for applicable taxes, platform fees, refunds, and other adjustments before calculating your net revenue.
**4. Your revenue moves to your connected account**
Kelviq creates a separate transfer to your Stripe Express connected account. The funds appear there after Stripe makes them available.
**5. Stripe pays your bank account**
You can withdraw cleared funds to the bank account attached to your connected account.
> [!NOTE]
>
> Kelviq uses Stripe Connect's **separate charges and transfers** model. It does not use direct charges or destination charges. See the [full payment flow](/legal/payment-flow-diagram) for the technical breakdown.
## Who handles the card details?
The checkout may carry Kelviq's design, but Stripe provides the sensitive payment fields. Stripe.js tokenizes the details inside those fields and returns identifiers Kelviq can use to complete the payment. Kelviq receives the payment result, not the customer's full card number or CVC.
Stripe can also request 3D Secure authentication when it is required by the card issuer or recommended for fraud prevention. This adds an identity check without exposing the customer's card details to Kelviq.
Banks can still decline or reverse a payment, customers can open disputes, and Stripe or a card network can request a review. Kelviq handles those events as the Merchant of Record.
## Where does the money actually sit?
During processing, the money appears in Stripe balances. It first appears in Kelviq's Stripe platform balance. After the separate transfer, your share appears in your Stripe connected-account balance. Stripe then sends the payout to your registered bank account.
A Kelviq payout balance is not a bank deposit, escrow account, or insured investment. It represents amounts due under the Seller Agreement and can change because of refunds, chargebacks, fees, currency conversion, or a valid reserve.
You can track this flow in two places:
* The Kelviq **Payouts** dashboard shows processing funds, cleared funds, withdrawals, and payout history.
* Your Stripe connected-account details show the verified business and bank account used for payouts.
Read [Payouts](/payouts/overview) for the dashboard workflow.
## What protects my funds if Kelviq closes my account?
The Seller Agreement sets out Kelviq's payout obligations and when a payout can be delayed or reserved:
* Completed transactions move to your available balance after processing.
* Final payouts remain due if Kelviq ends the service, subject to open disputes and a permitted reserve.
* A reserve must have a stated amount or percentage and an expected duration.
* A post-termination reserve can remain in place for up to 180 days to cover refunds, chargebacks, or other transaction liabilities.
This is not a guarantee that nothing can go wrong. A processor failure, bank delay, dispute, compliance action, or insolvency can still affect the timing or recovery of funds. What you do have is a written payment flow, transaction records, and contractual payout terms against which you can check Kelviq's handling of your money. Review [Payout Terms](/legal/master-service-agreement#7-payout-terms) and [Reserves and Holdbacks](/legal/master-service-agreement#8-reserves-and-holdbacks) for the controlling language.
## How much notice will I get before a suspension?
There is no single advance-notice period for every suspension, including every non-fraud suspension. Kelviq may need to act quickly because of suspected fraud, prohibited activity, failed verification, a legal requirement, a payment-network instruction, or a restriction on the connected Stripe account.
The 30-day notice in the Seller Agreement applies when Kelviq terminates an account for convenience. It does not apply to the grounds for immediate termination listed in that agreement. If you believe a policy suspension or termination was made in error, you can appeal it. Kelviq responds to appeals within 10 business days under the [Acceptable Use Policy](/legal/approved-prohibited-policy#83-appeal-process).
### Will my customers' subscriptions cancel immediately?
Not in every case. A suspension and a subscription cancellation are separate actions. However, Kelviq may need to block checkout or future renewals, depending on why the account was restricted. The Seller Agreement does not promise uninterrupted subscription collection during a suspension. If a notice does not clearly say which functions are affected, contact [support@kelviq.com](mailto:support@kelviq.com) before making changes for your customers.
## Where do the Stripe Customer and Subscription records live?
Kelviq manages the buyer-facing Stripe Customer, PaymentIntent, invoice, and Subscription records on its Stripe platform account. Your Stripe Express connected account receives transfers and payouts; it does not receive an independent copy of every platform billing object.
An ID such as `cus_...` or `sub_...` points to an object inside the Stripe account where it was created. It cannot simply be reassigned to another Stripe account. Under the [Seller Agreement](/legal/master-service-agreement#111-buyer-data), Kelviq controls buyer payment and transaction data in its role as Merchant of Record. You still own your product and content, and Kelviq provides the buyer information needed for fulfillment and customer support.
## Can I move to another provider?
A billing migration is possible in some cases, but it is not an automatic account transfer. Contact [support@kelviq.com](mailto:support@kelviq.com) before closing your Kelviq account or disconnecting Stripe. Kelviq will review the request with Stripe and the receiving provider, then explain what can be transferred.
Stripe supports secure exports of eligible card data to a PCI DSS Level 1 payment processor. Some credentials, including cards saved through Link, cannot be exported. Subscription records and payment history also need to be mapped or recreated in the new billing system; moving card data alone does not move an active subscription.
Migration scope and timing depend on the receiving provider, Stripe's requirements, applicable privacy rules, and any unresolved refunds, disputes, or reserves. See [Stripe's payment-data export process](https://docs.stripe.com/get-started/data-migrations/pan-export) for the processor-level requirements.
## Questions about your account
For an account-specific explanation of a payout, reserve, suspension, or migration, email [support@kelviq.com](mailto:support@kelviq.com). Include your Kelviq business name and the relevant transaction, payout, or subscription IDs, but never send a full card number or CVC.
# Review Process
Source: https://docs.kelviq.com/getstarted/review-process
Because Kelviq operates as your **Merchant of Record (MOR)**, we are legally and financially responsible for every transaction processed through our platform. To ensure the safety of the ecosystem and comply with global financial regulations, we must strictly review the businesses we support.
This guide outlines what we support, what we prohibit, and how our review process works.
## 1. Supported Business Models
Kelviq is optimized for **digital products and software**. We are the best fit for:
* **SaaS & Software:** B2B or B2C web applications, desktop software, AI tools, and browser extensions.
* **Digital Downloads:** E-books, PDFs, design templates, audio files, video courses, and fonts.
* **Membership Sites:** Newsletters, research communities, and gated content.
* **Professional Services:** *Only if sold alongside a software product (e.g., "Setup Fee" or "Priority Support").*
> **The Golden Rule:**
> Your product should be **digital**, **owned by you** (or properly licensed), and **deliverable immediately** upon purchase.
### ⛔ The Three Fundamental Limitations
Before proceeding, please note the three "Hard No" rules of our MOR service:
1. **Digital Only:** We cannot support physical goods, shipping, or logistics.
2. **No Manual Services:** We cannot support 1-on-1 coaching, consulting, or agencies where the primary value is your manual labor.
3. **No Marketplaces:** You must be the original creator or licensed distributor. You cannot use Kelviq to process payments for other unverified sellers.
## 2. Global Compliance & High-Risk Categories (Prohibited)
We are required by law and our financial partners to block any product or service that is illegal, deceptive, or creates a high financial risk.
This list is not exhaustive, and we reserve the right to review, suspend, or terminate any account that we believe violates the spirit of these rules.
### A. Illegal, Deceptive, or Hateful Content
* Any product or service that is illegal in the U.S. or your customer's location.
* Content that infringes on someone else's Intellectual Property (IP), copyright, or trademark. (e.g., pirated software, stolen ebooks, cracked license keys).
* Hate speech or content that promotes violence, self-harm, or discrimination against any group.
* Deceptive marketing practices, fake testimonials, or unrealistic "get rich quick" schemes.
* Low-value, exploitative content (e.g., rapidly AI-generated ebooks with minimal substance sold at a high price).
* Identity services, fake IDs, or products that support academic cheating.
### B. Financial and Legal Services
* **Regulated Financial Products:** We cannot sell investment advice, trading signals, cryptocurrency, NFTs, lending services, or act as a money transmitter.
* **Gambling:** All forms of gambling, betting, or games of chance.
* **Regulated Professional Services:** We cannot sell legal, tax, or accounting advice.
* **Donations:** We cannot process donations where no specific product is delivered.
* **Regulatory Evasion:** Any service designed to help others evade taxes, mask their identity, or bypass financial regulations.
### C. Adult & Regulated Industries
* **Adult Content:** Explicitly sexual content (videos, images, text), whether real or **AI-generated**. This includes "AI Girlfriend/Boyfriend" services, NSFW chatbots, and services related to OnlyFans.
* **Controlled Substances:** Alcohol, tobacco, vaping products, drugs, or drug paraphernalia.
* **Weapons:** Firearms, ammunition, explosives, or combat training.
* **Pharmaceuticals:** Prescription drugs or unverified medical/wellness supplements.
### D. Platform & Technology Abuse
* **Spam & Scraping:** Tools for mass cold emailing, lead scraping, or sending bulk SMS/WhatsApp messages.
* **Hacking & Cheating:** Tools designed to cheat in games, bypass software rules, or gain unauthorized access.
* **Circumvention Tools:** Services that bypass API limits, platform rules, or geo-restrictions (e.g., IP cloaking).
* **Surveillance Tech:** Products that violate user privacy or enable unauthorized surveillance (e.g., keyloggers, stalkerware).
## 3. Restricted Businesses (Requires Extensive Review)
The following businesses are not automatically prohibited, but they are considered high-risk and require extensive review by our compliance team. Approval is not guaranteed and may be subject to additional conditions (like holding funds in reserve).
* **AI-Powered Services:** Especially tools for AI-generated text, images, or voice. We need to review your product to ensure it has proper safeguards against generating harmful, deceptive, or adult content.
* **Marketing Services:** Some marketing tools may be allowed, but we must review them to ensure they do not facilitate spam or mass outreach.
* **Pre-Orders & Paid Waitlists:** We generally **do not** allow pre-orders. This is a very high-risk category for an MOR. In rare cases, we may make an exception for established creators, but funds will be withheld until the product is verifiably shipped.
## 4. Prohibited & Supported Countries
### Prohibited Countries
As a U.S. company, Kelviq is required to comply with U.S. sanctions laws (OFAC). We are **strictly prohibited** from doing business with any individual or entity located in:
* North Korea
* Iran
* Cuba
* Syria
* Russia
* Belarus
* The regions of Crimea, Donetsk, and Luhansk
### Supported Payout Countries
We support businesses registered in **100+ countries**. You can receive payouts in your local bank account in currencies including USD, EUR, GBP, CAD, AUD, INR, and more.
| | | |
| :- | :- | :- |
| 🇦🇱 Albania | 🇦🇬 Antigua & Barbuda | 🇦🇷 Argentina |
| 🇦🇲 Armenia | 🇦🇺 Australia | 🇦🇹 Austria |
| 🇧🇸 Bahamas | 🇧🇭 Bahrain | 🇧🇪 Belgium |
| 🇧🇯 Benin | 🇧🇴 Bolivia | 🇧🇦 Bosnia & Herzegovina |
| 🇧🇼 Botswana | 🇧🇳 Brunei | 🇧🇬 Bulgaria |
| 🇰🇭 Cambodia | 🇨🇦 Canada | 🇨🇱 Chile |
| 🇨🇴 Colombia | 🇨🇷 Costa Rica | 🇨🇮 Côte d'Ivoire |
| 🇨🇾 Cyprus | 🇨🇿 Czech Republic | 🇩🇰 Denmark |
| 🇩🇴 Dominican Republic | 🇪🇨 Ecuador | 🇪🇬 Egypt |
| 🇸🇻 El Salvador | 🇪🇪 Estonia | 🇪🇹 Ethiopia |
| 🇫🇮 Finland | 🇫🇷 France | 🇬🇲 Gambia |
| 🇩🇪 Germany | 🇬🇭 Ghana | 🇬🇷 Greece |
| 🇬🇹 Guatemala | 🇬🇾 Guyana | 🇭🇰 Hong Kong |
| 🇭🇺 Hungary | 🇮🇳 India | 🇮🇸 Iceland |
| 🇮🇪 Ireland | 🇮🇱 Israel | 🇮🇹 Italy |
| 🇯🇲 Jamaica | 🇯🇵 Japan | 🇯🇴 Jordan |
| 🇰🇪 Kenya | 🇰🇼 Kuwait | 🇱🇻 Latvia |
| 🇱🇹 Lithuania | 🇱🇺 Luxembourg | 🇲🇴 Macao SAR China |
| 🇲🇬 Madagascar | 🇲🇹 Malta | 🇲🇺 Mauritius |
| 🇲🇽 Mexico | 🇲🇩 Moldova | 🇲🇨 Monaco |
| 🇲🇳 Mongolia | 🇲🇦 Morocco | 🇳🇦 Namibia |
| 🇳🇱 Netherlands | 🇳🇿 New Zealand | 🇳🇬 Nigeria |
| 🇲🇰 North Macedonia | 🇳🇴 Norway | 🇴🇲 Oman |
| 🇵🇰 Pakistan | 🇵🇦 Panama | 🇵🇾 Paraguay |
| 🇵🇪 Peru | 🇵🇭 Philippines | 🇵🇱 Poland |
| 🇵🇹 Portugal | 🇶🇦 Qatar | 🇷🇴 Romania |
| 🇷🇼 Rwanda | 🇸🇦 Saudi Arabia | 🇸🇳 Senegal |
| 🇷🇸 Serbia | 🇸🇬 Singapore | 🇸🇰 Slovakia |
| 🇸🇮 Slovenia | 🇿🇦 South Africa | 🇰🇷 South Korea |
| 🇪🇸 Spain | 🇱🇰 Sri Lanka | 🇱🇨 St. Lucia |
| 🇸🇪 Sweden | 🇨🇭 Switzerland | 🇹🇼 Taiwan |
| 🇹🇿 Tanzania | 🇹🇭 Thailand | 🇹🇹 Trinidad & Tobago |
| 🇹🇳 Tunisia | 🇹🇷 Turkey | 🇦🇪 United Arab Emirates |
| 🇬🇧 United Kingdom | 🇺🇸 United States | 🇺🇾 Uruguay |
| 🇺🇿 Uzbekistan | 🇻🇳 Vietnam | |
*This list is determined by our payment provider and is subject to change. If your country is not listed here, you will not be able to complete onboarding or receive payouts.*
## 5. The Review Process
Before you can process real payments, your account must pass our verification process. You can integrate and test in "Sandbox Mode" immediately, but "Live Mode" requires approval.
There are two steps to complete before you can start accepting real payments:
### Step 1: Business Verification
Open **Settings → Business details** in the Kelviq dashboard.
Fill in your business details: logo, country, website, a support email that buyers will see, a short overview of what you sell, your estimated monthly revenue, and your social profiles. Kelviq places the country from your organization profile first in the country selector. Check it before you save.

Click **Save details**. The page then shows that your details are under review, and you can carry on creating products and testing in sandbox while you wait.

**What happens next:**
* Most reviews are completed within **24 hours**.
* If we need more information, we'll reach out to you via email.
* Once your verification is approved, you'll receive a confirmation email.
### Step 2: Set Up Payouts
After your business is verified, you need to set up payouts to receive money from your sales. We partner with **Stripe** to enable secure payouts.
Navigate to the **Payouts** section in your dashboard, or go directly to [app.kelviq.com/payout](https://app.kelviq.com/payout).

Click the **Set up payouts** button. You'll be redirected to Stripe to complete a quick setup:
1. **Verify your identity** - Stripe will ask for a few details to verify your identity and business.
2. **Connect your bank account** - Add your bank account details to receive payouts.
Once you complete the Stripe setup, you'll be redirected back to Kelviq.
### You're Ready to Go Live
Once both steps are completed (Business Verification and Payout Setup), you can start accepting real payments from customers.
## 6. Testing Payments
> [!WARNING]
>
> Do not use your own real card to test payment flows.
For testing, you have two options:
1. **Use Sandbox Mode** - Switch to sandbox mode in your dashboard to test the entire checkout flow with test card numbers.
2. **Use 100% Discount Coupons** - Create a discount coupon with 100% off to test the checkout workflow in live mode without being charged.
## 7. Enforcement & Monitoring
Once you are live, we continuously monitor account activity to prevent fraud and ensure ongoing compliance.
* **Refund Rate Monitoring:** If your refund rate exceeds acceptable thresholds (typically >10%), we may reach out to help you improve your product quality or support.
* **Chargeback Monitoring:** We maintain a strict low-tolerance policy for chargebacks. Excessive dispute levels may result in account review.
* **Prohibited Activity:** If an account is found to be selling prohibited items or engaging in money laundering/fraud after approval, the account will be suspended immediately and funds may be held to cover potential disputes.
## 8. Have a Question About Your Product?
We know this list is long, and the line can be blurry, especially with new AI tools. We want to support innovative products.
If you are unsure if your product is allowed, **please contact us at [hi@kelviq.com](mailto:hi@kelviq.com) *before* you launch.**
Our simple "gut check" is: **Is this a high-quality, legitimate product that you are proud of, that you own the rights to, and that is delivered digitally?** If you can answer "yes" to all three, you are probably in the clear.
# Agent Toolkit
Source: https://docs.kelviq.com/guides/agent-toolkit
Install Kelviq into Claude Code with one plugin — or add the Kelviq skills to any of 70+ AI coding agents.
The [Kelviq Agent Toolkit](https://github.com/kelviq/agent-toolkit) packages everything an AI coding agent needs to work with Kelviq — the full [MCP server](/guides/mcp-server) plus five **Agent Skills** that teach the agent Kelviq's workflows — behind a single install.
Skills are the missing half of tool access. The MCP server gives your agent 57 typed tools, but tools alone don't tell it that a new plan sits in DRAFT until published, that checkout is the default way to charge a customer, or that a retried usage report can double-count. The skills carry exactly that working knowledge, and they load automatically whenever a task looks Kelviq-shaped.
> [!NOTE]
>
> **The Agent Toolkit is new.** The skills and their names are stable, but content will keep improving alongside the platform. The bundled MCP server is always the latest published `@kelviq/mcp-server` — no toolkit update needed when the server improves.
***
## Install — Claude Code
Two commands, in any Claude Code session:
```
/plugin marketplace add kelviq/agent-toolkit
/plugin install kelviq@kelviq
```
This installs the full bundle: the MCP server is configured for you (no `claude mcp add`, no `--` separator gotchas) and all five skills are available immediately.
The server reads your API keys from environment variables. It uses your **sandbox** environment unless `KELVIQ_ENV=production`, so start with sandbox keys. Set them in your shell profile before starting Claude Code:
```bash theme={null}
export KELVIQ_SERVER_API_KEY=""
export KELVIQ_CLIENT_API_KEY="" # only needed for promotion_get
```
Get keys at [app.kelviq.com/settings/api-keys](https://app.kelviq.com/settings/api-keys).
**Verify**: run `/mcp` — you should see `kelviq ✓ Connected, 57 tools`. Then ask something Kelviq-shaped ("set up a Pro plan for my product") and watch the relevant skill activate.
> [!TIP]
>
> No keys yet? Install anyway. The docs tools (`docs_search`, `docs_read`) work without any credentials, so your agent can answer Kelviq questions from the documentation immediately. API tools return a clear `Configuration error` until a key is set.
***
## Install — any other agent
The five skills are also published through the open skills ecosystem, installable into 70+ agents (Cursor, Codex, GitHub Copilot, OpenCode, Zed, and more):
```bash theme={null}
npx skills add kelviq/agent-toolkit
```
Add `--skill ` to install a single skill instead of the full set:
```bash theme={null}
npx skills add kelviq/agent-toolkit --skill setup-payments
```
> [!NOTE]
>
> This path installs **skills only** — it doesn't configure an MCP server for you. Each skill starts with a short Setup section showing the `.mcp.json` snippet to connect `@kelviq/mcp-server` on your agent; see the [MCP server guide](/guides/mcp-server) for client-specific instructions.
***
## The five skills
| Skill | What it teaches |
| - | - |
| `kelviq` | The hub. Connecting any agent to Kelviq, the ground rules (docs-first, error handling, confirm-before-destructive), the concept model (identifiers vs UUIDs, feature types, plan lifecycle, environments), and routing to the right skill or prompt for the task. |
| `pricing-plans` | Designing and publishing the catalog: products, features, plans, entitlements, the DRAFT → publish lifecycle, and customer-migration decisions on publish. |
| `setup-payments` | Selling plans and managing subscribers: checkout sessions, subscriptions, trials, upgrades, cancellation, and authenticated customer portal links. |
| `usage-based-billing` | Enforcing what was sold: runtime entitlement checks served from the edge, safe usage reporting for Meter features, and the license key lifecycle. |
| `pricing-as-code` | The [Kelviq CLI](/cli/overview) workflow: pull the live catalog into `kelviq.config.ts`, edit in git, preview the diff, apply, and promote sandbox to production. |
### `kelviq` — the hub
Loads for any Kelviq task that doesn't match a more specific skill. It carries the knowledge every other skill assumes:
* **Identifiers vs UUIDs** — when an endpoint wants the URL-safe `identifier` and when it wants the UUID, and how to resolve one from the other.
* **Feature types** — `BOOLEAN` (flag), `CUSTOMIZABLE` (numeric quantity), `METER` (usage-tracked), and how entitlement shapes differ per type.
* **Plan lifecycle** — plans are created in DRAFT and invisible until published; updating a published plan creates a new draft.
* **Environments** — sandbox by default, `KELVIQ_ENV=production` only for live data, and why "not found" errors often mean "right key, wrong environment".
* **A customer-access diagnosis flow** — comparing what a customer *has* (`entitlements_get`), what they *bought* (`subscription_list`), and what their plan *grants* (`plan_entitlements_list`).
### `pricing-plans`
The golden path from empty account to published plan: create the product, create the features, create the plan (in DRAFT), attach entitlements, set prices, publish, verify. Includes the two decisions agents most often get wrong: whether to migrate existing customers on publish (`updateFeatures` / `updatePricing` — always a deliberate, confirmed choice), and where prices are managed (the dashboard or the CLI — the MCP server deliberately has no price-write tool).
### `setup-payments`
Checkout-first selling. The skill teaches that `checkout_create_session` is the default way to charge a customer — direct subscription creation charges off-session and is only enabled for some organizations — plus trial handling (`trialEnd`), safe plan changes (`paymentBehavior`), cancellation types, and the customer portal (always the signed URL from `portal_session_create`; a bare portal URL doesn't authenticate).
### `usage-based-billing`
Runtime enforcement. Entitlement checks via the edge-served `entitlements_get` (built to be cheap enough to call on the request path), the difference between `SET` and `DELTA` usage reporting — and why a blindly retried `DELTA` double-counts — plus license key activation, validation, and deactivation with `instanceId` handling.
### `pricing-as-code`
Wraps the [Kelviq CLI](/cli/overview) loop for agents: `kelviq pull` → edit `kelviq.config.ts` in a branch → `kelviq push --dry-run` → review → apply → `kelviq promote` from sandbox to production. The skill has a built-in safety rule: an agent may run dry-runs and show you diffs freely, but **applying a real change always requires your explicit approval** — it will never chain a dry-run into an unattended `--yes`.
***
## Skills, prompts, and tools — how they fit
The toolkit layers three things, each doing what it's best at:
1. **Tools** (from the MCP server) do the work — 57 typed API calls, each carrying [safety annotations](https://modelcontextprotocol.io/docs/concepts/tools#tool-annotations) so clients can auto-approve reads and confirm destructive writes.
2. **Skills** (from this toolkit) supply the judgment — which tool, in what order, with which confirmations. They activate automatically based on what you ask.
3. **Prompts** (from the MCP server) are guided, step-by-step flows you invoke explicitly — `setup_product`, `launch_plan`, and `diagnose_customer`. The skills route to them rather than duplicating them.
Deep reference detail (field lists, price shapes, config schemas) intentionally lives in none of these — the skills direct the agent to `docs_search` / `docs_read`, so answers always come from the current documentation.
***
## Sandbox and production
The bundled server uses your **sandbox** environment unless you set `KELVIQ_ENV=production` — separate data, separate keys, nothing crosses between them. Build and test in sandbox, and switch to production (with production API keys) only when you want the agent to work with live data. See the [sandbox guide](/guides/sandbox) for details.
```bash theme={null}
export KELVIQ_ENV="production"
export KELVIQ_SERVER_API_KEY=""
```
***
## Updating
* **MCP server**: nothing to do. The toolkit runs `npx -y @kelviq/mcp-server@latest`, which checks for the newest published version each time the server starts.
* **Skills** (Claude Code): `/plugin marketplace update kelviq` pulls the latest marketplace content.
* **Skills** (other agents): re-run `npx skills add kelviq/agent-toolkit`.
***
## Troubleshooting
**`kelviq:` tools aren't available** — on Claude Code, check `/mcp`; if the server isn't listed, reinstall the plugin. On other agents, the skills-only install doesn't include the server — follow the Setup section in any installed skill or the [MCP server guide](/guides/mcp-server).
**`Configuration error` from API tools** — `KELVIQ_SERVER_API_KEY` isn't visible to the process that launched your agent. Set it in your shell profile (not just the current terminal) and restart the agent.
**"Not found" on an identifier you can see in the dashboard** — your key and `KELVIQ_ENV` are probably pointing at the other environment. Sandbox and production are fully separate; a key only works against its own environment.
***
## Related
* [MCP server guide](/guides/mcp-server) — manual per-client setup, full tool list, architecture
* [Kelviq CLI](/cli/overview) — the pricing-as-code workflow the `pricing-as-code` skill teaches
* [Sandbox](/guides/sandbox) — test environment setup
* [github.com/kelviq/agent-toolkit](https://github.com/kelviq/agent-toolkit) — the toolkit source
# MCP Server
Source: https://docs.kelviq.com/guides/mcp-server
Control Kelviq from Claude, Cursor, and any AI assistant that speaks the Model Context Protocol.
`@kelviq/mcp-server` is the official [Model Context Protocol](https://modelcontextprotocol.io) server for Kelviq. It plugs into any MCP-compatible client — Claude Code, Claude Desktop, Cursor, Devin Desktop (formerly Windsurf), VS Code, Antigravity — and exposes the full Kelviq API as **typed tools** that the LLM can call directly, plus the entire Kelviq documentation as **searchable resources**.
Instead of asking your assistant to "write me a curl command," you describe the outcome ("create a Pro plan under product X and publish it") and it does the work in a single turn.
> [!TIP]
>
> **Using Claude Code?** The fastest install is the [Agent Toolkit](/guides/agent-toolkit) plugin — two commands set up this MCP server *and* five Kelviq skills that teach Claude the workflows. This page covers manual setup for every MCP client.
> [!NOTE]
>
> **57 tools** spanning connection checks, customers, checkout, subscriptions, discounts, webhooks, entitlements, licenses, usage, offerings, promotions, the customer portal, and full catalog management (products, plans, features) — plus a resource for every docs page and the full OpenAPI spec, and **3 guided prompts** for the most common workflows. Every tool carries [MCP safety annotations](https://modelcontextprotocol.io/docs/concepts/tools#tool-annotations), so clients can auto-approve read-only calls while confirming destructive ones.
***
## How It Works
The server runs locally on your machine as a Node process started by your AI client. It speaks the MCP **stdio transport** — your client launches `npx -y @kelviq/mcp-server@latest`, connects over stdio, and discovers the tools and resources advertised by the server.
When the LLM decides to call a Kelviq tool, the server:
1. Validates the LLM-provided arguments against the tool's Zod schema.
2. Adds your API key as a Bearer token and sends the HTTPS request to the appropriate Kelviq host.
3. Returns the JSON response (or a structured `{"error": {...}}` object) back to the LLM.
API errors are returned as content, not exceptions — the LLM reads them and reacts (retries with a fix, asks you to clarify, etc).
***
## Step 1: Get Your API Keys
1. Sign in at [app.kelviq.com](https://app.kelviq.com).
2. Navigate to **Settings → API keys**.
3. Copy:
* **Server API Key** — required for all API tools.
* **Client API Key** — required only for the `promotion_get` tool.
> [!WARNING]
>
> Never commit API keys to version control or paste them into shared chats. Use your MCP client's `env` configuration mechanism — most clients store secrets in their own credentials file.
***
## Step 2: Add the Server to Your AI Client
Every client uses the same underlying command — `npx -y @kelviq/mcp-server@latest` — with your API keys and `KELVIQ_ENV` passed as environment variables. What differs is the config file location and, in a few cases, the root JSON key. Pick the tab for your client.
> [!NOTE]
>
> The server uses your **sandbox** environment unless `KELVIQ_ENV=production`. Use keys from the same environment — to work with live data, use production keys and set `KELVIQ_ENV=production`.
**Claude Code**
```bash theme={null}
claude mcp add kelviq \
--env KELVIQ_SERVER_API_KEY= \
--env KELVIQ_CLIENT_API_KEY= \
--env KELVIQ_ENV=sandbox \
-- npx -y @kelviq/mcp-server@latest
```
> [!NOTE]
>
> The `--` separator is required so that `claude` doesn't intercept `-y` as one of its own flags. Put all `--env` arguments **before** the `--`.
Verify the connection with `/mcp` inside a Claude Code session — you should see `kelviq ✓ Connected, 57 tools`.
**Claude Desktop**
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or the equivalent for your OS:
```json theme={null}
{
"mcpServers": {
"kelviq": {
"command": "npx",
"args": ["-y", "@kelviq/mcp-server@latest"],
"env": {
"KELVIQ_SERVER_API_KEY": "server-...",
"KELVIQ_CLIENT_API_KEY": "client-...",
"KELVIQ_ENV": "sandbox"
}
}
}
}
```
Restart Claude Desktop — the hammer icon shows `kelviq` with all tools.
**Cursor**
Open **Cursor Settings → Tools and MCP → Add new MCP server**, or edit `.cursor/mcp.json` (project) / `~/.cursor/mcp.json` (global) directly:
```json theme={null}
{
"mcpServers": {
"kelviq": {
"command": "npx",
"args": ["-y", "@kelviq/mcp-server@latest"],
"env": {
"KELVIQ_SERVER_API_KEY": "server-...",
"KELVIQ_CLIENT_API_KEY": "client-...",
"KELVIQ_ENV": "sandbox"
}
}
}
}
```
Project-level config overrides global config when a server of the same name exists in both.
**Windsurf / Devin Desktop**
As of June 2026, Windsurf was rebranded **Devin Desktop** by Cognition. Existing installs update automatically — same editor, new name. The MCP config location also changed with the rebrand:
* **Devin Desktop (current):** `%APPDATA%\devin\config.json` (Windows) — via **Settings → Devin Local → Configuration → MCP servers**
* **Legacy Windsurf installs:** `~/.codeium/windsurf/mcp_config.json`
Add Kelviq under the `mcpServers` key:
```json theme={null}
{
"mcpServers": {
"kelviq": {
"command": "npx",
"args": ["-y", "@kelviq/mcp-server@latest"],
"env": {
"KELVIQ_SERVER_API_KEY": "server-...",
"KELVIQ_CLIENT_API_KEY": "client-...",
"KELVIQ_ENV": "sandbox"
}
}
}
}
```
Save, then refresh or restart the app — `kelviq` should appear under **MCP servers** in Settings, where you can enable it and toggle individual tools.
> [!NOTE]
>
> If your `config.json` isn't empty, check its existing top-level structure before pasting — merge `kelviq` alongside whatever's already there rather than overwriting the file.
**VS Code (Copilot)**
Add a `.vscode/mcp.json` in your workspace, or run **"MCP: Open User Configuration"** from the Command Palette for a global `mcp.json`. VS Code uses `servers`, not `mcpServers`, as the root key:
```json theme={null}
{
"servers": {
"kelviq": {
"command": "npx",
"args": ["-y", "@kelviq/mcp-server@latest"],
"env": {
"KELVIQ_SERVER_API_KEY": "server-...",
"KELVIQ_CLIENT_API_KEY": "client-...",
"KELVIQ_ENV": "sandbox"
}
}
}
}
```
Save, then click the **Start** CodeLens above the `kelviq` entry (or reload the window) and confirm it under the tools icon in Copilot Chat's Agent mode.
> [!TIP]
>
> Already have Kelviq configured in Cursor or Devin Desktop (formerly Windsurf)? Under **Settings → Chat → MCP → Discovery**, check the matching box (e.g. "Windsurf configurations") to auto-import that server instead of retyping it. You can also use the `inputs` array to be prompted for the keys instead of storing them in plaintext.
**Antigravity**
Open the "..." menu in the Agent panel → **MCP Servers** → **Manage MCP Servers** → **View raw config**. This opens `mcp_config.json` at:
* **Windows:** `C:\Users\\.gemini\antigravity\mcp_config.json`
* **macOS/Linux:** `~/.gemini/antigravity/mcp_config.json`
```json theme={null}
{
"mcpServers": {
"kelviq": {
"command": "npx",
"args": ["-y", "@kelviq/mcp-server@latest"],
"env": {
"KELVIQ_SERVER_API_KEY": "server-...",
"KELVIQ_CLIENT_API_KEY": "client-...",
"KELVIQ_ENV": "sandbox"
}
}
}
}
```
> [!NOTE]
>
> The Antigravity **CLI** (as opposed to the IDE) uses a separate config path: `~/.gemini/antigravity-cli/mcp_config.json`.
**Other Clients**
Any client supporting stdio MCP servers works. Point it at:
```bash theme={null}
npx -y @kelviq/mcp-server@latest
```
with the env vars listed below.
***
## Step 3: Try It
In a new session, type a prompt like:
> *Search Kelviq docs for license keys.*
or
> *List my Kelviq products.*
Your AI client should pick the right tool, call it, and show the result inline. If you see a `Configuration error`, your env vars didn't make it through — re-check the config above.
***
## Environment Variables
| Variable | Required | Default | Purpose |
| - | - | - | - |
| `KELVIQ_SERVER_API_KEY` | Yes, for all API tools | — | Server API key (Bearer token for the core API) |
| `KELVIQ_CLIENT_API_KEY` | Only for `promotion_get` | — | Client API key (Bearer token for the edge host) |
| `KELVIQ_ENV` | No | `sandbox` | `sandbox` or `production`. Set to `production` to work with live data — switches both API hosts automatically. Use a key from the same environment: keys only work against their own environment. |
| `KELVIQ_BASE_URL` | No | per `KELVIQ_ENV` | Advanced: override the core API host |
| `KELVIQ_EDGE_URL` | No | per `KELVIQ_ENV` | Advanced: override the edge host (entitlements + promotions) |
> [!TIP]
>
> Docs tools (`docs_search`, `docs_read`) work with no API keys at all. Even without a key, the LLM can answer Kelviq questions from the bundled documentation.
***
## Tool Reference
Tools are grouped by domain. Each tool name uses `domain_action` form (`customer_create`, `plan_publish`). The LLM picks the right tool from the description automatically — you rarely need to name a tool explicitly.
Wherever a tool takes a product or feature reference (`plan_create`, `plan_entitlements_add`, `discount_create`'s `appliesTo`, `offering_get_product`), it accepts the identifier or the UUID; the server resolves an identifier before calling the API.
### Connection
| Tool | Description |
| - | - |
| `connection_check` | One authenticated read with the server key. Returns the environment, both API hosts and whether the key is `ok`, `missing`, `rejected` or errored, never the key itself. The first thing to call when something returns 401/403. |
### Customers
| Tool | Description |
| - | - |
| `customer_create` | Create a customer. Required: `customerId`. Optional: `email`, `name`, `metadata`, `billingAddress`. |
| `customer_update` | Patch an existing customer by `customerId`. Only fields you pass are updated. |
### Checkout & Portal
| Tool | Description |
| - | - |
| `checkout_create_session` | Create a hosted checkout session. Returns `checkoutUrl` + `checkoutSessionId`. |
| `portal_session_create` | Create a customer portal session. Share the returned `signedPortalUrl` with the customer — the bare `customerPortalUrl` does not authenticate on its own. |
### Subscriptions
| Tool | Description |
| - | - |
| `subscription_create` | Create a subscription directly (skip checkout). Requires off-session charging to be enabled for your organization — otherwise use `checkout_create_session`. |
| `subscription_list` | List subscriptions, filter by customer, paginated. |
| `subscription_update` | Change plan / charge period / features. Free plans must use `checkout_create_session` to upgrade. |
| `subscription_cancel` | Cancel `IMMEDIATE`, `CURRENT_PERIOD_ENDS`, or `SPECIFIC_DATE`. |
### Entitlements, Licenses, Usage
| Tool | Description |
| - | - |
| `entitlements_get` | Fetch entitlements for a customer. Pass `featureId` to scope. |
| `license_activate` | Activate a license key for a specific instance. Returns `instanceId`. |
| `license_deactivate` | Deactivate a license instance. |
| `license_validate` | Validate a license key (optionally bound to an instance). |
| `usage_report` | Report metered usage. `behaviour: SET` replaces, `DELTA` increments. |
### Offerings & Promotions
| Tool | Description |
| - | - |
| `offering_get_product` | Fetch public pricing scoped to a product UUID. Includes localized prices + customer subscription state. |
| `promotion_get` | Resolve the active promotion for the caller's location. Uses Client API key. |
### Discounts
| Tool | Description |
| - | - |
| `discount_list` | List discount codes. Optional `search` across name and code, paginated. |
| `discount_create` | Create a percentage or fixed-amount code, optionally limited to a product (identifier or UUID) and its plans with `appliesTo`. Merchant of Record organizations only. |
| `discount_retrieve` | Fetch one discount, including how often it's been redeemed and whether it's active. |
| `discount_update` | Enable or disable a code with `isActive`. Nothing else can change after creation. |
| `discount_archive` | Archive a code so it can no longer be used. |
### Webhooks
| Tool | Description |
| - | - |
| `webhook_endpoint_list` | List endpoints with their URLs, events and enabled state. |
| `webhook_endpoint_create` | Register a public URL for the events you choose. Localhost and private addresses are rejected. |
| `webhook_endpoint_retrieve` | Fetch one endpoint. |
| `webhook_endpoint_update` | Change an endpoint's URL, events or enabled state. |
| `webhook_endpoint_delete` | Delete an endpoint. Deliveries stop immediately. |
| `webhook_log_list` | Delivery attempts from the last 30 days, with status codes and retries. |
Signing secrets are hidden in webhook tool results. Pass `revealSecret: true` only when you need the value — your app reads the secret from its env file, so it rarely needs to appear in a conversation.
### Catalog: Products
| Tool | Description |
| - | - |
| `product_list` | List catalog products. Optional `search` substring + `page`. |
| `product_create` | Create a product. Required: `name`, `taxCode`. Optional `imageKeys` (S3 file keys from the `POST /media/` API endpoint). |
| `product_retrieve` | Fetch one product by UUID or identifier. |
| `product_update` | Patch a product. |
| `product_archive` | Archive (soft-delete) a product. |
| `product_settings_get` | VPN / Tor / proxy policy, default currency, product URL. |
| `product_settings_update` | Patch any subset of those settings. |
| `product_image_update` | Edit display fields of a product image (`name`, `ordering`, `thumbnail`, `enabled`). |
| `product_image_delete` | Remove an image from a product. |
### Catalog: Features
| Tool | Description |
| - | - |
| `feature_list` | List catalog features. |
| `feature_create` | Create a feature. Required: `name`, `featureType` (`BOOLEAN` / `CUSTOMIZABLE` / `METER`). |
| `feature_retrieve` | Fetch one feature. |
| `feature_archive` | Archive a feature. Plans referencing it keep working. |
### Catalog: Plans
Plans are versioned. Updates create a draft; `plan_publish` releases it.
| Tool | Description |
| - | - |
| `plan_list` | List all plans in the account. |
| `plan_create` | Create a plan under a product (by identifier or UUID). Plan starts in `DRAFT`. |
| `plan_retrieve` | Fetch one plan. |
| `plan_update` | Patch a plan. Updates to a published plan automatically create a new draft. |
| `plan_archive` | Archive a plan. Existing subscriptions keep working. |
| `plan_publish` | Publish the latest draft. `updateFeatures` / `updatePricing` flags migrate existing customers. |
| `plan_discard_draft` | Drop the current draft, keep the published version. |
| `plan_prices_list` | List all prices configured on a plan. |
| `plan_entitlements_list` | List feature entitlements on a plan. |
| `plan_entitlements_add` | Attach feature entitlements, each feature by identifier or UUID. |
| `plan_entitlement_update` | Patch a single entitlement's `details`. |
| `plan_entitlement_delete` | Remove an entitlement. |
| `plan_file_update` | Edit display fields of a plan-attached file. |
| `plan_file_delete` | Remove a file from a plan. |
| `plan_file_download` | Download URL/stream for a plan file (buyer-facing deliverables). |
### Docs (no API key required)
| Tool | Description |
| - | - |
| `docs_search` | Full-text search across the bundled Kelviq docs. Returns `{slug, title, snippet, score}`. |
| `docs_read` | Fetch a full markdown doc by slug. |
***
## Resources
Resources let your AI client pin pages into context. The server advertises three categories:
| URI pattern | Content |
| - | - |
| `kelviq://docs/{section}/{slug}` | Every page in the Kelviq docs |
| `kelviq://openapi.json` | The full OpenAPI 3.0 spec (every operation and schema) |
| `kelviq://README` | Top-level Kelviq overview |
For exhaustive per-endpoint request/response shapes, the **OpenAPI spec is the source of truth**. Ask your assistant *"check the OpenAPI spec for createSubscription"* and it will read the resource directly.
***
## Built-in Prompts
The server ships three MCP prompts that encode Kelviq's golden-path workflows — including the gotchas (plans start as invisible drafts until published; features attach by identifier or UUID; pricing is set in the dashboard or with the [Kelviq CLI](/cli/overview), not through the MCP server). In Claude Code they appear as slash commands (e.g. `/kelviq:setup_product`); other MCP clients list them under prompts.
| Prompt | Arguments | What it guides |
| - | - | - |
| `setup_product` | `productName?`, `planName?` | Full catalog bootstrap: product → features → plan → entitlements → publish → verify. |
| `launch_plan` | `productId?`, `planName?` | Add and publish a new plan on an existing product, reusing existing features. |
| `diagnose_customer` | `customerId` | Cross-check a customer's entitlements, subscriptions, and plan configuration; ends with concrete next actions. |
## Example Prompts
Describe the outcome, not the API call. A few realistic prompts that exercise the toolchain:
> **Build a catalog from scratch**
>
> > *Create a Kelviq product called "Notion Plus" with tax code `saas`, then create three plans (Free, Pro Monthly, Pro Yearly), add an `api-calls` feature with limits 1000 / 50000 / unlimited, and publish all three plans.*
>
> Calls (in order): `product_create` → `feature_create` → `plan_create` × 3 → `plan_entitlements_add` × 3 → `plan_publish` × 3. Set each plan's prices in the [dashboard](https://app.kelviq.com) or with the [Kelviq CLI](/cli/overview), which manages pricing as reviewable code.
> **Issue a checkout link**
>
> > *Create a checkout session for plan `pro-monthly` for a new customer, success URL [https://example.com/done](https://example.com/done).*
>
> Calls: `checkout_create_session`. Returns the `checkoutUrl` you can hand to your buyer.
> **License key management**
>
> > *Activate license `LIC-ABCDE` for customer `geojacob`, instance name "MacBook Pro M3", then validate it.*
>
> Calls: `license_activate` → `license_validate`.
> **Inspect customer state**
>
> > *Show me all subscriptions for customer `acme-42` and tell me which features they have access to.*
>
> Calls: `subscription_list` → `entitlements_get`.
> **Documentation lookup**
>
> > *How do digital downloads work in Kelviq? What's the file-size limit?*
>
> Calls: `docs_search` → `docs_read`. No API key needed — works offline.
***
## Tips for Effective Prompts
* **Mention "Kelviq" early.** LLMs typically have many MCP tools loaded. Saying "create a *Kelviq* customer" steers the model toward this server's tools instead of guessing.
* **Use `docs_search` for fuzzy questions.** When you're unsure of an identifier or behaviour, ask the LLM to *"search Kelviq docs"* first — it grounds the follow-up calls in real semantics.
* **Trust the errors.** Tools return `{"error": {"status", "message"}}` shapes for API errors. The LLM reads them and adjusts automatically (e.g. "Invalid product identifier" → asks you for the right one).
* **Combine tools in one prompt.** The LLM is happy to chain calls. "Create the product, then publish all its plans" is one prompt, not five.
***
## Troubleshooting
### `Executable not found in $PATH: "node"` in Claude Code
GUI/launcher processes don't source `~/.zshrc`, so nvm's `node` isn't visible to them. Pick one fix:
1. **Symlink node into a system path** (recommended):
```bash theme={null}
sudo ln -s "$(which node)" /usr/local/bin/node
```
One-time fix for every MCP server you'll ever add.
2. **Use the absolute node path:**
```bash theme={null}
claude mcp add kelviq \
--env KELVIQ_SERVER_API_KEY=... \
-- /Users/you/.nvm/versions/node/v20.19.0/bin/node \
$(npm root -g)/@kelviq/mcp-server/dist/server.js
```
3. **Install Node via Homebrew:**
```bash theme={null}
brew install node
```
Puts `node` on the system PATH so all GUI tools see it.
### `error: unknown option '-y'` from `claude mcp add`
`claude` is intercepting `-y` as one of its own flags. Use `--` to separate Claude's args from the actual command:
```bash theme={null}
claude mcp add kelviq \
--env KELVIQ_SERVER_API_KEY=... \
-- npx -y @kelviq/mcp-server@latest
```
### Tool returns `{"error": {"status": "Configuration error", ...}}`
A required env var is missing. The error message names which one — set it in your client config and reconnect.
### Tool returns HTTP 401 or 403
Ask your assistant to call `connection_check`: it reports the environment the server is using and whether the key was accepted, without showing the key. Almost always, the key belongs to the other environment: a sandbox key only works against sandbox, and a production key only against production.
* The server uses sandbox unless `KELVIQ_ENV=production`. If your key is a production key, add `KELVIQ_ENV=production` to the server's environment and reconnect.
* The error message includes a hint naming the environment the server is using, and the startup log shows it too (`kelviq-mcp ready (env=…`). Make sure it matches your key.
* If neither shows an environment, your client is running an outdated copy of the server. Use `npx -y @kelviq/mcp-server@latest` in your client config.
### Tool returns `{"error": {"status": "Bad request", "message": "Invalid product identifier"}}`
Your API key works but the identifier doesn't exist on this account. Most common cause: the identifier exists in the other environment — sandbox and production have separate catalogs. Check `KELVIQ_ENV`.
### Test Without an AI Client
Use the official [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to browse and call tools directly from a browser:
```bash theme={null}
KELVIQ_SERVER_API_KEY=... KELVIQ_CLIENT_API_KEY=... \
npx -y @modelcontextprotocol/inspector \
npx -y @kelviq/mcp-server@latest
```
***
## Source & Contributing
* **npm** — [`@kelviq/mcp-server`](https://www.npmjs.com/package/@kelviq/mcp-server)
* **GitHub** — [`kelviq/kelviq-mcp-server`](https://github.com/kelviq/kelviq-mcp-server)
* **Issues** — [github.com/kelviq/kelviq-mcp-server/issues](https://github.com/kelviq/kelviq-mcp-server/issues)
* **License** — MIT
The repo is the canonical reference for tool schemas, build process, and contribution guidelines. New Kelviq API endpoints are added by extending the hand-written Zod schemas in `src/schemas/kelviq.ts` and the per-domain files in `src/tools/`.
***
## Need Help?
* Email us at **[hi@kelviq.com](mailto:hi@kelviq.com)**
* [Book a demo](https://cal.com/alok-vats-fkpnc9/30min)
# Migrate from Cashfree
Source: https://docs.kelviq.com/guides/migrate-from-cashfree
Get help moving from Cashfree to Kelviq
Need help moving from Cashfree to Kelviq? Contact [hi@kelviq.com](mailto:hi@kelviq.com) to discuss your current setup.
# Sandbox
Source: https://docs.kelviq.com/guides/sandbox
Build and test your Kelviq integration without affecting production data or processing real payments
Kelviq's sandbox is a separate environment for development and testing. You can configure products and plans, create customers, exercise checkout flows, and test your integration without affecting your live organization or processing real payments.
## How isolation works
Sandbox and production use separate data and credentials.
| Resource | Sandbox behavior |
| - | - |
| Data | Products, plans, customers, subscriptions, and orders are isolated from production |
| API keys | Work only with sandbox hosts |
| Payments | Use test card details; no real charge is made |
This separation prevents development activity from changing live customer or billing data. Before launch, configure the resources your integration needs in production and replace all sandbox credentials and URLs.
## Get started
1. Sign in to the [Kelviq dashboard](https://app.kelviq.com).
2. Open the organization dropdown and click **Enter sandbox**.
3. Create the products, plans, prices, and other test data you need.
4. Open **Settings → API keys** and copy the sandbox key required by your integration.
> [!WARNING]
>
> Never use a production Server API Key in test code. Store Server API Keys in environment variables and use Client API Keys only in supported browser integrations.
## Connect to the sandbox API
Use the sandbox hosts with sandbox-scoped credentials:
| API | Sandbox base URL |
| - | - |
| Core API | `https://sandboxapi.kelviq.com/api/v1` |
| Edge API | `https://edge.sandboxapi.kelviq.com/api/v1` |
For example:
```bash theme={null}
curl https://sandboxapi.kelviq.com/api/v1/catalog/products/ \
--header "Authorization: Bearer $KELVIQ_SANDBOX_SERVER_API_KEY" \
--header "Accept: application/json"
```
Changing only the URL is not enough: the request must also use an API key created while the dashboard is in Sandbox mode.
## Use the sandbox with an SDK
Set `environment` to `sandbox` when initializing an official SDK.
```typescript Node.js theme={null}
import { Kelviq } from "@kelviq/node-sdk";
const client = new Kelviq({
accessToken: process.env.KELVIQ_SANDBOX_SERVER_API_KEY,
environment: "sandbox",
});
```
```python Python theme={null}
from kelviq_sdk import Kelviq
client = Kelviq.create_sync_client(
access_token=KELVIQ_SANDBOX_SERVER_API_KEY,
environment="sandbox",
)
```
```tsx React theme={null}
{children}
```
See the [Node SDK](/backend-integration/node-sdk), [Python SDK](/backend-integration/python-sdk), or [frontend SDK guides](/frontend-integration/react-sdk) for full setup instructions.
## Test checkout
Create a checkout in the sandbox and use these test card details. Use any future expiration date and a valid test CVC.
| Card number | Result |
| - | - |
| `4242 4242 4242 4242` | Successful payment |
| `4000 0000 0000 0002` | Declined payment |
| `4000 0025 0000 3155` | Payment requiring authentication |
> [!WARNING]
>
> Do not enter a real card number in the sandbox.
After payment, confirm that the test order, customer, subscription, and entitlements have the expected state in Sandbox mode.
## Test a subscription renewal
> [!NOTE]
>
> Daily billing is not enabled by default. Any sandbox organization can request it by contacting [Kelviq support](mailto:hi@kelviq.com).
After Daily billing is enabled, use it to test a renewal without waiting for a monthly or yearly billing cycle.
1. Enter Sandbox mode in the Kelviq dashboard.
2. Open the test plan you want to use.
3. Under **Billing cycles**, select **Daily** and enter a test price.
4. Publish the plan.
5. Start a subscription with a sandbox test card.
6. After the next billing cycle, check the order, invoice, subscription status, and entitlements in Sandbox mode.
If you create the checkout session through the API, set `chargePeriod` to `DAILY`:
```json theme={null}
{
"planIdentifier": "sandbox-renewal-test",
"chargePeriod": "DAILY",
"customerId": "test-customer-001",
"successUrl": "https://example.com/billing/success"
}
```
## Can I test production with my own card?
No. Do not place a live order using your own card just to test production credentials. Payment providers can treat a self-payment as suspicious and block the card, even when the amount is small. There is no safe minimum price for this test.
Use one of these options instead:
* Keep payment and renewal testing in Sandbox.
* If you need to inspect the production checkout, [create a 100% discount code](/product-catalog/discounts) so no real payment is collected.
* Check production credentials, URLs, and environment settings without completing a purchase.
## Prepare for production
Before accepting live payments:
1. Complete the required [business review and payout setup](/getstarted/review-process).
2. Configure and publish your production products and plans.
3. Replace sandbox API keys with production API keys.
4. Replace sandbox base URLs or set each SDK's environment to `production`.
5. Run a final check that no sandbox credentials or hosts remain in your production configuration.
> [!TIP]
>
> Use clearly named variables such as `KELVIQ_SANDBOX_SERVER_API_KEY` and `KELVIQ_PRODUCTION_SERVER_API_KEY` to reduce the chance of mixing environments.
# Set up with AI
Source: https://docs.kelviq.com/guides/set-up-with-ai
Hand billing to your AI coding agent with a prompt generated from your Kelviq account
**Settings → Set up with AI** writes a prompt for your coding agent. Paste it into Claude Code, Cursor, Codex, Lovable, Replit, Bolt, or any agent that can edit files and run commands, and the agent integrates Kelviq into your app.
It asks before changing anything in Kelviq and never needs your keys in the chat. Your agent can build and test everything with sandbox keys, then you go live when it's ready. You can also set it up directly in production.

## Two ways to start
**Integrate with my app** is for a catalog you have already built. The agent connects the plans you have published: it adds a checkout route, a billing portal route, feature checks, and a webhook route that verifies signatures. If your app has a public URL, it registers the webhook too.
**Set up everything** is for a new product. The agent reads your app and your requirements, proposes plans, prices, limits, license keys and discount codes, and shows you what it will create. Nothing is created in Kelviq until you approve. It then builds the integration the same way.

## Steps
1. **Add your keys to the project.** Click **Copy for your .env file** and paste the block into your project's `.env.local` or `.env`, or into your app builder's secrets. The agent reads the keys from the file. Don't paste them into the chat.
2. **Describe what you want.** Plain language is fine: "one plan at \$9/month with license keys, up to 3 devices per key, no free plan". Or add a common request, such as a monthly and yearly toggle or a 7-day free trial. The agent follows these on top of its defaults and asks if something conflicts.
3. **Give the prompt to your agent.** Click **Copy prompt**, open your agent in your app's project, and paste it as the first message.
The agent checks your keys with the Kelviq CLI without printing them and builds the app. In sandbox, it ends with a test purchase you can try with a [test card](/guides/sandbox).
## Sandbox or production
The page gives you keys and a prompt for the mode you're in. Switch to sandbox in the dashboard to get sandbox keys and a sandbox prompt.
[Business verification](/getstarted/review-process) and adding your keys to your project stay with you.
On Claude Code, install the [Kelviq plugin](/guides/agent-toolkit) for extra tools. The prompt picks them up automatically.
## Related documentation
* [Agent Toolkit](/guides/agent-toolkit)
* [MCP server](/guides/mcp-server)
* [CLI overview](/cli/overview)
# Subscription Integration
Source: https://docs.kelviq.com/guides/subscription-integration
A simple guide to add subscription billing to your SaaS in 5 API calls.
Add subscription billing to your SaaS with just a few API calls. This guide covers the REST API — for framework-specific guides, see [Next.js Integration](/guides/subscription-integration-for-nextjs).
***
## Overview
Kelviq handles your subscription billing with these endpoints:
| Step | Endpoint | Purpose |
| - | - | - |
| 1 | Create Customer | Register user (optional) |
| 2 | Create Checkout | Start payment flow |
| 3 | Get Subscription | Check active plans |
| 4 | Update Subscription | Upgrade/downgrade |
| 5 | Check Entitlements | Gate features |
***
## Step 1: Create Customer (Optional)
You have two options for creating customers:
### Option 1: Auto-create during checkout
Skip this step. Pass `email` or `customerId` in the checkout request — Kelviq will auto-create the customer.
### Option 2: Create customer explicitly
Create a customer record when a user signs up.
```javascript theme={null}
const response = await fetch("https://api.kelviq.com/api/v1/customers/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_SERVER_API_KEY"
},
body: JSON.stringify({
customerId: "user-123",
name: "John Doe",
email: "john@example.com"
})
});
const customer = await response.json();
```
> [!TIP]
>
> Pre-creating customers is useful when you want to track users in the dashboard before they subscribe.
***
## Step 2: Create Checkout Session
When a user wants to subscribe, create a checkout session and redirect them.
### Request
```javascript theme={null}
const response = await fetch("https://api.kelviq.com/api/v1/checkout/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_SERVER_API_KEY"
},
body: JSON.stringify({
planIdentifier: "pro",
chargePeriod: "MONTHLY",
customerId: "user-123",
successUrl: "https://yourapp.com/success"
})
});
const { checkoutUrl } = await response.json();
```
### Response
```json theme={null}
{
"checkoutUrl": "https://www.kelviq.com/checkout/cs_fBhCIBerpe1pAsRW6P4wYc8WHqMdfdr457hd3oOLjhAFx/",
"checkoutSessionId": "cs_test_b1sNi7D6u9iMCUFV1UZi9ZwerfaiXKdOOmr3DCUW6XdCZIr5Id1F7G"
}
```
> [!NOTE]
>
> Redirect user to `checkoutUrl` → They complete payment → Redirected back to your `successUrl`
### Required Parameters
| Parameter | Description |
| - | - |
| `planIdentifier` | The identifier of the plan (e.g., `"pro"`, `"enterprise"`) |
| `chargePeriod` | Billing cycle: `"MONTHLY"`, `"YEARLY"`, `"ONE_TIME"`, `"WEEKLY"`, `"DAILY"`, `"TWENTY_EIGHT_DAYS"`, `"THREE_MONTHS"`, `"SIX_MONTHS"` |
| `successUrl` | URL to redirect after successful payment |
### Optional Parameters
| Parameter | Description |
| - | - |
| `customerId` | Your customer ID (auto-creates if new) |
| `offeringId` | UUID of the offering |
| `pricingTableId` | UUID of the pricing table |
| `ipAddress` | Customer IP for location-based pricing |
***
## Step 3: Get Subscription Details
Check if a customer has an active subscription.
### Request
```javascript theme={null}
const response = await fetch(
"https://api.kelviq.com/api/v1/subscriptions/?customer_id=user-123",
{
headers: {
"Authorization": "Bearer YOUR_SERVER_API_KEY"
}
}
);
const { results } = await response.json();
const activeSubscription = results.find(s => s.status === "active");
```
### Response
```json theme={null}
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": "09d706ca-58f3-4fb2-818f-5b8623d67e6e",
"status": "active",
"plan": {
"name": "Pro Plan",
"identifier": "pro"
},
"amount": "49.00",
"recurrence": "month",
"currency": "usd",
"startDate": "2024-01-01",
"endDate": null,
"customerId": "user-123"
}
]
}
```
> [!NOTE]
>
> Kelviq handles failed card payment retries and emails the customer a link to pay. Your application does not need its own retry process or payment recovery screen. See [Failed subscription payments](/product-catalog/subscriptions#failed-subscription-payments).
***
## Step 4: Update Subscription
Allow users to upgrade or downgrade their plan.
### Request
```javascript theme={null}
const response = await fetch(
"https://api.kelviq.com/api/v1/subscriptions/09d706ca-58f3-4fb2-818f-5b8623d67e6e/update/",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_SERVER_API_KEY"
},
body: JSON.stringify({
planIdentifier: "enterprise",
chargePeriod: "MONTHLY",
paymentBehavior: "activate_on_payment"
})
}
);
const data = await response.json();
```
### Response
```json theme={null}
{
"subscriptionId": "09d706ca-58f3-4fb2-818f-5b8623d67e6e"
}
```
### Required Parameters
| Parameter | Description |
| - | - |
| `planIdentifier` | The identifier of the new plan |
| `chargePeriod` | Billing cycle: `"MONTHLY"`, `"YEARLY"`, etc. |
### Optional Parameters
| Parameter | Description |
| - | - |
| `paymentBehavior` | Set to `"activate_on_payment"` to keep the current subscription active until payment for the change succeeds. Omit it to apply the update immediately. |
### Activation after payment
Use `activate_on_payment` when a plan change should not affect access until Kelviq has collected payment.
| Payment path | What happens |
| - | - |
| Saved card with no customer action | Kelviq processes the payment immediately. The updated subscription becomes `active`, and the previous subscription becomes `superseded`. |
| UPI, 3DS, or another method requiring customer action | The updated subscription starts as `incomplete`. The existing subscription stays `active` until payment succeeds. |
| Payment succeeds | The updated subscription becomes `active`, and the previous subscription becomes `superseded`. |
| The immediate charge fails, or payment is not completed before expiry | The updated subscription becomes `incomplete_expired`. The existing subscription remains `active`. |
> [!WARNING]
>
> Do not grant the new plan's entitlements just because the update request returned successfully. Confirm that the updated subscription is `active`, or read the customer's current entitlements.
***
## Step 5: Check Entitlements
Check what features a customer has access to based on their subscription.
### Request
```javascript theme={null}
const response = await fetch(
"https://edge.api.kelviq.com/api/v1/entitlements/?customerId=user-123",
{
headers: {
"Authorization": "Bearer YOUR_CLIENT_API_KEY"
}
}
);
const { entitlements } = await response.json();
```
> [!WARNING]
>
> Use your **Client API Key** for this endpoint, not your Server API Key.
### Response
```json theme={null}
{
"customerId": "user-123",
"entitlements": [
{
"featureId": "export-data",
"featureType": "BOOLEAN",
"hasAccess": true
},
{
"featureId": "max-projects",
"featureType": "METER",
"hasAccess": true,
"usageLimit": 100,
"currentUsage": 45,
"remaining": 55
},
{
"featureId": "team-size",
"featureType": "CUSTOMIZABLE",
"hasAccess": true,
"usageLimit": 5
}
]
}
```
### Entitlement Types
| Type | Description | Fields |
| - | - | - |
| **BOOLEAN** | Feature on/off | `hasAccess` |
| **METER** | Usage-based limits | `usageLimit`, `currentUsage`, `remaining` |
| **CUSTOMIZABLE** | Configurable value | `usageLimit` |
***
## Complete Flow
```
1. User signs up
→ Create customer via API (or auto-create via email)
2. User visits pricing page
→ Create checkout session
→ Redirect to checkout URL
→ User completes payment
→ Redirect back to your app
3. In your app
→ Get subscription details
→ Check entitlements
→ Show/hide features based on access
4. User wants to upgrade
→ Update the subscription with paymentBehavior: "activate_on_payment"
→ Keep the existing subscription active while payment is pending
→ After payment succeeds, the new subscription becomes active
→ The previous subscription becomes superseded
```
***
## Need Help?
Have questions or need implementation support?
* Email us at **[hi@kelviq.com](mailto:hi@kelviq.com)**
* [Book a demo](https://cal.com/alok-vats-fkpnc9/30min)
# Subscription Integration for Next.js
Source: https://docs.kelviq.com/guides/subscription-integration-for-nextjs
A step-by-step guide to add subscription billing and entitlements to your Next.js app.
Integrate Kelviq subscription billing into your Next.js application using the App Router and React SDK.
***
## Prerequisites
* Next.js 14+ with App Router
* A Kelviq account with plans configured
* API keys from **Settings → API keys** in the dashboard
***
## Step 1: Get Your API Keys
1. Sign up at [app.kelviq.com](https://app.kelviq.com)
2. Navigate to **Settings → API keys**
3. Copy two keys:
* **Server API Key** — For API routes (keep secret)
* **Client API Key** — For React SDK (safe to expose)
### Add to Environment Variables
Add these to your `.env.local` file:
```env theme={null}
# Backend (server-side only)
KELVIQ_SERVER_API_KEY=server-xxxxxxxx
KELVIQ_ENV=sandbox
# Frontend (can be exposed to client)
NEXT_PUBLIC_KELVIQ_CLIENT_API_KEY=client-xxxxxxxx
NEXT_PUBLIC_KELVIQ_ENV=sandbox
# Your app URL (for redirects)
NEXT_PUBLIC_APP_URL=https://yourdomain.com
```
Build with sandbox keys — keys only work against their own environment. When you go live, switch both keys to production keys and set both `KELVIQ_ENV` values to `production`.
> [!WARNING]
>
> Never expose your server API key to the frontend or commit it to version control.
### Point API Routes at the Right Environment
Create `lib/kelviq.ts` so every API route calls the host that matches `KELVIQ_ENV`:
```typescript theme={null}
export const KELVIQ_API_URL =
process.env.KELVIQ_ENV === "production"
? "https://api.kelviq.com/api/v1"
: "https://sandboxapi.kelviq.com/api/v1";
```
***
## Step 2: Define Your Plans
### In Kelviq Dashboard
1. Go to **Product catalog → Products**, open your product, and add a plan for each tier with:
* **Plan Identifier**: `base`, `pro`, `enterprise` (code-friendly names)
* **Price**: Monthly/yearly pricing
* **Features**: List of included features
### Add features & limits
For each plan, add features & limits:
| Type | Description | Example |
| - | - | - |
| **Boolean (`BOOLEAN`)** | Feature is on/off | `export-data` |
| **Meter feature (`METER`)** | Feature has usage limits | `max-projects: 10` |
| **Customizable (`CUSTOMIZABLE`)** | Feature has configurable value | `team-size: 5` |
**Example Plan Structure:**
| Feature | Base (\$9/mo) | Pro (\$49/mo) | Enterprise (\$199/mo) |
| - | - | - | - |
| max-projects | 10 | 100 | Unlimited |
| export-data | No | Yes | Yes |
| team-size | 1 | 5 | 50 |
***
## Step 3: Install the React SDK
```bash theme={null}
npm install @kelviq/react-sdk
```
***
## Step 4: Create Customers (Optional)
You have two options for creating customers in Kelviq:
### Option 1: Auto-create during checkout
Skip this step entirely. When creating a checkout session, pass the customer's `email` or `customerId` — Kelviq will automatically create the customer record if it doesn't exist.
### Option 2: Create customer explicitly
Create a customer record when a user signs up. This gives you more control and lets you track customers before they subscribe.
**Create API Route** — `app/api/customer/route.ts`:
```typescript theme={null}
import { NextRequest, NextResponse } from "next/server";
import { KELVIQ_API_URL } from "@/lib/kelviq";
export async function POST(request: NextRequest) {
const { customerId, name, email } = await request.json();
const response = await fetch(
`${KELVIQ_API_URL}/customers/`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.KELVIQ_SERVER_API_KEY}`,
},
body: JSON.stringify({ customerId, name, email }),
}
);
const data = await response.json();
return NextResponse.json(data, { status: response.status });
}
```
**Call from Client Component:**
```typescript theme={null}
async function createKelviqCustomer(userId: string) {
await fetch("/api/customer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
customerId: userId, // Your internal user ID
name: "John Doe",
email: "john@example.com",
}),
});
}
```
> [!TIP]
>
> Pre-creating customers is useful when you want to track users in the Kelviq dashboard before they subscribe, or when you need to store additional customer metadata.
***
## Step 5: Implement Checkout
Allow users to subscribe to a plan.
### Create Checkout API Route
Create `app/api/checkout/route.ts`:
```typescript theme={null}
import { NextRequest, NextResponse } from "next/server";
import { KELVIQ_API_URL } from "@/lib/kelviq";
export async function POST(request: NextRequest) {
const { planIdentifier, customerId, chargePeriod } = await request.json();
const response = await fetch(
`${KELVIQ_API_URL}/checkout/`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.KELVIQ_SERVER_API_KEY}`,
},
body: JSON.stringify({
planIdentifier,
chargePeriod: chargePeriod || "MONTHLY",
customerId, // Customer auto-created if new
successUrl: `${process.env.NEXT_PUBLIC_APP_URL}/success`,
}),
}
);
const data = await response.json();
return NextResponse.json(data);
}
```
### Pricing Page
Create `app/pricing/page.tsx`:
```tsx theme={null}
"use client";
const plans = [
{ id: "base", name: "Base", price: 9 },
{ id: "pro", name: "Pro", price: 49 },
{ id: "enterprise", name: "Enterprise", price: 199 },
];
export default function PricingPage() {
const handleSubscribe = async (planId: string) => {
const response = await fetch("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
planIdentifier: planId,
customerId: getCurrentUserId(), // Your auth system
}),
});
const { checkoutUrl } = await response.json();
window.location.href = checkoutUrl;
};
return (
> );
> }
> ```
***
## Need Help?
Have questions or need implementation support?
* Email us at **[hi@kelviq.com](mailto:hi@kelviq.com)**
* [Book a demo](https://cal.com/alok-vats-fkpnc9/30min)
# Webhooks
Source: https://docs.kelviq.com/guides/webhooks
Receive real-time event notifications from Kelviq to your server.
Kelviq can push event notifications to your server whenever something meaningful happens — a subscription is created, an invoice is paid, or a refund is issued. This guide explains how to register an endpoint, verify incoming requests, and handle events.
***
## How It Works
When an event occurs, Kelviq creates a `WebhookEvent` and fans it out to all enabled endpoints you have registered for that event type. Each delivery is a single `POST` request with a JSON body and signed headers.
A delivery is considered successful when your endpoint returns a `2xx` status code. Kelviq retries a delivery up to **3 times**, with a 60-second delay between attempts, when the request fails to connect, times out after 10 seconds, or your endpoint returns a `5xx` status code or `429 Too Many Requests`. Other `4xx` and `3xx` responses are not retried; Kelviq does not follow redirects.
***
## Registering an Endpoint
Go to **Settings → Webhooks** in the [Kelviq dashboard](https://app.kelviq.com/settings/webhooks/new) to create a webhook endpoint. You will specify:
* **URL** — The public HTTPS URL Kelviq will POST events to.
* **Events** — The subset of event types you want to subscribe to.
After creation, Kelviq generates a signing secret for the endpoint in the format `kq_whsec_`. Store this secret securely — you will need it to verify signatures.
> [!WARNING]
>
> Your signing secret is shown only once. If you lose it, you must regenerate it from the dashboard.
***
## Inspect and resend deliveries
Open **Settings → Webhooks** and select an endpoint to view its delivery logs. Each delivery shows the event type, status, response code, and response body.
To retry a delivery:
1. Fix the endpoint or deployment issue that caused the delivery to fail.
2. Open the failed delivery in the webhook logs.
3. Click **Resend**.
4. Review the new delivery attempt and its response.
Kelviq automatically retries timeouts, connection failures, and `5xx` or `429` responses, but not other `4xx` or `3xx` responses. Use a manual resend to redeliver those after you fix the endpoint, or to retry immediately instead of waiting for the next automatic attempt.
> [!WARNING]
>
> Automatic retries and manual resends can deliver the same event more than once. Process webhooks idempotently using the event `id` or the `webhook-id` header.
***
## Event Types
| Event | Description |
| - | - |
| `checkout.completed` | A customer successfully completed a checkout session and payment was collected. |
| `customer.created` | A new customer record was created in your organization. |
| `customer.updated` | A customer's profile data (name, email, metadata) was updated. |
| `order.created` | A new order was placed. |
| `order.updated` | An existing order was updated. |
| `order.refunded` | A full or partial refund was issued against an order. |
| `subscription.created` | A new subscription was activated for a customer. |
| `subscription.updated` | A subscription's details were modified — including when a cancellation is scheduled for the end of the current period (status stays active; `end_date` is set). |
| `subscription.cancelled` | A subscription actually reached cancelled status. For a period-end cancellation, this fires later, when the period ends — not at the moment it was scheduled. |
| `subscription.plan_changed` | A subscription was moved to a different plan. |
| `subscription.paused` | A customer paused their subscription from the customer portal. The payload includes `paused_at` and `pause_resumes_at`. No other subscription events are sent while it stays paused. |
| `subscription.resumed` | A paused subscription started billing again, either at its scheduled date or by a resume call. Clear any cached entitlement checks for the customer on both events. |
| `subscription.trial_will_end` | A subscription's trial period is ending soon. |
| `feature.usage_alert` | A customer's feature usage crossed a configured threshold. |
| `invoice.created` | A new invoice was generated. |
| `invoice.paid` | An invoice was paid and funds were collected. |
| `invoice.payment_failed` | A subscription invoice payment failed or requires customer action. |
| `refund.created` | A refund was initiated. |
| `refund.updated` | A refund's status changed (e.g., from pending to completed). |
| `payment_method.created` | A new payment method was added for a customer. |
| `payment_method.updated` | A payment method's status or default state changed. |
| `payment_method.deleted` | A payment method was removed from a customer. |
***
## Checkout, subscription, and payment lifecycle
### Checkout and subscription creation
`checkout.completed` confirms that checkout finished successfully, but it does not include `subscription_id`. Kelviq creates the subscription after checkout and sends its ID in `subscription.created`.
Use your customer identifier or checkout `metadata` to correlate the two events. See [Add metadata to checkout](/checkout/checkout-metadata) for request and webhook examples. Do not treat the checkout redirect as proof that a subscription has been created.
### Custom field responses
When a checkout session collects [custom fields](/checkout/custom-fields), the customer's responses are included as `custom_fields_data` in `data.object` of `checkout.completed` and of the `order.created`, `order.updated`, and `order.refunded` events for the resulting order. The object is keyed by custom field identifier and is empty (`{}`) when the session collected no custom fields.
### Successful invoice payments
Kelviq sends `invoice.paid` whenever an invoice is paid, including invoices that are created directly with a `PAID` status. Use this event for payment-driven work such as recording revenue, starting an asynchronous provisioning job, or notifying an internal system.
For request-time access checks, query the customer's current entitlements instead of relying on a local webhook-derived subscription state.
### Failed invoice payments
Kelviq sends `invoice.payment_failed` when a subscription invoice payment fails or requires customer action. Use `data.object.subscription_id` and the embedded `customer` to identify the affected account. The invoice `status` is `PAYMENT_FAILED` for a failed attempt and can remain `OPEN` when the payment requires customer action.
This event can be followed by `invoice.paid` if the customer completes authentication or a later retry succeeds. Handle both events idempotently and treat the latest invoice and subscription state as authoritative.
### Subscription end dates
Subscription webhook objects include `end_date` inside `data.object`. Kelviq sets the field when a subscription has been cancelled or is scheduled to cancel on a future date. Otherwise, the value is `null`.
Use `end_date` for the subscription's scheduled or final end. `billing_period_end_time` describes the current billing period instead and should not be treated as the subscription's cancellation date.
### Failed renewal payments
Kelviq does not currently send a separate `payment.failed` event. For a failed card renewal:
1. The subscription moves to `past_due`.
2. Kelviq emails the customer a payment link.
3. Kelviq sends `subscription.updated` with the new subscription state.
4. Kelviq retries collection up to eight times over 14 days.
5. If all recovery attempts fail, Kelviq cancels the subscription and sends `subscription.cancelled`.
Do not remove access when you receive the first `subscription.updated` with `past_due` unless that is your own policy. Use the final subscription status or a live entitlement check to decide when access should end.
> [!NOTE]
>
> The retry window applies only to card payments. Kelviq does not retry a failed non-card renewal; the subscription is cancelled after the failure.
### Billing period field names
Subscription payloads use `billing_period_start_time` and `billing_period_end_time`. Match these names exactly when parsing dates. They describe the current billing period; use `end_date` for the subscription's scheduled or final end.
### Plan changes
By default, Kelviq applies a plan change immediately, charges any prorated difference, and sends `subscription.plan_changed`. After the generated invoice is paid, Kelviq sends the related order and invoice events.
If you update a subscription with `paymentBehavior: "activate_on_payment"`, the existing subscription remains `active` while payment is pending. A payment method that requires customer action leaves the updated subscription in `incomplete`. After payment succeeds, the updated subscription becomes `active` and the previous subscription becomes `superseded`. If an immediate charge fails, or the customer does not complete payment before expiry, the updated subscription becomes `incomplete_expired` and the existing subscription remains active.
Do not provision the new plan from the update API response alone. Read the latest subscription status or query the customer's current entitlements before changing access.
### Pauses and resumes
A pause sends `subscription.paused` with `status: "paused"`, `paused_at`, and `pause_resumes_at`, which is `null` for an open-ended pause. Treat it like a cancellation for access: entitlement checks already return `false` for a paused customer, so clear any cached result when the event arrives (`client.entitlements.invalidateCache` in Node, `client.entitlements.invalidate_cache` in Python).
The subscription is quiet until it resumes. Renewals that fall inside the pause are skipped without `subscription.updated` or invoice events. `subscription.resumed` arrives when billing starts again, at the scheduled date or after a resume call, with `status: "active"` and a new billing period. Usage counters are reset at that point.
Both events fire whichever surface made the change: the dashboard, the customer portal, or your API call.
### Partial and full refunds
`order.refunded` identifies whether the order is partially or fully refunded through its `status`:
* `PARTIAL_REFUND` for a partial refund
* `REFUNDED` for a full refund
The `order.refunded` payload does not include the refunded amount. Read `amount` or `amount_units` from `refund.created` and `refund.updated` when you need the exact value.
***
## Request Headers
Every webhook request from Kelviq includes the following headers:
| Header | Description |
| - | - |
| `webhook-id` | A unique UUID identifying this specific webhook event delivery. |
| `webhook-timestamp` | Unix timestamp (seconds) of when the event was dispatched. |
| `webhook-signature` | HMAC-SHA256 signature. Format: `v1,{signature}` |
| `Content-Type` | Always `application/json`. |
***
## Payload Structure
The request body is a JSON object with the following top-level fields:
```json theme={null}
{
"id": "3a7f1c2d-8e4b-4a9f-b123-0e5d6c7a8b9e",
"type": "subscription.created",
"created_at": "2024-06-01T12:00:00Z",
"data": {
"object": { ... },
"previous_attributes": { ... }
}
}
```
| Field | Type | Description |
| - | - | - |
| `id` | `string` (UUID) | The unique ID of this event. Matches the `webhook-id` header. |
| `type` | `string` | The event type (e.g., `subscription.created`). |
| `created_at` | `string` (ISO 8601) | UTC timestamp of when the event was created. |
| `data.object` | `object` | The primary resource affected by the event. |
| `data.previous_attributes` | `object` | *(Update events only)* The resource's changed attributes before the event. Omitted if not applicable. |
> **Example: checkout.completed payload**
>
> ```json theme={null}
> {
> "id": "1619afec-caab-40c7-827c-6af4f3acc9a3",
> "type": "checkout.completed",
> "created_at": "2026-04-01T17:28:39Z",
> "data": {
> "object": {
> "id": "cs_jBy6ViggULoyDMW9ikaw4ydur1NUa6dCORT6vqKPbPPW8",
> "object": "checkout_session",
> "status": "success",
> "checkout_type": "ONE_TIME",
> "customer_id": "936cf361-3a4b-454c-a464-41ccc7600c2d",
> "customer": {
> "id": "936cf361-3a4b-454c-a464-41ccc7600c2d",
> "name": "Geo Jacob",
> "email": "geojacob@example.com",
> "customer_id": "geo-jacob",
> "billing_address": {
> "line1": null,
> "line2": null,
> "city": null,
> "state": null,
> "postal_code": "680121",
> "country": "IN"
> }
> },
> "organization": "9b759915-5387-4e61-90e9-2162318e8d17",
> "amount": "10.00",
> "amount_units": 1000,
> "currency": "USD",
> "discount_code": "",
> "features": null,
> "plan": {
> "name": "Exc Tax Plan",
> "version": 1,
> "identifier": "exc-tax-plan"
> },
> "product": {
> "id": "df398021-477b-4e67-972d-c763a2f2ba7b",
> "name": "Acme Analytics"
> },
> "billing_address": {
> "line1": null,
> "line2": null,
> "city": null,
> "state": null,
> "postal_code": "680121",
> "country": "IN"
> },
> "metadata": {
> "order_ref": "ABC-123",
> "source": "pricing_page"
> },
> "custom_fields_data": {
> "company-name": "Acme Inc",
> "purchase-order-number": "PO-4821"
> },
> "data": {
> "name": "Geo",
> "email": "geojacob@example.com",
> "referer": null,
> "cancel_url": null,
> "success_url": null,
> "tax_behavior": "EXCLUSIVE",
> "discounts_enabled": true
> },
> "created_on": "2026-04-01T17:27:59Z",
> "modified_on": "2026-04-01T17:28:39Z"
> }
> }
> }
> ```
> **Example: customer.created payload**
>
> ```json theme={null}
> {
> "id": "0662641a-b415-46eb-a78a-c136ce12fa60",
> "type": "customer.created",
> "created_at": "2026-04-01T07:05:56Z",
> "data": {
> "object": {
> "id": "7dd79eff-180f-4b07-a213-ebafd29e9725",
> "object": "customer",
> "customer_id": "geo-jacob",
> "name": "Geo Jacob",
> "email": "geo@example.com",
> "details": {},
> "metadata": {},
> "billing_address": {},
> "subscriptions": 0,
> "organization": "9b759915-5387-4e61-90e9-2162318e8d17",
> "created_on": "2026-04-01T07:05:56Z",
> "modified_on": "2026-04-01T07:05:56Z"
> }
> }
> }
> ```
> **Example: customer.updated payload**
>
> ```json theme={null}
> {
> "id": "221a4d03-1cf4-47ae-9ed8-9f8f79ae989d",
> "type": "customer.updated",
> "created_at": "2026-04-01T07:32:42Z",
> "data": {
> "object": {
> "id": "7dd79eff-180f-4b07-a213-ebafd29e9725",
> "object": "customer",
> "customer_id": "geo-jacob",
> "name": "Geo Jacob v2",
> "email": "geo@example.com",
> "details": {},
> "metadata": {},
> "billing_address": {},
> "subscriptions": 0,
> "organization": "9b759915-5387-4e61-90e9-2162318e8d17",
> "created_on": "2026-04-01T07:05:56Z",
> "modified_on": "2026-04-01T07:32:42Z"
> },
> "previous_attributes": {
> "name": "Geo Jacob"
> }
> }
> }
> ```
> **Example: order.created payload**
>
> ```json theme={null}
> {
> "id": "3f6a9c2e-1b4d-4e8f-9a7c-5d2e8b1f4a60",
> "type": "order.created",
> "created_at": "2026-04-01T17:36:41Z",
> "data": {
> "object": {
> "id": "ORD-20260401173639-H8IO7",
> "object": "order",
> "status": "COMPLETE",
> "billing_type": "ONE_TIME",
> "customer_id": "936cf361-3a4b-454c-a464-41ccc7600c2d",
> "customer": {
> "id": "936cf361-3a4b-454c-a464-41ccc7600c2d",
> "name": "Geo Jacob",
> "email": "geojacob@example.com",
> "customer_id": "geo-jacob",
> "billing_address": {}
> },
> "organization": "9b759915-5387-4e61-90e9-2162318e8d17",
> "is_renewal": false,
> "custom_fields_data": {
> "company-name": "Acme Inc",
> "purchase-order-number": "PO-4821"
> },
> "paid_at": "2026-04-01T17:36:41Z",
> "amount_total": "10.00",
> "amount_total_units": 1000,
> "currency": "USD",
> "subscription_id": "2c78e8f0-892e-406c-9896-18c69d3026c4",
> "product": {
> "id": "df398021-477b-4e67-972d-c763a2f2ba7b",
> "name": "Acme Analytics"
> },
> "plan": {
> "name": "Exc Tax Plan",
> "version": 1,
> "identifier": "exc-tax-plan"
> },
> "features": null,
> "created_on": "2026-04-01T17:36:41Z",
> "modified_on": "2026-04-01T17:36:41Z"
> }
> }
> }
> ```
> **Example: order.updated payload**
>
> ```json theme={null}
> {
> "id": "7c1e4b9a-2d5f-4a3c-8e6b-0f9d2a7c3e51",
> "type": "order.updated",
> "created_at": "2026-04-01T17:36:41Z",
> "data": {
> "object": {
> "id": "ORD-20260401173639-H8IO7",
> "object": "order",
> "status": "COMPLETE",
> "billing_type": "ONE_TIME",
> "customer_id": "936cf361-3a4b-454c-a464-41ccc7600c2d",
> "customer": {
> "id": "936cf361-3a4b-454c-a464-41ccc7600c2d",
> "name": "Geo Jacob",
> "email": "geojacob@example.com",
> "customer_id": "geo-jacob",
> "billing_address": {
> "line1": null,
> "line2": null,
> "city": null,
> "state": null,
> "postal_code": "680121",
> "country": "IN"
> }
> },
> "organization": "9b759915-5387-4e61-90e9-2162318e8d17",
> "is_renewal": false,
> "custom_fields_data": {
> "company-name": "Acme Inc",
> "purchase-order-number": "PO-4821"
> },
> "paid_at": "2026-04-01T17:36:41Z",
> "amount_total": "10.00",
> "amount_total_units": 1000,
> "currency": "USD",
> "subscription_id": "2c78e8f0-892e-406c-9896-18c69d3026c4",
> "product": {
> "id": "df398021-477b-4e67-972d-c763a2f2ba7b",
> "name": "Acme Analytics"
> },
> "plan": {
> "name": "Exc Tax Plan",
> "version": 1,
> "identifier": "exc-tax-plan"
> },
> "features": null,
> "created_on": "2026-04-01T17:36:41Z",
> "modified_on": "2026-04-01T17:36:41Z"
> },
> "previous_attributes": {
> "status": "PENDING"
> }
> }
> }
> ```
> **Example: order.refunded payload**
>
> ```json theme={null}
> {
> "id": "c1fb8514-fec6-44fa-9196-7dabe2f153ac",
> "type": "order.refunded",
> "created_at": "2026-04-02T03:48:32Z",
> "data": {
> "object": {
> "id": "ORD-20260401173639-H8IO7",
> "object": "order",
> "status": "REFUNDED",
> "billing_type": "ONE_TIME",
> "customer_id": "936cf361-3a4b-454c-a464-41ccc7600c2d",
> "customer": {
> "id": "936cf361-3a4b-454c-a464-41ccc7600c2d",
> "name": "Geo Jacob",
> "email": "geojacob@example.com",
> "customer_id": "geo-jacob",
> "billing_address": {
> "line1": null,
> "line2": null,
> "city": null,
> "state": null,
> "postal_code": "680121",
> "country": "IN"
> }
> },
> "organization": "9b759915-5387-4e61-90e9-2162318e8d17",
> "is_renewal": false,
> "custom_fields_data": {
> "company-name": "Acme Inc",
> "purchase-order-number": "PO-4821"
> },
> "paid_at": "2026-04-01T17:36:41Z",
> "amount_total": "10.00",
> "amount_total_units": 1000,
> "currency": "USD",
> "subscription_id": "2c78e8f0-892e-406c-9896-18c69d3026c4",
> "product": {
> "id": "df398021-477b-4e67-972d-c763a2f2ba7b",
> "name": "Acme Analytics"
> },
> "plan": {
> "name": "Exc Tax Plan",
> "version": 1,
> "identifier": "exc-tax-plan"
> },
> "features": null,
> "created_on": "2026-04-01T17:36:41Z",
> "modified_on": "2026-04-02T03:48:32Z"
> },
> "previous_attributes": {
> "status": "COMPLETE"
> }
> }
> }
> ```
> **Example: subscription.created payload**
>
> ```json theme={null}
> {
> "id": "3a7f1c2d-8e4b-4a9f-b123-0e5d6c7a8b9e",
> "type": "subscription.created",
> "created_at": "2026-03-11T05:25:34Z",
> "data": {
> "object": {
> "id": "a22f1b2a-b922-4a28-9b0e-75e826988b4b",
> "object": "subscription",
> "status": "active",
> "plan": {
> "name": "Pro",
> "version": 1,
> "identifier": "pro"
> },
> "product": {
> "id": "b4ea7e75-5dbe-4b3b-96e0-9a31dfe5a600",
> "name": "Acme Analytics"
> },
> "amount": "21.24",
> "amount_units": 2124,
> "currency": "USD",
> "recurrence": "1 month",
> "start_date": "2026-03-11",
> "end_date": null,
> "billing_period_start_time": "2026-03-11T05:25:33Z",
> "billing_period_end_time": "2026-04-10T06:28:58Z",
> "trial_start": "2026-03-11T05:25:33Z",
> "trial_end": "2026-03-25T05:25:33Z",
> "customer": {
> "id": "1bb69d0f-39f8-4db8-aace-e003d6e5f3ed",
> "name": "Geo Jacob",
> "email": "geo@example.com",
> "customer_id": "1",
> "billing_address": {
> "city": "Bangalore",
> "line1": null,
> "line2": null,
> "state": "Karnataka",
> "country": "IN",
> "postal_code": "560001"
> }
> },
> "features": null,
> "organization": "2a3664c0-cbd2-4f1c-a98f-1b4f65e3256c",
> "created_on": "2026-03-11T05:25:34Z",
> "modified_on": "2026-03-11T05:25:34Z"
> }
> }
> }
> ```
> **Example: subscription.updated payload**
>
> ```json theme={null}
> {
> "id": "be4b0fa8-2e82-4664-b4f8-c3ab1622086c",
> "type": "subscription.updated",
> "created_at": "2026-04-02T05:21:33Z",
> "data": {
> "object": {
> "id": "ff258fd6-9a31-4eda-b3c8-000a24530890",
> "object": "subscription",
> "status": "active",
> "amount": "40.00",
> "amount_units": 4000,
> "currency": "USD",
> "recurrence": "1 month",
> "start_date": "2026-04-02",
> "end_date": null,
> "organization": "9b759915-5387-4e61-90e9-2162318e8d17",
> "features": null,
> "plan": {
> "name": "Inc tax sub",
> "version": 1,
> "identifier": "inc-tax-sub"
> },
> "product": {
> "id": "df398021-477b-4e67-972d-c763a2f2ba7b",
> "name": "Acme Analytics"
> },
> "customer": {
> "id": "936cf361-3a4b-454c-a464-41ccc7600c2d",
> "name": "Geo Jacob",
> "email": "geojacob@example.com",
> "customer_id": "bc43b1a034a643309ee39d2f1a4d2131",
> "billing_address": {
> "line1": null,
> "line2": null,
> "city": null,
> "state": null,
> "postal_code": "680121",
> "country": "IN"
> }
> },
> "billing_period_start_time": "2026-04-02T05:04:43Z",
> "billing_period_end_time": "2026-05-02T05:04:43Z",
> "trial_start": "2026-04-02T05:04:43Z",
> "trial_end": "2026-04-16T05:04:43Z",
> "created_on": "2026-04-02T05:21:28Z",
> "modified_on": "2026-04-02T05:21:33Z"
> },
> "previous_attributes": {
> "status": null,
> "billing_period_start_time": null
> }
> }
> }
> ```
> **Example: subscription.cancelled payload**
>
> ```json theme={null}
> {
> "id": "cacae554-d67e-4c6c-9d51-9d68f5c9b574",
> "type": "subscription.cancelled",
> "created_at": "2026-04-02T05:02:47Z",
> "data": {
> "object": {
> "id": "0faa77b7-2d42-4b6f-b682-d012987adad2",
> "object": "subscription",
> "status": "canceled",
> "amount": "40.00",
> "amount_units": 4000,
> "currency": "USD",
> "recurrence": "1 month",
> "start_date": "2026-04-02",
> "end_date": "2026-05-02",
> "organization": "9b759915-5387-4e61-90e9-2162318e8d17",
> "features": null,
> "plan": {
> "name": "Inc tax sub",
> "version": 1,
> "identifier": "inc-tax-sub"
> },
> "product": {
> "id": "df398021-477b-4e67-972d-c763a2f2ba7b",
> "name": "Acme Analytics"
> },
> "customer": {
> "id": "936cf361-3a4b-454c-a464-41ccc7600c2d",
> "name": "Geo Jacob",
> "email": "geojacob@example.com",
> "customer_id": "bc43b1a034a643309ee39d2f1a4d2131",
> "billing_address": {
> "line1": null,
> "line2": null,
> "city": null,
> "state": null,
> "postal_code": "680121",
> "country": "IN"
> }
> },
> "billing_period_start_time": "2026-04-02T04:25:13Z",
> "billing_period_end_time": "2026-05-02T04:25:13Z",
> "trial_start": "2026-04-02T04:25:13Z",
> "trial_end": "2026-04-16T04:25:13Z",
> "created_on": "2026-04-02T04:25:20Z",
> "modified_on": "2026-04-02T05:02:47Z"
> },
> "previous_attributes": {
> "status": "active"
> }
> }
> }
> ```
> **Example: subscription.plan\_changed payload**
>
> ```json theme={null}
> {
> "id": "b9c2e1f0-7d3a-4b8e-a456-1f2a3b4c5d6e",
> "type": "subscription.plan_changed",
> "created_at": "2026-03-04T11:53:12Z",
> "data": {
> "object": {
> "id": "d1284180-beba-465a-b756-9a300324245e",
> "object": "subscription",
> "status": "active",
> "plan": {
> "name": "Pro",
> "version": 4,
> "identifier": "pro"
> },
> "product": {
> "id": "988dba89-0a96-4cb5-870a-34810011c1f1",
> "name": "demo"
> },
> "amount": "29.00",
> "amount_units": 2900,
> "currency": "USD",
> "recurrence": "1 month",
> "start_date": "2026-02-26",
> "end_date": null,
> "billing_period_start_time": "2026-02-26T05:25:11Z",
> "billing_period_end_time": "2026-03-26T05:25:11Z",
> "trial_start": "2026-02-26T05:25:11Z",
> "trial_end": "2026-03-12T05:25:11Z",
> "customer": {
> "id": "798e663f-2fe9-43d7-879f-2549263e20d1",
> "name": "Geo",
> "email": "geo@example.com",
> "customer_id": "geo123",
> "billing_address": {}
> },
> "features": null,
> "organization": "5d0bec80-a7c1-41ae-bd24-ea9228500586",
> "created_on": "2026-03-04T11:53:12Z",
> "modified_on": "2026-03-04T11:53:14Z"
> },
> "previous_attributes": {
> "id": "065ee4e2-b643-4f78-bac0-9bd4afcba963",
> "object": "subscription",
> "status": "active",
> "plan": {
> "name": "Enterprise",
> "version": 1,
> "identifier": "enterprise"
> },
> "product": {
> "id": "988dba89-0a96-4cb5-870a-34810011c1f1",
> "name": "demo"
> },
> "amount": "99.00",
> "amount_units": 9900,
> "currency": "USD",
> "recurrence": "1 month",
> "start_date": "2026-02-26",
> "end_date": null,
> "billing_period_start_time": "2026-02-26T05:25:11Z",
> "billing_period_end_time": "2026-03-26T05:25:11Z",
> "trial_start": "2026-02-26T05:25:11Z",
> "trial_end": "2026-03-12T05:25:11Z",
> "customer": {
> "id": "798e663f-2fe9-43d7-879f-2549263e20d1",
> "name": "Geo",
> "email": "geo@example.com",
> "customer_id": "geo123",
> "billing_address": {}
> },
> "features": null,
> "organization": "5d0bec80-a7c1-41ae-bd24-ea9228500586",
> "created_on": "2026-03-04T11:29:19Z",
> "modified_on": "2026-03-04T11:29:23Z"
> }
> }
> }
> ```
> **Example: subscription.paused payload**
>
> ```json theme={null}
> {
> "id": "c4d1a2b3-9e8f-4a7b-b123-4c5d6e7f8a9b",
> "type": "subscription.paused",
> "created_at": "2026-09-30T10:00:00Z",
> "data": {
> "object": {
> "id": "d1284180-beba-465a-b756-9a300324245e",
> "object": "subscription",
> "status": "paused",
> "paused_at": "2026-09-30T10:00:00Z",
> "pause_resumes_at": "2026-12-31T23:00:00Z",
> "plan": {
> "name": "Pro",
> "version": 4,
> "identifier": "pro"
> },
> "product": {
> "id": "988dba89-0a96-4cb5-870a-34810011c1f1",
> "name": "demo"
> },
> "amount": "29.00",
> "amount_units": 2900,
> "currency": "USD",
> "recurrence": "1 month",
> "start_date": "2026-02-26",
> "end_date": null,
> "billing_period_start_time": "2026-09-26T05:25:11Z",
> "billing_period_end_time": "2026-10-26T05:25:11Z",
> "customer": {
> "id": "798e663f-2fe9-43d7-879f-2549263e20d1",
> "name": "Geo",
> "email": "geo@example.com",
> "customer_id": "geo123",
> "billing_address": {}
> },
> "organization": "5d0bec80-a7c1-41ae-bd24-ea9228500586",
> "created_on": "2026-02-26T05:25:11Z",
> "modified_on": "2026-09-30T10:00:00Z"
> }
> }
> }
> ```
> **Example: subscription.resumed payload**
>
> ```json theme={null}
> {
> "id": "e5f6a7b8-1c2d-4e3f-a456-7b8c9d0e1f2a",
> "type": "subscription.resumed",
> "created_at": "2026-12-31T23:00:00Z",
> "data": {
> "object": {
> "id": "d1284180-beba-465a-b756-9a300324245e",
> "object": "subscription",
> "status": "active",
> "paused_at": null,
> "pause_resumes_at": null,
> "plan": {
> "name": "Pro",
> "version": 4,
> "identifier": "pro"
> },
> "product": {
> "id": "988dba89-0a96-4cb5-870a-34810011c1f1",
> "name": "demo"
> },
> "amount": "29.00",
> "amount_units": 2900,
> "currency": "USD",
> "recurrence": "1 month",
> "start_date": "2026-02-26",
> "end_date": null,
> "billing_period_start_time": "2026-12-31T23:00:00Z",
> "billing_period_end_time": "2027-01-31T23:00:00Z",
> "customer": {
> "id": "798e663f-2fe9-43d7-879f-2549263e20d1",
> "name": "Geo",
> "email": "geo@example.com",
> "customer_id": "geo123",
> "billing_address": {}
> },
> "organization": "5d0bec80-a7c1-41ae-bd24-ea9228500586",
> "created_on": "2026-02-26T05:25:11Z",
> "modified_on": "2026-12-31T23:00:00Z"
> }
> }
> }
> ```
> **Example: subscription.trial\_will\_end payload**
>
> ```json theme={null}
> {
> "id": "7c6d5e4f-3b2a-4190-8e7d-6c5b4a3f2e1d",
> "type": "subscription.trial_will_end",
> "created_at": "2026-08-08T05:25:34Z",
> "data": {
> "object": {
> "id": "a22f1b2a-b922-4a28-9b0e-75e826988b4b",
> "object": "subscription",
> "status": "trialing",
> "plan": {
> "name": "Pro",
> "version": 1,
> "identifier": "pro"
> },
> "product": {
> "id": "b4ea7e75-5dbe-4b3b-96e0-9a31dfe5a600",
> "name": "Acme Analytics"
> },
> "amount": "21.24",
> "amount_units": 2124,
> "currency": "USD",
> "recurrence": "1 month",
> "start_date": "2026-08-01",
> "end_date": null,
> "billing_period_start_time": "2026-08-01T05:25:33Z",
> "billing_period_end_time": "2026-08-31T05:25:33Z",
> "trial_start": "2026-08-01T05:25:33Z",
> "trial_end": "2026-08-11T05:25:33Z",
> "customer": {
> "id": "1bb69d0f-39f8-4db8-aace-e003d6e5f3ed",
> "name": "Geo Jacob",
> "email": "geo@example.com",
> "customer_id": "1",
> "billing_address": {
> "city": "Bangalore",
> "line1": null,
> "line2": null,
> "state": "Karnataka",
> "country": "IN",
> "postal_code": "560001"
> }
> },
> "features": null,
> "organization": "2a3664c0-cbd2-4f1c-a98f-1b4f65e3256c",
> "created_on": "2026-08-01T05:25:34Z",
> "modified_on": "2026-08-01T05:25:34Z"
> }
> }
> }
> ```
>
> `data.object` uses the same subscription payload shape as `subscription.created`/`subscription.updated`; the trial's remaining time is in `trial_end`. Kelviq sends this once, a few days before `trial_end`. It's skipped if the subscription has already converted to `active` or has no trial days left.
> **Example: feature.usage\_alert payload**
>
> ```json theme={null}
> {
> "id": "575a1aa4-3414-4e08-b4ad-6bfd3e96322d",
> "type": "feature.usage_alert",
> "created_at": "2026-05-18T18:48:59Z",
> "data": {
> "object": {
> "id": "a1c88c9d-19cf-48cc-b194-6cc6ebb356ee",
> "object": "feature_usage_alert",
> "feature": {
> "id": "245d745f-28ca-4856-81fe-1e1f5c6c7ebd",
> "name": "API calls",
> "identifier": "api-calls",
> "feature_type": "METER"
> },
> "customer": {
> "id": "d2b6c9ac-bcd5-4e59-8c7e-b6472485e112",
> "name": "Geo",
> "email": "geo@example.com",
> "customer_id": "fcec7a06c7484584b8a4d92711bfe528",
> "billing_address": {
> "city": null,
> "line1": null,
> "line2": null,
> "state": null,
> "country": "IN",
> "postal_code": "680121"
> }
> },
> "usage_limit": 5,
> "current_usage": 5,
> "threshold_type": "PERCENTAGE",
> "threshold_value": 90,
> "period_start_time": "2026-05-18T00:00:00Z",
> "period_end_time": null,
> "subscription_id": "137e4652-5540-4a3b-9bce-fa393b028539",
> "organization": "2a3664c0-cbd2-4f1c-a98f-1b4f65e3256c",
> "created_on": "2026-05-18T17:57:16Z",
> "modified_on": "2026-05-18T17:57:17Z"
> }
> }
> }
> ```
> **Example: invoice.created payload**
>
> ```json theme={null}
> {
> "id": "2f31925f-10fc-41eb-8cb9-3cdf2b4e9e29",
> "type": "invoice.created",
> "created_at": "2026-04-02T05:56:33Z",
> "data": {
> "object": {
> "id": "INV-20260402055633-FCM4B",
> "object": "invoice",
> "status": "OPEN",
> "paid_at": null,
> "amount_total": "40",
> "amount_total_units": 4000,
> "currency": "USD",
> "due_date": null,
> "attempt_count": 0,
> "next_payment_attempt": null,
> "failure_details": null,
> "payment_link": "https://portal.kelviq.com/acme-inc/pay/aW5fMVJwb1pvU0JzdEN6eW9jUzdpUG1pdGgz",
> "order_id": null,
> "customer_id": "c479275c-6987-4e0b-9aca-46e6e83226aa",
> "customer": {
> "id": "c479275c-6987-4e0b-9aca-46e6e83226aa",
> "name": "Geo Jacob",
> "email": "geojacob@example.com",
> "customer_id": "geo-jacob",
> "billing_address": {
> "line1": null,
> "line2": null,
> "city": null,
> "state": null,
> "postal_code": "680121",
> "country": "IN"
> }
> },
> "subscription_id": "a2951973-aca5-408d-bd9c-1d86827ed463",
> "organization": "9b759915-5387-4e61-90e9-2162318e8d17",
> "product": {
> "id": "df398021-477b-4e67-972d-c763a2f2ba7b",
> "name": "Acme Analytics"
> },
> "created_on": "2026-04-02T05:56:33Z",
> "modified_on": "2026-04-02T05:56:33Z"
> }
> }
> }
> ```
> **Example: invoice.paid payload**
>
> ```json theme={null}
> {
> "id": "9d2c7e54-3f1b-4a8c-9e6d-2b5f8e1a4c7b",
> "type": "invoice.paid",
> "created_at": "2026-04-02T06:00:00Z",
> "data": {
> "object": {
> "id": "INV-20260402055633-FCM4B",
> "object": "invoice",
> "status": "PAID",
> "paid_at": "2026-04-02T06:00:00Z",
> "amount_total": "40",
> "amount_total_units": 4000,
> "currency": "USD",
> "due_date": null,
> "attempt_count": 1,
> "next_payment_attempt": null,
> "failure_details": null,
> "payment_link": null,
> "order_id": null,
> "customer_id": "c479275c-6987-4e0b-9aca-46e6e83226aa",
> "customer": {
> "id": "c479275c-6987-4e0b-9aca-46e6e83226aa",
> "name": "Geo Jacob",
> "email": "geojacob@example.com",
> "customer_id": "geo-jacob",
> "billing_address": {
> "line1": null,
> "line2": null,
> "city": null,
> "state": null,
> "postal_code": "680121",
> "country": "IN"
> }
> },
> "subscription_id": "a2951973-aca5-408d-bd9c-1d86827ed463",
> "organization": "9b759915-5387-4e61-90e9-2162318e8d17",
> "product": {
> "id": "df398021-477b-4e67-972d-c763a2f2ba7b",
> "name": "Acme Analytics"
> },
> "created_on": "2026-04-02T05:56:33Z",
> "modified_on": "2026-04-02T06:00:00Z"
> },
> "previous_attributes": {
> "status": "OPEN",
> "paid_at": null
> }
> }
> }
> ```
> **Example: invoice.payment\_failed payload**
>
> ```json theme={null}
> {
> "id": "1b8b128e-31c8-48b2-9d0b-f477ce543d58",
> "type": "invoice.payment_failed",
> "created_at": "2026-08-01T10:14:22Z",
> "data": {
> "object": {
> "id": "INV-20260801101420-Q7M2P",
> "object": "invoice",
> "organization": "9b759915-5387-4e61-90e9-2162318e8d17",
> "status": "PAYMENT_FAILED",
> "customer_id": "c479275c-6987-4e0b-9aca-46e6e83226aa",
> "customer": {
> "id": "c479275c-6987-4e0b-9aca-46e6e83226aa",
> "name": "Geo Jacob",
> "email": "geojacob@example.com",
> "customer_id": "geo-jacob",
> "billing_address": {
> "line1": null,
> "line2": null,
> "city": null,
> "state": null,
> "postal_code": "680121",
> "country": "IN"
> }
> },
> "product": {
> "id": "df398021-477b-4e67-972d-c763a2f2ba7b",
> "name": "Acme Analytics"
> },
> "subscription_id": "a2951973-aca5-408d-bd9c-1d86827ed463",
> "order_id": null,
> "payment_link": "https://portal.kelviq.com/acme-inc/pay/aW5fMVJwb1pvU0JzdEN6eW9jUzdpUG1pdGgz",
> "amount_total": "40",
> "amount_total_units": 4000,
> "currency": "USD",
> "paid_at": null,
> "due_date": "2026-08-01T10:14:22Z",
> "attempt_count": 1,
> "next_payment_attempt": "2026-08-04T10:14:22Z",
> "failure_details": {
> "code": "card_declined",
> "message": "Your card was declined.",
> "payment_method_type": "card"
> },
> "created_on": "2026-08-01T10:14:20Z",
> "modified_on": "2026-08-01T10:14:22Z"
> }
> }
> }
> ```
>
> `attempt_count`, `next_payment_attempt`, and `payment_link` are also included on `invoice.created` and `invoice.paid` payloads. `failure_details` (with `code`, `message`, and `payment_method_type`) describes the most recent failed attempt and is `null` once the invoice moves past that attempt (e.g. a later `invoice.paid` for the same invoice). `payment_link` is `null` whenever the invoice isn't currently payable (e.g. already paid).
> **Example: refund.created payload**
>
> ```json theme={null}
> {
> "id": "6e76547b-9b6b-4504-af36-c8f646fa91fe",
> "type": "refund.created",
> "created_at": "2026-04-02T05:41:21Z",
> "data": {
> "object": {
> "id": "ca435ba3-d951-472d-a836-370787df3026",
> "object": "refund",
> "status": "PROCESSING",
> "amount": "20.0",
> "amount_units": 2000,
> "currency": "USD",
> "reason": "REQUESTED_BY_CUSTOMER",
> "order_id": "ORD-20260402042513-8M80C",
> "customer": {
> "id": "c479275c-6987-4e0b-9aca-46e6e83226aa",
> "name": "Geo Jacob",
> "email": "geojacob@example.com",
> "customer_id": "geo-jacob",
> "billing_address": {
> "line1": null,
> "line2": null,
> "city": null,
> "state": null,
> "postal_code": "680121",
> "country": "IN"
> }
> },
> "subscription_id": "a2951973-aca5-408d-bd9c-1d86827ed463",
> "organization": "9b759915-5387-4e61-90e9-2162318e8d17",
> "created_on": "2026-04-02T05:41:21Z",
> "modified_on": "2026-04-02T05:41:21Z"
> }
> }
> }
> ```
> **Example: refund.updated payload**
>
> ```json theme={null}
> {
> "id": "e2b8d4f1-6a3c-4f9e-b7d2-9c1a5e3f8b04",
> "type": "refund.updated",
> "created_at": "2026-04-02T05:41:30Z",
> "data": {
> "object": {
> "id": "ca435ba3-d951-472d-a836-370787df3026",
> "object": "refund",
> "status": "SUCCEEDED",
> "amount": "20.0",
> "amount_units": 2000,
> "currency": "USD",
> "reason": "REQUESTED_BY_CUSTOMER",
> "order_id": "ORD-20260402042513-8M80C",
> "customer": {
> "id": "c479275c-6987-4e0b-9aca-46e6e83226aa",
> "name": "Geo Jacob",
> "email": "geojacob@example.com",
> "customer_id": "geo-jacob",
> "billing_address": {
> "line1": null,
> "line2": null,
> "city": null,
> "state": null,
> "postal_code": "680121",
> "country": "IN"
> }
> },
> "subscription_id": "a2951973-aca5-408d-bd9c-1d86827ed463",
> "organization": "9b759915-5387-4e61-90e9-2162318e8d17",
> "created_on": "2026-04-02T05:41:21Z",
> "modified_on": "2026-04-02T05:41:30Z"
> }
> }
> }
> ```
> **Example: payment\_method.created payload**
>
> ```json theme={null}
> {
> "id": "7f9c9a3e-2b41-4b7a-9d2e-5c8f1a6b3d4e",
> "type": "payment_method.created",
> "created_at": "2026-04-02T06:12:04Z",
> "data": {
> "object": {
> "id": 482,
> "object": "payment_method",
> "organization": "9b759915-5387-4e61-90e9-2162318e8d17",
> "customer_id": "geo-jacob",
> "customer": {
> "id": "936cf361-3a4b-454c-a464-41ccc7600c2d",
> "name": "Geo Jacob",
> "email": "geojacob@example.com",
> "customer_id": "geo-jacob",
> "billing_address": {
> "line1": null,
> "line2": null,
> "city": null,
> "state": null,
> "postal_code": "680121",
> "country": "IN"
> }
> },
> "method_type": "card",
> "method_data": {
> "brand": "visa",
> "last4": "4242",
> "exp_month": 12,
> "exp_year": 2029,
> "funding": "credit",
> "country": "US"
> },
> "is_default": true,
> "status": "succeeded"
> }
> }
> }
> ```
> **Example: payment\_method.updated payload**
>
> ```json theme={null}
> {
> "id": "b1d4e5f6-3c72-4a9b-8e1d-6f2a9c4b7e8d",
> "type": "payment_method.updated",
> "created_at": "2026-04-02T06:15:47Z",
> "data": {
> "object": {
> "id": 482,
> "object": "payment_method",
> "organization": "9b759915-5387-4e61-90e9-2162318e8d17",
> "customer_id": "geo-jacob",
> "customer": {
> "id": "936cf361-3a4b-454c-a464-41ccc7600c2d",
> "name": "Geo Jacob",
> "email": "geojacob@example.com",
> "customer_id": "geo-jacob",
> "billing_address": {
> "line1": null,
> "line2": null,
> "city": null,
> "state": null,
> "postal_code": "680121",
> "country": "IN"
> }
> },
> "method_type": "card",
> "method_data": {
> "brand": "visa",
> "last4": "4242",
> "exp_month": 12,
> "exp_year": 2029,
> "funding": "credit",
> "country": "US"
> },
> "is_default": true,
> "status": "succeeded"
> }
> }
> }
> ```
> **Example: payment\_method.deleted payload**
>
> ```json theme={null}
> {
> "id": "e3a2b1c0-9d8e-47f6-b5a4-1c2d3e4f5a6b",
> "type": "payment_method.deleted",
> "created_at": "2026-04-02T06:20:12Z",
> "data": {
> "object": {
> "id": 482,
> "object": "payment_method",
> "organization": "9b759915-5387-4e61-90e9-2162318e8d17",
> "customer_id": "geo-jacob",
> "customer": {
> "id": "936cf361-3a4b-454c-a464-41ccc7600c2d",
> "name": "Geo Jacob",
> "email": "geojacob@example.com",
> "customer_id": "geo-jacob",
> "billing_address": {
> "line1": null,
> "line2": null,
> "city": null,
> "state": null,
> "postal_code": "680121",
> "country": "IN"
> }
> },
> "method_type": "card",
> "method_data": {
> "brand": "visa",
> "last4": "4242",
> "exp_month": 12,
> "exp_year": 2029,
> "funding": "credit",
> "country": "US"
> },
> "is_default": false,
> "status": "succeeded"
> }
> }
> }
> ```
***
## Verifying Signatures
> [!WARNING]
>
> Always verify the signature before processing a webhook. Skipping verification exposes your endpoint to spoofed requests from third parties.
Kelviq signs every request using **HMAC-SHA256**. To verify a request:
1. Read the `webhook-id` and `webhook-timestamp` headers.
2. Construct the signed string by concatenating: `{webhook-id}.{webhook-timestamp}.{raw-request-body}` (joined with `.`)
3. Compute HMAC-SHA256 over the signed string using your endpoint's signing secret as the key.
4. Compare the hex digest to the signature in the `webhook-signature` header (strip the `v1,` prefix before comparing).
5. Reject the request if the signatures do not match.
> [!NOTE]
>
> The raw request body must be used exactly as received — before any JSON parsing. The compact JSON serialization (no spaces) is what Kelviq sends and signs.
Optionally, also check that `webhook-timestamp` is within a few minutes of your server's current time to defend against replay attacks.
***
## Code Examples
> [!NOTE]
>
> Use `express.raw()` (not `express.json()`) in Node.js so that `req.body` contains the unmodified request bytes. Parsing the body before verification will break the signature check. On a framework built on the Fetch API (Next.js route handlers, Hono, Remix, Cloudflare Workers), read the body with `await request.text()` and take each header with `request.headers.get(...)`, since `request.headers` is a `Headers` object rather than a plain object.
```javascript Node.js theme={null}
const crypto = require('crypto');
function verifyWebhook(req, signingSecret) {
const webhookId = req.headers['webhook-id'];
const webhookTimestamp = req.headers['webhook-timestamp'];
const webhookSignature = req.headers['webhook-signature'];
if (!webhookId || !webhookTimestamp || !webhookSignature) {
throw new Error('Missing required webhook headers');
}
// Defend against replay attacks (5-minute tolerance)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(webhookTimestamp, 10)) > 300) {
throw new Error('Webhook timestamp is too old');
}
// Reconstruct the signed payload
const rawBody = req.body; // must be the raw Buffer/string, not parsed JSON
const signedPayload = `${webhookId}.${webhookTimestamp}.${rawBody}`;
// Compute HMAC-SHA256
const expectedSignature = crypto
.createHmac('sha256', signingSecret)
.update(signedPayload, 'utf8')
.digest('hex');
// Strip the "v1," prefix from the header value
const [, receivedSignature] = webhookSignature.split(',');
if (!crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
)) {
throw new Error('Webhook signature mismatch');
}
return JSON.parse(rawBody);
}
// Express handler example
app.post('/webhooks/kelviq', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try {
event = verifyWebhook(req, process.env.KELVIQ_WEBHOOK_SECRET);
} catch (err) {
console.error('Webhook verification failed:', err.message);
return res.status(400).send('Bad Request');
}
switch (event.type) {
case 'subscription.created':
console.log('New subscription:', event.data.object.id);
break;
case 'invoice.paid':
console.log('Invoice paid:', event.data.object.id);
break;
default:
console.log('Unhandled event type:', event.type);
}
res.status(200).json({ received: true });
});
```
```python Python theme={null}
import hashlib
import hmac
import json
import time
from flask import Flask, request, abort
app = Flask(__name__)
SIGNING_SECRET = "kq_whsec_your_secret_here" # load from environment
def verify_webhook(payload_bytes: bytes, webhook_id: str, webhook_timestamp: str, webhook_signature: str) -> dict:
# Defend against replay attacks (5-minute tolerance)
now = int(time.time())
if abs(now - int(webhook_timestamp)) > 300:
raise ValueError("Webhook timestamp is too old")
# Reconstruct the signed payload (body must be the raw bytes decoded as utf-8)
signed_payload = f"{webhook_id}.{webhook_timestamp}.{payload_bytes.decode('utf-8')}"
# Compute HMAC-SHA256
expected_signature = hmac.new(
SIGNING_SECRET.encode("utf-8"),
signed_payload.encode("utf-8"),
hashlib.sha256,
).hexdigest()
# Strip the "v1," prefix from the header value
_, received_signature = webhook_signature.split(",", 1)
if not hmac.compare_digest(expected_signature, received_signature):
raise ValueError("Webhook signature mismatch")
return json.loads(payload_bytes)
@app.route("/webhooks/kelviq", methods=["POST"])
def handle_webhook():
webhook_id = request.headers.get("webhook-id")
webhook_timestamp = request.headers.get("webhook-timestamp")
webhook_signature = request.headers.get("webhook-signature")
try:
event = verify_webhook(request.get_data(), webhook_id, webhook_timestamp, webhook_signature)
except ValueError as e:
print(f"Webhook verification failed: {e}")
abort(400)
event_type = event.get("type")
if event_type == "subscription.created":
print("New subscription:", event["data"]["object"]["id"])
elif event_type == "invoice.paid":
print("Invoice paid:", event["data"]["object"]["id"])
else:
print("Unhandled event type:", event_type)
return {"received": True}, 200
```
```go Go theme={null}
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"net/http"
"strconv"
"strings"
"time"
)
const signingSecret = "kq_whsec_your_secret_here" // load from environment
type WebhookEvent struct {
ID string `json:"id"`
Type string `json:"type"`
CreatedAt string `json:"created_at"`
Data json.RawMessage `json:"data"`
}
func verifyWebhook(body []byte, webhookID, webhookTimestamp, webhookSignature string) (*WebhookEvent, error) {
// Defend against replay attacks (5-minute tolerance)
ts, err := strconv.ParseInt(webhookTimestamp, 10, 64)
if err != nil {
return nil, errors.New("invalid webhook-timestamp header")
}
if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
return nil, errors.New("webhook timestamp is too old")
}
// Reconstruct the signed payload
signedPayload := fmt.Sprintf("%s.%s.%s", webhookID, webhookTimestamp, string(body))
// Compute HMAC-SHA256
mac := hmac.New(sha256.New, []byte(signingSecret))
mac.Write([]byte(signedPayload))
expectedSig := hex.EncodeToString(mac.Sum(nil))
// Strip the "v1," prefix from the header value
parts := strings.SplitN(webhookSignature, ",", 2)
if len(parts) != 2 {
return nil, errors.New("malformed webhook-signature header")
}
receivedSig := parts[1]
expectedBytes, _ := hex.DecodeString(expectedSig)
receivedBytes, _ := hex.DecodeString(receivedSig)
if !hmac.Equal(expectedBytes, receivedBytes) {
return nil, errors.New("webhook signature mismatch")
}
var event WebhookEvent
if err := json.Unmarshal(body, &event); err != nil {
return nil, fmt.Errorf("failed to parse webhook body: %w", err)
}
return &event, nil
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "cannot read body", http.StatusBadRequest)
return
}
defer r.Body.Close()
event, err := verifyWebhook(
body,
r.Header.Get("webhook-id"),
r.Header.Get("webhook-timestamp"),
r.Header.Get("webhook-signature"),
)
if err != nil {
http.Error(w, "Bad Request: "+err.Error(), http.StatusBadRequest)
return
}
switch event.Type {
case "subscription.created":
fmt.Println("New subscription:", event.ID)
case "invoice.paid":
fmt.Println("Invoice paid:", event.ID)
default:
fmt.Println("Unhandled event type:", event.Type)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"received":true}`))
}
func main() {
http.HandleFunc("/webhooks/kelviq", webhookHandler)
http.ListenAndServe(":8080", nil)
}
```
***
## Best Practices
* **Return `2xx` fast.** Acknowledge the webhook immediately and process it asynchronously. Long-running handlers increase the risk of timeouts and duplicate retries.
* **Make handlers idempotent.** The same event may be delivered more than once. Use the top-level event `id` or the `webhook-id` header as the deduplication key.
* **Validate the timestamp.** Reject requests where `webhook-timestamp` is more than 5 minutes from your server's clock to prevent replay attacks.
* **Use `hmac.compare_digest` / `hmac.Equal` / `crypto.timingSafeEqual`.** Constant-time comparison prevents timing side-channel attacks.
* **Store the raw body before parsing.** Signature verification operates on the raw bytes, not the deserialized object.
***
## Need Help?
* Email us at **[hi@kelviq.com](mailto:hi@kelviq.com)**
* [Book a demo](https://cal.com/alok-vats-fkpnc9/30min)
# Acceptable Use Policy
Source: https://docs.kelviq.com/legal/approved-prohibited-policy
Kelviq's policy on approved, restricted, and prohibited business categories
> [!NOTE]
>
> **Version 1.0** | Effective Date: February 11, 2026
## 1. Purpose
Kelviq, Inc. ("Kelviq") operates as a Merchant of Record (MoR) for digital product sellers. As the MoR, Kelviq is legally and financially responsible for every transaction processed through the platform. This policy defines the types of businesses, products, and activities that are approved, restricted, or prohibited on the Kelviq platform.
This policy is designed to ensure compliance with applicable laws, regulations, payment network rules (including Visa and Mastercard operating regulations), U.S. sanctions laws (OFAC), and the terms of our payment processing agreements with Stripe, Inc.
This policy is incorporated by reference into the Kelviq Seller Master Service Agreement and applies to all Sellers on the platform.
## 2. Approved Business Categories
Kelviq is designed for digital products and software. The following business categories are approved for the platform:
### 2.1 SaaS and Software
* Business-to-business (B2B) and business-to-consumer (B2C) web applications
* Desktop software and browser extensions
* AI tools and AI-powered software products (subject to additional review per Section 4)
* Developer tools, APIs, and SDKs
### 2.2 Digital Downloads
* E-books and digital publications (PDF, EPUB)
* Design templates, fonts, and design assets
* Audio files, music, and sound effects (original or properly licensed)
* Video courses and educational content
* Photography presets, filters, and digital art
### 2.3 Membership and Subscription Content
* Paid newsletters and research publications
* Membership communities with gated content
* Online course platforms with recurring access
### 2.4 Professional Services (Limited)
Professional services are approved only when sold as add-ons to a software product. Examples include setup fees, priority support packages, and onboarding assistance for a software product. Standalone manual services (consulting, coaching, agency work) are not approved.
> [!NOTE]
>
> **Approval criteria:** The product must be digital, owned by the Seller (or properly licensed), and deliverable immediately upon purchase.
## 3. Fundamental Platform Limitations
The following categories are not supported on the Kelviq platform under any circumstances. These limitations are inherent to our MoR model and cannot be waived.
| Limitation | Description |
| - | - |
| **Digital only** | Kelviq cannot support physical goods, shipping, logistics, or any product that requires physical delivery. |
| **No manual services** | Kelviq cannot support 1-on-1 coaching, consulting, freelancing, or agency services where the primary deliverable is manual labor (not a digital product). |
| **No marketplace or resale** | Sellers must be the original creator or licensed distributor. Sellers cannot use Kelviq to process payments for other unverified third-party sellers. |
## 4. Prohibited Business Categories
The following categories are strictly prohibited. Kelviq will not approve accounts in these categories, and any existing account found to be operating in a prohibited category will be suspended immediately. This list is aligned with Stripe's prohibited business list and applicable card network rules.
### 4.1 Illegal, Deceptive, or Harmful Content
> [!WARNING]
>
> The following are strictly prohibited:
* Products or services that are illegal in the United States or the Buyer's jurisdiction
* Content that infringes on third-party intellectual property, copyrights, trademarks, or patents (including pirated software, stolen e-books, cracked license keys)
* Hate speech or content promoting violence, self-harm, or discrimination
* Deceptive marketing, fake testimonials, or unrealistic income or results claims
* Low-value exploitative content (including mass-generated AI content with minimal substance sold at inflated prices)
* Identity services, fake identification documents, or academic cheating services
* Multi-level marketing (MLM) or pyramid scheme products
### 4.2 Financial and Legal Services
* Investment advice, trading signals, or financial planning services
* Cryptocurrency sales, token sales, or NFT minting/trading
* Lending services, debt collection, or money transmission
* Gambling, betting, lotteries, or games of chance (including fantasy sports with entry fees)
* Legal, tax, or accounting advice as a service
* Donation processing where no specific product is delivered
* Services designed to help others evade taxes, mask identity, or bypass financial regulations
### 4.3 Adult Content and Regulated Industries
* Explicitly sexual content (videos, images, text), whether real or AI-generated
* "AI Girlfriend/Boyfriend" services, NSFW chatbots, or sexual companion services
* Content related to OnlyFans management or adult content distribution
* Alcohol, tobacco, vaping products, e-cigarettes
* Drugs, drug paraphernalia, or controlled substances
* Firearms, ammunition, explosives, or combat/weapons training
* Prescription drugs, unverified medical/wellness supplements, or unapproved health claims
### 4.4 Technology and Platform Abuse
* **Spam tools:** mass cold email software, lead scraping tools, bulk SMS/WhatsApp senders
* **Hacking tools:** software designed to gain unauthorized access to systems, networks, or accounts
* **Cheating tools:** software designed to cheat in games or bypass software licensing
* **Circumvention tools:** services that bypass API rate limits, platform rules, or geo-restrictions (IP cloaking, proxy tools for evasion)
* **Surveillance technology:** keyloggers, stalkerware, or tools that enable unauthorized monitoring of individuals
* Counterfeit goods or unauthorized replicas of branded products
### 4.5 Telemarketing and Debt Collection
* Telemarketing services, including outbound call centers and robocall operations
* Debt collection agencies or services
* Lead generation services that involve unsolicited outbound communication (phone, SMS, or email)
### 4.6 Other Prohibited Activities
* Money laundering, structuring, or processing transactions that are not genuine product sales
* Terrorist financing or transactions with designated persons on sanctions lists
* Activities that violate U.S. sanctions (OFAC) or export control regulations
* Transactions designed to test or exploit the payment system (carding, card testing)
## 5. Restricted Business Categories
The following categories are not automatically prohibited but are considered high-risk. Accounts in these categories require additional review by the Kelviq compliance team. Approval is not guaranteed and may be subject to conditions such as reserve requirements, payout holds, or additional monitoring.
### 5.1 AI-Powered Products and Services
AI tools that generate text, images, voice, video, or other content require review to verify:
* The product has appropriate safeguards against generating harmful, deceptive, adult, or illegal content
* The product does not facilitate deepfakes, impersonation, or disinformation
* The product's marketing accurately represents its capabilities
* The Seller has content moderation policies in place
### 5.2 Marketing and Outreach Tools
Some marketing tools may be approved, but the following require additional review:
* Email marketing tools (must have anti-spam compliance, opt-in verification, and unsubscribe functionality)
* Social media automation tools (must comply with platform terms of service)
* Lead generation tools (must not facilitate scraping or unauthorized data collection)
### 5.3 Pre-orders and Paid Waitlists
Pre-orders are generally not approved due to the high chargeback risk for an MoR. In rare cases, exceptions may be made for established creators with a proven track record. If approved, funds will be held in reserve until the product is verifiably delivered.
### 5.4 High-Value Digital Products
Products priced above \$500 per transaction may require additional review due to elevated chargeback risk.
## 6. Prohibited Countries and Sanctions Compliance
Kelviq is a U.S. company and is required to comply with U.S. sanctions laws administered by the Office of Foreign Assets Control (OFAC). Kelviq is strictly prohibited from doing business with any individual or entity located in the following countries and regions:
> [!WARNING]
>
> **Sanctioned Countries and Regions:**
>
> * North Korea
> * Iran
> * Cuba
> * Syria
> * Russia
> * Belarus
> * Crimea, Donetsk, and Luhansk regions of Ukraine
Kelviq also screens all Sellers against the OFAC Specially Designated Nationals (SDN) list and other applicable sanctions lists. Sellers identified on any sanctions list will be immediately blocked from the platform.
This list is subject to change based on updates to U.S. sanctions regulations. Sellers are required to notify Kelviq immediately if they become aware of any sanctions-related issues affecting their account.
## 7. Supported Payout Countries
Kelviq supports businesses registered in 90+ countries for payouts. You can receive payouts in your local bank account in currencies including USD, EUR, GBP, CAD, AUD, INR, and more.
| | | |
| :- | :- | :- |
| 🇦🇱 Albania | 🇦🇬 Antigua & Barbuda | 🇦🇷 Argentina |
| 🇦🇲 Armenia | 🇦🇺 Australia | 🇦🇹 Austria |
| 🇧🇸 Bahamas | 🇧🇭 Bahrain | 🇧🇪 Belgium |
| 🇧🇯 Benin | 🇧🇴 Bolivia | 🇧🇦 Bosnia & Herzegovina |
| 🇧🇼 Botswana | 🇧🇳 Brunei | 🇧🇬 Bulgaria |
| 🇰🇭 Cambodia | 🇨🇦 Canada | 🇨🇱 Chile |
| 🇨🇴 Colombia | 🇨🇷 Costa Rica | 🇨🇮 Côte d'Ivoire |
| 🇨🇾 Cyprus | 🇨🇿 Czech Republic | 🇩🇰 Denmark |
| 🇩🇴 Dominican Republic | 🇪🇨 Ecuador | 🇪🇬 Egypt |
| 🇸🇻 El Salvador | 🇪🇪 Estonia | 🇪🇹 Ethiopia |
| 🇫🇮 Finland | 🇫🇷 France | 🇬🇲 Gambia |
| 🇩🇪 Germany | 🇬🇭 Ghana | 🇬🇷 Greece |
| 🇬🇹 Guatemala | 🇬🇾 Guyana | 🇭🇰 Hong Kong |
| 🇭🇺 Hungary | 🇮🇳 India | 🇮🇸 Iceland |
| 🇮🇪 Ireland | 🇮🇱 Israel | 🇮🇹 Italy |
| 🇯🇲 Jamaica | 🇯🇵 Japan | 🇯🇴 Jordan |
| 🇰🇪 Kenya | 🇰🇼 Kuwait | 🇱🇻 Latvia |
| 🇱🇹 Lithuania | 🇱🇺 Luxembourg | 🇲🇴 Macao SAR China |
| 🇲🇬 Madagascar | 🇲🇹 Malta | 🇲🇺 Mauritius |
| 🇲🇽 Mexico | 🇲🇩 Moldova | 🇲🇨 Monaco |
| 🇲🇳 Mongolia | 🇲🇦 Morocco | 🇳🇦 Namibia |
| 🇳🇱 Netherlands | 🇳🇿 New Zealand | 🇳🇬 Nigeria |
| 🇲🇰 North Macedonia | 🇳🇴 Norway | 🇴🇲 Oman |
| 🇵🇰 Pakistan | 🇵🇦 Panama | 🇵🇾 Paraguay |
| 🇵🇪 Peru | 🇵🇭 Philippines | 🇵🇱 Poland |
| 🇵🇹 Portugal | 🇶🇦 Qatar | 🇷🇴 Romania |
| 🇷🇼 Rwanda | 🇸🇦 Saudi Arabia | 🇸🇳 Senegal |
| 🇷🇸 Serbia | 🇸🇬 Singapore | 🇸🇰 Slovakia |
| 🇸🇮 Slovenia | 🇿🇦 South Africa | 🇰🇷 South Korea |
| 🇪🇸 Spain | 🇱🇰 Sri Lanka | 🇱🇨 St. Lucia |
| 🇸🇪 Sweden | 🇨🇭 Switzerland | 🇹🇼 Taiwan |
| 🇹🇿 Tanzania | 🇹🇭 Thailand | 🇹🇹 Trinidad & Tobago |
| 🇹🇳 Tunisia | 🇹🇷 Turkey | 🇦🇪 United Arab Emirates |
| 🇬🇧 United Kingdom | 🇺🇸 United States | 🇺🇾 Uruguay |
| 🇺🇿 Uzbekistan | 🇻🇳 Vietnam | |
> [!NOTE]
>
> This list is determined by Stripe and is subject to change. Sellers located in countries not on the supported list cannot complete onboarding or receive payouts.
## 8. Enforcement and Monitoring
Kelviq continuously monitors Seller accounts after approval to detect and prevent violations of this policy. Monitoring includes:
### 8.1 Ongoing Monitoring
* Transaction pattern analysis for unusual activity, velocity changes, or suspicious behavior
* Refund rate monitoring (accounts exceeding 10% refund rates may be reviewed)
* Chargeback rate monitoring (accounts exceeding 0.75% chargeback rates will be reviewed, accounts exceeding 1% may be suspended)
* Product listing review (periodic review of product descriptions and content for accuracy and compliance)
* Customer complaints and fraud reports
### 8.2 Enforcement Actions
If a Seller is found to be in violation of this policy, Kelviq may take one or more of the following actions:
1. Issuing a warning and requesting immediate corrective action
2. Imposing a reserve or payout hold on the Seller's account
3. Suspending the Seller's ability to process new transactions
4. Terminating the Seller's account
5. Withholding funds to cover potential disputes, chargebacks, or liabilities
6. Reporting the Seller to law enforcement or regulatory authorities where required by law
### 8.3 Appeal Process
Sellers who believe their account was suspended or terminated in error may contact Kelviq at [support@kelviq.com](mailto:support@kelviq.com) to request a review. Kelviq will review the appeal and respond within 10 business days. Kelviq's decision on appeals is final.
## 9. Alignment with Card Network Rules
This policy is designed to be consistent with the prohibited and restricted business categories defined by Stripe, Visa, Mastercard, and other applicable payment networks. In cases where card network rules are more restrictive than this policy, the card network rules will control.
Kelviq regularly reviews updates to card network operating regulations and Stripe's prohibited business list to ensure this policy remains current. If card network rules change, Kelviq may update this policy and notify affected Sellers.
## 10. Policy Updates
Kelviq may update this policy from time to time to reflect changes in law, regulations, card network rules, or Kelviq's business practices. Material changes will be communicated to Sellers with at least 30 days' notice. Continued use of the platform after the effective date of a change constitutes acceptance of the updated policy.
## 11. Contact
If you are unsure whether your product or business is allowed on Kelviq, contact us before launching:
> **Kelviq, Inc.**
>
> 2261 Market Street STE 46163
> San Francisco, CA 94114
>
> Email: [hi@kelviq.com](mailto:hi@kelviq.com)
# Buyer Terms
Source: https://docs.kelviq.com/legal/buyer-terms
Terms and conditions for buyers purchasing through Kelviq
> [!NOTE]
>
> **Effective Date:** December 23, 2025
These Buyer Terms are a contract between you and Kelviq, Inc. ("Kelviq", "we", "us"). By using Kelviq checkout or the customer portal, you agree to these terms.
## 1. Who We Are
> **Kelviq, Inc.**
>
> 2261 Market Street STE 46163
> San Francisco, CA 94114
>
> Email: [support@kelviq.com](mailto:support@kelviq.com)
Kelviq acts as the Merchant of Record for all purchases made through Kelviq checkout. This means your payment is made to Kelviq, and Kelviq issues your receipt or invoice, handles refunds (when approved), and manages disputes and chargebacks. Kelviq's name will appear on your bank or card statement as the merchant for the transaction.
## 2. Who These Terms Cover
These terms apply to buyers who use:
* Kelviq checkout and payment pages
* The Kelviq customer portal (orders, invoices, downloads, license keys, subscriptions, refunds)
## 3. Eligibility
You must be 18 or older to buy through Kelviq. By placing an order, you confirm you are an adult and you have the right to use the payment method.
## 4. Your Order
When you place an order, you agree that:
* The information you provide is accurate (name, email, billing details).
* You are allowed to buy and use the product under your local laws.
* We may decline, cancel, or limit an order if we believe it is suspicious, fraudulent, or not allowed.
## 5. Prices, Currency, and Taxes
* Prices and currency are shown at checkout before you pay.
* If taxes are required (based on location and rules), they will be shown at checkout or included in the total, depending on the product and region.
* If you use discounts, they apply only as shown at checkout.
## 6. Payments
* Kelviq uses Stripe to process payments.
* Kelviq does not store full card numbers on our servers.
* Your bank or payment provider may charge extra fees (example: foreign transaction fees). Those fees are not controlled by Kelviq.
## 7. Statement Descriptor
When you make a purchase through Kelviq, your bank or card statement will show "KELVIQ" (or "KELVIQ\*" followed by a product identifier) as the merchant name. This is because Kelviq is the Merchant of Record for the transaction.
## 8. Digital Delivery and Access
Kelviq delivers digital goods through the customer portal, such as:
* Download links or access links
* License keys
* Order and invoice history
Access and downloads may require you to verify your email using an OTP code.
> [!NOTE]
>
> **Important:** The product itself is created and maintained by the seller. Kelviq handles payment and delivery links, but the seller controls product content, updates, and product support (unless the checkout page says Kelviq provides that support).
## 9. License Keys and Usage Rules
If your purchase includes a license key or similar access token:
* You must keep it secure and not share it unless the product terms allow sharing.
* We may disable a key if we believe it was shared, resold, or misused, or if a payment is reversed (chargeback, reversal, or unpaid invoice).
## 10. Subscriptions
If you buy a subscription:
* Your subscription renews automatically until you cancel.
* Renewal timing and price are shown at checkout or in your portal.
* You can cancel from the customer portal. Cancellation typically takes effect at the end of the current billing period unless stated otherwise at checkout.
* If a renewal payment fails, we will retry the charge. For card payments, access will remain available for up to 2 weeks before the subscription is canceled. For other payment methods, the subscription will be canceled immediately if payment fails.
## 11. Refunds
Unless a different refund policy is displayed at checkout, the default refund window for digital products purchased through Kelviq is **30 days** from the date of purchase. Refunds may also be subject to any additional rules that apply in your country.
**How refunds work:**
* You can request a refund in the customer portal or by emailing [support@kelviq.com](mailto:support@kelviq.com).
* If a refund is approved, it is sent back to the original payment method when possible.
* Some fees may not be refundable if your payment provider keeps them (this varies by method and country).
* We may refuse refund requests that appear abusive, fraudulent, or not allowed by the refund terms or local law.
* For subscription products, refunds apply to the most recent billing period only, unless otherwise required by law.
## 12. Disputes and Chargebacks
If you file a chargeback or payment dispute with your bank:
* We may suspend or remove access to the product while the dispute is open.
* We may ask for details to investigate (example: proof of purchase, order details).
* If the dispute is decided against you, you may lose access and may not be eligible for a refund through Kelviq.
> [!TIP]
>
> If you have a problem, contact [support@kelviq.com](mailto:support@kelviq.com) first. It is usually faster than a bank dispute.
## 13. Fraud Prevention and Order Blocking
We use risk checks to prevent fraud and abuse. This can include automatic decisions that:
* Block a payment
* Require extra verification
* Limit certain transactions
If your payment is blocked by mistake, contact [support@kelviq.com](mailto:support@kelviq.com) and we will review.
## 14. Prohibited Activity
You may not use Kelviq checkout or the customer portal to:
* Break laws or payment network rules
* Attempt fraud, chargeback abuse, or evasion of security checks
* Access digital goods you did not purchase
* Reverse engineer, scrape, or attack our systems
* Use stolen payment methods or unauthorized accounts
> [!WARNING]
>
> We may suspend access or cancel orders if we believe prohibited activity occurred.
## 15. Third Party Products and Seller Responsibility
Products sold through Kelviq come from independent sellers. Unless the checkout page clearly says otherwise:
* The seller is responsible for product content, accuracy, and support.
* Kelviq is not the maker of the product.
If you need product help (bugs, usage, features), the seller is usually the right contact. If you are not sure how to reach them, email [support@kelviq.com](mailto:support@kelviq.com) and we will help route it.
## 16. Service Changes
We may change checkout, portal features, or delivery methods to improve reliability or comply with laws. We may also update these terms. If changes are important, we will update the effective date and may provide notice in the portal or at checkout.
## 17. Warranty Disclaimer
To the fullest extent allowed by law:
* Kelviq provides checkout and portal access "as is" and "as available".
* We do not promise uninterrupted service or that every transaction will succeed.
* We do not make promises about the seller's product quality or fitness for your needs.
> [!NOTE]
>
> Some places do not allow certain disclaimers, so some of the above may not apply to you.
## 18. Limits of Liability
To the fullest extent allowed by law:
* Kelviq is not liable for indirect losses (lost profits, lost data, or business interruption).
* Kelviq's total liability for a claim related to a specific purchase is limited to the amount you paid to Kelviq for that purchase.
> [!NOTE]
>
> Some places do not allow certain limits, so some of the above may not apply to you.
## 19. Termination and Access Removal
We may suspend or end your access to the portal or digital goods if:
* A payment is reversed or not paid.
* You violate these terms.
* We must do so to comply with law or protect users.
You can stop using Kelviq at any time. If you have an account or portal access, you can request help via [support@kelviq.com](mailto:support@kelviq.com).
## 20. Governing Law and Venue
These terms are governed by the laws of California, excluding conflict of law rules. Any dispute must be brought in state or federal courts located in San Francisco, California, unless local law requires another venue.
## 21. Contact
Questions about orders, refunds, or these terms: [support@kelviq.com](mailto:support@kelviq.com)
# Transaction Flow
Source: https://docs.kelviq.com/legal/compliant-user-flow
End-to-end user flow documentation for the Kelviq Merchant of Record platform
## 1. Overview
This document describes the end-to-end user flow for both Sellers and Buyers on the Kelviq platform. Kelviq operates as a Merchant of Record (MoR) for digital product sellers, meaning Kelviq is the legal seller to the Buyer for all transactions processed through Kelviq Checkout.
The flows described below demonstrate how Kelviq clearly identifies itself as the MoR at every touchpoint, ensuring compliance with card network rules and providing transparency to Buyers about who they are transacting with.
## 2. Key Parties
> **Kelviq (Platform/MoR)**
>
> Kelviq, Inc. operates the platform, hosts the checkout experience, processes payments through Stripe, and acts as the Merchant of Record for all transactions. Kelviq appears on Buyer bank statements, issues receipts and invoices, and handles refunds and disputes.
> **Seller (Connected Account)**
>
> The digital product creator who lists products on the platform, sets pricing, and is responsible for product quality, content, and customer support. The Seller receives payouts through a Stripe Express Connected Account after fees and reserves are deducted.
> **Buyer (End Customer)**
>
> The person or entity purchasing a digital product through Kelviq Checkout. The Buyer's payment relationship is with Kelviq as the MoR.
> **Stripe (Payment Processor)**
>
> Stripe, Inc. provides the payment infrastructure. Kelviq uses Stripe Connect with separate charges and transfers. Kelviq's platform Stripe account receives the payment, and a separate transfer is made to the Seller's Connected Account.
## 3. Seller Onboarding Flow
The Seller onboarding flow ensures KYC compliance, business verification, and proper agreement acceptance before any live payments are processed.
**1. Account Creation**
Seller creates an account on Kelviq.
**2. Business & Product Details**
Seller provides business information and product details for review.
**3. Agreement Acceptance**
Seller reviews and accepts the Kelviq Seller Master Service Agreement, which includes acknowledgment of Kelviq's role as MoR.
**4. Stripe Connect Onboarding**
Seller is redirected to Stripe Express onboarding to complete identity verification, business verification, and payout setup.
**5. Kelviq Review**
Kelviq reviews the Seller's products and business against the Approved and Prohibited Business Policy. Accounts may be approved, require additional information, or be declined.
**6. Go Live**
Once approved, the Seller's checkout links are activated and can accept live payments.
## 4. Buyer Purchase Flow
The Buyer purchase flow ensures the Buyer clearly understands that Kelviq is the Merchant of Record at every step of the transaction.
**1. Product Discovery**
Buyer finds the Seller's product through the Seller's website, marketing, or other channels. The Seller's branding is present at this stage.
**2. Checkout Initiation**
Buyer clicks a purchase link and is directed to a Kelviq-hosted checkout page. The checkout page displays Kelviq branding and clearly identifies Kelviq as the Merchant of Record.
**3. Terms Acceptance**
Before completing the purchase, the Buyer is presented with links to the Kelviq Buyer Terms. By completing the purchase, the Buyer agrees to these terms.
**4. Payment**
Buyer enters payment details. The payment is processed by Stripe and received by Kelviq's platform Stripe account.
**5. Receipt**
Buyer receives an email receipt from Kelviq, Inc. The receipt clearly identifies Kelviq as the Merchant of Record, includes Kelviq's contact information, and itemizes the purchase.
**6. Fulfillment**
Buyer receives access to the digital product (download link, license key, or access credentials) through the Kelviq Customer Portal or via email.
**7. Bank Statement**
The charge appears on the Buyer's bank or card statement as "KELVIQ" or "KELVIQ\* \[product identifier]".
## 5. Post-Purchase Flows
### 5.1 Refund Flow
Refunds can be initiated by either the Seller or through Kelviq as the Merchant of Record.
**1. Request**
Buyer requests a refund by contacting the Seller directly (via contact form or email) or by emailing Kelviq at [support@kelviq.com](mailto:support@kelviq.com).
**2. Review & Approval**
The Seller reviews the request and can issue a refund directly through the Seller Dashboard. If the request is made through Kelviq, Kelviq reviews it against the applicable refund policy.
**3. Processing**
Once approved, the refund is processed through Stripe. The refund is issued to the Buyer's original payment method. The refunded amount is deducted from the Seller's balance.
### 5.2 Dispute and Chargeback Flow
Chargebacks and payment disputes are managed by Kelviq as the Merchant of Record.
**1. Notification**
Kelviq receives chargeback notification from Stripe.
**2. Evidence Collection**
Kelviq gathers evidence (receipt, delivery confirmation, terms accepted) and may request additional information from the Seller.
**3. Response**
Kelviq submits the chargeback response to Stripe within required timelines.
**4. Resolution**
If the chargeback is decided in Kelviq's favor, the funds are returned. If decided against Kelviq, the chargeback amount and fees are deducted from the Seller's payout balance per the Seller MSA.
### 5.3 Subscription Management
For subscription products, the following ongoing flow applies:
* Subscription renewals are processed automatically by Kelviq through Stripe
* Renewal receipts are issued by Kelviq, Inc.
* Buyers can manage subscriptions (cancel, update payment method) through the Kelviq Customer Portal
* Cancellation takes effect at the end of the current billing period
* Failed renewal payments are retried by Kelviq. For card payments, access remains available for up to 2 weeks before the subscription is canceled. For other payment methods, the subscription is canceled immediately if payment fails
## 6. MoR Compliance Summary
The following table summarizes where and how Kelviq is identified as the Merchant of Record across all touchpoints.
| Touchpoint | Kelviq Identified as MoR |
| - | - |
| Checkout Page | Yes - Kelviq branding and legal name displayed |
| Payment Form | Yes - Kelviq is the entity processing the payment |
| Buyer Terms Link | Yes - Links to Kelviq Buyer Terms |
| Email Receipt | Yes - Issued by Kelviq, Inc. with contact info |
| Bank Statement | Yes - Descriptor shows "KELVIQ" |
| Customer Portal | Yes - Kelviq-branded portal for orders, downloads, and support |
| Refund Processing | Yes - Refunds issued by Kelviq |
| Dispute Handling | Yes - Kelviq responds to chargebacks |
## 7. Card Network Rule Compliance
Kelviq's user flows are designed to comply with the following card network requirements:
> **Transaction Clarity**
>
> The Buyer clearly understands that the transaction is with Kelviq at every point in the purchase flow. Kelviq's name and branding are visible on the checkout page, receipt, and bank statement.
> **Statement Descriptor Accuracy**
>
> The Statement Descriptor identifies Kelviq as the merchant, preventing Buyer confusion about the source of the charge.
> **Refund and Dispute Handling**
>
> Kelviq manages all refunds and disputes as the MoR, in accordance with card network timelines and procedures.
> **Customer Service Identification**
>
> Buyers are directed to Kelviq ([support@kelviq.com](mailto:support@kelviq.com)) for payment-related issues. Product-specific support is provided by the Seller, with clear disclosure of this division of responsibility in the Buyer Terms.
> **MCC Assignment**
>
> Kelviq assigns appropriate Merchant Category Codes for transactions processed through the platform, in compliance with card network requirements.
## 8. Contact
Questions about this document or Kelviq's compliance program should be directed to:
> **Kelviq, Inc.**
>
> 2261 Market Street STE 46163
> San Francisco, CA 94114
>
> Email: [support@kelviq.com](mailto:support@kelviq.com)
# Seller Agreement
Source: https://docs.kelviq.com/legal/master-service-agreement
Master Service Agreement for sellers using the Kelviq platform
> [!NOTE]
>
> **Effective Date:** February 11, 2026
> **Version:** 1.0
> **Kelviq, Inc.**
>
> 2261 Market Street STE 46163
> San Francisco, CA 94114
>
> Email: [support@kelviq.com](mailto:support@kelviq.com)
## 1. Introduction and Scope
This Seller Master Service Agreement (the "Agreement" or "MSA") is a binding contract between Kelviq, Inc., a Delaware corporation ("Kelviq", "we", "us", or "our"), and the business entity or individual who registers as a seller on the Kelviq platform ("Seller", "you", or "your").
By completing the Kelviq seller onboarding process and accepting this Agreement, you agree to be bound by its terms. If you are accepting on behalf of a company or other legal entity, you represent that you have the authority to bind that entity.
This Agreement governs your use of Kelviq's platform to sell digital products, including Kelviq's role as your Merchant of Record for payment processing, tax compliance, and transaction management.
## 2. Definitions
| Term | Definition |
| - | - |
| **Buyer** | Any end customer who purchases a Product through Kelviq Checkout. |
| **Chargeback** | A payment dispute initiated by a Buyer through their bank, card issuer, or payment method provider. |
| **Connected Account** | The Stripe account created for the Seller as part of the Kelviq onboarding process, used to receive payouts. |
| **Kelviq Checkout** | The hosted checkout experience operated by Kelviq where Buyers complete purchases. |
| **Kelviq Platform** or **Platform** | The Kelviq website, dashboard, APIs, checkout, and all related tools and services. |
| **Merchant of Record** or **MoR** | The entity that is the legal seller in a transaction, appears on the Buyer's bank or card statement, and is responsible for payment processing, receipts, refunds, disputes, and applicable tax obligations. |
| **Net Revenue** | The gross amount collected from a Buyer for a transaction, minus applicable taxes, Kelviq platform fees, payment processing fees, and any refunds or chargebacks. |
| **Payout** | The transfer of Net Revenue from Kelviq to the Seller's Connected Account or bank account. |
| **Platform Fees** | The fees charged by Kelviq for use of the Platform, as described in Section 6 and displayed in the Seller Dashboard. |
| **Product** | Any digital good, software, or subscription listed for sale by the Seller through the Kelviq Platform. |
| **Prohibited Activity** | Any activity described in Section 10 of this Agreement or the Kelviq Approved and Prohibited Business Policy. |
| **Statement Descriptor** | The text that appears on a Buyer's bank or card statement identifying the merchant for a transaction. |
| **Stripe** | Stripe, Inc. and its affiliates, the payment infrastructure provider used by Kelviq. |
## 3. Merchant of Record Relationship
### 3.1 Kelviq as Merchant of Record
For all transactions processed through Kelviq Checkout, Kelviq acts as the Merchant of Record. This means that Kelviq is the legal seller to the Buyer for the purpose of payment processing, receipts, invoices, refunds, disputes, chargebacks, and applicable tax collection and remittance.
Kelviq's name and branding will appear on Buyer bank and card statements, checkout pages, receipts, and invoices. The Statement Descriptor for transactions will identify Kelviq as the merchant.
### 3.2 What the MoR Relationship Means for Sellers
As the MoR, Kelviq takes on the following responsibilities on behalf of the Seller:
* Processing Buyer payments through Kelviq Checkout
* Issuing receipts and invoices to Buyers in Kelviq's name
* Handling payment disputes, chargebacks, and refund requests
* Collecting and remitting applicable sales tax, VAT, or GST where required
* Appearing as the merchant on Buyer bank and card statements
* Complying with payment network rules (Visa, Mastercard, and other applicable networks) for transactions processed through the Platform
> [!NOTE]
>
> The Seller remains responsible for the Product itself, including product quality, content accuracy, customer support for product-related issues, and fulfillment of any promises made to Buyers about the Product.
### 3.3 Relationship to Buyers
When a Buyer purchases a Product through Kelviq Checkout, the Buyer's contractual relationship for the payment transaction is with Kelviq. The Buyer's relationship for the Product and its use is with the Seller, unless the Seller's checkout page or product listing explicitly states otherwise.
## 4. Seller Onboarding and Verification
### 4.1 Account Registration
To sell through Kelviq, you must complete the seller onboarding process, which includes providing accurate business information, verifying your identity, and accepting this Agreement.
### 4.2 Know Your Customer (KYC) and Identity Verification
Kelviq is required to verify the identity of all Sellers before enabling live payment processing. As part of onboarding, you must provide:
* Legal name of the business or individual
* Business registration details (where applicable)
* Business address and country of incorporation or residence
* Identity verification documents as requested by Kelviq or its payment partners
* Tax identification information where required by law
Kelviq reserves the right to request additional information or documentation at any time to maintain compliance with applicable Anti-Money Laundering (AML), Counter-Terrorism Financing (CTF), and sanctions regulations.
### 4.3 Stripe Connected Account
As part of the onboarding process, the Seller must create and maintain a Stripe Connected Account to receive Payouts. By completing this step, the Seller agrees to be bound by the Stripe Connected Account Agreement (available at stripe.com/legal/connect-account) as it may be updated from time to time.
The Seller acknowledges that Kelviq is not a party to the Stripe Connected Account Agreement and that Stripe may independently impose requirements, restrictions, or limitations on the Seller's Connected Account.
### 4.4 Business Review
Before enabling live payment processing, Kelviq will review the Seller's business, products, and intended use of the Platform. Kelviq may approve, conditionally approve, or reject a Seller application at its sole discretion. Approval criteria are described in the Kelviq Approved and Prohibited Business Policy.
Kelviq may re-review a Seller's account at any time and may suspend or terminate access if the Seller no longer meets approval criteria.
### 4.5 Ongoing Compliance
The Seller must promptly notify Kelviq of any material changes to their business, including changes to the types of Products sold, business structure, ownership, country of operation, or any event that could affect the Seller's eligibility under this Agreement or the Approved and Prohibited Business Policy.
## 5. Seller Obligations
### 5.1 Product Responsibility
The Seller is solely responsible for:
* The quality, accuracy, and legality of all Products listed on the Platform
* Ensuring that Products do not infringe on any third-party intellectual property rights, copyrights, trademarks, or patents
* Providing accurate and non-misleading product descriptions, pricing, and marketing materials
* Delivering digital Products to Buyers promptly after purchase (through Kelviq's delivery mechanisms or the Seller's own fulfillment process)
* Providing customer support for product-related issues, including bugs, feature questions, and usage guidance
### 5.2 Legal Compliance
The Seller must comply with all applicable laws and regulations, including but not limited to:
* Consumer protection laws in jurisdictions where Products are sold
* Export control and sanctions regulations (including U.S. OFAC sanctions)
* Data protection and privacy laws applicable to the Seller's business and customers
* Intellectual property laws
* Payment network rules established by Visa, Mastercard, and other applicable card networks
### 5.3 Cooperation with Disputes
When Kelviq receives a Chargeback, dispute, or refund request related to a Seller's Product, the Seller must cooperate promptly by providing requested information, evidence, and documentation within the timeframes specified by Kelviq. Failure to cooperate may result in the dispute being resolved against the Seller.
### 5.4 Accurate Information
The Seller must keep all account information, business details, and product listings current and accurate. The Seller must not provide false, misleading, or incomplete information to Kelviq, Buyers, or any third party in connection with the Platform.
## 6. Fees
### 6.1 Platform Fees
Kelviq charges Platform Fees for use of the Platform services, including payment processing, checkout hosting, and MoR services. The current fee schedule is displayed in the Seller Dashboard and on the Kelviq website ([kelviq.com/pricing](https://kelviq.com/pricing)).
Kelviq may update the fee schedule with at least 30 days' prior written notice to the Seller. Continued use of the Platform after the effective date of a fee change constitutes acceptance of the updated fees.
### 6.2 Payment Processing Fees
Payment processing fees are included in the Platform Fee. The Seller is not charged separate payment processing fees by Kelviq. All fees are displayed in the Seller Dashboard and deducted from the gross transaction amount before the Seller's Net Revenue is calculated.
### 6.3 Fee Deduction
All fees (Platform Fees and payment processing fees) are deducted automatically from the gross transaction amount before Payouts are made to the Seller. The Seller can view a breakdown of all fees in the Seller Dashboard.
### 6.4 Taxes on Fees
Fees quoted by Kelviq are exclusive of any taxes on the fees themselves. If applicable taxes are required to be charged on Kelviq's fees, they will be added to the fee amount.
## 7. Payout Terms
### 7.1 Payout Schedule
Funds from completed transactions are added to the Seller's available balance after processing. Sellers can withdraw their available balance at any time through the Seller Dashboard.
Kelviq may impose a holding period for new Seller accounts during the first 30 days of activity, or for accounts that present elevated risk factors as determined by Kelviq's compliance team.
### 7.2 Payout Method
Payouts are made to the Seller's bank account through the Seller's Stripe Connected Account. The Seller is responsible for maintaining a valid, active bank account and keeping payout details up to date in the Stripe dashboard.
### 7.3 Payout Currency
Payouts are made in the currency associated with the Seller's bank account. Currency conversion, if applicable, is handled by Stripe and subject to Stripe's exchange rates and conversion fees.
### 7.4 Minimum Withdrawal Threshold
Kelviq may set a minimum withdrawal threshold. The current minimum threshold is displayed in the Seller Dashboard.
### 7.5 Payout Holds and Delays
> [!WARNING]
>
> Kelviq may delay, suspend, or withhold Payouts in the following circumstances:
* Pending investigation of suspected fraud, Prohibited Activity, or policy violations
* Open chargebacks or disputes that may result in reversals
* Incomplete or failed KYC verification
* Seller account suspension or termination
* Legal or regulatory requirements, including court orders, subpoenas, or law enforcement requests
* Negative account balance due to refunds, chargebacks, or fee adjustments
## 8. Reserves and Holdbacks
### 8.1 Reserve Rights
Kelviq reserves the right to require a reserve on the Seller's account if Kelviq reasonably determines that the Seller's account presents an elevated financial risk. Risk factors that may trigger a reserve include, but are not limited to:
* High chargeback or dispute rates (typically exceeding 1% of transactions)
* High refund rates (typically exceeding 10% of transactions)
* Sale of products in restricted categories (as defined in the Approved and Prohibited Business Policy)
* Pre-order or delayed delivery products
* Sudden or unexplained spikes in transaction volume
* Negative media coverage or consumer complaints about the Seller's products
### 8.2 Reserve Amounts and Duration
If a reserve is imposed, Kelviq will notify the Seller in writing of the reserve percentage or fixed amount and the expected duration. Reserves may be implemented as a percentage of each transaction (rolling reserve) or as a fixed amount withheld from the Seller's balance. Kelviq will review reserves periodically and release them when the risk conditions that triggered the reserve have been resolved.
### 8.3 Post-Termination Reserve
Upon termination of this Agreement, Kelviq may hold a reserve for up to **180 days** to cover potential chargebacks, refunds, or other liabilities arising from transactions processed during the term of the Agreement.
## 9. Disputes, Chargebacks, and Refunds
### 9.1 Chargeback and Dispute Handling
As the Merchant of Record, Kelviq manages the chargeback and dispute process with payment networks and issuing banks. When a Chargeback is received:
1. Kelviq will notify the Seller and request relevant information and evidence to respond to the dispute
2. The Seller must provide the requested information within the due date mentioned in the notification
3. Kelviq will submit the dispute response to the payment network on behalf of the Seller
4. If the dispute is resolved in favor of the Buyer, the Chargeback amount (plus any applicable fees) will be deducted from the Seller's balance or future Payouts
### 9.2 Chargeback Liability
The Seller is financially responsible for all Chargebacks related to their Products. This includes the original transaction amount and a **\$15 dispute fee** per chargeback. Additional fees imposed by payment networks or processors may also apply.
> [!WARNING]
>
> If the Seller's chargeback rate exceeds the thresholds set by payment networks (typically 0.75%-1.0% of transactions), Kelviq may impose additional restrictions, require a reserve, or terminate this Agreement.
### 9.3 Refund Policy
Kelviq manages refund requests from Buyers. The applicable refund policy is displayed to Buyers at checkout and in the Kelviq customer portal. Refunds may be issued:
* At the Seller's direction through the Seller Dashboard
* By Kelviq in accordance with the refund policy displayed at checkout
* By Kelviq where required by applicable consumer protection law
* By Kelviq to resolve disputes or prevent chargebacks
Refund amounts are deducted from the Seller's balance or future Payouts. Payment processing fees may or may not be refunded depending on the payment method and processor terms.
### 9.4 Excessive Refunds
If the Seller's refund rate exceeds 10% of transactions over any rolling 30-day period, Kelviq may review the Seller's account and take action including requiring product changes, imposing payout holds, or terminating this Agreement.
## 10. Prohibited Activities
The Seller must not engage in any activity that violates applicable law, payment network rules, or the Kelviq Approved and Prohibited Business Policy (available at docs.kelviq.com and incorporated into this Agreement by reference).
> [!WARNING]
>
> Without limiting the foregoing, the Seller must not:
* Sell physical goods, shipping, or logistics services through the Platform
* Sell manual services (coaching, consulting, agency work) where the primary value is manual labor, unless sold as an add-on to a software product
* Operate a marketplace or process payments for unverified third-party sellers through the Platform
* Sell products that are illegal, deceptive, infringing, hateful, or harmful as described in the Approved and Prohibited Business Policy
* Sell regulated financial products, gambling services, adult content, controlled substances, weapons, or pharmaceuticals
* Sell tools designed for spam, hacking, cheating, surveillance, or circumvention of platform rules
* Process transactions that are not genuine sales of Products (including money laundering, structuring, or fraudulent transactions)
* Provide false or misleading information to Kelviq, Buyers, or payment partners
* Attempt to evade Kelviq's review process, compliance monitoring, or enforcement actions
## 11. Data Processing and Privacy
### 11.1 Buyer Data
In connection with transactions processed through the Platform, Kelviq collects and processes Buyer personal data as described in the Kelviq Buyer Privacy Policy (kelviq.com/buyer-privacy). The Seller acknowledges and agrees that Kelviq controls the collection and processing of Buyer payment data and transaction data as the Merchant of Record.
Kelviq may share certain Buyer information with the Seller as needed for product fulfillment and customer support, as described in the Buyer Privacy Policy. The Seller must handle all Buyer data received from Kelviq in compliance with applicable data protection laws.
### 11.2 Seller Data
Kelviq collects and processes Seller personal and business data as described in the Kelviq Privacy Policy (kelviq.com/privacy). Seller data is used for account management, KYC verification, payout processing, compliance, and platform operations.
### 11.3 Stripe Data Processing
The Seller acknowledges that Stripe processes Seller and transaction data as described in Stripe's Privacy Policy (stripe.com/privacy). By creating a Stripe Connected Account, the Seller consents to Stripe's data processing practices.
## 12. Intellectual Property
### 12.1 Seller Content
The Seller retains ownership of all intellectual property in the Products and content uploaded to the Platform. The Seller grants Kelviq a limited, non-exclusive, worldwide license to host, display, process, and distribute the Seller's content solely as necessary to operate the Platform and fulfill transactions.
### 12.2 Kelviq Platform
Kelviq retains all rights in the Platform, including software, design, APIs, documentation, and branding. Nothing in this Agreement grants the Seller any rights in Kelviq's intellectual property beyond the limited right to use the Platform as described in this Agreement.
### 12.3 Trademarks
Neither party may use the other party's trademarks, logos, or brand names without prior written consent, except as reasonably necessary to perform under this Agreement (for example, Kelviq may display the Seller's product name and branding in the checkout experience).
## 13. Representations and Warranties
The Seller represents and warrants that:
* The Seller has the legal authority to enter into this Agreement and perform its obligations
* All information provided to Kelviq during onboarding and thereafter is true, accurate, and complete
* The Seller owns or has valid licenses for all Products sold through the Platform
* The Products do not infringe on any third-party intellectual property rights
* The Seller will comply with all applicable laws, regulations, and payment network rules
* The Seller is not located in, and will not conduct business with individuals or entities in, any country or region subject to U.S. sanctions (as listed in the Approved and Prohibited Business Policy)
* The Seller will not use the Platform for any Prohibited Activity
## 14. Indemnification
### 14.1 Seller Indemnification
The Seller agrees to defend, indemnify, and hold harmless Kelviq, its officers, directors, employees, and agents from and against any claims, damages, losses, liabilities, costs, and expenses (including reasonable legal fees) arising from or related to:
* The Seller's Products, including product defects, misrepresentations, or intellectual property infringement
* The Seller's breach of this Agreement
* The Seller's violation of applicable law or payment network rules
* Disputes between the Seller and Buyers that are not attributable to Kelviq's actions as MoR
* The Seller's handling of Buyer data received from Kelviq
### 14.2 Indemnification Procedure
Kelviq will promptly notify the Seller of any claim subject to indemnification and will provide reasonable cooperation in the defense of the claim. The Seller may not settle any claim without Kelviq's prior written consent if the settlement would impose obligations on Kelviq or admit liability on Kelviq's behalf.
## 15. Limitation of Liability
To the fullest extent permitted by law:
* Kelviq is not liable for indirect, incidental, special, consequential, or punitive damages, including lost profits, lost revenue, lost data, or business interruption, arising from or related to this Agreement or the Seller's use of the Platform.
* Kelviq's total aggregate liability for all claims arising from or related to this Agreement is limited to the total amount of Platform Fees paid by the Seller to Kelviq in the 12 months preceding the event giving rise to the claim.
These limitations apply regardless of the theory of liability (contract, tort, strict liability, or otherwise) and even if Kelviq has been advised of the possibility of such damages.
> [!NOTE]
>
> Some jurisdictions do not allow certain limitations of liability. In those jurisdictions, the above limitations apply to the maximum extent permitted by law.
## 16. Term and Termination
### 16.1 Term
This Agreement is effective from the date the Seller completes onboarding and accepts this Agreement, and continues until terminated by either party.
### 16.2 Termination by Seller
The Seller may terminate this Agreement at any time by providing written notice to Kelviq at [support@kelviq.com](mailto:support@kelviq.com). Termination takes effect upon completion of all pending transactions, resolution of open disputes, and final Payout of any remaining balance (subject to the post-termination reserve described in Section 8.3).
### 16.3 Termination by Kelviq
Kelviq may terminate this Agreement immediately upon written notice if:
* The Seller breaches any material provision of this Agreement
* The Seller engages in Prohibited Activity
* The Seller's chargeback or refund rates exceed acceptable thresholds
* Kelviq is required to do so by law, regulation, or payment network rules
* The Seller's Stripe Connected Account is suspended or terminated by Stripe
* Kelviq ceases to offer the Platform services
Kelviq may also terminate this Agreement for convenience with 30 days' written notice.
### 16.4 Effects of Termination
Upon termination:
* The Seller's access to the Platform will be disabled
* Kelviq will process final Payouts for completed transactions, subject to any reserve hold described in Section 8.3
* The Seller must cease using Kelviq's branding, APIs, and checkout
* Sections that by their nature should survive termination will survive, including Sections 9 (Disputes), 11 (Data), 14 (Indemnification), 15 (Limitation of Liability), and 17 (Governing Law)
## 17. Governing Law and Dispute Resolution
This Agreement is governed by the laws of the State of California, without regard to conflict of laws principles.
Any dispute arising from or related to this Agreement must be brought in the state or federal courts located in San Francisco, California, unless applicable law requires a different venue.
Before initiating any legal proceeding, the parties agree to attempt to resolve disputes through good faith negotiation for a period of at least 30 days.
## 18. General Provisions
### 18.1 Entire Agreement
This Agreement, together with the Kelviq Approved and Prohibited Business Policy, the Kelviq Privacy Policy, and the Kelviq Buyer Terms, constitutes the entire agreement between the parties regarding the subject matter herein. This Agreement supersedes any prior agreements, understandings, or representations related to the same subject matter.
### 18.2 Amendments
Kelviq may update this Agreement from time to time. Material changes will be communicated to the Seller with at least 30 days' notice. Continued use of the Platform after the effective date of a change constitutes acceptance of the updated Agreement.
### 18.3 Assignment
The Seller may not assign or transfer this Agreement without Kelviq's prior written consent. Kelviq may assign this Agreement in connection with a merger, acquisition, or sale of substantially all of its assets.
### 18.4 Severability
If any provision of this Agreement is found to be unenforceable, the remaining provisions continue in full force and effect.
### 18.5 Waiver
Failure by either party to enforce any provision of this Agreement does not constitute a waiver of that provision or the right to enforce it in the future.
### 18.6 Notices
Notices under this Agreement must be sent to the email addresses on file. Kelviq may also provide notices through the Seller Dashboard. Notices are deemed received when delivered to the email address or dashboard.
### 18.7 Independent Contractors
The parties are independent contractors. Nothing in this Agreement creates an employment, partnership, joint venture, or agency relationship between the parties.
### 18.8 Force Majeure
Neither party is liable for delays or failures caused by events beyond reasonable control, including natural disasters, government actions, pandemics, internet outages, or payment network disruptions.
## 19. Contact
Questions about this Agreement should be directed to:
> **Kelviq, Inc.**
>
> 2261 Market Street STE 46163
> San Francisco, CA 94114
>
> Email: [support@kelviq.com](mailto:support@kelviq.com)
# Payment Processing
Source: https://docs.kelviq.com/legal/payment-flow-diagram
Stripe Connect integration and payment flow documentation for Kelviq
## 1. Payment Architecture Overview
Kelviq uses Stripe Connect with **separate charges and transfers** as its payment integration model. Under this model:
* **Kelviq is the Merchant of Record (MoR)** for all transactions. The payment charge is created on Kelviq's platform Stripe account.
* After a successful charge, Kelviq initiates a **separate transfer** to the Seller's Stripe Connected Account for the Seller's share of the revenue (Net Revenue).
* Kelviq deducts Platform Fees and applicable taxes before transferring funds to the Seller.
* The Buyer sees "KELVIQ" (or a Kelviq-branded descriptor) on their bank or card statement.
* Kelviq is responsible for refunds, disputes, and chargebacks as the settlement merchant.
### 1.1 Key Entities
> **Buyer**
>
> Pays for digital products through Kelviq Checkout
> **Kelviq (MoR)**
>
> Platform Stripe account that receives the charge
> **Seller**
>
> Stripe Express Connected Account that receives transfers
> **Stripe**
>
> Payment processor handling charges, transfers, and payouts
## 2. Purchase Payment Flow
The following describes the step-by-step payment flow for a standard purchase through Kelviq Checkout.
**1. Checkout Initiated**
Buyer clicks purchase link and lands on Kelviq-hosted checkout page.
**2. Payment Details Entered**
Buyer enters payment details on Kelviq-hosted checkout (card data collected by Stripe.js).
**3. PaymentIntent Created**
Stripe creates a PaymentIntent on Kelviq's platform account.
**4. Charge Processed**
After successful charge, Kelviq calculates fee split.
**5. Fee Calculation**
Kelviq deducts: Platform Fee + Applicable Taxes = Net Revenue for Seller.
**6. Transfer Initiated**
Kelviq initiates separate Transfer to Seller's Connected Account.
**7. Payout to Seller**
Stripe processes payout to Seller's bank account on schedule.
## 3. Fee Breakdown Example
The following illustrates how a \$100 one-time payment on a domestic card is split, before the Seller reaches \$5,000 in total sales. After \$5,000, the base fee is 3.5% + \$0.40.
| Line Item | Amount |
| - | - |
| Gross Transaction | \$100.00 |
| Kelviq Platform Fee (2.9% + \$0.40) | -\$3.30 |
| **Seller Net Revenue** | **\$96.70** |
**Additional fees may apply:**
* **International cards:** +1.5% (cards issued outside the United States)
* **Subscriptions:** +0.7% (recurring billing transactions)
> [!NOTE]
>
> The Platform Fee includes all payment processing costs. The complete fee schedule is on the [Fees](/getstarted/fees) page.
## 4. Refund Payment Flow
Refunds can be initiated by either the Seller or through Kelviq as the Merchant of Record.
**1. Request**
Buyer requests a refund by contacting the Seller directly (via contact form or email) or by emailing Kelviq at [support@kelviq.com](mailto:support@kelviq.com).
**2. Review & Approval**
The Seller reviews the request and can issue a refund directly through the Seller Dashboard. If the request is made through Kelviq, Kelviq reviews it against the applicable refund policy.
**3. Processing**
Once approved, the refund is processed through Stripe. The refund is issued to the Buyer's original payment method. The refunded amount is deducted from the Seller's balance.
## 5. Chargeback/Dispute Payment Flow
When a Buyer files a chargeback with their bank, the dispute flow is handled by Kelviq as the MoR.
**1. Dispute Filed**
Buyer files chargeback with their bank or card issuer.
**2. Notification Chain**
Bank notifies Stripe, Stripe notifies Kelviq, and Kelviq notifies the Seller.
**3. Evidence Collection**
Seller provides evidence within the due date mentioned in the notification.
**4. Response Submitted**
Kelviq compiles and submits dispute response to Stripe.
**5. Card Network Review**
Card network reviews and decides (typically 60-75 days).
**6. Resolution**
**Won:** Funds returned to Kelviq/Seller. **Lost:** Chargeback amount + \$15 dispute fee deducted from Seller's balance.
## 6. Technical Implementation Details
### 6.1 Stripe Connect Configuration
| Configuration | Value |
| - | - |
| Account type | Stripe Connect with separate charges and transfers |
| Platform account | Kelviq, Inc. (standard Stripe account) |
| Connected accounts | Express accounts for Sellers (Stripe-hosted onboarding) |
| Charge creation | PaymentIntent created on Kelviq's platform account |
| Transfer | Separate Transfer object to Seller's Connected Account after successful charge |
| Statement descriptor | Set on the PaymentIntent, shows "KELVIQ" or a Kelviq-branded descriptor |
| Payout schedule | Manual withdrawals; Sellers can withdraw available balance anytime |
### 6.2 PCI Compliance
Kelviq uses **Stripe.js and Stripe Elements** on the checkout page. Card data is collected directly by Stripe and never passes through or is stored on Kelviq's servers. This keeps Kelviq at the lowest PCI compliance level (**SAQ-A**).
### 6.3 3D Secure and Strong Customer Authentication (SCA)
Kelviq supports **3D Secure (3DS)** authentication through Stripe for transactions where it is required by regulation or recommended for fraud prevention.
For Buyers in the **European Economic Area (EEA)** and the **United Kingdom**, Strong Customer Authentication (SCA) is applied in accordance with the Payment Services Directive 2 (PSD2) requirements. 3DS authentication is triggered automatically by Stripe based on the issuing bank's requirements and Stripe's risk assessment (Stripe Radar).
### 6.4 Tax Handling
Kelviq calculates and collects applicable **sales tax, VAT, or GST** at checkout based on the Buyer's location. Tax amounts are remitted to the appropriate tax authorities by Kelviq as the MoR. Tax is deducted from the gross transaction amount before the Seller's Net Revenue is calculated.
### 6.5 Multi-Currency Support
Kelviq supports checkout in **multiple currencies**. The Buyer pays in the currency displayed at checkout. Currency conversion for Seller payouts is handled by Stripe based on the Seller's Connected Account currency.
## 7. Contact
Questions about Kelviq's payment flow or Stripe integration should be directed to:
> **Kelviq, Inc.**
>
> 2261 Market Street STE 46163
> San Francisco, CA 94114
>
> Email: [support@kelviq.com](mailto:support@kelviq.com)
# Seller Terms
Source: https://docs.kelviq.com/legal/seller-terms
Platform Terms of Service for sellers using Kelviq
> [!NOTE]
>
> **Effective Date:** December 23, 2025
These Terms and Conditions ("Terms") are a contract between you and Kelviq, Inc. ("Kelviq", "we", "us"). By accessing or using our website, app, dashboard, checkout, APIs, or customer portal (together, the "Services"), you agree to these Terms.
## 1. Company Details and Contact
> **Kelviq, Inc.**
>
> 2261 Market Street STE 46163
> San Francisco, CA 94114
>
> Email: [support@kelviq.com](mailto:support@kelviq.com)
## 2. Who These Terms Apply To
These Terms apply to:
* Website visitors
* Users of the Kelviq app and dashboard (including sellers and team members)
* Buyers using checkout and the customer portal
> [!NOTE]
>
> If you are buying through checkout, your purchase is also governed by the Kelviq Buyer Terms. If you are a seller, your use of the platform to sell products is also governed by the Kelviq Seller Master Service Agreement. If there is a conflict between these Terms and the applicable specific agreement, the specific agreement controls.
## 3. Eligibility
You must be 18 or older to use the Services. If you use the Services on behalf of a company, you confirm you have authority to bind that company to these Terms.
## 4. Accounts and Access
* You are responsible for your account and any activity under it.
* Keep your login details and OTP codes secure.
* Tell us if you suspect unauthorized access.
* We can suspend or disable accounts to protect the Services, comply with law, or stop abuse.
## 5. The Services
Kelviq provides tools to help sellers sell digital goods, and to help buyers complete purchases and manage orders.
Kelviq may add, remove, or change features. We will try to avoid breaking changes, but we do not guarantee any specific feature will remain available.
## 6. Merchant of Record and Payments
For all purchases processed through Kelviq checkout, Kelviq acts as the **Merchant of Record**. This means Kelviq is the party shown as the seller on the Buyer's bank or card statement, receipts, invoices, and is responsible for payment processing, refunds, disputes, and applicable tax collection and remittance.
We use Stripe to process payments. We do not store full card numbers on our servers.
The Buyer's bank or card statement will display "KELVIQ" (or "KELVIQ\*" followed by a product identifier) as the merchant name for the transaction.
## 7. Fees
Some parts of the Services may require fees (for example, platform fees or processing fees). Fees, if any, will be shown in the dashboard, checkout flow, an order summary, or an agreement you accept during onboarding.
Unless required by law, fees are non-refundable.
## 8. Seller Responsibilities
If you sell through Kelviq ("Seller"), you agree to the following. For a complete description of the Seller's obligations, refer to the Kelviq Seller Master Service Agreement.
### 8.1 Your Products and Content
* You are responsible for your products, files, license terms, claims, and support promises.
* You will not sell illegal or prohibited items, or content that violates laws or third-party rights.
* You will keep your product description accurate and not misleading.
### 8.2 Customer Support
Unless the checkout page clearly says otherwise, you are responsible for product support, including bugs, feature questions, and usage issues. Kelviq may help route messages, but we are not required to provide product support.
### 8.3 Compliance
You agree to follow all applicable laws and payment network rules, including rules about prohibited products, sanctions, and consumer protection.
### 8.4 Stripe Connected Account
As part of using the platform as a Seller, you must create a Stripe Connected Account and accept the Stripe Connected Account Agreement. You acknowledge that Stripe may independently impose requirements on your Connected Account.
### 8.5 Disputes and Refunds
We may handle payment disputes and chargebacks as part of operating the checkout. We may ask you for information to respond to disputes. You agree to cooperate promptly.
## 9. Acceptable Use
> [!WARNING]
>
> You may not:
* Use the Services for illegal activity
* Attempt fraud, chargeback abuse, or evasion of security checks
* Upload malware or attempt to disrupt the Services
* Scrape, crawl, or reverse engineer the Services except where the law allows it
* Access or download content you did not purchase or do not have rights to
* Impersonate others or misrepresent your identity
We may remove content, block transactions, or suspend accounts to enforce these rules.
## 10. Content, Uploads, and Feedback
You keep ownership of content you upload (product files, text, branding). You grant Kelviq a limited license to host, process, and display that content only to operate the Services.
If you share feedback or suggestions, you allow Kelviq to use them without restriction or payment.
## 11. Intellectual Property
Kelviq owns the Services, including software, design, branding, and related materials. You may not copy, modify, or distribute Kelviq materials unless allowed by law or with our written permission.
## 12. Privacy
Our Privacy Policy explains how we handle personal information. By using the Services, you agree to our Privacy Policy. Stripe processes Seller and transaction data as described in Stripe's Privacy Policy.
## 13. Third Party Services
The Services may rely on third party tools (payments, hosting, email, fraud prevention). Those providers may have their own terms. Kelviq is not responsible for third party services we do not control.
## 14. Suspension and Termination
We may suspend or terminate access to the Services if:
* You violate these Terms
* We believe your activity creates risk, fraud, or harm
* We must do so to comply with law
* Your payments fail or are reversed (for buyer access or seller payouts)
You may stop using the Services at any time.
## 15. Disclaimers
To the fullest extent allowed by law, the Services are provided "as is" and "as available". We do not promise uninterrupted service, error-free operation, or that every payment will succeed.
Sellers are responsible for their products. Kelviq does not promise product quality, fitness, or outcomes from seller products.
> [!NOTE]
>
> Some regions do not allow certain disclaimers, so parts of this section may not apply to you.
## 16. Limitation of Liability
To the fullest extent allowed by law:
* Kelviq is not liable for indirect losses (lost profits, lost revenue, lost data, or business interruption).
* Kelviq's total liability for claims related to the Services is limited to the amount paid to Kelviq for the Services in the 12 months before the event giving rise to the claim.
* For claims tied to a specific buyer purchase, our total liability is limited to the amount paid for that purchase.
> [!NOTE]
>
> Some regions do not allow certain limits, so parts of this section may not apply to you.
## 17. Indemnity
If you are a **Seller**, you agree to defend and indemnify Kelviq from claims arising from your products or content, your promises to buyers, and your violation of law or third-party rights.
If you are a **Buyer**, you agree to indemnify Kelviq for claims arising from your misuse of the Services or violation of these Terms.
## 18. Governing Law and Venue
These Terms are governed by the laws of California, excluding conflict of laws rules. Any dispute must be brought in state or federal courts located in San Francisco, California, unless local law requires another venue.
## 19. Changes to These Terms
We may update these Terms. If changes are material, we will update the effective date and may provide notice through the website, app, dashboard, checkout, or portal. Continued use after the change means you accept the updated Terms.
## 20. Miscellaneous
* If a part of these Terms is unenforceable, the rest still applies.
* You may not transfer your rights under these Terms without our permission. We may assign these Terms as part of a merger, acquisition, or sale of assets.
* These Terms are the full agreement about the Services, except where you have a separate written agreement with Kelviq (such as the Seller Master Service Agreement).
## 21. Contact
Questions about these Terms: [support@kelviq.com](mailto:support@kelviq.com)
# Overview
Source: https://docs.kelviq.com/payouts/overview
Connect your bank account and track cash flow
We use Stripe to ensure you get paid fast and your tax documents are handled automatically.
## Setup
Before you can receive funds, you need to connect a bank account.
**1. Connect account**
Click **Payouts** in the sidebar, then click **Set up payouts**.
**2. Complete onboarding**
You will be redirected to a secure Stripe form. Choose your account type (Individual or Company) and enter your banking details.

## Dashboard
Once set up, the main Payouts view gives you a real-time snapshot of your cash flow.

### Key metrics
* **Available to payout**
The funds that have cleared and are ready to be transferred to your bank.
* **Processing amount**
Revenue that is currently being processed. You can see the estimated date for when these funds will become available.
* **Billing cycle**
Shows your current schedule. If funds are available, you can click **Withdraw funds** to initiate a manual transfer immediately.
### Payout history
Below the metrics, you will see a list of every payout we have sent to your bank.
* Check the status of past transfers.
* See the exact date the funds landed.
* View the destination bank account for each transfer.
## Payout questions
### Do I need a Stripe account?
No. You don't need to sign up for Stripe or have a Stripe account to sell with Kelviq or to get paid.
When you click **Set up payouts**, you verify your identity and add your bank account in a short form run by Stripe Connect. This creates a payout account that only receives your earnings from Kelviq. It's different from a regular Stripe account: you don't sell through it, and Stripe's limits on which countries can open a regular account don't apply.
That's why sellers in countries where Stripe doesn't offer its own accounts, such as India, can still use Kelviq and receive payouts. Check that your country is in the [supported payout countries](/getstarted/review-process#supported-payout-countries) before you go live.
### How long does a payout take?
Sales funds usually become available for payout after seven days. After Kelviq sends the payout, allow another few business days for your bank to post it. Bank holidays, compliance reviews, reserves, and the receiving bank can extend this timing.
### What is the minimum payout?
The minimum payout amount is \$10, or the equivalent amount in the payout currency.
### Are there payout fees?
No. Your full cleared balance is sent to your bank account or eligible debit card. If your bank account uses a different currency, the amount is converted when the payout is sent.
[See the full fee schedule →](/getstarted/fees)
### Do I need a registered company?
No. You can complete payout onboarding as an individual. If you are a sole proprietor and do not have a separate registered company, select **Individual** and provide your own legal and tax details.
### Can I receive a payout in another currency?
Yes. For example, a seller in Canada can receive a payout in USD when the connected bank account accepts USD and that payout currency is available through Stripe Connect. The payout currency follows the connected bank account. If conversion is required, Stripe applies its exchange rate and any conversion fees.
# Payout Settings
Source: https://docs.kelviq.com/payouts/payout-account
Manage bank accounts and transfer schedules
The **Payout Account** tab lets you control how and when you get paid.

## Bank account
View the currently connected bank account. You can also click the link to view your full verification status directly on Stripe.
# Transactions
Source: https://docs.kelviq.com/payouts/transactions
Audit individual payments, fees, and net earnings
The **Transactions** tab shows successful payments, refunds, and failed payments. Use it to see what the customer paid, the fees and tax deducted, and the amount added to your balance.

## Filtering
You can filter the list by status to find specific issues.
* **Success:** Completed payments.
* **Pending:** Payments that have not finished processing.
* **Refunded:** Money returned to customers.
* **Failed:** Declined cards or failed transfers.
The search field also matches payment method types. Search for `upi`, `card`, `link`, `amazon_pay`, or `sepa_debit` to see payments made through that method.
## Transaction details
Click a transaction row to open its details.
* **Gross total:** The full amount charged to the customer, including tax.
* **Fee:** The combined transaction fees deducted by Kelviq.
* **Tax:** Tax collected for the transaction and excluded from your net earnings.
* **Net earnings:** The amount added to your Kelviq balance after transaction fees and tax. There's no payout fee, so nothing more is deducted when the balance is sent to your bank.
* **Payment details:** The card or method used.
For a successful transaction:
```text theme={null}
Net = Gross - Fee - Tax
```
## How the Fee column is calculated
The **Fee** column shows one combined amount. Depending on the transaction, that amount can contain up to three components:
| Component | Rate | When it applies |
| - | - | - |
| Base fee before \$5,000 in total volume | 2.9% + \$0.40 | Transactions processed before your organization reaches \$5,000 in cumulative sales |
| Base fee after \$5,000 in total volume | 3.5% + \$0.40 | Subsequent transactions after your organization reaches \$5,000 in cumulative sales |
| Recurring billing | +0.7% | Subscription payments and renewals |
| International card | +1.5% | Cards issued outside the United States |
Recurring and international-card fees are added to the base rate when they apply. For example, an international subscription payment processed after the \$5,000 threshold is charged 5.7% + \$0.40: 3.5% base, 0.7% recurring, and 1.5% international.
### Example from the transaction list
For a \$57.60 domestic, one-time payment processed after the \$5,000 threshold:
```text theme={null}
Fee = ($57.60 × 3.5%) + $0.40
= $2.42 after rounding
Net = $57.60 gross - $2.42 fee - $9.60 tax
= $45.58
```
The percentage is calculated from the gross customer charge. The fixed \$0.40 is added once. Kelviq rounds the final fee to the smallest unit of the transaction currency.
[See the complete fee table and more examples →](/getstarted/fees)
> [!NOTE]
>
> There's no payout fee. See [Payout fees](/getstarted/fees#payout-fees).
# Create a feature in Kelviq
Source: https://docs.kelviq.com/product-catalog/create-a-feature
Create Boolean, Meter, or Customizable features in the Kelviq dashboard or with the API, then add them to a plan as entitlements.
A feature is something your product can enable, limit, or measure for a customer. Examples include analytics access, team size, API calls, storage, and AI credits.
Create the feature once, then reuse it across plans. Creating a feature does not grant it to anyone. You must add it to a plan as an entitlement before customers receive access.
## Choose a feature type
| Dashboard option | API value | Use it for |
| - | - | - |
| Boolean | `BOOLEAN` | On-or-off access, such as analytics, SSO, or priority support |
| Meter | `METER` | A quantity that is purchased or consumed, such as seats, API calls, storage, or credits |
| Customizable | `CUSTOMIZABLE` | A plan-specific numeric limit or setting, such as maximum projects or upload size |
> [!TIP]
>
> Use an identifier that is short, stable, and easy to use in code, such as `api-calls` or `advanced-analytics`. You can rename a feature later, but its identifier and type cannot be changed after creation.
## Method 1: Create a feature in the dashboard
1. Open **Product catalog → Features** in the [Kelviq dashboard](https://app.kelviq.com/features).
2. Click **Create feature**.
3. Enter a **Feature name**. This is the readable name shown in the dashboard.
4. Enter an **Identifier**. Your application uses this value when checking access or reporting usage.
5. Add a **Description** if your team needs more context about what the feature controls.
6. Choose the **Feature type**:
* **Boolean** for on-or-off access.
* **Meter** for purchased or consumed quantities.
* **Customizable** for a numeric limit or setting.
7. For a Meter feature, enter the singular and plural units. For example, use `call` and `calls`, or `seat` and `seats`.
8. Click **Create**.
Turn on **Create more** before saving if you want the form to remain open for another feature.
> [!NOTE]
>
> The dashboard creates Meter features for pre-aggregated usage. Your application reports the quantity with the usage reporting API or an SDK. A purchased quantity such as seats can also come from checkout or a subscription update.
## Method 2: Create a feature with the API
Use the Catalog Features API when features are managed from an internal tool, provisioning script, or automated setup flow. Send the Server API Key from a trusted backend.
```bash theme={null}
curl --request POST \
--url https://api.kelviq.com/api/v1/catalog/features/ \
--header "Authorization: Bearer $KELVIQ_SERVER_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"name": "API Calls",
"identifier": "api-calls",
"description": "Number of API calls available in a billing period.",
"featureType": "METER",
"featureDetails": {
"featureSubType": "PRE_AGGREGATED_USAGE",
"units": {
"singular": "call",
"plural": "calls"
}
}
}'
```
Use `https://sandboxapi.kelviq.com/api/v1/catalog/features/` with a sandbox Server API Key when testing. Sandbox and production features are separate.
### Request fields
| Field | Required | Description |
| - | - | - |
| `name` | Yes | Readable feature name |
| `featureType` | Yes | `BOOLEAN`, `METER`, or `CUSTOMIZABLE` |
| `identifier` | No | Stable code-facing identifier; Kelviq generates one from the name when omitted |
| `description` | No | Internal explanation of what the feature controls |
| `featureDetails` | For Meter setup | Meter subtype and units |
| `meter` | For raw-event meters | Aggregation and filter configuration |
| `metadata` | No | Additional key-value data for your own workflow |
For a Boolean or Customizable feature, the request can be shorter:
```json theme={null}
{
"name": "Advanced Analytics",
"identifier": "advanced-analytics",
"featureType": "BOOLEAN"
}
```
The API returns `201 Created` with the new feature, including its Kelviq UUID. See the [Create a feature API reference](/api-reference/features/create-a-feature) for the complete request and response schema.
## Add the feature to a plan
The feature remains independent until you attach it to a plan:
1. Open **Product catalog → Products** and select a product.
2. Open the plan you want to configure.
3. Go to **Features & limits** and choose **Manage features & limits**.
4. Search for the feature and add it to the plan.
5. Configure the entitlement value:
* Enable or disable a Boolean feature.
* Enter the allowed value for a Customizable feature.
* Set the limit, reset cadence, hard-limit behavior, rollover, or alerts for a Meter feature.
6. Save the plan. Publish the draft when it is ready for customers.
Read [Features & limits](/product-catalog/entitlements) to check access from your application, [Usage-based pricing](/product-catalog/usage-based-billing) to price metered consumption, or [Seat-based pricing](/product-catalog/seat-based-pricing) to sell purchased quantities.
## Common problems
| Problem | What to check |
| - | - |
| The feature does not appear for customers | Confirm that it was added to the correct plan and that the plan version was published |
| Usage reports return a feature error | Send the exact feature identifier, not its display name |
| A Meter feature cannot be created | Enter both the singular and plural unit names |
| The feature exists in Sandbox but not Production | Create it separately in the production environment |
| The identifier or type is wrong | Create a new feature; these fields cannot be changed after creation |
# Digital Downloads
Source: https://docs.kelviq.com/product-catalog/digital-downloads
Enable customers to download digital files after purchase
Digital Downloads allow you to attach downloadable files to your plans. Customers who purchase your plan can access these files after completing their purchase.
## Overview
Use Digital Downloads to deliver:
* **Course content** – PDFs, videos, or supplementary materials
* **E-books** – Digital books in PDF, EPUB, or other formats
* **Software** – Installers, license files, or application packages
* **Templates** – Design files, spreadsheets, or document templates
* **Any other digital content** your customers need
## Accessing Digital Downloads

1. Navigate to **Products** in the sidebar
2. Select the product containing your plan
3. Click on the plan you want to edit
4. Scroll down to the **Digital downloads** section
5. Click the expand arrow to open the section
## Uploading Files
### How to Upload

1. Open the **Digital downloads** section
2. Click anywhere in the upload area, or drag and drop files directly
3. Select one or more files from your computer
4. Files will begin uploading automatically
5. Once complete, files appear in the **Uploads** list
### Upload Progress

While uploading, you'll see:
* File name and size
* Progress bar showing upload percentage
* Status indicator (uploading, complete, or error)
### File Size Limits
* **Maximum file size**: 5 GB per file
* **Multiple files**: You can upload multiple files at once
### Supported File Types
All file types are supported, including:
* Documents: PDF, DOCX, TXT, etc.
* Images: PNG, JPG, GIF, SVG, etc.
* Videos: MP4, MOV, AVI, etc.
* Audio: MP3, WAV, FLAC, etc.
* Archives: ZIP, RAR, 7Z, etc.
* Software: EXE, DMG, APP, etc.
* Any other file format
## Managing Files
Each uploaded file has a toggle switch and a menu button (**⋯**) with additional options.
### Enable/Disable Files
You can temporarily hide files from customers without deleting them:

1. Find the file in the uploads list
2. Click the toggle switch next to the file
3. **Enabled** (toggle on): Customers can download this file
4. **Disabled** (toggle off): File is hidden from customers
This is useful when you need to temporarily remove access to a file or prepare files before making them available.
### File Actions Menu
Click the menu button (**⋯**) next to any file to access additional options:

#### Download
Use the **Download** option to:
* Verify that the file uploaded correctly
* Check file contents before enabling for customers
* Save a local backup of uploaded files
To download a file:
1. Click the **⋯** menu button next to the file
2. Select **Download**
3. The file will download to your computer
#### Delete
To permanently remove a file:
1. Click the **⋯** menu button next to the file
2. Select **Delete**
3. The file will be permanently removed
> **Note**: Deleted files cannot be recovered. Make sure you have a backup before deleting.
## Customer Experience
After a successful purchase, customers can:
1. Access the order confirmation page
2. View all downloadable files associated with their purchased plan
3. Click to download each file directly
Files remain accessible to customers as long as their subscription or purchase is active.
# Discounts
Source: https://docs.kelviq.com/product-catalog/discounts
Create coupons and promotional codes
You can manage all your coupons from the **Product Catalog**.
Navigate to **Product Catalog > Discounts** to see a list of every active and expired code.
You can create two types of discounts:
* **Percentage** — Reduces an eligible price by a percentage, such as 20%.
* **Fixed amount** — Subtracts a specific amount in a selected currency, such as USD 10 or INR 500.
For a fixed-amount discount, select the currency next to the amount. Kelviq places the currency from your organization profile first in the list, but you should still review the selection before creating the discount.
## Create a discount
**1. Start creation**
Click **+ Create discount** in the top right corner.
**2. Define value**
Choose the discount type (Percentage or Fixed amount) and enter the value.
* **Name:** The internal name for your reference.
* **Discount code:** The text the customer enters at checkout. You can type your own or click the **refresh icon** to auto-generate a secure random code.
**3. Configure rules**
Set the duration, limits, and product scope (detailed below).
**4. Create**
Click **Create discount** to make the code live immediately.

## Configuration options
You can control exactly how and when a discount applies.
### Duration
Recurring discount settings are enabled by default. Choose how long the discount should apply:
* Enter a number of billing cycles for a limited recurring discount.
* Select **Discount applies forever** to apply it to every eligible recurring invoice.
* Turn off **Recurring discount** when the code should apply only to the initial purchase.
### Usage & expiry
Control the scarcity of the coupon.
* **Redemption limit:** Set a total number of times this code can be used (e.g. "First 100 users").
* **Expiry date:** Set a hard deadline when the code stops working.
### Product restrictions
By default, a discount applies to the entire catalog. To restrict it:
1. Enable **Limit discount to selected products**.
2. Select a product in every restriction row you add.
3. Optionally select a specific plan price. Choose **Applies to all plans** to include every price under that product.
Kelviq will not create the discount while a restriction row has no product selected.
For fixed-amount discounts, the discount currency must match the currency of the selected price. Kelviq shows a validation error when the currencies do not match.
## Apply a discount at checkout
Customers can enter a code themselves, or you can pre-apply one through the Checkout API or a static checkout link. See [Apply a discount code at checkout](/checkout/apply-a-discount-code) for working examples and field names.
## Management
The main discounts tab shows the status of every code you have created.

* **Active:** The code is live and redeemable.
* **Expired:** The code has passed its end date or hit its usage limit.
# Features & limits (entitlements)
Source: https://docs.kelviq.com/product-catalog/entitlements
Entitlements decide which features and limits each customer gets
Check features, not plans. Your code asks whether a customer has a feature, and Kelviq answers from their plan. You can change plans, prices, and custom deals without changing code. In the API and SDKs, a feature on a plan is called an entitlement.

> [!NOTE]
>
> For SDK installation and API reference, see:
>
> * [Node SDK Documentation](https://docs.kelviq.com/backend-integration/node-sdk)
> * [Entitlements API Reference](https://docs.kelviq.com/api-reference/entitlements/retrieve-customer-entitlements)
***
## Why Entitlements?
### The Problem with Plan-Based Access
Traditional SaaS applications check access like this:
```javascript theme={null}
// ❌ The old way - hardcoded, brittle, requires code changes
if (user.plan === 'pro' || user.plan === 'enterprise') {
showAdvancedAnalytics();
}
if (user.plan === 'enterprise') {
enableSSO();
}
```
This approach has serious limitations:
* **Code changes required** for any pricing restructure
* **Deployment needed** to move a feature between plans
* **Engineering bottleneck** for every pricing experiment
* **Stale cache issues** when plans change
* **Custom deals are painful** requiring one-off code
### The Entitlements Approach
With Kelviq, you check features directly:
```javascript theme={null}
// ✅ The Kelviq way - flexible, decoupled, no code changes needed
const hasAnalytics = await client.entitlements.hasAccess({
customerId: "cust_123",
featureId: "advanced-analytics"
});
if (hasAnalytics) {
showAdvancedAnalytics();
}
```
Now you can:
* Move features between plans without code changes
* Create custom enterprise deals in minutes
* A/B test pricing without deployments
* Let Sales and Product iterate independently
***
## How It Works
### The Flow

1. Setup plans and features on Kelviq Dashboard
2. When a customer completes checkout, their entitlements are automatically assigned.
3. Your app checks access via SDK
4. Customer gets immediate access
### Access right after checkout
When a customer completes checkout:
1. Payment is processed
2. Entitlements are assigned **automatically**
3. Your app checks access via SDK
4. Customer gets immediate access
Your app checks access with Kelviq, so you don't have to copy subscription state into your own database. If you want to react to changes in your own systems, use [webhooks](/guides/webhooks).
***
## When the Entitlement API is unavailable
Kelviq serves entitlement checks through its edge API. The service targets P95 response times below 100 ms and an uptime SLA above 99.99%, but your access-control path should still account for a temporary network or API failure.
### Node and Python SDK fallback
The current Node and Python SDKs cache entitlement responses by default. Each process keeps a fresh in-memory entry for 60 seconds. If the edge API is unreachable or returns a server error, the SDK can return the last known entitlement state instead of failing the access check.
The in-memory cache is local to one process and is lost when that process restarts. For multiple application instances, workers, or containers, configure the optional Redis store. It shares the cache across instances and keeps the fallback state available through application restarts.
Usage and event reports that fail because of a temporary network problem are queued. The SDK retries them on the next report call, or you can flush the queue explicitly. An in-memory queue is suitable for one process; use Redis when the queue must be shared and durable.
> [!WARNING]
>
> The SDK cannot return a last known value for a customer or feature that has never been fetched. Decide whether your application should deny access, allow limited access, or show a temporary error when no cached state exists.
See the [Node SDK caching configuration](/backend-integration/node-sdk#caching-and-offline-resilience) or [Python SDK caching configuration](/backend-integration/python-sdk#caching-and-offline-resilience) for setup examples.
### Calling the API directly
If you call the Entitlement API without an SDK, keep your own last-known response in an in-memory or persistent cache. Store when the value was fetched, set a maximum fallback age that matches the risk of the feature, and refresh the record after the API recovers.
Use a short fallback window for high-risk actions such as exporting sensitive data or using an expensive Meter feature. A longer window may be reasonable for low-risk interface features. Do not silently grant access when there is no previous entitlement state.
If your architecture already maintains subscription state through webhooks, you can continue using that model. Process webhook events idempotently and reconcile the local record against the Entitlement API after an outage.
***
## Feature Types
Kelviq supports three types of features to cover all access control scenarios.
### Boolean Features
Simple on/off access control.

**Use for:**
* Feature flags (SSO, API access, white-labeling)
* Premium features (advanced analytics, custom branding)
* Access gates (admin panel, beta features)
**Example configuration:**
| Feature | Starter | Pro | Enterprise |
| - | - | - | - |
| Basic Dashboard | ✓ | ✓ | ✓ |
| Advanced Analytics | ✗ | ✓ | ✓ |
| SSO | ✗ | ✗ | ✓ |
| White Label | ✗ | ✗ | ✓ |
### Meter Features
Usage-based features with limits and tracking.
.png?fit=max\&auto=format\&n=dIayyoP2MxhiM3vQ\&q=85\&s=79944e812b9e3720e950502494285370)
**Use for:**
* API calls, requests, or operations
* Storage (GB, files, records)
* Team members or seats
* Messages, emails, or notifications
* AI tokens or credits
**Configuration options:**
| Option | Description |
| - | - |
| **Limit** | Maximum usage allowed (or unlimited) |
| **Reset Period** | When usage resets (monthly, weekly, daily, never) |
| **Hard Limit** | Block access when limit reached |
| **Soft Limit** | Allow overages but notify/warn |
| **Rollover** | Carry unused quota to next period |

**Example configuration:**
| Feature | Starter | Pro | Enterprise |
| - | - | - | - |
| API Calls | 1,000/mo | 100,000/mo | Unlimited |
| Storage | 5 GB | 100 GB | 1 TB |
| Team Members | 3 | 25 | Unlimited |
### Customizable Features
Dynamic values that aren't simple on/off or numeric limits.

**Use for:**
* Configuration values
* Tier-specific settings
* Dynamic feature behavior
***
## Soft Limits vs. Hard Limits
Choose the right enforcement strategy for each feature. [How usage limits work](/product-catalog/usage-limits) walks through resets, overage, and what happens at the limit.

### Hard Limits
Block access completely when the limit is reached.
**Best for:**
* Cost-sensitive features (AI tokens, bandwidth)
* Preventing abuse
* Strict contractual limits
### Soft Limits
Allow continued access but notify the user.
**Best for:**
* Driving upgrades without blocking users
* Features where occasional overages are acceptable
* Better user experience during high-usage periods
***
## Checkout Integration
When a customer goes through checkout, entitlements are automatically assigned. The key is passing the customer ID.
### Passing Customer ID
When creating a checkout session, include the customer identifier:
```javascript theme={null}
import { Kelviq, CHARGE_PERIOD_CHOICES } from '@kelviq/node-sdk';
const response = await client.checkout.createSession({
planIdentifier: "plan-pro-monthly",
chargePeriod: CHARGE_PERIOD_CHOICES.MONTHLY,
successUrl: "https://yourapp.com/checkout/success",
customerId: "cust_789", // Your customer ID - recommended
});
```
> [!TIP]
>
> Always pass a `customerId` when creating checkout sessions. If you don't, a new customer will be created automatically, which may lead to duplicate customer records.
For complete checkout API options, see the [Node SDK Documentation](https://docs.kelviq.com/backend-integration/node-sdk).
### New vs. Existing Customers
| Scenario | What Happens |
| - | - |
| **New customer ID** | Customer created automatically, entitlements assigned |
| **Existing customer** | Entitlements updated based on new purchase |
| **Upgrade** | New plan entitlements replace old ones |
| **Downgrade** | New plan entitlements replace old ones immediately |
### Automatic Assignment
After successful payment:
1. Customer record is created/updated
2. Plan entitlements are assigned instantly
3. Customer can access features immediately
***
## SDK Integration
### Quick Start
Install the SDK and start checking entitlements in minutes.
```bash npm theme={null}
npm install @kelviq/node-sdk
```
```bash yarn theme={null}
yarn add @kelviq/node-sdk
```
```javascript theme={null}
import { Kelviq } from '@kelviq/node-sdk';
const client = new Kelviq({
accessToken: process.env.KELVIQ_SERVER_API_KEY,
environment: process.env.KELVIQ_ENV, // 'sandbox' or 'production'
});
```
> [!NOTE]
>
> Get your API key from **Settings → [API keys](https://app.kelviq.com/settings/api-keys)** in the Kelviq dashboard.
### Checking Feature Access
```javascript theme={null}
// Check boolean feature access
const hasAccess = await client.entitlements.hasAccess({
customerId: "cust_123",
featureId: "advanced-analytics"
});
if (hasAccess) {
showAdvancedAnalytics();
} else {
showUpgradePrompt();
}
```
### Reporting Usage
For meter features, report usage after each consumption:
```javascript theme={null}
import { BEHAVIOUR_CHOICES } from '@kelviq/node-sdk';
// Increment usage by a value (DELTA)
await client.reporting.reportUsage({
value: 1,
customerId: "cust_123",
featureId: "api-calls",
behaviour: BEHAVIOUR_CHOICES.DELTA // Adds to current usage
});
// Or set absolute value (SET)
await client.reporting.reportUsage({
value: 150,
customerId: "cust_123",
featureId: "seats",
behaviour: BEHAVIOUR_CHOICES.SET // Replaces current usage
});
```
> [!NOTE]
>
> For complete SDK reference including all methods and parameters, see the [Node SDK Documentation](https://docs.kelviq.com/backend-integration/node-sdk).
> [!WARNING]
>
> The sandbox environment is completely separate from production. Use different API keys and note that sandbox data won't appear in production.
***
## Manage feature definitions
Open **Product Catalog → Features** to create and manage reusable feature definitions. See [Create a feature in Kelviq](/product-catalog/create-a-feature) for the complete dashboard and API workflows.
* Use the row action menu to edit a feature name.
* Metered values, percentages, and rollover limits use numeric inputs.
* When configuring a metered entitlement, set the usage reset and hard-limit behavior first, then configure credit rollover and usage alerts.
When adding features to a plan from **Manage features & limits**, search for a feature and click **Add**. Kelviq clears the search after the feature is added so you can search for the next feature immediately.
***
## Managing Customer Entitlements
### Viewing Customer Entitlements
In the Kelviq dashboard:
1. Navigate to **Customers**
2. Select a customer
3. View their **Features** section
You'll see:
* All entitled features
* Current usage vs. limits
* Reset dates for meters
* Whether values are overridden
### Overriding Entitlements
Sometimes you need to give a specific customer different access than their plan provides.
**Common scenarios:**
* Sales gave a prospect extra API calls for a POC
* Customer needs temporary limit increase
* VIP customer gets special access
* Compensating for a service issue
**To override:**
1. Go to the customer's detail page
2. Find the feature in the Features section
3. Click the menu (⋯) → **Override usage**
4. Set the new value
5. Save
Overrides are:
* Per-customer (don't affect the plan)
* Marked with an override indicator
* Can be reverted anytime
### Resetting Usage
For meter features, you can reset a customer's usage:
1. Go to the customer's detail page
2. Find the Meter feature
3. Click the menu (⋯) → **Reset usage**
This sets their current usage back to 0, giving them their full quota again.
***
## Plan Inheritance
Create plan hierarchies to reduce duplication and maintain consistency.
### How It Works
```
Enterprise Plan
└── inherits from → Pro Plan
└── inherits from → Starter Plan
```
When you set up inheritance:
* Child plan automatically gets parent's features
* You can override specific values in the child
* Changes to parent propagate to children (unless overridden)
### Override Indicators
| Icon | Meaning |
| - | - |
| 🔗 Inherited | Value comes from parent plan, stays in sync |
| ⚡ Overridden | Value was customized, no longer syncs with parent |
### Use Cases
**Tiered plans:**
* Starter: 1,000 API calls
* Pro: inherits Starter + overrides API calls to 100,000 + adds analytics
* Enterprise: inherits Pro + overrides to unlimited + adds SSO
**Regional plans:**
* US Pro: standard features
* EU Pro: inherits US Pro + adds GDPR compliance features
***
## Subscription Lifecycle
### What Happens During...
#### Successful Purchase
* Entitlements assigned immediately
* Customer can access features right away
#### Upgrade
* New plan entitlements replace old ones
* Happens immediately
* Existing usage counters are retained and evaluated against the new limits
#### Downgrade
* New plan entitlements replace old ones immediately
* Existing usage counters are retained
* A customer can become over limit when the new limit is below current usage
#### Cancellation
* Immediate cancellation revokes plan access immediately
* Period-end cancellation keeps entitlements active until the subscription expires
#### Failed Payment
* A failed card renewal moves the subscription to `past_due`
* Kelviq retries card payments for up to 14 days before cancellation
* Failed non-card renewals are not retried and the subscription is cancelled
#### Refund
* Cancelling mid-period does not automatically create a refund
* The merchant decides whether to issue a full, partial, or no refund
> [!NOTE]
>
> Manage subscriptions programmatically using the [Subscriptions API](https://docs.kelviq.com/backend-integration/node-sdk#subscriptions).
***
# Free plans and trials
Source: https://docs.kelviq.com/product-catalog/free-and-trials
Set up lead magnets and trial periods
You do not always need to charge upfront. Kelviq lets you offer completely free plans or time restricted trials to help acquire users.

## Free plans
Use a free plan to offer a permanent zero cost tier. This is perfect for open source products, lead magnets, or a basic hobby tier where you want to gather signups without requiring a credit card.

1. Open your draft plan
2. Toggle the switch in the top right from Paid to Free
3. Save your changes
Users who select this plan bypass the payment step entirely. They still get access to the customer portal and any deliverables or entitlements attached to the plan.
### Putting new signups on a free plan
Entitlement checks return `false` for a customer Kelviq has never seen, so a free plan only takes effect once the customer has a subscription to it. To enrol someone at signup rather than sending them through checkout:
1. Create the customer **with an email address** — `customers.create` in the SDK, or `POST /customers/`.
2. Create the subscription — `POST /subscriptions/create/` with the plan's identifier and `chargePeriod: "ONE_TIME"`.
Keep that order, and enrol at signup, before the customer reaches checkout. Creating a subscription for a customer Kelviq has not seen fails with `Subscription requires a customer with a valid email address` — and the failed attempt still registers the customer, without an email. Creating a checkout session with a `customerId` registers the customer the same way. From then on `POST /customers/` answers `Customer already exists`, and the portal refuses that customer too. Recover by patching the email onto the existing record with `PATCH /customers/{customerId}/`, after which the subscription call succeeds. Treat `Customer already exists` as a signal to patch rather than an error to surface.
Enrolling this way also records a \$0 order for the plan.
Check for an existing subscription first, with `customer_id` on `GET /subscriptions/`. Creating a second subscription for the same customer is allowed: the new one becomes `active` and the previous one becomes `superseded`. Nothing breaks, but enrolling on every visit churns through subscription records for no reason.
## Free trials
Free trials let customers test a paid plan for a specific number of days before their card is charged.
> [!NOTE]
>
> Trials are available only when the plan has at least one recurring billing cycle. A one-time-only plan does not show trial settings and will not display a trial at checkout.

1. Open a paid plan draft.
2. Enable at least one recurring billing cycle, such as **28 days**, **Monthly**, or **Yearly**.
3. Enable **Free trial**.
4. Enter the number of days the trial should last.
If you later disable the final recurring billing cycle and leave only one-time pricing, Kelviq removes the trial from the one-time offer.
When a customer signs up they must enter a valid payment method. We authorize the card but do not charge it. When the trial period ends Kelviq automatically charges the card and begins the regular billing cycle.
If the user cancels before the trial ends they are not charged.
# License Key Management
Source: https://docs.kelviq.com/product-catalog/license-keys
Generate and manage license keys for your products
License keys allow you to generate unique activation keys for each customer purchase. This guide covers how to enable and configure licenses on your plans, manage them from orders, and how your customers can activate and validate licenses using the API.
***

## 1. Enabling Licenses on a Plan
License keys are configured at the **plan level**. When enabled, every order placed for that plan will automatically generate a unique license key for the customer.
### How to Enable Licenses

1. Go to **Product catalog → Products** → Select your product → Select the plan you want to configure
2. Scroll down to the **License keys** section
3. Toggle the switch to **enable** license keys
### Configuration Options
Once enabled, you can configure the following settings:
#### Activation Limit
Control how many times a single license key can be activated across different devices.

| Setting | Description |
| - | - |
| **Enabled** | Limit activations to a specific number (e.g., 5 devices) |
| **Disabled** | Unlimited activations allowed |
> **Use case**: Set a limit of 3 if you want customers to use the license on up to 3 computers.
#### License Expiry
Define how long the license key remains valid after purchase.

| Setting | Description |
| - | - |
| **Enabled** | License expires after a specified duration |
| **Disabled** | License never expires |
When expiry is enabled, you can set:
* **Duration value**: A number (e.g., 1, 6, 12)
* **Duration unit**: Days, Months, or Years
> **Example**: Setting duration to `12 Months` means the license expires 12 months after the purchase date.
### Save Your Configuration
After configuring the settings, click **Save Changes**. All future orders for this plan will use these license settings.
***
## 2. Managing licenses from an order
Once a customer places an order for a plan with licenses enabled, you can manage their license from the order. Go to **Sales → Orders**, click the customer, and open the **License** tab.
### Viewing License Information
The **License** tab displays:

* **License Key**: The unique key assigned to this order
* **Expiry Date**: When the license expires (or "Never expires")
* **Activation Status**: Active count vs. limit (e.g., "2/5 activations" or "3/Unlimited")
* **License Status**: Active or Deactivated
### Available Actions
Click the **⋯** menu button on the license card to access these options:

#### 🔄 Regenerate Key

Creates a brand new license key and invalidates the old one.
* The old key will immediately stop working
* Customer must use the new key for future activations
* Existing activations will stop working
> ⚠️ **Warning**: This action cannot be undone. Use when a key has been compromised or shared inappropriately.
#### ✏️ Edit Activation Limit

Modify how many devices can use this specific license.
* Toggle activation limit on/off
* Set a specific number (1-100 activations)
* Changes apply immediately
> **Use case**: Upgrade a customer from 3 seats to 10 seats without issuing a new license.
#### 📅 Manage Expiry

Update the license expiration date.
* Toggle expiry on/off
* Set a specific expiration date using the calendar picker
* Extend or shorten the validity period
> **Use case**: Extend a license by 3 months as a courtesy for a loyal customer.
#### ⛔ Deactivate License
Permanently disable the entire license.
* All existing activations will stop working
* Customers will no longer be able to activate or validate the key
* The license key becomes invalid
> ⚠️ **Warning**: This action cannot be undone. Use for refunds or policy violations.
### Viewing Active Activations
Click on the license card to view the **License Details** page, which shows:
* All instances where the license is currently activated
* Instance name or ID
* Last seen timestamp (when the license was last validated)
* Activation metadata (device info, if provided)
#### Revoking Individual Activations

For each active instance, you can click **Revoke** to:
* Remove that specific instance's access
* Free up an activation slot
* The license remains valid for other instances
> **Use case**: Customer got a new computer and wants to transfer their license. Revoke the old instance to free up a slot.
***
## 3. License API for Developers
Your customers (or their software) can interact with licenses using these API endpoints. Use these to integrate license checking into your application.
### Authentication
The Activate, Validate, and Deactivate License APIs accept either a **Client API Key** or a **Server API Key** as a Bearer token:
```bash theme={null}
Authorization: Bearer YOUR_API_KEY
```
Choose the key based on where the request originates:
| Request origin | API key | Guidance |
| - | - | - |
| Browser, desktop app, mobile app, CLI, or plugin | Client API Key | Safe to include in distributed client software. The customer still supplies the license key. |
| Your backend or another trusted server | Server API Key | Keep it secret and load it from your server environment. |
Both key types can call `POST /license/activate/`, `POST /license/validate/`, and `POST /license/deactivate/`. Use a key from the same environment as the license: a sandbox key with a sandbox license, or a production key with a production license.
> [!WARNING]
>
> Never expose a Server API Key in frontend code or software distributed to customers. Use a Client API Key for those integrations.
### Reading a customer's key from your server
A customer's keys come back with their purchase records, authenticated with your server API key. Orders, subscriptions and invoices all carry a `license` array, and each entry holds `licenseKey` alongside `activationUsage`, `activationLimit` and `expiresOn`.
Which record to read depends on how the plan is sold:
* **One-time purchases** produce an order, not a subscription. Read `GET /orders/`, where each order's `billingType` is `ONE_TIME`. Orders have no customer filter — pass the customer's id or email as `search` to narrow the list to them.
* **Recurring plans** produce a subscription. Read `GET /subscriptions/?customer_id=`.
This matters for how much you ask of the customer. A signed-in web app already knows who the customer is, so it can read the key server-side and activate on their behalf — there is no need to make them find an email and paste a key back into a session you control. Ask for the key only where the software has no way to look it up itself: a desktop app, CLI, plugin, or anything running off your server, which cannot hold a server API key.
### 3.1 Activate a License
Activates a license key on a specific device/instance.
**Endpoint**: `POST /license/activate/`
**Authentication**: Client API Key or Server API Key
**When to use**:
* When the user enters their license key for the first time
* When your application is installed on a new device
**What it does**:
* Validates the license key exists and is valid
* Checks if activation limit has been reached
* Creates a new activation instance
* Returns activation details including instance ID
**Key behaviors**:
* Fails if the license is expired
* Fails if the license is deactivated
* Fails if activation limit is reached
* Succeeds and increments the activation count
📖 **Full documentation**: [Activate License API](https://docs.kelviq.com/api-reference/license/activate-license)
***
### 3.2 Validate a License
Checks if a license is valid and currently active.
**Endpoint**: `POST /license/validate/`
**Authentication**: Client API Key or Server API Key
**When to use**:
* On application startup to verify the license
* Periodically during runtime
* Before granting access to licensed features
* When checking license status without activating
**What it does**:
* Confirms the license key exists
* Checks if the license is active (not deactivated)
* Verifies the license hasn't expired
* Returns current license status and metadata
**Response includes**:
* License validity status
* Expiration date
* Activation count and limit
* License metadata
📖 **Full documentation**: [Validate License API](https://docs.kelviq.com/api-reference/license/validate-license)
***
### 3.3 Deactivate a License
Removes an activation from a specific device/instance.
**Endpoint**: `POST /license/deactivate/`
**Authentication**: Client API Key or Server API Key
**When to use**:
* When the user uninstalls your application
* When transferring a license to another device
* When the user explicitly signs out
**What it does**:
* Removes the device/instance from active activations
* Decrements the activation count
* Frees up a slot for activation on another device
* The license itself remains valid
**Key behaviors**:
* Requires both the license key and instance ID
* Only deactivates the specific instance, not the entire license
* The license can be re-activated on the same or different device
📖 **Full documentation**: [Deactivate License API](https://docs.kelviq.com/api-reference/license/deactivate-license)
***
## 4. Common Workflows
### Workflow A: New Customer Purchase
```
1. Customer purchases a plan with licenses enabled
2. Order is created → License key is automatically generated
3. Customer receives license key (via email, dashboard, etc.)
4. Customer enters key in your application
5. Application calls Activate API
6. License is now active on customer's device
```
For software the customer signs in to, skip steps 3 and 4: read the key from their order or subscription as in [Reading a customer's key](#reading-a-customers-key-from-your-server) and call Activate for them.
### Workflow B: Customer Switches Devices
```
1. Customer uninstalls app from old device (calls Deactivate API)
2. Activation slot is freed
3. Customer installs app on new device
4. Customer enters same license key
5. Application calls Activate API
6. License is now active on the new device
```
### Workflow C: Validating on Application Startup
```
1. Application launches
2. Check if license was previously activated (stored locally)
3. Call Validate API with the license key
4. If valid → Grant access to features
5. If invalid/expired → Prompt user to renew or re-enter key
```
### Workflow D: Admin Extends a License
```
1. Customer requests extension
2. Admin opens the order's License tab
3. Click license menu → "Manage expiry"
4. Select new expiration date
5. Save changes
6. Customer's next validation will reflect the new expiry
```
### Workflow E: Handling a Compromised Key
```
1. Admin notices suspicious activity (too many activations)
2. Open the order's License tab
3. Revoke unauthorized instances
4. If key is widely leaked: Regenerate the key
5. Send new key to legitimate customer
6. Old key stops working immediately
```
***
## Quick Reference
| Task | Where | Action |
| - | - | - |
| Enable licenses for a plan | Plan Details | Toggle "License keys" switch |
| Set activation limit (plan) | Plan Details → License keys | Enable limit + set number |
| Set expiry duration (plan) | Plan Details → License keys | Enable expiry + set duration |
| View customer's license | Order's License tab | See license card |
| Copy license key | Order's License tab | Click on license key |
| Regenerate key | Order's License tab → ⋯ menu | "Regenerate key" |
| Change activation limit | Order's License tab → ⋯ menu | "Edit activation limit" |
| Change expiry date | Order's License tab → ⋯ menu | "Manage expiry" |
| View all activations | Order's License tab | Click license card |
| Revoke a device | License Details | Click "Revoke" on instance |
| Deactivate entire license | Order's License tab → ⋯ menu | "Deactivate" |
***
## Important Notes
⚠️ **Irreversible Actions**:
* **Regenerating a key**: The old key immediately stops working. All existing activations are invalidated.
* **Deactivating a license**: The license is permanently disabled and cannot be reactivated.
✅ **Best Practices**:
* Set reasonable activation limits based on your pricing model
* Use the Validate API on application startup for security
* Store the instance ID locally after activation for proper deactivation
* Communicate expiry dates clearly to customers before they expire
* Consider grace periods before fully blocking expired licenses
# Links
Source: https://docs.kelviq.com/product-catalog/links
Share links with your customers
Links allow you to share important URLs with customers after they purchase your plan. Use links to grant access to communities, resources, or any web-based content.
## Overview
Use Links to share:
* **Community access** – Discord servers, Slack workspaces, or private forums
* **Knowledge bases** – Notion pages, documentation sites, or wikis
* **Membership portals** – Member-only websites or dashboards
* **Resource libraries** – Google Drive folders, Dropbox links, or online courses
* **Any other web content** your customers need access to
## Accessing Links

1. Navigate to **Products** in the sidebar
2. Select the product containing your plan
3. Click on the plan you want to edit
4. Scroll down to the **Links** section
5. Click the expand arrow to open the section
The section header shows the current count of saved links (e.g., "Links (3)").
## Adding Links
### How to Add a Link

1. Open the **Links** section
2. You'll see two input fields:
* **Link name** – A descriptive name for the link
* **Link URL** – The full URL starting with `https://`
3. Enter the link name (e.g., "Private Discord community")
4. Enter the full URL (e.g., `https://discord.gg/your-invite`)
5. Click **+ Add link** to add another row if needed
6. Click **Save** to save all links
### Link Name Examples
Choose clear, descriptive names that tell customers what they'll access:
| Good Examples | Why It Works |
| - | - |
| Private Discord Community | Clear destination |
| Course Materials (Notion) | Describes content and platform |
| Member Dashboard | Indicates exclusive access |
| Resource Library | Explains what's inside |
### URL Requirements
* URLs must start with `https://` or `http://`
* URLs must be valid and properly formatted
* Each link requires both a name and URL to be saved
## Managing Links
### Edit Existing Links
1. Open the **Links** section
2. Modify the link name or URL directly in the input fields
3. Click **Save** to update
### Remove a Link

1. Open the **Links** section
2. Click the **×** button next to the link you want to remove
3. Click **Save** to confirm the removal
### Cancel Changes
If you've made changes but want to discard them:
1. Click **Cancel**
2. All unsaved changes will be reverted to the last saved state
## Customer Experience
After a successful purchase, customers can:
1. Access the order confirmation page
2. View all links associated with their purchased plan
3. Click any link to open it in a new tab
Links remain accessible to customers as long as their subscription or purchase is active.
# Location based pricing
Source: https://docs.kelviq.com/product-catalog/location-based-pricing
Set custom prices for specific countries to optimize global conversion
Location based pricing is a feature that allows you to set custom prices for specific countries to optimize global conversion.
## Set up localized prices
To offer a localized price open your plan configuration and expand the Price localization card.
> [!NOTE]
>
> Regional prices inherit the base price's usage reset, usage alert, and credit rollover settings. Regional pricing changes the localized monetary price; it does not create a separate usage-policy configuration.
### 1. Choose a currency
Select your target currency from the dropdown list. You can choose options like Euro or Indian Rupee. Click Add localized price to open the configuration panel.

### 2. Set the fixed base charge
Kelviq automatically displays the billing cycles you already enabled for your default base price and usage based pricing. Enter the exact localized amount you want to charge for each active interval.

### 3. Localize usage based features
If your plan includes Meter features like API calls Kelviq pulls those in automatically right below the fixed charge section. You can set localized unit prices and flat fees for every single usage tier.
You must configure these usage rates across all active billing periods. Use the tabs to switch between your monthly and yearly configurations and ensure every input has a defined price.

### 4. Save and stack regions
Click Save to lock in the location based pricing configuration. The currency will appear in your localized pricing list.
You can repeat this process to add as many localized currencies as you need.
## Fraud protection
Kelviq analyzes the IP address, browser fingerprint, and issuing bank of the credit card during checkout. If a customer attempts to use a card from a high income country while pretending to be in a lower income region we block the transaction or force the standard global price automatically.
# One time payments
Source: https://docs.kelviq.com/product-catalog/one-time-payments
Charge a flat fee for lifetime access or digital downloads
Not every product needs a subscription. If you are selling a lifetime software license, a template, or charging a one time setup fee for an AI agent, you use a flat base price.
## Set up a one time price

1. Open your draft plan and ensure it is set to Paid
2. Under the billing cycles select One time
3. Enter the total amount you want to charge
4. Click Save
> [!NOTE]
>
> One-time payments do not support free trials. To offer a trial, add and enable a recurring billing cycle on the plan.
Customers pay this amount exactly once during checkout. If you attach digital downloads or license keys to this plan they gain permanent access to those assets immediately after payment.
If an existing customer already has a saved payment method, you can also [create a one-time charge from your backend](/sales/create-a-one-time-charge) without sending them through checkout again.
## Combine with usage
Pay as you go usage is billed monthly, so it can't be added to a One Time price. To charge a fixed fee plus usage, use a **Monthly** price and add a Pay as you go usage charge. See [Pay as you go](/product-catalog/pay-as-you-go).
# Pay as you go
Source: https://docs.kelviq.com/product-catalog/pay-as-you-go
Bill customers each month for the usage they record
With pay as you go, the customer does not buy a block of usage upfront. Your backend reports what they consume, and Kelviq bills the resulting amount at the end of the monthly subscription period. Use it for API calls, AI tokens, storage, compute time, or any other metered cost.
The same plan can contain a monthly base price and one or more usage charges. You define the billable feature and its rates in the dashboard; your application supplies the usage.
> [!NOTE]
>
> Monthly must be the plan's only billing period. Pay as you go can't be used on a plan that also offers yearly, quarterly, one-time, or another billing period.
## Before you start
You will need:
* A product and a plan in Kelviq
* A [Meter feature](/product-catalog/create-a-feature), such as `api-calls` or `tokens`
* A monthly price on the plan
* A server API key for the same environment as the plan
Pay as you go supports **Flat**, **Package**, **Tiered**, and **Volume** pricing. It does not support **Stair-step** pricing.
## Configure the plan
1. Open the product and edit its plan.
2. In the plan's billing periods, keep **Monthly** enabled and turn off every other period.
3. Open **Usage charges** and select the Meter feature you want to bill for. If the list is empty, [create a Meter feature](/product-catalog/create-a-feature) from **Product catalog → Features** first.
4. For **Billing model**, choose **Pay-as-you-go**.

5. Choose a pricing model and enter its rates.
6. Add [usage alerts](/product-catalog/usage-alerts) if you want to notify your application when usage crosses a threshold.
7. Save the plan. If you edited a published plan, publish the new version before testing it.
When you select Pay as you go, Kelviq keeps Monthly as the plan's only billing period. Usage reset and credit rollover controls are removed because there is no prepaid balance to reset or carry into the next period.
## Choose a pricing model
The billing model controls *when* the customer pays. The pricing model controls *how* Kelviq calculates the charge.
| Pricing model | How it bills | Example |
| - | - | - |
| Flat | Uses one rate for every unit | \$0.001 per API call |
| Package | Bills in blocks of units | \$2 per 10,000 tokens |
| Tiered | Applies each rate only to the units inside that tier | First 100,000 tokens free, then \$0.0002 per token |
| Volume | Uses the rate for the customer's final usage band across all units | \$0.01 per request, or \$0.007 for all requests after 100,000 |
See [Usage-based pricing](/product-catalog/usage-based-billing) for the full calculation rules and examples for each model.
### Combine a base price with usage
You can charge a fixed monthly subscription and add usage on top. For example:
```text theme={null}
Base subscription: $20 per month
Included usage: 100,000 tokens
Additional usage: $0.0002 per token
```
If the customer uses 120,000 tokens, the first 100,000 fall into a zero-priced tier. The remaining 20,000 tokens cost \$4, so the period total is \$24 before tax.
## Report usage from your backend
Kelviq can only bill usage that your application reports. Send usage from trusted server-side code after the billable action succeeds. Do not expose a server API key in browser or mobile code.
### Node.js
Use `DELTA` when each call adds to the current meter:
```ts theme={null}
import { BEHAVIOUR_CHOICES, Kelviq } from "@kelviq/node-sdk";
const client = new Kelviq({
accessToken: process.env.KELVIQ_SERVER_API_KEY!,
});
await client.reporting.reportUsage({
customerId: "customer_001",
featureId: "api-calls",
value: 1,
behaviour: BEHAVIOUR_CHOICES.DELTA,
});
```
Use `SET` when you already maintain a cumulative total and want Kelviq's meter to match it:
```ts theme={null}
await client.reporting.reportUsage({
customerId: "customer_001",
featureId: "api-calls",
value: 1250,
behaviour: BEHAVIOUR_CHOICES.SET,
});
```
| Field | What to send |
| - | - |
| `customerId` | The same customer identifier used for the subscription |
| `featureId` | The identifier of the Meter feature on the plan |
| `value` | A whole-number usage quantity |
| `behaviour` | `DELTA` to add usage or `SET` to replace the current value |
> [!WARNING]
>
> A repeated `DELTA` call adds the value again. Make sure retries do not submit the same unit of work twice. If your system periodically sends an authoritative running total, use `SET` instead.
### REST API
You can send the same update without an SDK:
```bash theme={null}
curl --request POST \
--url https://api.kelviq.com/api/v1/report/usage/ \
--header "Authorization: Bearer $KELVIQ_SERVER_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"customerId": "customer_001",
"featureId": "api-calls",
"value": 1,
"behaviour": "DELTA"
}'
```
See [Report pre-aggregated usage](/api-reference/reporting/report-pre-aggregated-usage) for the request and response schema.
## How billing works
1. The customer starts a monthly subscription.
2. Your backend reports usage throughout the period.
3. Kelviq updates the meter whenever it receives a valid report.
4. At the end of the subscription period, Kelviq applies the configured pricing rules to the final usage.
5. The meter starts a new monthly period on the customer's subscription renewal date.
Reporting usage does not charge the customer immediately. It updates the quantity Kelviq will use for the period's bill.
## Add alerts without stopping usage
Usage alerts are optional, but they are useful when a bill can grow during the month. You can set absolute thresholds for a pay-as-you-go feature, then handle the `feature.usage_alert` webhook to send an email, show an in-app warning, or suggest a different plan.
You do not need a separate pay-as-you-go webhook to calculate the bill. Usage reports drive the meter; alerts only notify your application when a threshold is crossed.
See [Usage alerts](/product-catalog/usage-alerts) for configuration and webhook examples.
## Test in sandbox
Sandbox and production have separate products, customers, subscriptions, and API keys. To test the complete flow:
1. Switch the dashboard to Sandbox.
2. Create or publish a monthly plan with a pay-as-you-go charge.
3. Complete a test checkout so the sandbox customer has an active subscription.
4. Initialize the Node SDK with `environment: "sandbox"`, or send REST requests to `https://sandboxapi.kelviq.com/api/v1`.
5. Report a small usage value.
6. Open the customer's metered entitlement and confirm that the recorded usage changed as expected.
See [Sandbox](/guides/sandbox) for environment setup and test-payment details.
## Troubleshooting
### Pay as you go shows a billing-period error
Remove every billing period except Monthly. A plan with both Monthly and Yearly pricing is not eligible, even if you only enter a usage rate under Monthly.
### Stair-step cannot be selected
Stair-step pricing is not available with Pay as you go. Choose Flat, Package, Tiered, or Volume. If you need fixed usage bands, use an advance commitment instead.
### Reported usage does not appear
Check that the API key, customer, plan, and feature all belong to the same environment. Also confirm that `customerId` and `featureId` exactly match the identifiers in Kelviq.
### Usage is too high
Look for duplicate `DELTA` calls or a mismatch between the unit your code reports and the unit your price expects. For package pricing, remember that a partially used package is billed as a complete package.
### Credit rollover is missing
This is expected. Rollover applies to unused advance-commitment credits, not usage billed after the period ends.
# Plans
Source: https://docs.kelviq.com/product-catalog/plans
Configure pricing, features, and deliverables
A plan is the commercial package you offer to customers. It bundles your pricing logic with the actual value you deliver, such as software downloads, community access, or feature limits.
You can create multiple plans for a single product. For example, you might have a "Starter" plan for hobbyists and a "Pro" plan for businesses.
For sales-led pricing, see [Create an Enterprise plan](/product-catalog/create-an-enterprise-plan). It covers both reusable Enterprise tiers and bespoke customer amounts.
## Plan structure
Every plan consists of configurable components that define what the user gets.
> **Pricing**
>
> Define the cost. Choose between recurring subscriptions (monthly/yearly) or one-time lifetime payments.
> **License keys**
>
> Issue software licenses. Generate unique keys upon purchase and enforce activation limits.
> **Features & limits**
>
> Set limits. Control access to specific features, API quotas, or seat limits via the SDK.
> **Downloads**
>
> Attach files. Upload software binaries, e-books, or course content.
> **Links**
>
> Grant access. Direct customers to private Discord servers or Notion templates.
## Plan lifecycle
You manage plans through a simple status flow.
* **Draft**
The default state for new plans. These are not visible to customers.
* **Published**
The plan is live and checkout is enabled.
* **Unpublished changes**
A draft version of an existing published plan. You are editing a copy, so your live plan remains safe until you hit publish again.
* **Archived**
The plan is hidden from new customers but remains active for existing subscribers.
***
## Create a plan
You create plans directly inside a product container.
**1. Start creation**
Navigate to the **Product detail page** and click **+ Create plan**.

**2. Define basics**
Fill in the plan details.
* **Plan name:** The public name (e.g. "Pro", "Starter").
* **Identifier:** The unique ID used in your code. You can override the auto-generated ID.
* **Description:** Optional text shown on invoices and checkout.

> [!TIP]
>
> Enable the **Create more** toggle to quickly add multiple plans in a row without closing the modal.
**3. Save draft**
Click **Create**. The plan will appear in your list with a **Draft** badge.
***
## Configure pricing
Click on any draft plan to enter the configuration view. The first step is to set how you charge.
## Pricing models
Once your plan draft exists you can configure how you charge. We split these into dedicated guides.
* [One time payments](/product-catalog/one-time-payments)
* [Subscriptions](/product-catalog/subscriptions)
* [Enterprise and bespoke plans](/product-catalog/create-an-enterprise-plan)
* [Seat based pricing](/product-catalog/seat-based-pricing)
* [Usage-based pricing](/product-catalog/usage-based-billing)
* [Free plans and trials](/product-catalog/free-and-trials)
* [Location based pricing](/product-catalog/location-based-pricing)

### Free vs. Paid
Use the toggle in the top right to switch between a free or paid plan.

* **Free plan:** Users are not charged. Useful for lead magnets or open-source tiers.
* **Paid plan:** Unlocks the billing configuration options below.
### Billing cycles
Select the intervals you want to offer. You can enable multiple intervals for a single plan.

* **28 days:** A recurring four-week subscription.
* **Monthly / Yearly:** Standard recurring subscriptions.
* **3 Months:** Quarterly billing.
* **6 Months:** Billing twice a year.
* **One time:** A single non-recurring payment (lifetime deal).
The 28-day period is useful when you need a consistent four-week renewal schedule rather than a calendar-month renewal.
> [!TIP]
>
> To test a renewal without waiting a full month, ask support to enable daily billing for your sandbox. See [Test a subscription renewal](/guides/sandbox#test-a-subscription-renewal).
### Location based pricing
You can set specific prices for different countries rather than relying on auto-conversion.
1. Click **+ Add localized price**.
2. Select the currency (e.g. EUR, INR).
3. Enter the price for that market.
***
## Add deliverables
Configure what the customer receives immediately after purchase. These assets are delivered directly via the customer portal or email.
### Digital downloads
Upload files that are securely delivered after purchase.
1. Expand the **Digital downloads** accordion.
2. Drag and drop your files (up to 5GB total).
3. Customers will see these in their portal immediately after checkout.
### License keys
If you are selling software that requires activation, you can issue unique license keys.
1. Expand the **License keys** accordion.
2. **Activation limit:** Set how many machines or instances can use a single key (e.g. 1 for personal, 5 for team).
3. **License duration:** Define how long the key remains valid (e.g. 1 Year).
### Links
Provide access to resources hosted elsewhere.
1. Expand the **Links** accordion.
2. Add the **Link name** (e.g. Private Discord community).
3. Add the **Link URL**.
***
## Features & limits
Configure feature access, usage quotas, and API limits.
* **Toggle features:** Turn specific capabilities on or off (e.g. "SSO Access").
* **Numeric limits:** Set quotas (e.g. "5 Projects", "10,000 API calls").
***
## Publishing & Versioning
We protect your live customers by creating a separate version when you edit a plan.
### How it works
* **Safe editing**
When you edit a **Published** plan, we automatically create a new **Unpublished** version. Your live plan remains untouched so customers can keep buying without interruption.
* **Unpublished changes**
You will see a yellow badge indicating you are working on a draft version. You can make as many changes as you want here without affecting the live site.
* **Publishing**
When you click **Publish**, this new version replaces the old one.
### Pushing updates
Because the new version replaces the old one, you need to decide what happens to your current subscribers.
When you click **Publish**, we will ask you to confirm the rollout strategy.
* **For price changes:** You decide whether to grandfather existing users on the old price or migrate them to the new one.
* **For feature changes:** You decide whether to push the new entitlements to existing users immediately or keep them on their legacy configuration.
If you grandfather existing customers, they keep their current price and only new customers see the updated price. If you migrate existing customers, the new price applies to their future renewals.
Most recurring card and debit-card payments do not require the customer to approve the new price again. Some payment methods have additional rules. For example, an Indian UPI AutoPay mandate may require the customer to approve a renewal when the updated amount exceeds the applicable regulatory threshold.
If you change your mind while editing, you can click **Discard** to delete the unpublished version and revert to the live state.
### Plan actions
Click the **⋯** menu next to the **Publish** button (**Publish updates** on a plan with unpublished changes) to see more options.
| Action | Description |
| - | - |
| **Edit** | Open the plan configuration to make changes |
| **Get Link** | Copy the direct checkout link for this plan |
| **Country restrictions** | Limit which countries can purchase this plan |
| **Tax behavior** | Choose whether prices include tax or tax is added at checkout |
| **Delete** | Remove the plan |
| **Restore last published version** | Shown on a plan with unpublished changes. Discard unpublished changes and revert to the live version |
| **View published version** | Shown on a plan with unpublished changes. Preview what customers currently see on the live plan |
# Products
Source: https://docs.kelviq.com/product-catalog/products
Create and manage your high-level product containers
A product is the top-level container for what you sell. It groups together your pricing plans, features, and assets into a single family.
Before you can create plans or configure billing logic, you need to define the product they belong to.

## Create a product
Follow these steps to set up a new product.
**1. Go to Products**
Navigate to **Product Catalog > Products** in the sidebar.
**2. Start creation**
Click **+ Create product** in the top right corner.

**3. Enter details**
Fill out the basic information for your product.
* **Product name** — The public name shown to customers, such as “Pro Templates”.
* **Description** — A short summary of the offering.
* **Media** — Upload up to 10 images to showcase the product on checkout. We recommend a 4:3 aspect ratio (1600 × 1200 px).
Kelviq assigns an internal UUID when the product is created. You do not need to create or manage a separate product identifier in the dashboard.
**4. Finish**
Click **Create**. You will be redirected to the product details page.

## Next steps
Now that you have a product container, you can define the pricing plans and deliverables inside it.
# Proration
Source: https://docs.kelviq.com/product-catalog/proration
What a customer pays when they change plans in the middle of a billing period
When a customer changes plans mid-period, proration decides what they pay for the time left. Kelviq credits the unused part of the old plan and charges for the rest of the period on the new plan. You choose whether that difference is charged now, added to the next invoice, or skipped.
Both examples below use monthly plans, with the change made exactly halfway through the month.
## Upgrade: \$100 → \$200
The unused half of the old plan is worth a \$50 credit. The remaining half of the new plan costs \$100. The difference is \$50.
| Option | Today | Next renewal |
| - | - | - |
| **Immediate charge** | \$50 charged | \$200 |
| **Prorate on next invoice** | Nothing | \$200 + \$50 = \$250 |
| **No proration** | Nothing | \$200 |
## Downgrade: \$200 → \$100
The unused half of the old plan is worth a \$100 credit. The remaining half of the new plan costs \$50. The customer is owed \$50.
| Option | Today | Next renewal |
| - | - | - |
| **Immediate charge** | Nothing; \$50 is kept as account credit | \$100, less the \$50 credit = \$50 |
| **Prorate on next invoice** | Nothing | \$100 − \$50 = \$50 |
| **No proration** | Nothing | \$100 |
The new plan and its limits apply as soon as the change is made, whichever option you pick. Amounts depend on the exact time of the change.
## Set your defaults
Open **Settings → Subscription proration**. There are two defaults:
| Setting | Applies to | Default |
| - | - | - |
| **Plan upgrades & downgrades** | A customer moving to a different plan | Immediate charge |
| **Plan version migrations** | Moving customers to a new version of the same plan | Prorate on next invoice |

> [!WARNING]
>
> Switching between monthly and yearly billing always invoices immediately, whatever you choose here. The change starts a new billing period and sets the new renewal date. With **No proration**, unused time on the old interval isn't credited.
## Override per change
Pass `prorationBehavior` when you update a subscription through the API or SDK. It overrides the default for that change.
| Value | Dashboard name |
| - | - |
| `IMMEDIATE_CHARGE` | Immediate charge |
| `PRORATE_NEXT_INVOICE` | Prorate on next invoice |
| `NO_PRORATION` | No proration |
```ts theme={null}
import { CHARGE_PERIOD_CHOICES, PRORATION_BEHAVIORS } from '@kelviq/node-sdk';
await client.subscriptions.update({
subscriptionId: 'sub_123',
planIdentifier: 'pro',
chargePeriod: CHARGE_PERIOD_CHOICES.MONTHLY,
prorationBehavior: PRORATION_BEHAVIORS.PRORATE_NEXT_INVOICE,
});
```
To see the exact amounts before you commit, call `client.subscriptions.previewUpdate` with the same fields, or `POST /subscriptions/{subscriptionId}/update/preview/`.
To keep the old plan until the customer pays for the new one, see [Wait for payment before changing access](/product-catalog/subscriptions#wait-for-payment-before-changing-access).
# Seat based pricing
Source: https://docs.kelviq.com/product-catalog/seat-based-pricing
Charge teams for the number of seats they purchase
Seat based pricing ties the subscription price to the number of people who can use an account. A team that buys eight seats pays for eight, even if only six are occupied today.
This guide covers seats purchased in advance. The chosen quantity is included when you create checkout, and your application uses the resulting entitlement as its team-size limit.
> [!NOTE]
>
> If you want to bill for the number of seats that were actually active during the month, use [pay as you go](/product-catalog/pay-as-you-go) instead. Report the active-seat total with `SET` and let Kelviq bill it at the end of the period.
## How the price is calculated
A plan can consist entirely of seat charges or combine them with a fixed base price.
```text theme={null}
Monthly base price: $20
Price per seat: $12
Seats purchased: 8
Monthly subtotal: $20 + ($12 × 8) = $116
```
Kelviq carries the seat quantity from checkout into the subscription. The resulting entitlement tells your application how many seats the customer purchased.
## Create the seat feature
These steps create a Meter feature specifically for seats. For the general dashboard and API workflows, see [Create a feature in Kelviq](/product-catalog/create-a-feature).
1. Open **Product catalog → Features**.
2. Click **Create feature**.
3. Enter a clear name such as `Seats`.
4. Use a stable identifier such as `seats`.
5. Select **Meter** as the feature type.
6. Set the singular unit to `seat` and the plural unit to `seats`.
7. Create the feature.
Use a Meter feature rather than a Customizable feature. Kelviq can attach a price to the Meter feature and accept its quantity during checkout.
> [!NOTE]
>
> Purchased-seat pricing does not require usage reports. Although Seats is a Meter feature, its billable quantity comes from checkout and subscription updates.
## Add the seat price to a plan
1. Open the product and edit the plan you want to sell.
2. Make the plan **Paid** and select its billing periods, such as Monthly and Yearly.
3. Enter the plan's base price. Use this for the fixed part of the subscription, if any.
4. Open **Usage charges**.

5. Select the `seats` feature.
6. For **Billing model**, choose **Advance commitment**. The customer is purchasing the seats before using them.
7. For **Pricing model**, choose **Flat** for a standard per-seat price.
8. Enter the price per unit for every billing period. For example, use `$12` under Monthly and `$120` under Yearly.
9. Save the price and publish the plan.

The seat rate belongs under Usage charges even though this is not usage billed in arrears. **Advance commitment** is what makes the selected quantity an upfront seat purchase.
## Other ways to price seats
Most teams start with one price per seat. You can also sell seats in packages, lower the rate at higher quantities, or charge a fixed amount for each range.
| Pricing model | Seat calculation | Example |
| - | - | - |
| Flat | Every seat has the same price | 8 seats × \$12 = \$96 |
| Package | Seats are sold in fixed-size packs | \$50 for each block of 5 seats |
| Tiered | Each range of seats uses its own rate | Seats 1–10 at \$15, then seats 11–25 at \$12 |
| Volume | The final seat count sets the rate for every seat | 1–10 seats at \$15 each; 11+ seats at \$12 each |
| Stair-step | The seat count selects one fixed charge | \$100 for up to 10 seats; \$220 for up to 25 |
See [Usage-based pricing](/product-catalog/usage-based-billing) for the calculation rules for Package, Tiered, Volume, and Stair-step pricing.
## Send the seat count to checkout
Kelviq's pricing UI can show a quantity input for a priced Meter feature. If you build your own pricing page, send the selected seat count in the checkout request.
### Node.js
```ts theme={null}
import { CHARGE_PERIOD_CHOICES, Kelviq } from "@kelviq/node-sdk";
const client = new Kelviq({
accessToken: process.env.KELVIQ_SERVER_API_KEY!,
});
const seatCount = 8;
const session = await client.checkout.createSession({
planIdentifier: "team-plan",
chargePeriod: CHARGE_PERIOD_CHOICES.MONTHLY,
customerId: "customer_001",
successUrl: "https://app.example.com/billing/success",
features: [
{
identifier: "seats",
quantity: seatCount,
},
],
});
return Response.redirect(session.checkoutUrl);
```
### REST API
```bash theme={null}
curl --request POST \
--url https://api.kelviq.com/api/v1/checkout/ \
--header "Authorization: Bearer $KELVIQ_SERVER_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"planIdentifier": "team-plan",
"chargePeriod": "MONTHLY",
"customerId": "customer_001",
"successUrl": "https://app.example.com/billing/success",
"features": [
{
"identifier": "seats",
"quantity": 8
}
]
}'
```
The `features` array uses the feature identifier, not its dashboard UUID. `quantity` is the number of seats the customer wants to purchase.
> [!WARNING]
>
> Create the checkout session on your backend. Validate that the quantity is a positive whole number, and never expose the Server API Key in browser code.
See [Create a checkout session](/api-reference/checkout/create-a-checkout-session) for the full request schema.
## Enforce the purchased limit
After payment and subscription creation, read the customer's seat entitlement from your backend. For a Meter feature, `usageLimit` is the purchased capacity.
```ts theme={null}
const entitlement = await client.entitlements.getEntitlement({
customerId: "customer_001",
featureId: "seats",
});
const purchasedSeats = entitlement?.usageLimit ?? 0;
const activeMembers = await db.teamMember.count({
where: { teamId: "team_001", status: "active" },
});
if (activeMembers >= purchasedSeats) {
throw new Error("No seats available. Add another seat before inviting a member.");
}
```
Check the limit when the team owner sends or accepts an invitation. A frontend-only check is easy to bypass, so keep the final enforcement on your server.
`hasAccess` answers whether the customer can use the feature. It does not tell you whether one more team member fits. Compare your active member count with `usageLimit` for that decision.
## Change the seat count
Use the subscription update endpoint when a customer adds or removes seats. Send the complete new quantity, not the difference from the old quantity.
```ts theme={null}
import { CHARGE_PERIOD_CHOICES } from "@kelviq/node-sdk";
await client.subscriptions.update({
subscriptionId: "dffaf07e-4517-47db-ba3a-59a05aa2d465",
planIdentifier: "team-plan",
chargePeriod: CHARGE_PERIOD_CHOICES.MONTHLY,
features: [
{
identifier: "seats",
quantity: 12,
},
],
});
```
The update request still needs `planIdentifier` and `chargePeriod`, even when the plan is not changing. After the request succeeds, read the entitlement again before making the additional capacity available.
Before reducing a subscription below the current team size, ask the customer to remove or deactivate members. Otherwise, the subscription and your application's active-member count will disagree.
See [Update a subscription](/api-reference/subscriptions/update-a-subscription) for the complete API request.
## Purchased seats versus active seats
Choose one source of truth for billing.
| Model | Where the quantity comes from | When the customer pays |
| - | - | - |
| Purchased seats | Checkout and subscription updates | At the start of the billing period |
| Active seats | Usage reported by your backend | At the end of each monthly period |
For active-seat billing, configure the Seats feature as Pay as you go and send the current active-seat total with `SET`. Do not send both a purchased quantity and usage reports for the same seat charge, or the two models will conflict.
## Seats are not license activations
A seat normally represents a member of a team or workspace. A license activation represents a device or software installation.
Use seat based pricing when the customer pays for people who can join an account. Use [license keys](/product-catalog/license-keys) when you need to limit how many devices can activate downloaded software. A single customer may use both, but they should have separate limits.
## Test in sandbox
1. Switch the dashboard to Sandbox.
2. Create the Seats feature and a seat based plan there.
3. Create a checkout session with a small quantity, such as 3.
4. Confirm that checkout shows the expected base and seat charges.
5. Complete the test payment.
6. Read the `seats` entitlement and confirm that its limit matches the purchased quantity.
7. Update the subscription to 5 seats and check the entitlement again.
Sandbox and production use separate catalog data and API keys. See [Sandbox](/guides/sandbox) before moving the integration to production.
## Troubleshooting
### The total does not change with the seat count
Confirm that Seats is a Meter feature, that it has a price under Usage charges, and that the checkout request includes the exact `seats` identifier in `features`.
### The quantity input is missing
Publish the latest version of the plan and check that the seat feature is enabled and priced for the selected billing period.
### The entitlement has the wrong limit
Check that the checkout, customer, plan, and API key belong to the same environment. Then inspect the checkout or subscription request and confirm that `quantity` contains the complete desired seat count.
### A team can exceed its purchased seats
Kelviq stores the entitlement, but your application must enforce it when members are invited or activated. Compare the active-member count with `usageLimit` on your backend.
### A seat reduction would leave too many active members
Do not remove members automatically unless your product explicitly promises that behavior. Ask a team owner to choose which members lose access, then submit the lower quantity.
# Subscriptions
Source: https://docs.kelviq.com/product-catalog/subscriptions
Set up recurring billing and understand renewals and failed payments
Subscriptions are the default for traditional SaaS products. You charge customers on a regular schedule to maintain access to your application.
## Set up recurring billing

1. Open a draft plan and make sure it is set to **Paid**.
2. Under **Billing cycles**, select the recurring intervals you want to offer, such as **28 days**, **Monthly**, **3 Months**, **6 Months**, or **Yearly**.
3. Enter the price for each interval.
4. Click **Save**.
You can enable multiple intervals on one plan. Customers choose among the enabled intervals at checkout.
> [!NOTE]
>
> A 28-day subscription renews every four weeks. It is not the same as a calendar-month subscription. If the **28 days** option is not visible for your organization, contact Kelviq support to have it enabled.
## Subscription lifecycle
Kelviq handles recurring payments automatically. This includes:
* Charging the saved payment method at the end of each billing cycle
* Sending receipts and tax invoices
* Retrying failed card payments
* Applying prorated charges when a customer changes plans during a billing cycle
Check the current subscription status with the Kelviq SDK or API. If your application needs event notifications, use [webhooks](/guides/webhooks).
## Change a subscription
By default, plan upgrades and downgrades take effect immediately. As soon as the change is processed, Kelviq applies the new plan and its feature limits.
For a paid change, you can wait until payment succeeds before activating the new plan. Pass `paymentBehavior: "activate_on_payment"` when you update the subscription:
```json theme={null}
{
"planIdentifier": "premium-plan",
"chargePeriod": "MONTHLY",
"paymentBehavior": "activate_on_payment"
}
```
Omit `paymentBehavior` to keep the default immediate-update behavior.
### Control how a change is billed
Proration decides what the customer pays for the rest of the period: charged now (`IMMEDIATE_CHARGE`), added to the next invoice (`PRORATE_NEXT_INVOICE`), or skipped (`NO_PRORATION`). Set your defaults in **Settings → Subscription proration**, or pass `prorationBehavior` when you update a subscription.
[Proration](/product-catalog/proration) walks through each option with an upgrade and a downgrade example. The defaults are also available through the API as `updateSubscriptionProrationBehavior` and `migrateSubscriptionProrationBehavior` on `PATCH /organizations/settings/`.
### Wait for payment before changing access
`activate_on_payment` keeps the customer's current subscription active while payment for the change is pending. This prevents an unpaid plan change from replacing working access.
| Payment stage | Current subscription | Updated subscription |
| - | - | - |
| A saved card is charged successfully without customer action | Changes to `superseded` | Changes to `active` |
| UPI, a 3DS card, or another method requires customer action | Remains `active` | Starts as `incomplete` |
| The customer completes payment | Changes to `superseded` | Changes to `active` |
| The immediate charge fails, or the customer does not complete payment before expiry | Remains `active` | Changes to `incomplete_expired` |
Card payments that do not require authentication are usually completed during the update request, so the status change can happen immediately. When the payment method requires customer action, continue using the existing subscription until the updated subscription becomes `active`.
> [!WARNING]
>
> A successful update API response does not always mean the new plan is active. When you use `activate_on_payment`, check the subscription status or the customer's current entitlements before granting access to the new plan.
### Upgrades
Under the default `IMMEDIATE_CHARGE` proration behavior, Kelviq prorates the price change against the time left in the current billing period. It credits the unused portion of the old plan, charges for the remaining portion of the new plan, and collects the net difference. See [Control how a change is billed](#control-how-a-change-is-billed) for the other options.
With `activate_on_payment`, Kelviq waits for that payment to succeed before activating the upgraded plan.
For example, if a customer moves from a \$100 monthly plan to a \$200 monthly plan halfway through the period, the approximate calculation is:
| Item | Amount |
| - | -: |
| Unused half of the old plan | -\$50 credit |
| Remaining half of the new plan | \$100 charge |
| Net amount collected | \$50 |
The exact amount depends on when the upgrade is processed. Existing usage counters are retained and evaluated against the new plan's limits.
### Downgrades
Under the default payment behavior, downgrades also apply immediately. If the customer has unused value on the higher-priced plan, Kelviq converts it to account credit and applies that credit to future invoices.
> [!WARNING]
>
> An immediate downgrade can lower a customer's limits below their current usage. Check the customer's entitlements after the change and decide how your application should handle any over-limit state.
## Cancel a subscription
When cancelling, choose whether the subscription should end immediately or at the end of the current billing period. A plan downgrade and a subscription cancellation are separate actions: the downgrade follows the payment behavior you choose, while the cancellation follows the timing option you select.
## Pause a subscription
A paused subscription stops billing and stops access. The customer isn't charged, their entitlements return `hasAccess: false`, and plan changes are blocked until the subscription resumes. Cancelling stays available.
Customers pause from the [customer portal](/customer-portal/overview) when a [retention offer](/customer-portal/retention) includes a pause. They choose 1, 3, or 6 months.
A pause has to cover at least one full billing period, so a 1-month pause on a yearly plan is refused. Only an `active` subscription can be paused, not one on trial, past due, already paused, or scheduled to cancel. Free and manually billed subscriptions can't be paused.
### Resume a subscription
A paused subscription resumes on its own at the end of the pause. Choose what happens on resume in **Settings → Retention → Advanced settings**:
| | Reset billing cycle (default) | Keep billing day |
| - | - | - |
| Charged on resume | A full period | Nothing |
| Next charge | One period after the resume date | The original billing day |
| Billing day afterwards | The resume date | Unchanged |
For a monthly plan billed on the 1st, paused on 10 September and resumed on 20 October, reset charges a full month on 20 October and bills on the 20th from then on. Keep billing day charges nothing and bills on 1 November as before, which gives the customer free access from the 20th to the 1st. A customer who resumes inside a period they've already paid for is never charged again, whichever behavior you pick.
On resume, entitlements come back with usage reset to zero and a fresh period.
### What changes while paused
The subscription has `status: "paused"`, `pausedAt`, and `pauseResumesAt` (`null` for an open-ended pause). Kelviq sends `subscription.paused` and `subscription.resumed` [webhooks](/guides/webhooks), whichever surface the change came from. Webhook payloads use snake\_case, so the same fields arrive there as `paused_at` and `pause_resumes_at`. Renewals that would fall inside the pause are skipped silently, so a paused subscription sends no other events until it resumes.
If you cache entitlement checks, clear them when you receive `subscription.paused`, otherwise the customer keeps access until the cache expires. Both SDKs have a call for it: `client.entitlements.invalidateCache({ customerId })` in Node and `client.entitlements.invalidate_cache(customerId=...)` in Python.
### Retention offers
Before a customer cancels, you can offer them a reason to stay. In the dashboard retention settings, switch retention on and map each cancellation reason to an offer:
* **Discount**: a percentage off for a set number of billing cycles, applied as a single-use coupon.
* **Pause**: a break of one of the lengths you allow. Billing and access stop, then resume on a renewal boundary.
* **Support**: a link to a booking page or support form, for reasons a conversation can fix.
The customer portal shows these offers automatically. To build the same flow in your own app, call [Get retention offers](/api-reference/subscriptions/get-retention-offers) for the subscription. It returns every cancellation reason with its configured offer and whether this subscription is eligible for it right now, so you can show an unavailable offer greyed out with a reason. When the customer takes an offer, call [Accept a retention offer](/api-reference/subscriptions/accept-a-retention-offer) with the reason and the offer. If they cancel anyway, call [Cancel a subscription](/api-reference/subscriptions/cancel-a-subscription) with the same reason as `cancellationFeedback`.
> [!NOTE]
>
> An offer must be the one configured for the reason the customer picked, and eligibility is checked again when it is accepted. The repeat policy you set decides whether a customer who has already taken an offer can be offered it again, so discounts cannot be stacked by reopening the cancellation flow.
## Refund a cancelled subscription
Kelviq does not automatically refund unused time when a subscription is cancelled mid-period. You decide whether to issue a full refund, a partial refund, or no refund.
For a usage-based subscription, calculate the refund under your own policy using the customer's consumed usage and remaining time. Kelviq does not currently calculate usage-adjusted refunds for you. Once you decide the amount, open the corresponding order in the dashboard and issue the full or partial refund there.
## Failed subscription payments
A failed automatic card payment does not cancel the customer's payment authorization. Kelviq can use the same authorization for later attempts and future billing cycles.
If the customer pays the failed card invoice manually, automatic payments remain active. Kelviq will still use the saved payment method for the next billing cycle.
### Card payment retries
When an automatic card payment fails, the subscription moves to `past_due`. Kelviq retries the payment up to eight times over the next 14 days. If recovery is unsuccessful, Kelviq cancels the subscription.
After each failed attempt, Kelviq emails the customer a link to pay the invoice. The customer can also use the [Customer Portal](/customer-portal/managing-payment-methods) to update their payment method.
You do not need to schedule retries, send card payment links, or ask the customer to set up automatic payments again. Kelviq handles this recovery flow.
> [!NOTE]
>
> This retry schedule applies only to cards. Kelviq does not retry a failed non-card renewal or send a recovery payment link. The subscription is canceled after the payment fails.
# Tax settings
Source: https://docs.kelviq.com/product-catalog/tax-settings
Configure price display rules and B2B exemptions
You manage your tax configuration in two places. You set how prices display on individual plans and you set how business exemptions are handled globally.
## Plan tax behavior
By default Kelviq adds tax on top of your listed price at checkout. If you want customers to see the final price upfront you can enable tax inclusive pricing. You configure this per plan.

To change how prices are displayed open your plan draft. Click the menu icon next to the Publish button and select Tax behavior.

Toggle Include tax in prices to turn on inclusive pricing. Kelviq will ask you to select the specific currencies where the listed price should include tax.

If the plan has only one currency, only that currency is listed. This lets you mix billing styles on a single plan. You can select EUR to give European buyers an inclusive experience while leaving USD unchecked so North American buyers pay tax at checkout.
## Business tax ID behavior
When you sell to other businesses they are often exempt from paying sales tax or VAT. Kelviq handles this verification automatically. If a business enters a valid tax ID at checkout we apply a reverse charge and drop the tax requirement.
If you enabled tax inclusive pricing in the steps above you need to tell Kelviq how to handle the final math for these exempt B2B customers.
Open **Settings → Tax settings**.

Turn on the Business tax ID behavior toggle. When this is active Kelviq automatically reduces the checkout total by the included tax amount if the customer enters a valid business tax ID.
If you leave this disabled the checkout total remains the exact same and we attribute the extra margin directly to your account.
# Usage alerts
Source: https://docs.kelviq.com/product-catalog/usage-alerts
Configure usage thresholds and receive feature.usage_alert webhooks
Usage alerts tell your application when a customer's Meter feature crosses a threshold. Use them with any Meter feature, such as API calls, credits, tokens, or storage.
> [!NOTE]
>
> A usage alert has two parts. You configure the threshold on the plan, then receive the `feature.usage_alert` event through a webhook. Turning on the alert does not email the customer or show an in-app message by itself. Your webhook handler decides what happens next.
## How usage alerts work
1. Your application reports usage to Kelviq with `SET` or `DELTA` behavior.
2. Kelviq updates the customer's meter for the current usage period.
3. When current usage crosses a configured threshold, Kelviq creates a `feature.usage_alert` event.
4. Kelviq sends the event to webhook endpoints subscribed to that event type.
5. Your application can send an email, show an in-app warning, notify a support team, or offer an upgrade.
Usage alerts do not stop usage, change a price, or charge the customer. Use a hard limit to stop usage at zero balance, and configure usage pricing when customers should pay for additional consumption.
## Choose a threshold type
Kelviq supports percentage and absolute-unit thresholds.
| Dashboard option | API value | When it triggers | Valid values |
| - | - | - | - |
| Percentage | `PERCENTAGE` | Current usage reaches a percentage of the feature's usage limit | 1–100 |
| Balance | `BALANCE` | Current usage reaches an absolute number of feature units | Any number greater than or equal to 1 |
Suppose a plan includes 10,000 API calls each month:
* A 75% percentage threshold is crossed when current usage reaches 7,500 calls.
* A balance threshold of 7,500 is crossed at the same usage count, but it is stored as an absolute number of calls rather than a percentage.
Percentage thresholds are easier to reuse across plans with different limits. Balance thresholds are useful when the absolute number matters, such as 10,000 API calls or 1,000 AI credits.
You can add more than one threshold. For example, use 75% for an early warning and 90% for a stronger upgrade or top-up prompt.
## Configure usage alerts in the dashboard
1. Open **Product Catalog → Products** and select the product.
2. Open the plan that contains the Meter feature.
3. Edit **Usage charges** or open **Manage features & limits**.
4. Select the Meter feature and enter its usage value or pricing configuration.
5. Turn on **Usage alert**.
6. Choose **Percentage** or **Balance** as the threshold type.
7. Click **Add threshold** and enter each threshold.
8. Save the configuration. If Kelviq creates a draft version of a published plan, publish the draft before testing it.

If the Usage alert switch is disabled while managing an entitlement, make sure you selected a Meter feature and entered a value. Percentage alerts also need a finite usage limit so Kelviq can calculate the percentage used.
> [!NOTE]
>
> Regional prices inherit usage alerts from the base price. Change the base price when the same alert configuration should apply to every regional price.
## Configure usage alerts through the API
To update one plan entitlement, send a `PATCH` request to:
```text theme={null}
/api/v1/catalog/plans/{plan_identifier}/features/{feature_identifier}/
```
`feature_identifier` is the feature's identifier, such as `api-calls`, not its UUID.
```bash theme={null}
curl --request PATCH \
--url https://api.kelviq.com/api/v1/catalog/plans/pro/features/api-calls/ \
--header "Authorization: Bearer $KELVIQ_SERVER_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"details": {
"value": 10000,
"hasUnlimitedUsage": false,
"reset": "EVERY_MONTH",
"resetTime": "BEGINNING_OF_PERIOD",
"usageAlerts": {
"enabled": true,
"thresholds": [75, 90],
"thresholdType": "PERCENTAGE"
},
"hardLimit": false
}
}'
```
The `usageAlerts` object contains:
| Field | Type | Description |
| - | - | - |
| `enabled` | boolean | Turns alerts on or off for the entitlement |
| `thresholds` | number\[] | Percentage or unit values to watch |
| `thresholdType` | string | `PERCENTAGE` or `BALANCE` |
Updating a published plan creates a new draft version. Publish that draft before expecting the new thresholds to apply. See [Update plan entitlement](/api-reference/plan-entitlements/update-plan-entitlement) for the complete request schema.
## Create the webhook endpoint
The threshold configuration tells Kelviq when to create an alert. The webhook endpoint is how your application receives it.
1. Open **Settings → Webhooks** in the Kelviq dashboard.
2. Create an endpoint with a public HTTPS URL.
3. Subscribe the endpoint to `feature.usage_alert`.
4. Save the signing secret. Kelviq shows it once.
5. Verify every delivery before reading its data.
The webhook endpoint and usage reports must use the same environment. A production webhook does not receive alerts created from sandbox usage, and a sandbox webhook does not receive production alerts.
See the [Webhooks guide](/guides/webhooks) for signature verification, retries, delivery logs, and manual resends.
## Usage alert payload
The event identifies the customer, feature, usage limit, current usage, threshold, subscription, and usage period.
```json theme={null}
{
"id": "575a1aa4-3414-4e08-b4ad-6bfd3e96322d",
"type": "feature.usage_alert",
"created_at": "2026-05-18T18:48:59Z",
"data": {
"object": {
"id": "a1c88c9d-19cf-48cc-b194-6cc6ebb356ee",
"object": "feature_usage_alert",
"feature": {
"id": "245d745f-28ca-4856-81fe-1e1f5c6c7ebd",
"name": "API calls",
"identifier": "api-calls",
"feature_type": "METER"
},
"customer": {
"id": "d2b6c9ac-bcd5-4e59-8c7e-b6472485e112",
"name": "Geo",
"email": "geo@example.com",
"customer_id": "customer-123"
},
"usage_limit": 10000,
"current_usage": 9000,
"threshold_type": "PERCENTAGE",
"threshold_value": 90,
"period_start_time": "2026-05-01T00:00:00Z",
"period_end_time": "2026-06-01T00:00:00Z",
"subscription_id": "137e4652-5540-4a3b-9bce-fa393b028539",
"organization": "2a3664c0-cbd2-4f1c-a98f-1b4f65e3256c"
}
}
}
```
The fields most alert handlers use are:
| Field | Use |
| - | - |
| Top-level `id` | Idempotency key for deduplicating webhook deliveries |
| `feature.identifier` | Maps the alert to a feature in your application |
| `customer.customer_id` | Maps the alert to your own user or account |
| `customer.email` | Address for a customer notification, if you choose to send one |
| `usage_limit` | Configured usage allowance |
| `current_usage` | Usage recorded when the event was created |
| `threshold_type` | Whether `threshold_value` is a percentage or absolute unit count |
| `threshold_value` | The configured threshold that was crossed |
| `period_start_time`, `period_end_time` | Usage period associated with the alert |
| `subscription_id` | Subscription that owns the entitlement |
## Handle `feature.usage_alert`
This Express example verifies the request and sends the alert to an application queue. The queue can handle email or in-app notifications outside the webhook request.
```typescript theme={null}
import express from "express";
import {
validateEvent,
WebhookVerificationError,
} from "@kelviq/node-sdk";
const app = express();
app.post(
"/webhooks/kelviq",
express.raw({ type: "application/json" }),
async (req, res) => {
try {
const event = validateEvent(
req.body,
req.headers,
process.env.KELVIQ_WEBHOOK_SECRET!,
);
if (event.type === "feature.usage_alert") {
const alert = event.data.object;
// `notificationQueue` is your own queue or background-job service.
await notificationQueue.add("usage-alert", {
eventId: event.id,
customerId: alert.customer.customer_id,
email: alert.customer.email,
featureId: alert.feature.identifier,
currentUsage: alert.current_usage,
usageLimit: alert.usage_limit,
thresholdType: alert.threshold_type,
thresholdValue: alert.threshold_value,
});
}
return res.sendStatus(202);
} catch (error) {
if (error instanceof WebhookVerificationError) {
return res.sendStatus(403);
}
return res.sendStatus(500);
}
},
);
```
Kelviq may deliver the same event more than once because of automatic retries or a manual resend. Store the top-level event `id` or `webhook-id` header before enqueueing work, and skip the event if you already processed it.
Return a `2xx` response quickly. Do not wait for an email provider or another slow service inside the webhook request.
## Test an alert in sandbox
1. Create a sandbox webhook endpoint subscribed to `feature.usage_alert`.
2. Configure a low threshold on a sandbox plan, such as 1 unit or 1%.
3. Report enough usage to cross the threshold.
4. Open **Settings → Webhooks**, select the endpoint, and inspect the delivery log.
5. Confirm the event's customer, feature, threshold, and usage period.
For example, report a usage increment with the Node SDK:
```typescript theme={null}
import { BEHAVIOUR_CHOICES, Kelviq } from "@kelviq/node-sdk";
const kelviq = new Kelviq({
accessToken: process.env.KELVIQ_SANDBOX_SERVER_API_KEY!,
environment: "sandbox",
});
await kelviq.reporting.reportUsage({
customerId: "customer-123",
featureId: "api-calls",
value: 2,
behaviour: BEHAVIOUR_CHOICES.DELTA,
});
```
If delivery fails, fix the endpoint and use **Resend** from the delivery log. Keep the handler idempotent because a resend can deliver the same event again.
## Troubleshooting
| Problem | What to check |
| - | - |
| The Usage alert switch is disabled | Confirm that the feature is a meter and has a configured value |
| No webhook arrives | Confirm the endpoint subscribes to `feature.usage_alert`, the plan version is published, and usage crossed a configured threshold |
| Sandbox testing produces no production delivery | Make sure usage reports, plan configuration, API key, and webhook all belong to the same environment |
| Percentage threshold does not match expectations | Compare `current_usage` with `usage_limit`; percentage thresholds measure usage against that limit |
| The handler runs twice | Deduplicate using the event `id` or `webhook-id`; retries and manual resends can duplicate a delivery |
| Customers receive no email | Kelviq emits the webhook event; your application must send the email or in-app notification |
# Usage-based pricing
Source: https://docs.kelviq.com/product-catalog/usage-based-billing
Charge customers for the actual work your product performs
Usage-based pricing charges customers for what they use: API calls, tokens, compute, or any other Meter feature.
## Set up a usage plan
The example below is a **Pro** plan: \$29 a month, 10,000 API calls included, then \$0.002 per call. [How usage limits work](/product-catalog/usage-limits) explains what happens at the limit.
**Dashboard**
Open the plan's pricing and expand **Usage charges**, then follow the steps below.

**CLI**
Define the plan in `kelviq.config.ts`. The entitlement sets the included 10,000 calls. The Pay as you go price bills calls past that at the end of each month.
```ts kelviq.config.ts theme={null}
import { feature, plan } from '@kelviq/cli/config';
export const apiCalls = feature({
identifier: 'api-calls',
name: 'API calls',
type: 'METER',
featureDetails: {
featureSubType: 'PRE_AGGREGATED_USAGE',
units: { singular: 'call', plural: 'calls' },
},
});
export const pro = plan({
identifier: 'pro',
name: 'Pro',
product: 'my-app',
entitlements: [
{ feature: 'api-calls', value: 10000, reset: 'EVERY_MONTH', hardLimit: false },
],
prices: [
{
priceType: 'PAID',
currency: 'USD',
chargeCatalogPrice: [
{
feature: null,
priceModel: 'FLAT',
paymentType: 'ADVANCE_COMMITMENT',
reset: 'NEVER',
charges: [{ chargePeriod: 'MONTHLY', priceData: { amount: 29 } }],
},
{
feature: 'api-calls',
priceModel: 'TIERED',
paymentType: 'PAY_AS_YOU_GO',
reset: 'EVERY_MONTH',
charges: [
{
chargePeriod: 'MONTHLY',
priceData: { currency: 'USD' },
tiers: [
{ upTo: 10000, unitAmount: 0, flatAmount: 0 },
{ upTo: 'inf', unitAmount: 0.002, flatAmount: 0 },
],
},
],
},
],
},
],
});
```
Preview the change, then push and publish it:
```bash theme={null}
npx kelviq push --dry-run
npx kelviq push --publish
```
See the [CLI overview](/cli/overview) and the [config reference](/cli/kelviq-config).
### 1. Select or create a Meter feature
Choose the metric you want to track from the dropdown list, such as API requests or tokens consumed. If the list is empty, [create a Meter feature](/product-catalog/create-a-feature) from **Product catalog → Features**, then return to the plan.

### 2. Choose your billing model
Select how payments are structured.

**Advance commitment**
Customers pay upfront for a baseline allotment of usage. This gives your users a predictable budget. They buy a set bucket of AI tokens or API requests at the start of the month. They consume what they paid for and avoid surprise overages.
**Pay as you go**
Customers pay for their recorded usage at the end of the monthly subscription period. You can bill for API calls, tokens, storage, compute time, or another Meter feature, either on its own or alongside a monthly base price. See [Pay as you go](/product-catalog/pay-as-you-go) for setup, usage reporting, sandbox testing, and troubleshooting.
> [!NOTE]
>
> Monthly must be the plan's only billing period. Pay as you go does not support Stair-step pricing or credit rollover.
### 3. Configure the pricing model
Kelviq supports five distinct ways to calculate the cost. Here is how the math works for each model.

***
#### Flat
The simplest way to charge for usage. Set one fixed rate for every unit consumed. No tiers, packages, or volume discounts. The total scales directly with usage.
**Best for** API products and services where the cost of serving each unit stays relatively consistent.
**Example** A translation API charging for each API call.
**Sample calculation**
| | |
| - | - |
| Rate | \$0.01 per API call |
| Usage | 15,000 API calls |
| **Total** | 15,000 × \$0.01 = **\$150** |

***
#### Package
Instead of charging for every unit individually you sell usage in predefined packages. When a customer exceeds one package they are charged for the next full package, even if they only use part of it.
**Best for** Products that want simple pricing, predictable billing increments, and a minimum charge for reserved usage.
**Example** An AI image generator selling images in packages of 100.
**Sample calculation**
| | |
| - | - |
| Package | 100 images for \$10 |
| Usage | 205 images |
| Packages required | 3 |
| **Total** | 3 × \$10 = **\$30** |

***
#### Tiered
Units are filled into pricing buckets. As usage increases it moves into cheaper brackets, while units in earlier brackets remain charged at their original rate.
**Why it's useful** Encourages higher adoption while helping you cover fixed costs for lower-volume users. Especially useful for developer platforms.
**Example** An LLM wrapper platform charging based on token usage.
**Sample calculation**
| Tier | Rate |
| - | - |
| First 1,000 tokens | \$0.10 / token |
| Next 4,000 tokens | \$0.05 / token |
A customer uses 3,500 tokens.
| | |
| - | - |
| Tier 1 | 1,000 × \$0.10 = \$100 |
| Tier 2 | 2,500 × \$0.05 = \$125 |
| **Total** | \$100 + \$125 = **\$225** |
Kelviq tracks each pricing bracket automatically.

***
#### Volume
Similar to tiered pricing, but with one major difference. Once usage reaches a specific threshold, every unit consumed during that billing cycle is charged at the new rate.
**Why it's useful** Creates a strong incentive for high-volume customers to expand their usage by rewarding them with a lower rate across their entire bill.
**Example** An API platform charging based on the number of requests processed.
**Sample calculation**
| Volume | Rate |
| - | - |
| 1 to 100,000 API calls | \$0.01 / call |
| 100,001+ API calls | \$0.007 / call (applies to all calls) |
A customer makes 120,000 API calls.
| | |
| - | - |
| **Total** | 120,000 × \$0.007 = **\$840** |
| Without the volume discount | 120,000 × \$0.01 = \$1,200 |
Kelviq automatically applies the correct rate when customers cross a volume threshold, with no custom billing logic required.

***
#### Stair-step
Customers pay a fixed fee based on the usage bracket they fall into, regardless of the exact number of units used within that bracket. There is no per-unit rate.
**Why it's useful** Combines the simplicity of subscription plans with the flexibility of usage-based scaling.
**Example** An email delivery platform charging based on monthly email volume.
**Sample calculation**
| Step | Price |
| - | - |
| Up to 10,000 emails | Flat \$50 |
| 10,001 to 50,000 emails | Flat \$150 |
A customer sends 12,000 emails. They fall into the second step and pay a flat **\$150**.
Kelviq automatically moves customers into the correct pricing step as their usage grows, with no custom billing logic required.

***
### 4. Set the pricing rules
Once you pick a pricing model you define the exact boundaries.
If you select Tiered you specify the first unit and last unit for each bucket. You can charge a per unit price and a flat fee within that same tier.
You can also set localized pricing for usage-based pricing. This lets you define different per unit rates for specific countries and currencies.
#### Regional pricing and usage controls
When you add a regional price, Kelviq carries over the base price's usage reset, usage alert, and credit rollover settings. This keeps entitlement behavior consistent while allowing the monetary price and currency to vary by region.
Update these controls on the base price when you want the same behavior across its regional prices.
### 5. Set the usage reset cadence
Configure how often the usage meter resets. Credit rollover is calculated when this reset happens.
Use **Never** for a lifetime limit, or choose a recurring reset such as every week or every month. A meter set to **Never** has no period boundary at which to roll unused credits forward.
For monthly resets, choose either the first day of the month or the customer's subscription start date.
### 6. Configure rollover and alerts
#### Credit rollover
Credit rollover lets a customer keep part of an advance commitment they did not use before the meter resets. It is available for **Advance commitment** usage prices, not **Pay as you go** prices.
To configure it:
1. In the plan editor, expand **Usage charges** and open the Meter feature.
2. Choose **Advance commitment** as the billing model.
3. Set the included **Value** and a recurring **Usage reset**.
4. Turn on **Credit rollover**.
5. Enter the **Percentage** of unused units to carry forward. The value can be from 0% to 100%.
6. Enter a **Limit** in units to cap the rollover amount.

For example, suppose a plan includes 10,000 credits each month. The customer has 2,000 credits left when the meter resets, and rollover is set to 50% with a 750-unit limit. Fifty percent of the unused balance is 1,000 credits, so the limit reduces the rollover to 750. The customer's next period starts with 10,750 credits.
> [!NOTE]
>
> The rollover limit and the usage hard limit do different jobs. The rollover limit caps the credits carried into a new period. The hard limit stops additional usage when the customer's balance reaches zero.
#### Usage alerts
Usage alerts watch a Meter feature for percentage or absolute-unit thresholds. When a customer crosses a threshold, Kelviq sends `feature.usage_alert` to your subscribed webhook endpoint. Your application can then send an email, show an in-app warning, or start an upgrade flow.
See [Usage alerts](/product-catalog/usage-alerts) for dashboard and API configuration, the webhook payload, a Node.js handler, and sandbox testing steps.
### 7. Stack multiple usage metrics

AI products rarely track just one variable. You can add as many metered usage features as you want to a single plan. Just click the Add usage pricing button at the bottom of the section to set up another metric.
## Check access and report usage
Report usage from your backend each time the feature is used, then check what's left before the next request.
```ts Node theme={null}
import { BEHAVIOUR_CHOICES } from '@kelviq/node-sdk';
// Count one API call
await client.reporting.reportUsage({
customerId: 'cust_123',
featureId: 'api-calls',
value: 1,
behaviour: BEHAVIOUR_CHOICES.DELTA,
});
// Check what's left
const ent = await client.entitlements.getEntitlement({
customerId: 'cust_123',
featureId: 'api-calls',
});
if (!ent || (!ent.hasAccess && ent.hardLimit)) {
// No access, or a hard limit was reached: block or ask the customer to upgrade
}
// Past a soft limit, let the call through: it's billed as overage
```
```python Python theme={null}
# Count one API call
client.reporting.report_usage(
customerId="cust_123",
featureId="api-calls",
value=1,
behaviour="DELTA",
)
# Check what's left
ent = client.entitlements.get_entitlement(
customerId="cust_123",
featureId="api-calls",
)
if not ent or (not ent.hasAccess and ent.hardLimit):
# No access, or a hard limit was reached: block or ask the customer to upgrade
...
# Past a soft limit, let the call through: it's billed as overage
```
`remaining` is the usage left in the current period. `hasAccess` turns `false` when it reaches 0; `hardLimit` tells you whether to block. See the [Node SDK](/backend-integration/node-sdk#entitlements) and [Python SDK](/backend-integration/python-sdk) for every method.
## Model a plan with different kinds of limits
Not every plan limit should be a meter. Choose the feature type based on what the value means and how your application will use it.
| Plan capability | Feature type | Suggested configuration |
| - | - | - |
| Messages | Meter | Reset every month |
| Training runs | Meter | Reset every day |
| Active agents | Customizable | Store the plan's allowed count |
| Knowledge sources | Customizable | Store the plan's allowed count |
| WhatsApp access | Boolean | Enable on the plans that include it |
| Analytics | Boolean | Enable on the plans that include it |
| Priority support | Boolean | Enable on the plans that include it |
For metered limits, consider usage alerts at 80% and 90%. These thresholds give customers time to reduce usage or upgrade before they reach a hard limit. Rollover and overage are optional; turn them on only where they fit your pricing. See [How usage limits work](/product-catalog/usage-limits).
When a customer changes plans, Kelviq retains their existing usage counters and evaluates them against the new limits. A customer who has already consumed more than the downgraded limit may therefore become over limit as soon as the downgrade takes effect.
# How usage limits work
Source: https://docs.kelviq.com/product-catalog/usage-limits
What a limit is, when usage resets, what happens when a customer reaches the limit, and how overage is billed
A usage limit is the amount of a Meter feature a plan includes, such as 10,000 API calls a month. Your app reports usage, Kelviq counts it against the limit, and your app checks what's left before letting the customer use more.
This page follows one example: the **Pro** plan of an AI app costs \$29 a month and includes 10,000 `api-calls`, reset monthly.
## The terms
| Term | What it means |
| - | - |
| **Base fee** | The fixed price of the plan, charged each billing period whatever the usage. \$29 a month in the example. |
| **Meter feature** | A feature Kelviq counts, such as API calls, tokens, credits, or seats. |
| **Usage limit** | How much of a Meter feature the plan includes. Can be a number or unlimited. |
| **Current usage** | How much the customer has used since the last reset. |
| **Remaining** | Usage limit minus current usage. |
| **Reset** | When current usage goes back to 0. |
| **Hard limit** | Usage is blocked once the limit is reached. |
| **Soft limit** | Usage can go past the limit. Your app decides what to do, such as warn the customer or bill overage. |
| **Overage** | Usage past the included limit, billed at a per-unit rate. |
| **Customizable feature** | A number or setting per plan, such as max upload size or number of projects. Kelviq stores the value; it doesn't count usage against it. |
## How usage is counted
Your backend reports usage for a customer and a Meter feature:
* `DELTA` adds to current usage. Send it each time something is used: one API call, 1,200 tokens.
* `SET` replaces current usage. Use it for numbers you already track, such as seats in use.
In the example, after 6,500 calls the customer's entitlement looks like this:
```json theme={null}
{
"featureId": "api-calls",
"featureType": "METER",
"hasAccess": true,
"hardLimit": true,
"usageLimit": 10000,
"currentUsage": 6500,
"remaining": 3500
}
```
See [Features & limits](/product-catalog/entitlements#sdk-integration) for the SDK calls.
## When usage resets
Each Meter feature on a plan has a **usage reset**: never, or a recurring period such as every day, month, quarter, or year. For monthly resets you choose the first day of the month or the customer's subscription start date.
A reset of **Never** makes the limit a lifetime allowance. Credit packs work this way.
## What happens at the limit
For a Meter feature, `hasAccess` is `true` while `remaining` is above 0 or the limit is unlimited.
* **Hard limit:** at 10,000 calls, `hasAccess` turns `false` and additional usage is stopped until the next reset or an upgrade.
* **Soft limit:** usage keeps counting past 10,000. `hasAccess` is also `false` here, so check `hardLimit` before blocking. If the plan has an overage price, let the customer continue and the extra usage is billed.
Use [usage alerts](/product-catalog/usage-alerts) to warn customers before they get there, for example at 80% and 90%.
## Overage
To charge for usage past the included amount, add a **Pay as you go** usage price to the plan, for example a Tiered price whose first 10,000 calls are free and each call after that costs \$0.002. Kelviq bills the overage at the end of each monthly period from the usage you report. See [Pay as you go](/product-catalog/pay-as-you-go).
## Rollover
With an **Advance commitment** price, part of the unused allowance can carry into the next period. In the example, with 50% rollover capped at 750, a customer who has 2,000 calls left at the reset starts the next month with 10,750. See [credit rollover](/product-catalog/usage-based-billing#credit-rollover).
## Plan changes
When a customer upgrades or downgrades, the new plan's limits apply immediately and current usage is kept. A customer who used 6,500 calls and downgrades to a plan with 5,000 is over the limit straight away.
## Change one customer's limit
From the customer's page in the dashboard, you can override a limit for that customer or reset their usage to 0. Plan limits don't change. See [Managing customer entitlements](/product-catalog/entitlements#managing-customer-entitlements).
## If Kelviq can't be reached
The Node and Python SDKs return the last known entitlement when the API is unreachable, and queue usage reports to retry later. See [When the Entitlement API is unavailable](/product-catalog/entitlements#when-the-entitlement-api-is-unavailable).
# Create a promotion
Source: https://docs.kelviq.com/promotions/create-a-promotion
Set up purchasing power parity discounts, country overrides, and security enforcement
A promotion ties a set of discounts to one or more of your products and plans. Once created, eligible visitors will see their local discount automatically.
**1. Navigate to Promotions**
Go to **Promotions** in the sidebar, then click **+ Create promotion**.
**2. Enter a promotion name**
Give the promotion a descriptive internal name (e.g. *PPP pricing* or *Black Friday*). This is not shown to customers.
The name field is required — you cannot create a promotion without one.
**3. Select products and plans**
Choose which products and plans this promotion applies to. You can select multiple products, and for each product you can pick one or more plans.
> [!NOTE]
>
> Only **Published** plans appear in the selector. If a plan is missing, open the product in **Product catalog → Products**, then publish the plan.
At least one plan must be selected before you can create the promotion.
**4. Set up PPP group discounts**
Kelviq groups countries into economic tiers based on purchasing power. Enter a discount percentage for each group you want to target.
* Enter a value between 1–100 in the **Discount %** field for the relevant group.
* Leave the field empty (or set it to 0) for groups you do not want to discount.
You can see which countries belong to each group by expanding the group row.
**5. Add country-specific overrides (optional)**
To override the group discount for a specific country, use the **Country-specific discounts** section.
1. Search for the country by name.
2. Click the country to add it to the list.
3. Enter the exact discount percentage for that country.
Country overrides take precedence over the group discount. For example, if India is in a group with 30% off but you want to offer 40% for India specifically, add India as a country override with 40%.
**6. Configure security & enforcement**
Security toggles prevent abuse by blocking users who are masking or misrepresenting their location. All toggles are **enabled by default** — disable only if you have a specific reason to.
| Toggle | Description |
| - | - |
| **VPN protection** | Block users connecting through a VPN |
| **Tor Browser protection** | Block anonymous traffic routed via Tor |
| **Proxy server protection** | Block known proxy IP addresses |
| **Apple Relay protection** | Block users using Apple Private Relay |
| **Strict country matching** | Require the billing address country to match the discount country |
| **Crawler protection** | Block bots from accessing localized pricing |
**7. Create the promotion**
Click **Create promotion**. You will be taken to the promotion detail page where you can integrate the banner and customize its appearance.
## Editing a promotion
To update discounts, products, or security settings after creation, open the promotion and go to the **Edit rules** tab. Changes take effect immediately.
# Customize the banner
Source: https://docs.kelviq.com/promotions/customize-banner
Adjust the appearance and behavior of the Promotions UI banner to match your brand
Open a promotion and go to the **Customize banner** tab to control how the discount banner looks and behaves on your site.
## Appearance
* **Country message** — the text displayed to visitors. Use `{discount}` as a placeholder for the discount percentage and `{country}` for the visitor's country name.
Example: *"You qualify for a % discount in ."*
* **Background color** — the banner's background color. Accepts any hex value.
* **Background transparency** — opacity of the background, from 0 (fully transparent) to 100 (fully opaque).
* **Font color** — the main text color.
* **Highlight font color** — the color used for emphasized text (e.g. the discount percentage).
* **Font size** — text size in pixels.
* **Border radius** — corner rounding of the banner container, in pixels.
## Behavior
* **Placement** — choose whether the banner appears at the **top** or **bottom** of the viewport.
* **Close icon** — when enabled, a close button lets visitors dismiss the banner.
* **Unstyled mode** — when enabled, all styling controls are disabled and the banner renders without any Kelviq styles. Use this if you want to apply your own CSS from scratch.
> [!NOTE]
>
> Enabling unstyled mode will override all appearance settings above. You will need to style the banner entirely with your own CSS.
## Save changes
Click **Update banner** to apply your changes. The banner updates in real time for new visitors.
# Integrate the Promotions UI
Source: https://docs.kelviq.com/promotions/integrate
Add the Kelviq promotions banner to your app using HTML/JS, React, or the Direct API
After creating a promotion, open it and go to the **Integration** tab. You will find ready-to-use code snippets for your stack — copy the snippet, paste in your `promotionId` and Client API key, and the banner will render automatically for eligible visitors.
You will need two values:
* **`promotionId`** — the unique ID of the promotion (pre-filled in the snippet).
* **`accessToken`** — your Client API key. Find it in **Settings → API keys → Client API key**.
***
## HTML / JS
Add the script snippet to the `` of your page. It loads the SDK asynchronously, initializes it with your promotion, and renders the banner automatically. You can also call `getUpdatedPrice()`, `updatePriceElement()`, and `updatePrice()` to display discounted prices on the page.
> **[Promotions UI JS SDK](/frontend-integration/promotions-ui-js)**
>
> Installation, `init()` options, auto price update with data attributes, JS API reference, and CSS hooks.
***
## React / Next.js
Install `@kelviq/react-promotions-ui`, wrap your app in ``, and place `` where you want the banner to appear. Use `KQPriceFormatted` or the granular price components to show discounted prices anywhere on the page.
> **[React Promotions UI SDK](/frontend-integration/react-promotions-ui)**
>
> Installation, `KQProvider` props, `KQBanner`, `KQPriceFormatted`, granular price components, and sandbox setup.
***
## Direct API
Fetch promotion data directly from the REST API if you want to apply discounts in a custom checkout flow or build your own UI. The response includes the active discount percentage for the caller's detected location.
The ready-to-use `curl` snippet is available in the **Integration** tab of your promotion.
> **[GET /promotions](/api-reference/promotions/get-promotions)**
>
> Full API reference — request parameters, response schema, and example response.
# Promotions
Source: https://docs.kelviq.com/promotions/overview
Boost global sales with purchasing power parity and country-specific discounts
Promotions let you offer location-based discounts to customers around the world — automatically. Set up purchasing power parity (PPP) pricing, enforce security rules to prevent abuse, and render a customizable banner or discounted prices on your storefront — all from the Kelviq dashboard.
Promotions work with the products and plans you already have.
> **PPP group discounts**
>
> Apply discount percentages to economic tier groups — countries with similar purchasing power are grouped automatically. Override any group with a country-specific discount when you need finer control.
> **Show custom price on website**
>
> Display the discounted price directly on your pricing page using the Promotions UI SDK. Supports formatted prices, granular price components, and original vs. discounted price comparison.
> **Security enforcement**
>
> Block VPNs, proxies, Tor users, and crawlers from claiming localized discounts they're not entitled to. Enforced automatically at checkout.
> **Promotions UI banner**
>
> Render a fully customizable banner on your site that shows customers their local discount. Available as an HTML snippet, React component, or via the API.
## How it works
1. **Create a promotion** — give it a name, pick which products and plans it applies to, and configure discounts.
2. **Integrate** — add the Kelviq SDK snippet or React component to your frontend. The banner renders automatically for eligible visitors.
3. **Customize** — adjust the banner's appearance (colors, font, placement, close icon) to match your brand.
## Next steps
> **[Create a promotion](/promotions/create-a-promotion)**
>
> Step-by-step guide to setting up PPP discounts, country overrides, and security rules.
> **[Integrate the Promotions UI](/promotions/integrate)**
>
> Add the banner to your site using HTML/JS, React, or the Direct API.
> **[Customize the banner](/promotions/customize-banner)**
>
> Change colors, font, placement, and more to match your brand.
# Core concepts
Source: https://docs.kelviq.com/quickstart/core-concepts
Kelviq has four building blocks: products, plans, features, and customers. This page uses one example throughout: **Acme AI**, a writing assistant with a Free and a Pro plan.
```mermaid theme={null}
flowchart LR
F["Features ai-credits · max-projects · priority-support"] --> P["Plan: Pro $29/month 10,000 ai-credits/month"]
P --> C["Customer: cust_123 subscribed to Pro"]
C --> B["Balance 6,500 used · 3,500 remaining"]
```
***
## Products
A product is the thing you sell. In the example, the product is **Acme AI**.
[Learn more about products →](/product-catalog/products)
***
## Plans
A plan packages a product's price and features. Acme AI has two:
| | Free | Pro |
| - | - | - |
| Price | \$0 | \$29/month |
| `ai-credits` | 500/month | 10,000/month |
| `max-projects` | 3 | 50 |
| `priority-support` | Off | On |
[Learn more about plans →](/product-catalog/plans)
***
## Features & limits
A feature is something a plan grants. You create a feature once and add it to as many plans as you like, each with its own value. In the API and SDKs, the value a plan gives a feature is called an entitlement.
There are three feature types:
| Type | What it does | In the example |
| - | - | - |
| **Boolean** | Turns something on or off | `priority-support` |
| **Meter** | Counts usage against a limit that can reset | `ai-credits` |
| **Customizable** | Stores a number or setting for each plan | `max-projects` |
Your app checks the feature, not the plan. Moving `priority-support` to the Free plan is a dashboard change, not a deploy.
[Learn more about features & limits →](/product-catalog/entitlements) · [How usage limits work →](/product-catalog/usage-limits)
***
## Digital downloads
A digital download is a file that your customers can download after purchasing.
Examples: e-books, software, music, videos.
[Learn more about digital downloads →](/product-catalog/digital-downloads)
***
## License keys
A license key is a code that your customers can use to activate a digital download.
Examples: software activation, game keys, e-books.
[Learn more about license keys →](/product-catalog/license-keys)
***
## Links
Links allow you to share important URLs with customers after they purchase your plan.
Examples: Discord servers, Slack workspaces, Notion pages, member-only dashboards.
[Learn more about links →](/product-catalog/links)
***
## Customers
A customer is a person or company that buys from you. Create a customer with your own ID, such as `cust_123`, when someone signs up in your app. When they buy Pro, Kelviq links the subscription to that customer and their `ai-credits` balance starts at 10,000.
Each time they use a credit, your backend reports it. After 6,500 credits, Kelviq shows 3,500 remaining until the monthly reset.
[Learn more about customers →](/customers-management/overview)
***
## Typical workflows
### AI and usage-based products
Billing for actual work delivered rather than just seat access.
1. Create features like AI tokens, API calls, or workflow runs
2. Create a product
3. Create plans with a base fee plus usage charges
4. Set usage limits on each plan, with a soft or hard limit
5. Integrate the SDK to report usage back to Kelviq as the AI performs work
6. Customer subscribes and pays automatically based on their actual consumption
### One-time products
Selling digital downloads, license keys, or access links:
1. Create a **product**
2. Create a **plan** with one-time pricing
3. Add **digital downloads**, **license keys**, or **links** to the plan
4. Share your checkout link or embed a pricing table
5. Customer purchases → receives their files, keys, and links
### SaaS subscriptions
Selling recurring access with feature limits:
1. Create **features** (e.g., API calls, storage, AI tokens)
2. Create a **product**
3. Create **plans** with recurring pricing (monthly/yearly)
4. Add **features & limits** to each plan (e.g., 1,000 API calls/month)
5. Integrate the **SDK** to check feature access and enforce limits
6. Customer subscribes → SDK controls what they can access
***
## Integrate with your app
To bring everything you set up in the dashboard into your app, use one of these options:
* **SDKs** ([Node](/backend-integration/node-sdk), [Python](/backend-integration/python-sdk), [React](/frontend-integration/react-sdk), [JavaScript](/frontend-integration/js-sdk)) — check feature access and enforce usage limits in code
* **[APIs](/api-reference/authentication)** — use for custom needs such as syncing usage or triggering upgrades
> [!TIP]
>
> **Webhooks are optional.** The SDKs and UI widgets read access from Kelviq directly. Use [webhooks](/guides/webhooks) when you want events in your own systems.
[See the full integration guide →](/quickstart/integrate-with-your-app)
***
## Need help?
Have questions, need help with implementation, or want a walkthrough?\
Email us at **[hi@kelviq.com](mailto:hi@kelviq.com)** or [book a demo](https://cal.com/alok-vats-fkpnc9/30min).
***
# Dashboard setup
Source: https://docs.kelviq.com/quickstart/dashboard-setup
Before you can integrate Kelviq into your app, you need to configure your **products, plans, and features** in the dashboard.\
This defines your pricing and what each plan includes.
> [!TIP]
>
> Using Claude Code, Cursor, or another coding agent? [Set up with AI](/guides/set-up-with-ai) gives you a prompt that creates your products, plans, and features for you. You approve each change.
***
## Create a product
1. In the sidebar, go to **Product catalog → Products** and click **Create product**
2. Enter a product name (e.g., "Design Course", "Pro Plan", "Premium Templates")
3. Click **Save** — you'll be redirected to the product page
***
## Create a plan
From the product page:
1. Click **Create plan**
2. Enter a plan name (e.g., “Basic Plan”)
3. Click **Save** — you’ll be redirected to the plan details page
***
## Add pricing to the plan
From the plan page, click the **Price** section to configure pricing:
1. Select billing cycles: **Monthly**, **3 Months**, **6 Months**, **Yearly**, **28 Days** (when enabled), or **One Time**
2. Enter prices for each selected billing cycle
3. (Optional) Enable **Free trial** and set the trial period in days
4. Click **Save**
### Usage-based pricing
To charge for consumption, such as compute or agent runs:
1. In the Price section, add a usage charge
2. Pick the Meter feature to bill, such as AI tokens or workflow executions
3. Set the price per unit
4. Keep a flat base price too if you want hybrid pricing
See [Usage-based pricing](/product-catalog/usage-based-billing).
### Regional pricing
To offer localized prices in different currencies:
1. In the Price section, click **Add Region** under Regional Pricing
2. Select a currency (e.g., Indian Rupee, Euro)
3. Enter prices for each billing cycle in that currency
4. Click **Add Currency**
This allows you to set specific prices for different markets rather than relying on automatic currency conversion.
***
## Add digital downloads
If your plan includes downloadable content (e-books, software, course materials):
1. Click the **Digital downloads** section
2. Upload files that customers will receive after purchase
3. Files are securely delivered to customers upon successful payment
***
## Add license keys
To automatically generate unique license keys for each purchase:
1. Click the **License keys** section
2. Toggle license key generation **on**
3. Keys are automatically created and delivered to customers after purchase
You can use these keys to validate customers in your application.
***
## Add features and limits
Click the **Features & limits** section to configure feature access and usage quotas for this plan.
1. Click **Add feature**
2. A modal will open where you can create or select features
3. Configure the feature value for this plan
### Feature types
Features are created independently and can be attached to multiple plans with different values:
* **Boolean** — on or off
Examples: priority support, bonus content access, community access, custom branding
* **Customizable** — a fixed numeric limit, no usage reporting
Examples: number of downloads, max projects, storage limit (10 GB, 100 GB)
* **Meter feature** — tracked consumption, usage events sent to Kelviq, resets each billing cycle
Examples: API calls, AI generations, video renders, exports per month
Once added, features appear in the plan's feature list. You can reorder or remove them as needed.
***
## Publish the plan
Click **Publish** on the plan page. Plans stay drafts until you publish them, and customers can only buy published plans.
***
## Next steps
Your plan is now published and ready for customers.
Next, you can integrate Kelviq into your app:
* Use [checkout links](/checkout/static-checkout-url) to let customers buy your plans
* Create a customer in Kelviq when a user signs up in your app
* Use the Kelviq [SDKs](/quickstart/integrate-with-your-app) to check feature access and track usage
***
## Need help?
Have questions, need help with implementation, or want a walkthrough?\
Email us at **[hi@kelviq.com](mailto:hi@kelviq.com)** or [book a demo](https://cal.com/alok-vats-fkpnc9/30min).
***
# Integrate with your app
Source: https://docs.kelviq.com/quickstart/integrate-with-your-app
After setting up your product and plans in the dashboard, you can bring them into your app using the Kelviq SDKs and UI components.
> Note: In this guide we use the **Node SDK** and **React SDK** as examples.\
> Python and JavaScript SDKs are also available, and the steps are the same.
***
## 1. Create a customer (optional)
Typically, you create a customer record when someone signs up in your app.
When they complete a purchase, Kelviq maps the transaction to the customer record automatically.
This is optional, but creating a customer in Kelviq makes it easy to track subscriptions and usage in the dashboard.
### Get your API keys
Open **Settings → API keys**. Use the **server key** in your backend and the **client key** in your frontend.

### Install the Node SDK
```bash theme={null}
npm install @kelviq/node-sdk
```
```ts theme={null}
import { Kelviq } from "@kelviq/node-sdk";
// API keys are under Settings → API keys in the dashboard
const client = new Kelviq({
accessToken: process.env.KELVIQ_SERVER_API_KEY!,
});
// Create a customer on signup
const newCustomer = await client.customers.create({
customerId: "unique-customer-001",
email: "customer@example.com",
name: "Node SDK User",
});
```
***
## 2. Checkout links
Every published plan has a checkout link that sends customers straight to checkout.
### Get a checkout link
1. Go to **Product catalog → Products** and open your product
2. Open the plan's **⋯** menu and click **Get Link**
3. Copy the checkout URL
### Using checkout links
You can use checkout links in multiple ways:
* **Direct link** — Share the URL in emails, landing pages, or marketing campaigns
* **Button on your site** — Link your "Buy Now" or "Get Access" buttons to the checkout URL
* **With customer ID** — Append `?customer_id=unique-customer-001` to link the purchase to an existing customer
```html theme={null}
Buy Now
```
When a customer completes checkout, their subscription and features are available in Kelviq. See [Static checkout URLs](/checkout/static-checkout-url) for all link options.
***
## 3. Check feature access
Once a customer completes a purchase, their entitlements are available in the SDK.
You can check feature access in the frontend (React SDK) or backend (Node SDK).
In all examples we’ll keep using the same customer ID: `unique-customer-001`.
### React: show or hide UI based on entitlements
```tsx theme={null}
import { ShowWhenBooleanEntitled } from '@kelviq/react-sdk';
Checking…}
fallback={
Dark mode not available
}
>
```
### React: read all entitlements
The React hooks need your app wrapped in [`KelviqProvider`](/frontend-integration/react-sdk#kelviqprovider).
```tsx theme={null}
import { useAllEntitlements } from '@kelviq/react-sdk';
function EntitlementsList() {
const { data: entitlements } = useAllEntitlements();
return (
);
}
```
### Backend check with Node
`max-upload-size-mb` is a Customizable feature: each plan stores the largest upload it allows. `getEntitlement` returns `null` when the customer doesn't have the feature, so treat that as no access.
```ts theme={null}
const ent = await client.entitlements.getEntitlement({
customerId: "unique-customer-001",
featureId: "max-upload-size-mb",
});
const maxUploadMb = ent?.hasAccess ? ent.usageLimit ?? Infinity : 0;
if (uploadSizeMb > maxUploadMb) {
throw new Error("Upgrade required");
}
```
***
## 4. Report usage (Meter features)
For Meter features (like downloads, exports, AI generations, or API calls), send usage events from your backend.
```ts theme={null}
import { BEHAVIOUR_CHOICES } from '@kelviq/node-sdk';
await client.reporting.reportUsage({
customerId: "unique-customer-001",
featureId: "downloads",
value: 1,
behaviour: BEHAVIOUR_CHOICES.DELTA,
});
```
`DELTA` adds `value` to the customer's current usage, so send it each time something is used. `SET` replaces current usage with `value`, for numbers you already count yourself, such as seats in use.
***
## Need help?
Have questions, need help with implementation, or want a walkthrough?\
Email us at **[hi@kelviq.com](mailto:hi@kelviq.com)** or [book a demo](https://cal.com/alok-vats-fkpnc9/30min).
***
# Abandoned cart recovery
Source: https://docs.kelviq.com/sales/abandoned-cart-recovery
Automatically remind customers who start checkout but do not pay
Abandoned cart recovery emails customers who start a checkout but do not complete payment. Kelviq detects the abandoned checkout, sends up to three reminder emails over three days, and expires the checkout after four. You do not need to build anything: detection, scheduling, and deduplication are handled for you.
The feature is on by default for every organization. You can turn it off in **Settings → Customer emails**.
## How a checkout becomes abandoned
Kelviq marks a checkout as abandoned when all of the following are true:
* The checkout was created at least one hour ago.
* Its status is `open` or `processing`.
* The customer has not touched it in the last 30 minutes.
Recent customer activity delays the marking, so a customer who is still filling in payment details is not emailed.
A reminder also needs somewhere to go: the customer must have an email address, taken from the linked customer record or the email entered during checkout.
## Reminder schedule
All timings are measured from when the checkout session was created.
| Event | When |
| - | - |
| Checkout marked as abandoned | After 1 hour |
| First reminder email | After 24 hours |
| Second reminder email | After 48 hours |
| Final reminder email | After 72 hours |
| Checkout expires | After 4 days |
The background jobs that send these run hourly, so an email can arrive up to an hour after its nominal time.
The reminders are sequential. The second email is only sent after the first, and the third only after the second. Each email includes a **Return to checkout** link that takes the customer back to their session, and each send is recorded so a customer is never emailed twice for the same step.
The subject lines are:
1. `You left {productName} in checkout`
2. `Still interested in {productName}?`
3. `Last reminder about your {productName} checkout`
## What happens at expiration
Four days after creation, an unpaid checkout expires:
* The checkout status becomes `expired` and its payment status becomes `canceled`.
* The recovery link in earlier emails stops working.
* No further reminders are sent.
Completed, closed, or expired checkouts never receive reminders.
## Offer a discount in the final reminder
Turn on **Offer a discount in the final reminder** in **Settings → Customer emails** and enter a percentage. The final reminder then offers that much off the checkout total. Only one discount can be active per product, so the offer isn't sent if another discount already applies to the checkout.
## Turning it off and on
Use the **Abandoned cart recovery** toggle in **Settings → Customer emails** to control the feature for your whole organization.
* Turning it off stops future reminder emails. Emails that were already sent cannot be recalled.
* Turning it back on resumes eligible reminders for checkouts that have not yet expired.
> [!NOTE]
>
> The reminder timing and email content are fixed. There is currently no option to change the schedule, resend a reminder manually, or exclude individual checkouts.
To watch recovery in action, open [Checkout sessions](/sales/checkout-sessions). An abandoned checkout stays in its `open` or `processing` state until the customer returns and pays or the session expires.
# Checkout sessions
Source: https://docs.kelviq.com/sales/checkout-sessions
Every checkout attempt, paid or not, with a timeline of what the customer did
A checkout session is created each time a customer opens a checkout link or your app creates a checkout through the API. [Orders](/sales/orders) only show purchases that went through. Checkout sessions show every attempt, so you can see where customers stop.
Open **Sales → Checkout sessions**.

Each row shows the session ID, the amount and billing period, the status, the customer's country and email (or N/A if they never entered one), the product and plan, and when the session was created. Search by name or session ID, filter by status, and change the date range above the list.
## Session statuses
| Status | What it means |
| - | - |
| **Open** | The customer has the checkout and hasn't paid yet. |
| **Processing** | The customer submitted a payment and it's being confirmed. |
| **Success** | The payment went through and an [order](/sales/orders) was created. |
| **Closed** | The session ended without a payment before it expired. |
| **Expired** | Nobody paid within four days of the session being created. |
A session that's still **Open** or **Processing** an hour after it was created, with no customer activity in the last 30 minutes, is treated as abandoned and gets [recovery emails](/sales/abandoned-cart-recovery) if the customer entered an email address.
## Session details
Click a session to open its details: the items in the checkout, the customer, the payment method used, and the session timeline.

The timeline lists what happened in the session, newest first, with the time of each event. For example:
| Event | When it's added |
| - | - |
| **Checkout started** | The customer opened the checkout. Shows whether it's a subscription or one-time purchase, and the amount. |
| **Plan selected** | The customer picked a plan or billing period. Shows the plan, period, and price. |
| **Billing address updated** | The customer entered or changed their country or address. |
| **Checkout session abandoned** | The customer left without paying. |
| **Checkout session expired** | The session reached its four-day limit without a payment. |
Payment attempts appear in the timeline too, including the decline reason when a payment fails, such as `insufficient_funds`. Use it to answer "why didn't this customer get charged?" without asking them to try again.
## Custom field responses
If the checkout collected [custom fields](/checkout/custom-fields), the customer's answers are saved on the session as `customFieldsData` in the API, and sent as `custom_fields_data` in the `checkout.completed` [webhook](/guides/webhooks#custom-field-responses).
# Create a one-time charge
Source: https://docs.kelviq.com/sales/create-a-one-time-charge
Charge a customer's saved payment method without sending them through checkout
Use the Create Charge API when an existing customer has agreed to a one-time purchase and already has a usable payment method saved in Kelviq. The payment is attempted immediately, so the customer does not need to complete another checkout session.
Common examples include selling an add-on after the original checkout or collecting a one-time setup fee from an existing customer.
> [!WARNING]
>
> Create a charge from your backend only. It requires a Server API Key, which must never be exposed in browser or mobile-app code.
## Before you start
Make sure all of the following are true:
* The customer already exists in the same Kelviq environment as your API key.
* The customer has a usable default payment method from a previous checkout or an active subscription.
* The plan has a one-time price and you know its plan identifier.
* You have the customer's unique `customerId`.
Sandbox and production are isolated. A sandbox customer or payment method cannot be charged with a production key, and the reverse is also true.
## Create the charge
Send `POST /api/v1/charges/` with the plan and customer identifiers. `chargePeriod` must be `ONE_TIME`.
```bash cURL theme={null}
curl --request POST \
--url https://sandboxapi.kelviq.com/api/v1/charges/ \
--header "Authorization: Bearer $KELVIQ_SANDBOX_SERVER_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"planIdentifier": "lifetime-access",
"chargePeriod": "ONE_TIME",
"customerId": "cust_789",
"currencyCode": "USD",
"features": [
{
"identifier": "seats",
"quantity": 5
}
]
}'
```
```typescript Node.js SDK theme={null}
import { Kelviq } from "@kelviq/node-sdk";
const client = new Kelviq({
accessToken: process.env.KELVIQ_SANDBOX_SERVER_API_KEY,
environment: "sandbox",
});
const charge = await client.charges.create({
planIdentifier: "lifetime-access",
chargePeriod: "ONE_TIME",
customerId: "cust_789",
currencyCode: "USD",
features: [{ identifier: "seats", quantity: 5 }],
});
console.log(charge.id, charge.status, charge.amount);
```
```python Python SDK theme={null}
import os
from kelviq_sdk import Kelviq
client = Kelviq.create_sync_client(
access_token=os.environ["KELVIQ_SANDBOX_SERVER_API_KEY"],
environment="sandbox",
)
charge = client.charges.create(
planIdentifier="lifetime-access",
chargePeriod="ONE_TIME",
customerId="cust_789",
currencyCode="USD",
features=[{"identifier": "seats", "quantity": 5}],
)
print(charge.id, charge.status, charge.amount)
```
The `features` and `currencyCode` fields are optional. Include `features` when the customer is choosing a quantity, such as five seats. If the customer already has a currency, `currencyCode` must match it.
## Handle the response
A successful request returns `201 Created` with the charge record:
```json theme={null}
{
"id": "7c2f3a91-2d4e-4a8b-9b1c-6f0a2e5d9c11",
"startDate": "2026-06-20",
"endDate": null,
"billingPeriodStartTime": null,
"billingPeriodEndTime": null,
"amount": "49.00",
"recurrence": "",
"currency": "USD",
"status": "active",
"product": {
"name": "Invoice Test",
"id": "88e437b8-6017-405b-9328-e0f4e140bb79",
"identifier": "invoice-test"
},
"plan": {
"name": "Lifetime Access",
"identifier": "lifetime-access"
},
"features": [],
"trialDaysRemaining": 0,
"customerId": "cust_789",
"billingType": "ONE_TIME"
}
```
Store the returned `id` with your own purchase record. You can also confirm the payment from **Sales → Orders** in the Kelviq dashboard.
## Handle failed charges
The API returns `400 Bad Request` when Kelviq cannot complete the payment. The response includes a `detail` message describing the problem.
| Cause | What to do |
| - | - |
| No default payment method | Send the customer through checkout so they can add a payment method. |
| Payment declined | Ask the customer to update their payment method, then try again with their approval. |
| Payment requires authentication | Use checkout so the customer can complete the required authentication. |
| `chargePeriod` is not `ONE_TIME` | Use `ONE_TIME`, or use the Subscriptions API for recurring billing. |
| API key and customer are in different environments | Use the matching sandbox or production key and customer. |
Do not automatically retry a `400` response. If a request times out and the outcome is unclear, check **Sales → Orders** before sending it again to avoid charging the customer twice.
## When to use checkout instead
Use a [checkout session](/checkout/checkout-configuration) when the customer needs to:
* enter a payment method for the first time;
* complete card or bank authentication;
* review and confirm the purchase in a hosted payment flow; or
* start a recurring subscription.
For every request field and response property, see [Create a charge API reference](/api-reference/charges/create-a-charge).
# Orders
Source: https://docs.kelviq.com/sales/orders
Every purchase made through your checkout, with its status, amount, and quick actions
An order is created for each purchase made through your checkout: one-time payments, the first payment of a subscription, and each renewal. Open **Sales → Orders** to see them.

Each row shows the order ID, the customer's email and name, the product, the status, when the order was paid, and the amount. Search by name or order ID, filter by status, and change the date range with the filters above the list.
## Order statuses
| Status | What it means |
| - | - |
| **Pending payment** | The order was created and the payment hasn't completed yet. |
| **Complete** | The payment succeeded. Shown as **Completed** in the list. |
| **Failed** | The payment was attempted and declined, or it errored. No money was collected. |
| **Refunded** | The full amount was refunded to the customer. |
| **Partial refund** | Part of the amount was refunded. The rest stays with you. |
| **Canceled** | The payment was canceled before it completed. Nothing was charged. |
| **Fraudulent** | The payment was identified as fraudulent. |
The same values come back as `status` from the [Orders API](/api-reference/orders/list-orders): `PENDING`, `COMPLETE`, `FAILED`, `REFUNDED`, `PARTIAL_REFUND`, `CANCELLED`, and `FRAUDULENT`.
## Quick actions
Click **⋯** at the end of a row:
* **Refund payment:** refund the full amount or part of it.
* **Resend receipt:** email the receipt to the customer again.
* **Generate invoice:** create and download the invoice for the order.
## Order details
Click the customer on any row to open the order's details.

The panel shows the order status and date, the customer's name, email, and country, and each item with its subtotal, discount, tax, and total. Click **⋯** at the top for the same actions as the list, plus **Copy signed portal link** to send the customer a link to their [customer portal](/customer-portal/overview).
The tabs below the totals show what the customer received and what happened to the order:
* **Downloads:** the files the customer can download. You can remove or hide a file for this order to revoke access.
* **License:** the license key issued for the order. You can edit the activation limit, change the expiry date, or deactivate the key.
* **Links:** the links shared with the customer after purchase.
* **Timeline:** every event on the order, newest first, such as **Order created**, **Order completed**, receipts and invoices sent, and payment results. Use it to check, for example, whether a customer was sent their receipt.
## Custom field responses
If the checkout collected [custom fields](/checkout/custom-fields), the customer's answers are saved on the order. You can see them in the order details drawer. They're returned as `customFieldsData` on the order in the API and as `custom_fields_data` in the `order.created`, `order.updated`, and `order.refunded` [webhooks](/guides/webhooks#custom-field-responses).
# Customer emails
Source: https://docs.kelviq.com/settings/customer-emails
The automated emails Kelviq sends to your customers, when each one goes out, and how to turn them off
Kelviq emails your customers at each step of a purchase or subscription: receipts, invoices, trial reminders, failed payment notices, and more. Every email is on by default. Open **Settings → Customer emails** to preview an email or turn it off.

Click the eye icon next to an email to preview it. Use its switch to stop sending it. Turning an email off stops future sends only.
## Subscriptions and lifecycle
| Email | When it's sent |
| - | - |
| **Trial expiring** | 3 days before a free trial converts to a paid subscription. |
| **Plan changed** | When the customer upgrades or downgrades. |
| **Subscription canceled** | When a subscription is canceled, to confirm it will end. |
## Payments and receipts
| Email | When it's sent |
| - | - |
| **Payment successful** | After each successful charge, one-time or recurring. |
| **Order receipt and confirmation** | Right after the customer completes checkout. |
| **Invoice email** | After each billing cycle, with the invoice. |
| **Refund processed** | When you issue a refund. |
## Dunning and recovery
These emails help you collect failed subscription payments. Keep them on unless you send your own.
| Email | When it's sent |
| - | - |
| **Upcoming invoice reminder** | Before a renewal charge. |
| **Payment failed** | As soon as a charge is declined. |
| **Payment past due** | As a follow-up when an invoice is still unpaid. |
| **Payment reminder** | For outstanding payments, before escalation. |
| **Card expired** | Before the renewal, when the customer needs to update their card. |
## Abandoned checkout recovery
Up to three reminders go to customers who start a checkout and leave without paying. One switch controls all three. You can also offer a percentage off the checkout total in the final reminder. Only one discount can be active per product, so the offer isn't sent if another discount already applies.
[Abandoned cart recovery](/sales/abandoned-cart-recovery) covers when a checkout counts as abandoned, the reminder schedule, and what happens when it expires.
## Customer account
| Email | When it's sent |
| - | - |
| **Payment method updated** | When the customer saves a new payment method in the [customer portal](/customer-portal/managing-payment-methods). |
# Members
Source: https://docs.kelviq.com/settings/members
Invite teammates to your Kelviq organization and manage their access
Members are the people who can sign in to your organization's dashboard. Kelviq doesn't charge per seat, so invite as many teammates as you need.
Open **Settings → Members** to see everyone in the organization with their role and status. The person who created the organization is its **Owner**.
## Invite a member
1. Click **Invite members**.
2. Enter their first name, last name, and email address.
3. Click **Send invite**.

Kelviq emails them an invitation to join your organization. If they don't have a Kelviq account yet, they're asked to create one. They show as **Invited** until they accept, then as **Active**.
## Manage invitations and members
Click **⋯** next to a person:
* **Resend invite** sends the invitation email again, for someone who's still **Invited**.
* **Cancel invite** withdraws an invitation that hasn't been accepted.
* **Remove** takes away a member's access to the organization immediately. This can't be undone; invite them again if you need to.
# Profile
Source: https://docs.kelviq.com/settings/profile
Your name, sign-in, time preferences, and organization name
Open **Settings → Profile** to change your own details and your organization's name.

## Your details
Change your **first name** and **last name** here. Your **primary email address** is the one you sign in with and can't be edited on this page.
If you signed up with Google, you sign in with Google by default. Click **Set password** to add a password so you can also sign in with your email address.
## Time preferences
* **Timezone:** the timezone the dashboard uses to show dates and times, such as when an order was paid.
* **Start week on:** the first day of the week in date ranges and calendars.
These change how dates are shown in the dashboard. They don't change billing dates, renewals, or anything customers see.
## Organization
**Organization name** is the name of your Kelviq organization. To change what customers see on checkout, such as your logo and colors, use [Checkout design](/checkout/checkout-design).
# Zapier
Source: https://docs.kelviq.com/settings/zapier
Connect Kelviq to Zapier to automate work across your other apps
Use Zapier to connect Kelviq to the rest of your tools without writing code, for example to send a team message or update a spreadsheet when something happens in Kelviq.

## Connect Kelviq to Zapier
1. Open **Settings → Integrations**.
2. Click **Configure in Zapier**. Zapier opens in a new tab.
3. Sign in to Zapier, or create a free account, and accept the invitation to use the Kelviq app.
4. In Zapier, create a Zap and choose **Kelviq** as the app. Pick the event you want, then connect your Kelviq account when Zapier asks.
The Kelviq app on Zapier is available by invitation, so it won't show up in Zapier's app search until you've accepted the invitation from step 3.
## Prefer code?
If you'd rather handle events in your own backend, use [webhooks](/guides/webhooks). Kelviq sends each event with its full payload to your server.