' \
--data '{
"event_name": "model.usage",
"external_customer_id": "cust-test-customer",
"properties": {
"credits": 1
}
}'
```
2. **Check Event Ingestion**
* Go to Events dashboard
* Locate your test event
* Verify the payload is correct
3. **Verify Aggregation**
* Check that the event appears in the correct billing period
* Verify the aggregation value is as expected
4. **Test Billing Impact**
* Ensure feature is in a plan
* Subscribe customer to the plan
* Check upcoming invoices
## Debugging Checklist
Use this checklist to systematically debug event ingestion issues:
### ✅ Event Configuration
* Feature exists and is active
* Event Name matches exactly
* Aggregation Function is correct
* Aggregation Field is set (if required)
### ✅ Event Payload
* All required fields present
* Event Name matches feature
* External Customer ID exists
* Properties contain aggregation field
* Property values are correct data type
### ✅ Customer Setup
* Customer exists in Flexprice
* External Customer ID matches
* Customer has active subscription
* Subscription includes the feature
### ✅ Billing Configuration
* Feature is added to a plan
* Plan has pricing for the feature
* Customer is subscribed to the plan
* Usage Reset setting is correct
## Advanced Validation
### Query Events by Customer
You can filter events by customer to see all usage for a specific account:
1. In the Events dashboard, use the filter options
2. Filter by External Customer ID
3. Review all events for that customer
### Check Aggregated Usage
1. Go to **Usage Tracking** → **Query**
2. Select your feature
3. Choose the customer and time period
4. View aggregated usage values
This displays how Flexprice is calculating the total usage for billing.
### Validate Billing Periods
For **Periodic** usage reset:
* Usage should reset at the start of each billing cycle
* Check that usage accumulates correctly within the period
For **Cumulative** usage reset:
* Usage should keep growing across billing cycles
* Verify no unexpected resets
## Troubleshooting Common Scenarios
### Scenario 1: Events Transmitted but Not Counted
**Symptoms**: Events appear in dashboard but don't affect billing
**Check**:
1. Feature configuration (Event Name, Aggregation Field)
2. Event properties (correct field name and data type)
3. Customer subscription status
4. Plan configuration
### Scenario 2: Wrong Aggregation Values
**Symptoms**: Billing shows incorrect quantities
**Check**:
1. Aggregation Field name matches exactly
2. Property values are correct data type
3. No duplicate or conflicting events
4. Usage Reset configuration
### Scenario 3: Events Missing from Dashboard
**Symptoms**: Events not visible in Events list
**Check**:
1. API response status (should be 202 Accepted)
2. Event Name validity
3. Customer existence
4. API key permissions
### Scenario 4: Unexpected Usage Resets
**Symptoms**: Usage resets when it shouldn't
**Check**:
1. Usage Reset setting (Periodic vs Cumulative)
2. Billing cycle configuration
3. Subscription changes
4. Feature configuration changes
## Best Practices for Validation
### 1. Test Before Production
Always test your event ingestion setup with a small number of events before going live.
### 2. Monitor Regularly
Check the Events dashboard regularly to ensure events are being processed correctly.
### 3. Use Consistent Identifiers
Use consistent Event Names and External Customer IDs across your system.
### 4. Include Debugging Information
Add useful properties to your events for debugging:
```json theme={null}
{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": {
"credits": 2,
"request_id": "req_abc123",
"version": "1.0"
},
"source": "api"
}
```
### 5. Set Up Alerts
Consider setting up alerts for:
* Failed event ingestion
* Unusual usage patterns
* Missing events
## Next Steps
After validating your events:
**[Connect to Billing](/docs/event-ingestion/connecting-to-billing)** - Set up pricing and subscriptions
## Getting Help
If you're still experiencing issues after following this validation guide:
1. Check the **[Event Debugger](/docs/event-ingestion/event-debugger)** for detailed error messages
2. Review your feature configuration carefully
3. Test with a simple event payload
4. Contact support with specific error messages and event examples
# Credit Balance
Source: https://docs.flexprice.io/docs/exportable-ui/credits-widgets/credit-balance
Wallet balance card showing current credits and monetary value
Use **Credit Balance** to show a customer's current wallet balance: credits, monetary value, and wallet status. Part of [Credits Widgets](/docs/exportable-ui/credits-widgets/credits-widgets).
```tsx theme={null}
```
## Supports
* Credit balance and monetary value
* Wallet status (active, frozen, closed)
* Empty state when there is no wallet
* Loading skeleton state
## Best used for
* Customer portal overview
* Billing page
* Account settings
## Import
```tsx theme={null}
import '@flexprice/ui/style.css';
import { CreditBalance, type CreditBalanceData } from '@flexprice/ui';
```
## Usage
```tsx theme={null}
const wallet: CreditBalanceData = {
id: 'wallet_1',
name: 'Main Wallet',
status: 'active',
creditBalance: 4200,
balance: 42,
currency: 'USD',
};
```
Pass `wallet={null}` to render the empty state (no wallet set up yet). See [Ways to provide data](/docs/exportable-ui/overview#ways-to-provide-data) for SDK, API, backend, and static JSON options.
## Map Flexprice API data
```ts theme={null}
import { adaptCreditBalance } from '@flexprice/ui';
const wallet = adaptCreditBalance(walletFromList, realtimeWalletBalance);
```
`realtimeWalletBalance` is optional. When provided, its real-time balance fields take precedence over the wallet list snapshot.
## See also
Overview of both credits components.
Transaction history table with pagination and multi-wallet selection.
# Credit History
Source: https://docs.flexprice.io/docs/exportable-ui/credits-widgets/credit-history
Wallet transaction history table with pagination and multi-wallet selection
Use **Credit History** to show a customer's wallet transactions: credits added, credits spent, and the reason for each. Part of [Credits Widgets](/docs/exportable-ui/credits-widgets/credits-widgets).
```tsx theme={null}
const [page, setPage] = useState(1);
```
## Supports
* Credit and debit transactions, with reason labels
* Pending-transaction styling
* Multiple wallets, with a selector shown when there is more than one
* Pagination
* Loading skeleton state
* Empty state when there are no transactions
## Best used for
* Customer portal billing tab
* Account settings
* Wallet detail pages
## Import
```tsx theme={null}
import '@flexprice/ui/style.css';
import { CreditHistory, type CreditTransaction } from '@flexprice/ui';
```
## Usage
Pagination is fully controlled: the component has no dependency on a router or URL, so you own the current page in your own state and pass it back in.
```tsx theme={null}
const [page, setPage] = useState(1);
const transactions: CreditTransaction[] = [
{ id: 'txn_1', type: 'credit', amount: 100, creditAmount: 100, reason: 'FREE_CREDIT_GRANT', createdAt: '2026-01-01T00:00:00Z' },
{ id: 'txn_2', type: 'debit', amount: 20, creditAmount: 20, reason: 'INVOICE_PAYMENT', createdAt: '2026-01-05T00:00:00Z' },
];
```
For accounts with more than one wallet, also pass `wallets`, `selectedWalletId`, and `onSelectWallet`:
```tsx theme={null}
```
See [Ways to provide data](/docs/exportable-ui/overview#ways-to-provide-data) for SDK, API, backend, and static JSON options.
## Map Flexprice API data
```ts theme={null}
import { adaptCreditTransactions, adaptWalletOptions } from '@flexprice/ui';
const transactions = adaptCreditTransactions(walletTransactions.items);
const wallets = adaptWalletOptions(walletList);
```
## See also
Overview of both credits components.
Wallet balance card showing current credits and monetary value.
# Credits Widgets
Source: https://docs.flexprice.io/docs/exportable-ui/credits-widgets/credits-widgets
Wallet balance and transaction history components
**Credits Widgets** are two independent components for showing a customer's prepaid credit wallet: [Credit Balance](/docs/exportable-ui/credits-widgets/credit-balance) and [Credit History](/docs/exportable-ui/credits-widgets/credit-history). Use one on its own, or place both together on a billing or account page.
Each component is presentational and takes its own prop shape. There is no shared parent component.
## Components
Wallet balance card showing current credits and monetary value.
Transaction history table with pagination and multi-wallet selection.
## Import
```tsx theme={null}
import '@flexprice/ui/style.css';
import { CreditBalance, CreditHistory } from '@flexprice/ui';
```
Each component page documents its own prop shape, an example, and the adapters for mapping Flexprice wallet API responses.
## See also
Install Flexprice UI, choose a data source, theme, and integrate components.
Metric cards, trend chart, breakdown table, and quota bars.
# Overview
Source: https://docs.flexprice.io/docs/exportable-ui/overview
Production-ready billing UI, pulled straight from the Flexprice dashboard, that renders from any data source
Building billing UI from scratch means re-solving problems Flexprice already solved: currency formatting, entitlement states, empty states, dark mode, responsive layout. **Flexprice UI** (`@flexprice/ui`) skips that work. It's the actual React components from the Flexprice dashboard, published as an installable library, so your pricing page, usage dashboard, and billing screens match a production system on day one instead of a first draft.
Every component is presentational: no fetching, no auth, no routing baked in. You own the data. Feed a component from the Flexprice SDK, the REST API, your own backend, or a plain JSON file, and it renders identically either way.
One CSS variable away from your brand.
Every component ships a light and dark treatment.
Typed props and adapters, no `any`.
Works in Next.js, Remix, and Astro.
## Components
| Component | Description |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| [Pricing Table](/docs/exportable-ui/pricing-widget/pricing-table) | Comparison grid of multiple plans, with billing period and currency controls. |
| [Pricing Card](/docs/exportable-ui/pricing-widget/pricing-card) | A single plan: price, entitlements, and a CTA. Compose your own layout with it. |
| Component | Description |
| ------------------------------------------------------------------------ | ------------------------------------------------------------ |
| [Metric Cards](/docs/exportable-ui/usage-widgets/metric-cards) | Headline numbers: revenue, cost, margin, and custom metrics. |
| [Usage Trend Chart](/docs/exportable-ui/usage-widgets/usage-trend-chart) | Line chart of usage over time, one series per feature. |
| [Usage Breakdown](/docs/exportable-ui/usage-widgets/usage-breakdown) | Grouped, sortable table of usage and cost by feature. |
| [Usage Quota](/docs/exportable-ui/usage-widgets/usage-quota) | Progress bars showing usage against each plan limit. |
| Component | Description |
| -------------------------------------------------------------------- | ---------------------------------------------------------------- |
| [Credit Balance](/docs/exportable-ui/credits-widgets/credit-balance) | Wallet balance card: credits, monetary value, and status. |
| [Credit History](/docs/exportable-ui/credits-widgets/credit-history) | Paginated wallet transaction history, with multi-wallet support. |
Every component page has a live, interactive **Preview** you can toggle between light and dark, right next to the **Code** that produces it.
The full Flexprice documentation, including every component on this page, as one plain-text file at `/llms-full.txt`. Open it to copy the raw text, or paste the link into an AI tool that can fetch URLs.
As more Flexprice surfaces become exportable, they ship here under the same pattern: install, bring your own data, render.
## Install
```bash npm theme={null}
npm install @flexprice/ui
```
```bash pnpm theme={null}
pnpm add @flexprice/ui
```
```bash yarn theme={null}
yarn add @flexprice/ui
```
### Peer dependencies
`react` and `react-dom` (v18).
### Stylesheet
Import the stylesheet once in your app:
```tsx theme={null}
import '@flexprice/ui/style.css';
```
You can also override appearance with CSS variables, Tailwind, CSS Modules, or other styling approaches — see [Theming](#theming).
## Ways to provide data
Every component follows the same pattern: fetch data from a source you choose, map it into the presentational props the component expects, and render.
### Option 1: Flexprice SDK (recommended)
```
Frontend → Flexprice SDK → Flexprice APIs → Components
```
Best for existing Flexprice customers, live data, and automatic catalog updates.
### Option 2: Flexprice REST APIs
```
Frontend → Flexprice REST API → Components
```
Best for Server Components, a backend proxy, or custom authentication.
### Option 3: Your own backend
```
Frontend → Your backend → Flexprice → Components
```
Your backend can cache responses, add auth, transform data, or merge internal fields.
### Option 4: Your own APIs
```
Frontend → Your API → Components
```
If your APIs already expose the data you need, map them into each component's prop shape. No Flexprice API dependency is required.
### Option 5: Static JSON
Best for documentation sites, marketing pages, and demos.
### Which approach to use
| Approach | Best for |
| ----------------------- | ---------------------------------------- |
| **Flexprice SDK** | Existing Flexprice users, live data |
| **Flexprice REST APIs** | Direct API control with live data |
| **Backend proxy** | Auth, caching, or server-side transforms |
| **Custom APIs** | Systems outside Flexprice |
| **Static JSON** | Marketing sites, demos, documentation |
Component pages document the exact prop shapes and any adapters for mapping Flexprice API responses.
## Theming
Every component renders inside an element carrying the `flexprice-ui` class, and that element is
where the theme tokens are declared. Override them with a selector that **matches that element** —
`--primary` is the accent hook:
```css theme={null}
.my-app .flexprice-ui {
--primary: 243 75% 59%;
}
```
Setting these variables on a wrapping *ancestor* has no effect. The package declares them on
`.flexprice-ui` itself, so an ancestor rule loses the cascade and the components render
unchanged. Target `.flexprice-ui` (or an element that also carries the class).
Values are HSL channels — `243 75% 59%`, not `hsl(243 75% 59%)` or `#4f46e5`.
Add the `dark` class to any ancestor to toggle dark mode:
```tsx theme={null}
{/* Flexprice UI components */}
```
Individual components may also accept theme-related props — see each component page.
## Event callbacks
Components expose callbacks for user actions (selection, checkout, contact sales, and similar). Wire them to analytics, [Checkout](/docs/checkout/overview), a payment provider, or your own flow. See each component page for the callbacks it supports.
## Server-side rendering
Components work with SSR in Next.js (App Router and Pages Router), Remix, and Astro. Fetch data on the server when you can, then pass it as props to avoid client-only waterfalls.
## Typical integration flow
Add `@flexprice/ui` and ensure `react` / `react-dom` are available as peer dependencies.
Import `@flexprice/ui/style.css` once, then import the components you need.
Pick Flexprice SDK, REST APIs, your backend, your own APIs, or static JSON — see [Ways to provide data](#ways-to-provide-data).
Load records from your source and map them into each component's presentational props (adapters are documented on the component pages).
Pass the mapped data (and any callbacks) into the component and render it in your page or layout.
## Future components
The package is designed to expand beyond the current exports. Planned surfaces include invoices, subscription details, and the full customer portal. New components follow the same pattern: install from `@flexprice/ui`, load data from any source, map into props, and render.
# Pricing Card
Source: https://docs.flexprice.io/docs/exportable-ui/pricing-widget/pricing-card
Individual plan card for dashboards, billing pages, upgrade modals, and custom layouts
Use **Pricing Card** to render a single plan anywhere in your product. Compose multiple cards yourself, or use [Pricing Table](/docs/exportable-ui/pricing-widget/pricing-table) for a ready-made grid.
```tsx theme={null}
```
## Supports
* Feature list
* Custom badges
* Billing cadence
* Usage pricing
* Trial messaging
* CTA buttons
## Best used for
* Dashboard
* Billing page
* Subscription selector
* Upgrade modal
* Checkout
## Import
```tsx theme={null}
import '@flexprice/ui/style.css';
import { PricingCard } from '@flexprice/ui';
```
## Usage
```tsx theme={null}
```
Pass a presentational plan from the Flexprice SDK, REST, your backend, or static JSON. See [Ways to provide data](/docs/exportable-ui/overview#ways-to-provide-data).
## Map Flexprice API data
```ts theme={null}
import { adaptPlanToCard, filterAndSortPlans } from '@flexprice/ui';
const [plan] = filterAndSortPlans(plansWithData, 'USD', 'MONTHLY').map((p) =>
adaptPlanToCard(p, grants)
);
```
## See also
Install Flexprice UI, choose a data source, theme, and integrate components.
Comparison table for multiple plans, features, and entitlements.
# Pricing Table
Source: https://docs.flexprice.io/docs/exportable-ui/pricing-widget/pricing-table
Comparison table for showcasing multiple plans, features, and entitlements
Use **Pricing Table** to show a comparison table of multiple plans, with billing period and currency controls built in.
```tsx theme={null}
const plans: Plan[] = /* fetch + map */;
router.push(`/checkout/${planId}`)}
/>
```
## Supports
* Unlimited plans
* Feature comparison
* Usage limits and included entitlements
* Custom pricing units
* Custom CTA
* Recommended plan highlighting
* Billing period and currency controls
## Best used for
* Pricing pages
* Documentation
* Sales pages
## Import
```tsx theme={null}
import '@flexprice/ui/style.css';
import { PricingTable, type Plan } from '@flexprice/ui';
```
## Usage
```tsx theme={null}
const plans: Plan[] = /* fetch + map */;
router.push(`/checkout/${planId}`)}
/>
```
Wire `onSelectPlan` to [Checkout](/docs/checkout/checkout-sessions) or your own flow. See [Ways to provide data](/docs/exportable-ui/overview#ways-to-provide-data) for SDK, API, backend, and static JSON options.
## Map Flexprice API data
```ts theme={null}
import { adaptPlanToCard, filterAndSortPlans } from '@flexprice/ui';
const plans = filterAndSortPlans(plansWithData, 'USD', 'MONTHLY').map((p) =>
adaptPlanToCard(p, grants)
);
```
## See also
Install Flexprice UI, choose a data source, theme, and integrate components.
Single plan card for dashboards, modals, and custom layouts.
# Metric Cards
Source: https://docs.flexprice.io/docs/exportable-ui/usage-widgets/metric-cards
Grid of revenue, cost, margin, and custom usage metrics
Use **Metric Cards** to show a row of headline numbers: revenue, cost, margin, margin percent, and any custom metrics you define. Part of [Usage Widgets](/docs/exportable-ui/usage-widgets/usage-widgets).
```tsx theme={null}
```
## Supports
* Revenue, cost, margin, and margin percent cards
* Custom metrics (any name and value you pass in)
* Currency formatting per card
* Percent formatting
* Change indicators (up / down)
* Loading skeleton state
## Best used for
* Usage dashboards
* Customer portal overview
* Account summary pages
## Import
```tsx theme={null}
import '@flexprice/ui/style.css';
import { MetricCards, type MetricCardItem } from '@flexprice/ui';
```
## Usage
```tsx theme={null}
const metrics: MetricCardItem[] = [
{ id: 'revenue', titleKey: 'revenue', value: 12500, currency: 'USD' },
{ id: 'cost', titleKey: 'cost', value: 4200, currency: 'USD' },
{ id: 'margin', titleKey: 'margin', value: 8300, currency: 'USD', showChangeIndicator: true },
{ id: 'margin-percent', titleKey: 'marginPercent', value: 66.4, isPercent: true, showChangeIndicator: true },
];
```
`titleKey` is one of `revenue`, `cost`, `margin`, `marginPercent`, `cpm`, or `custom`. For `custom`, also pass `customLabel` with the display name. See [Ways to provide data](/docs/exportable-ui/overview#ways-to-provide-data) for SDK, API, backend, and static JSON options.
## Map Flexprice API data
```ts theme={null}
import { adaptMetricCards } from '@flexprice/ui';
const metrics = adaptMetricCards(costAnalytics, customAnalytics, {
show_revenue_metric: true,
show_cost_metrics: true,
show_custom_metrics: true,
});
```
## See also
Overview of all four usage components.
Line chart of usage over time, one series per feature.
Grouped, sortable table of usage and cost by feature.
# Usage Breakdown
Source: https://docs.flexprice.io/docs/exportable-ui/usage-widgets/usage-breakdown
Grouped, sortable table of usage and cost by feature
Use **Usage Breakdown** to show a detailed, sortable table of usage and cost per feature, with optional grouping. Part of [Usage Widgets](/docs/exportable-ui/usage-widgets/usage-widgets).
```tsx theme={null}
```
## Supports
* Sort by total usage or total cost
* Grouping (features roll up under a group, with an ungrouped bucket for the rest)
* Expand / collapse all groups
* Custom units per row
* Loading skeleton state
* Empty state when there is no data
## Best used for
* Usage dashboards
* Customer portal usage tab
* Cost analysis pages
## Import
```tsx theme={null}
import '@flexprice/ui/style.css';
import { UsageBreakdown, type UsageBreakdownRow } from '@flexprice/ui';
```
## Usage
```tsx theme={null}
const rows: UsageBreakdownRow[] = [
{ id: 'feat_api_calls', name: 'API Calls', groupId: 'grp_core', groupName: 'Core', totalUsage: 8200, unit: 'calls', totalCost: 41, currency: 'USD' },
{ id: 'feat_storage', name: 'Storage', totalUsage: 42, unit: 'GB', totalCost: 12.5, currency: 'USD' },
];
```
Rows without a `groupId` render in an "ungrouped" bucket. See [Ways to provide data](/docs/exportable-ui/overview#ways-to-provide-data) for SDK, API, backend, and static JSON options.
## Map Flexprice API data
```ts theme={null}
import { adaptUsageBreakdownRows } from '@flexprice/ui';
const rows = adaptUsageBreakdownRows(analytics.items);
```
## See also
Overview of all four usage components.
Grid of revenue, cost, margin, and custom usage metrics.
Line chart of usage over time, one series per feature.
# Usage Quota
Source: https://docs.flexprice.io/docs/exportable-ui/usage-widgets/usage-quota
Progress bars showing current usage against plan limits for each metered feature
Use **Usage Quota** to show a customer how much of each metered feature they've used against its plan limit, as a set of progress bars. Part of [Usage Widgets](/docs/exportable-ui/usage-widgets/usage-widgets).
```tsx theme={null}
```
## Supports
* One progress bar per metered feature
* Unlimited features (rendered without a limit)
* Over-limit styling
* Empty state when there is nothing to show
## Best used for
* Customer portal overview
* Account settings / usage tab
* Upgrade prompts near a limit
## Import
```tsx theme={null}
import '@flexprice/ui/style.css';
import { UsageQuota, type UsageQuotaItem } from '@flexprice/ui';
```
## Usage
```tsx theme={null}
const items: UsageQuotaItem[] = [
{ id: 'feat_api_calls', name: 'API Calls', currentUsage: 8200, limit: 10000, isUnlimited: false },
{ id: 'feat_storage', name: 'Storage', currentUsage: 42, limit: null, isUnlimited: true },
];
```
Only metered features have a quota to show: filter your data to metered entitlements before mapping into `items`. See [Ways to provide data](/docs/exportable-ui/overview#ways-to-provide-data) for SDK, API, backend, and static JSON options.
## Map Flexprice API data
```ts theme={null}
import { adaptUsageQuotaItems } from '@flexprice/ui';
const items = adaptUsageQuotaItems(usageSummary.features);
```
## See also
Overview of all four usage components.
Grouped, sortable table of usage and cost by feature.
Grid of revenue, cost, margin, and custom usage metrics.
# Usage Trend Chart
Source: https://docs.flexprice.io/docs/exportable-ui/usage-widgets/usage-trend-chart
Line chart of usage over time, one series per feature or event source
Use **Usage Trend Chart** to plot usage over time. Each series draws its own line, so you can compare multiple features or event sources on the same chart. Part of [Usage Widgets](/docs/exportable-ui/usage-widgets/usage-widgets).
```tsx theme={null}
```
## Supports
* Multiple series (one line per feature or source)
* Custom time ranges (pass whatever window of points you want)
* Zoom and reset
* Loading skeleton state
* Empty state when there is no data
## Best used for
* Usage dashboards
* Customer portal usage tab
* Account health pages
## Import
```tsx theme={null}
import '@flexprice/ui/style.css';
import { UsageTrendChart, type UsageTrendSeries } from '@flexprice/ui';
```
## Usage
```tsx theme={null}
const series: UsageTrendSeries[] = [
{
id: 'feat_api_calls',
name: 'API Calls',
points: [
{ timestamp: '2026-01-01T00:00:00Z', usage: 320 },
{ timestamp: '2026-01-02T00:00:00Z', usage: 410 },
{ timestamp: '2026-01-03T00:00:00Z', usage: 380 },
],
},
];
```
Filtering by feature (include or exclude a list of feature IDs) is handled before the data reaches the component: filter your `series` array, or filter at the adapter step below. See [Ways to provide data](/docs/exportable-ui/overview#ways-to-provide-data) for SDK, API, backend, and static JSON options.
## Map Flexprice API data
```ts theme={null}
import { adaptUsageTrendSeries } from '@flexprice/ui';
const series = adaptUsageTrendSeries(analytics.items, { feature_filter_mode: 'all' });
```
Pass `feature_filter_mode: 'include_list'` or `'exclude_list'` with `feature_ids` to filter which features appear.
## See also
Overview of all four usage components.
Grid of revenue, cost, margin, and custom usage metrics.
Grouped, sortable table of usage and cost by feature.
# Usage Widgets
Source: https://docs.flexprice.io/docs/exportable-ui/usage-widgets/usage-widgets
Metric cards, trend chart, breakdown table, and quota bars for showing customer usage
**Usage Widgets** are four independent components for showing a customer's usage: [Metric Cards](/docs/exportable-ui/usage-widgets/metric-cards), [Usage Trend Chart](/docs/exportable-ui/usage-widgets/usage-trend-chart), [Usage Breakdown](/docs/exportable-ui/usage-widgets/usage-breakdown), and [Usage Quota](/docs/exportable-ui/usage-widgets/usage-quota). Use one on its own, or combine them into a usage dashboard.
Each component is presentational and takes its own prop shape. There is no shared parent component: render the ones you need, in whatever layout fits your page.
## Components
Grid of revenue, cost, margin, and custom usage metrics.
Line chart of usage over time, one series per feature.
Grouped, sortable table of usage and cost by feature.
Progress bars for each metered feature against its plan limit.
## Import
```tsx theme={null}
import '@flexprice/ui/style.css';
import { MetricCards, UsageTrendChart, UsageBreakdown, UsageQuota } from '@flexprice/ui';
```
Each component page documents its own prop shape, an example, and the adapter for mapping Flexprice usage analytics API responses.
## See also
Install Flexprice UI, choose a data source, theme, and integrate components.
Wallet balance and transaction history components.
# Architecture
Source: https://docs.flexprice.io/docs/getting-started/architecture
How Flexprice is engineered for scale, reliability, and data integrity — the infrastructure, data flows, failure modes, and recovery guarantees behind the platform.
Flexprice is a usage metering, pricing, and billing engine that sits between your application and your payment providers. You stream usage events in, configure how each event, feature, or model is priced, and Flexprice handles everything downstream — metering, credit balances, entitlements, invoicing, and settlement — then plugs into your gateway of choice.
This document describes how the platform is built: its components, how data moves through them, what happens when something fails, and how the system recovers. It is written for the engineers and architects who need to satisfy themselves that Flexprice can carry production billing traffic.
## Design principles
The architecture is shaped by a small set of decisions that hold across every layer.
Every billing outcome is derived from raw usage events. Those events are persisted redundantly and retained for replay, so any downstream state can be rebuilt from first principles.
No single component failure causes data loss. Each stage of the pipeline has an independent durable store that absorbs the failure of the stage after it.
Every component is region-restricted. Data for a region never leaves it, and each region runs an independent stack.
The same artifacts that run Flexprice Cloud ship as Helm charts. Every dependency is open source and swappable, so a dedicated deployment is the cloud architecture, not a fork.
## System architecture
The platform is fully containerized. A single codebase runs in three modes — **API**, **Consumer**, and **Background Worker** — behind a load balancer, with a private data tier (multi-AZ) and a set of external components for analytics, orchestration, and webhook delivery.
Only the API service is exposed to the internet. It sits behind an Application Load Balancer and a WAF in public subnets; every database, broker, and cache lives in private subnets, across multiple availability zones, with no inbound internet route.
### Runtime services
All three services are the same image, started in different roles. This keeps deployment, versioning, and operational tooling uniform.
| Service | Responsibility |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **API** | The only internet-facing component. Authenticates and validates requests, serves the 200+ REST endpoints, writes transactional state to PostgreSQL, and publishes events to Kafka. |
| **Consumer** | Reads from Kafka and processes events asynchronously — usage ingestion, enrichment against PostgreSQL, writes to ClickHouse, alerting, and webhook fan-out. |
| **Background Worker** | Executes durable, long-running workflows on Temporal: billing cycles, scheduled jobs, retries, and multi-step operations that require state and guaranteed completion. |
### Data stores
Each store is chosen for one job and isolated to it. Every required component has an open-source equivalent that ships in the self-hosted charts. The one managed-cloud service below — DynamoDB — is optional and used only in Flexprice Cloud.
| Store | Role | Why this choice |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **PostgreSQL** (RDS) | System of record: customers, subscriptions, plans, pricing, entitlements, invoices, audit log. | Strong consistency and transactional integrity for configuration and financial state. |
| **ClickHouse** | Event store and analytics engine: raw events, enriched events, aggregations. | Column-oriented OLAP built for high-volume ingestion and sub-second aggregation over billions of rows. |
| **Kafka** (MSK) | Event backbone between API and Consumer. Multi-broker, multi-AZ. | Decouples ingestion from processing, buffers traffic spikes, and guarantees ordered, replayable delivery. |
| **Redis** (ElastiCache) | Hot-path cache for balances and frequently read configuration. | Sub-millisecond reads that keep latency-sensitive checks off the primary databases. |
| **DynamoDB** *(Flexprice Cloud only — optional)* | An additional ingestion-redundancy buffer for replay and recovery. | The most resilient redundancy layer in Flexprice Cloud: a fully managed, always-available key-value sink that survives even when the rest of the pipeline is degraded. It is not part of the core architecture — self-hosted and dedicated deployments run without it. |
| **S3** | Invoice PDFs, generated reports, scheduled exports, and long-term event archival. | Cheap, durable object storage for artifacts and cold data. |
DynamoDB is the only managed-cloud component in the architecture, and it is entirely optional. Flexprice uses it in Flexprice Cloud as the most resilient form of data-redundancy store. Self-hosted and dedicated deployments do not include it and require no cloud-specific component — durability is already guaranteed by Kafka's replayable log, the SDK's retries, and the S3 degraded-mode fallback.
## Event ingestion pipeline
Ingestion is the most infrastructure-heavy part of the system, because it is the part that must never lose data. Everything downstream — balances, invoices, analytics, reconciliation — is reconstructable as long as the events survive.
### Ingestion modes
You choose how events reach Flexprice based on how your systems are already built.
Server-side SDKs in all popular languages send events directly. The SDK runs in sync mode with configurable retries and fallback handling built in.
A Flexprice collector runs inside your infrastructure, pulls from your existing event bus, applies custom transformations to your internal format, and forwards to Flexprice.
For systems that prefer to call Flexprice directly, every ingestion path is also a plain authenticated REST endpoint.
### Ingestion flow
The receive path is deliberately lightweight. The API performs only static validation — a well-formed payload on an authenticated endpoint — then writes the event to Kafka, the durable backbone, before acknowledging. In Flexprice Cloud the API additionally writes to DynamoDB as an optional redundancy buffer; self-hosted deployments rely on Kafka's replayable log alone. Heavier work (enrichment, aggregation, ClickHouse writes) happens asynchronously off the Kafka stream, so a spike in volume never slows the acknowledgement path.
On the consumer side, events land in ClickHouse twice: a **raw events** table that is the immutable base for replay and reconciliation, and a **processed events** table where each event is enriched with the customer, subscription, feature, meter, price, and line item it maps to. Every event is traceable end to end, down to the exact entities it was billed against.
## Reliability and failure modes
The pipeline is designed so that the failure of any one component degrades gracefully and loses nothing. Each stage is backed by an independent durable store that absorbs the failure of the stage after it.
| Scenario | Behavior | Recovery |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Kafka unavailable** | The SDK's retries and the S3 degraded-mode fallback hold the events; in Flexprice Cloud they are also persisted to DynamoDB. The API surfaces the failure rather than dropping it silently. | Once Kafka recovers, replay jobs drain the buffered events — the S3 fallback in any deployment, plus DynamoDB in Flexprice Cloud — back into the pipeline. |
| **ClickHouse unavailable** | Events accumulate in Kafka; the consumer pauses. | The consumer resumes and replays the backlog from Kafka when ClickHouse returns. |
| **Flexprice fully unreachable** | After SDK retries are exhausted, an optional degraded mode writes each event to a customer-owned S3 bucket, keyed by event ID with the exact payload. | Server-to-server retry jobs read the bucket and re-ingest every event, then clear it. |
| **Duplicate delivery** | Events are idempotent on event ID, so retries and replays converge to the same state. | No manual intervention — deduplication is intrinsic. |
| **Bad data from upstream** | Raw events are retained untouched, separate from processed state. | Affected events can be corrected and replayed from the retained history. |
### Data recovery and replay
Because events are the currency of the system, they are retained well beyond their processing lifetime. In Flexprice Cloud, events held in DynamoDB are retained for up to one year and then archived to S3, giving **point-in-time replay** across the entire window; self-hosted deployments achieve the same replay from Kafka's retained log and S3 archival. If any downstream store is lost or corrupted, it can be rebuilt by replaying the retained events — no derived state is ever the only copy of anything.
The degraded-mode S3 fallback and its retry jobs are an opt-in, per-customer configuration deployed for enterprise workloads. It requires granting Flexprice server-to-server read access to the bucket.
## Real-time balances and alerting
The most latency-sensitive question in usage billing is *does this customer have balance to perform this action?* Flexprice answers it without forcing you onto its critical path.
### How balances are computed
Balances are never stored as a separate mutable number — they are derived from usage in ClickHouse. Every incoming event is rolled up into materialized views and pre-aggregated tables, so the current balance is a fast aggregation query rather than a running counter that can drift.
The **fetch-balance API** lets the caller decide the freshness it needs. Rather than a fixed server-side TTL, the caller specifies a maximum acceptable age per request: if the cached value is within that age it is returned immediately from cache; if it is staler, the value is recomputed from ClickHouse. Critical surfaces — the billing page, the customer portal — always read the live value.
### Push-based alerts
Most customers never query Flexprice in their hot path at all. Every event enqueues a per-customer aggregation that fires at most **once per customer per minute**, and that single trigger drives:
* Low-balance alerts
* Auto top-ups
* Entitlement-exhaustion alerts
These are pushed back to your event bus — API, Kafka, SNS, or SMS — as they happen. The common pattern is for customers to maintain a simple `has_balance` flag per customer in their own Redis, updated from these alerts, and gate actions on that flag. **Flexprice is never in the critical path of the decision.**
### Freshness guarantees
| Tier | Guarantee | How |
| -------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| **Standard** | Balances reconciled within 5 minutes | A fallback cron sweeps the trailing 5-minute window and triggers alerts. |
| **Enterprise (dedicated)** | Sub-minute SLA, tuned to requirement | Achieved by scaling ingestion parallelism and ClickHouse compute — the two levers that set end-to-end latency. |
## Service-level agreements
Flexprice publishes explicit availability and latency targets for the API service — the only internet-facing component — measured continuously and reported per region. Two operations carry their own latency objectives because they sit in the customer's hot path: checking entitlement and wallet balance, and reporting usage measurements.
### Availability
| Plan | Uptime SLA | Maximum downtime / month |
| ---------------------- | ---------- | ------------------------ |
| Standard and Premium | 99.95% | ≈ 21.9 minutes |
| Enterprise (dedicated) | 99.99% | ≈ 4.4 minutes |
Availability is measured per region in five-minute intervals as `1 − (failed requests ÷ total requests)` against the API service, aggregated over a calendar month. Failed requests are server-side `5xx` responses originating from Flexprice. The calculation excludes scheduled maintenance announced in advance, errors caused by client misuse (`4xx`, invalid payloads, throttling), and disruption outside Flexprice's control. The contractual SLA and any associated service credits are defined in your agreement.
### Latency targets
| Operation | Objective | Measured at |
| --------------------------------------------- | ----------------- | -------------------------------------------------------------------- |
| Entitlement / balance check | **P95 \< 500 ms** | API service (server-side), per region, excluding client network time |
| Usage measurement reporting (event ingestion) | **P95 ≈ 200 ms** | API service (server-side), per region, excluding client network time |
The balance check is the path most customers gate actions on. The ingestion target reflects the lightweight receive path described above: the API acknowledges after the durable write and defers enrichment off the Kafka stream. Response latency is distinct from balance freshness — how recently usage is reflected in the returned number — which is governed by the [freshness tiers](#freshness-guarantees) above, and can be tightened to sub-minute on enterprise dedicated deployments.
## Multi-region and data residency
The architecture is multi-region by default, with stacks in **US, India, and EU**. Every component in a region is restricted to that region, and the managed dependencies are configured against the matching regional cloud. Data for a region is processed and stored only within it, which lets enterprise deployments satisfy residency requirements without bespoke engineering.
## Observability
The entire platform is OpenTelemetry-native and streams both traces and logs. You can point it at your own OTel-compatible provider, so Flexprice telemetry lands alongside the rest of your stack rather than in a silo. For enterprise deployments, the internal dashboards are shared as exportable definitions so you start with the same operational view the Flexprice team uses.
## Analytics and reconciliation
Billing systems live or die on whether their numbers can be independently verified. Flexprice exposes its data at several levels so you can reconcile however you prefer.
ClickHouse (real-time event data) and PostgreSQL (subscriptions, invoices, configuration) are exposed over read-only connections to a BI tool such as Metabase, giving you full SQL access to build any reconciliation or analytics workflow.
A summarized customer-level view — usage, wallet balance, and the subscription, price, meter, feature, and line item every figure derives from — available out of the box without standing up a BI stack. The same API powers the built-in customer portal.
Hourly exports of processed event rows, fully enriched with their meter, feature, and price mappings, delivered to your S3 as CSV or JSON for ingestion into your own systems.
Every entity — meters, prices, features, customers, and more — exposes complete CRUD APIs. Any workflow Flexprice runs internally can be rebuilt on your side.
Invoices act as immutable checkpoints: each generated invoice snapshots aggregate usage and charges per customer and feature, so historical periods never require re-scanning the full event history. In parallel, every state change — entity created, updated, deleted — is written to a system-events audit table, streamed through Kafka, and delivered as webhooks via Svix to whatever endpoints you subscribe.
## Deployment models
The same architecture is delivered three ways, with no divergence in code between them.
Fully managed, multi-region SaaS. Flexprice operates the entire stack.
A single-tenant deployment in your own infrastructure, operated to an agreed SLA. Identical architecture, isolated to you.
Deploy with public Helm charts on Kubernetes, or on ECS. Open-source dependencies are bundled and can be swapped for your existing managed services.
Releases are versioned and tagged, so upgrades are explicit and reversible. Because every deployment model runs the same containers against the same dependencies, what is validated in the cloud is exactly what runs in a dedicated or self-hosted environment.
# Flexprice Cloud
Source: https://docs.flexprice.io/docs/getting-started/cloud
Learn how to quickly get started with Flexprice Cloud - the fastest way to integrate usage-based pricing
## Quick Setup
Getting started with Flexprice Cloud is straightforward and requires no infrastructure setup. Follow these simple steps:
1. Visit [Flexprice Cloud](https://admin.flexprice.io/auth)
2. Sign up using either:
* Google account
* Email and password
That's it! Your Flexprice Cloud instance is ready to use.
## Understanding Environments
When you first log in, you'll be placed in the **Sandbox** environment. Flexprice provides different environments to help you manage your pricing across different stages:
A pre-configured environment with Cursor pricing template for you to experiment and learn
Your live environment for real customer data and billing
## Exploring the Sample Setup
The Sandbox environment comes pre-configured with Cursor's pricing model, giving you a practical example of how to structure complex pricing:
Explore the detailed guide on how Cursor's pricing is implemented in Flexprice
## Setting up API Keys
To integrate Flexprice with your application, you'll need to generate an API key:
1. Navigate to the **Developers** tab
2. Click **Add** to create a new API key
3. Configure your key:
* **Name**: Give it a descriptive name (e.g., "Development")
* **Permissions**: Select "Read & Write"
* **Expiration**: Choose "Never" for long-term use
4. Click **Create**
5. **Important**: Copy your API key immediately - it will only be shown once!
### Using Your API Key
Include your API key in all requests to Flexprice API:
```bash theme={null}
curl -X POST https://api.cloud.flexprice.io/v1/customers \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Example Customer", "external_id": ""}'
```
Keep your API key secure and never expose it in client-side code or public repositories.
## Need Help?
If you need assistance or have questions:
* Email us at [support@flexprice.io](mailto:support@flexprice.io)
# Configuration
Source: https://docs.flexprice.io/docs/getting-started/configuration
Configure your self-hosted Flexprice instance
## Configuration Methods
Flexprice can be configured using:
1. Environment variables (recommended)
2. Configuration files
3. Command-line arguments (for specific overrides)
## Environment Variables
Environment variables are the recommended way to configure Flexprice. All environment variables are prefixed with `FLEXPRICE_`. Here's a comprehensive list of available variables:
### Deployment Configuration
| Variable | Description | Default | Required |
| --------------------------- | ------------------------------------------------------------------------------------------------- | ------- | -------- |
| `FLEXPRICE_DEPLOYMENT_MODE` | Deployment mode (local, api, consumer, temporal\_worker, aws\_lambda\_api, aws\_lambda\_consumer) | `local` | No |
| `FLEXPRICE_SERVER_ADDRESS` | Server address and port | `:8080` | No |
### Authentication Configuration
| Variable | Description | Default | Required |
| ------------------------------------- | --------------------------------------------- | ----------- | ---------------------- |
| `FLEXPRICE_AUTH_PROVIDER` | Authentication provider (flexprice, supabase) | `flexprice` | No |
| `FLEXPRICE_AUTH_SECRET` | Secret key for authentication | - | Yes |
| `FLEXPRICE_AUTH_SUPABASE_BASE_URL` | Supabase base URL (if using Supabase auth) | - | Only if using Supabase |
| `FLEXPRICE_AUTH_SUPABASE_SERVICE_KEY` | Supabase service key | - | Only if using Supabase |
| `FLEXPRICE_AUTH_API_KEY_HEADER` | Header name for API key authentication | `x-api-key` | No |
| `FLEXPRICE_AUTH_API_KEY_KEYS` | JSON string of API keys configuration | - | No |
### Database Configuration
| Variable | Description | Default | Required |
| --------------------------------- | --------------------------------------- | -------------- | -------- |
| `FLEXPRICE_POSTGRES_HOST` | PostgreSQL host | `localhost` | No |
| `FLEXPRICE_POSTGRES_PORT` | PostgreSQL port | `5432` | No |
| `FLEXPRICE_POSTGRES_USER` | PostgreSQL username | `flexprice` | No |
| `FLEXPRICE_POSTGRES_PASSWORD` | PostgreSQL password | `flexprice123` | Yes |
| `FLEXPRICE_POSTGRES_DBNAME` | PostgreSQL database name | `flexprice` | No |
| `FLEXPRICE_POSTGRES_SSLMODE` | PostgreSQL SSL mode (disable, require) | `disable` | No |
| `FLEXPRICE_POSTGRES_AUTO_MIGRATE` | Automatically run migrations on startup | `false` | No |
### ClickHouse Configuration
| Variable | Description | Default | Required |
| ------------------------------- | --------------------------------- | ---------------- | -------- |
| `FLEXPRICE_CLICKHOUSE_ADDRESS` | ClickHouse address (host:port) | `localhost:9000` | No |
| `FLEXPRICE_CLICKHOUSE_TLS` | Use TLS for ClickHouse connection | `false` | No |
| `FLEXPRICE_CLICKHOUSE_USERNAME` | ClickHouse username | `flexprice` | No |
| `FLEXPRICE_CLICKHOUSE_PASSWORD` | ClickHouse password | `flexprice123` | Yes |
| `FLEXPRICE_CLICKHOUSE_DATABASE` | ClickHouse database name | `flexprice` | No |
### Kafka Configuration
| Variable | Description | Default | Required |
| -------------------------------- | ------------------------------------------- | -------------------------- | ----------------------- |
| `FLEXPRICE_KAFKA_BROKERS` | Kafka brokers (comma-separated) | `localhost:29092` | No |
| `FLEXPRICE_KAFKA_CONSUMER_GROUP` | Kafka consumer group ID | `flexprice-consumer-local` | No |
| `FLEXPRICE_KAFKA_TOPIC` | Kafka topic for events | `events` | No |
| `FLEXPRICE_KAFKA_USE_SASL` | Use SASL authentication for Kafka | `false` | No |
| `FLEXPRICE_KAFKA_SASL_MECHANISM` | SASL mechanism (PLAIN, SCRAM-SHA-256, etc.) | - | Only if SASL is enabled |
| `FLEXPRICE_KAFKA_SASL_USER` | SASL username | - | Only if SASL is enabled |
| `FLEXPRICE_KAFKA_SASL_PASSWORD` | SASL password | - | Only if SASL is enabled |
| `FLEXPRICE_KAFKA_CLIENT_ID` | Kafka client ID | `flexprice-client-local` | No |
### Temporal Configuration
| Variable | Description | Default | Required |
| --------------------------------- | ------------------------------- | -------------------- | ---------------------------- |
| `FLEXPRICE_TEMPORAL_ADDRESS` | Temporal service address | `localhost:7233` | No |
| `FLEXPRICE_TEMPORAL_TLS` | Use TLS for Temporal connection | `false` | No |
| `FLEXPRICE_TEMPORAL_NAMESPACE` | Temporal namespace | `default` | No |
| `FLEXPRICE_TEMPORAL_TASK_QUEUE` | Temporal task queue | `billing-task-queue` | No |
| `FLEXPRICE_TEMPORAL_API_KEY` | Temporal API key | - | Only if using Temporal Cloud |
| `FLEXPRICE_TEMPORAL_API_KEY_NAME` | Temporal API key name | - | Only if using Temporal Cloud |
### Logging and Monitoring
| Variable | Description | Default | Required |
| ------------------------------ | ---------------------------------------- | ------------- | ------------------------- |
| `FLEXPRICE_LOGGING_LEVEL` | Logging level (debug, info, warn, error) | `info` | No |
| `FLEXPRICE_SENTRY_ENABLED` | Enable Sentry error reporting | `false` | No |
| `FLEXPRICE_SENTRY_DSN` | Sentry DSN | - | Only if Sentry is enabled |
| `FLEXPRICE_SENTRY_ENVIRONMENT` | Sentry environment | `development` | No |
| `FLEXPRICE_SENTRY_SAMPLE_RATE` | Sentry sampling rate (0.0-1.0) | `1.0` | No |
### DynamoDB Configuration
| Variable | Description | Default | Required |
| ------------------------------------- | ------------------------------ | ----------- | --------------------------- |
| `FLEXPRICE_DYNAMODB_IN_USE` | Use DynamoDB for event storage | `false` | No |
| `FLEXPRICE_DYNAMODB_REGION` | AWS region for DynamoDB | `us-east-1` | Only if DynamoDB is enabled |
| `FLEXPRICE_DYNAMODB_EVENT_TABLE_NAME` | DynamoDB table name for events | `events` | Only if DynamoDB is enabled |
### Webhook Configuration
| Variable | Description | Default | Required |
| ------------------------------------ | ---------------------------------------- | ------- | -------- |
| `FLEXPRICE_WEBHOOK_MAX_RETRIES` | Maximum number of webhook retry attempts | `3` | No |
| `FLEXPRICE_WEBHOOK_INITIAL_INTERVAL` | Initial retry interval (e.g., 1s) | `1s` | No |
| `FLEXPRICE_WEBHOOK_MAX_INTERVAL` | Maximum retry interval (e.g., 10s) | `10s` | No |
| `FLEXPRICE_WEBHOOK_MULTIPLIER` | Backoff multiplier for retries | `2.0` | No |
| `FLEXPRICE_WEBHOOK_MAX_ELAPSED_TIME` | Maximum total retry time (e.g., 2m) | `2m` | No |
### Event Publishing
| Variable | Description | Default | Required |
| ------------------------------------- | --------------------------------------------------- | ------- | -------- |
| `FLEXPRICE_EVENT_PUBLISH_DESTINATION` | Event publishing destination (kafka, dynamodb, all) | `kafka` | No |
## Using a .env File
For local development, you can use a `.env` file to set environment variables. Create a file named `.env` in the root directory of your project:
```bash theme={null}
# Deployment Configuration
FLEXPRICE_DEPLOYMENT_MODE=local
FLEXPRICE_SERVER_ADDRESS=":8080"
# Authentication Configuration
FLEXPRICE_AUTH_PROVIDER="flexprice"
FLEXPRICE_AUTH_SECRET="your_secure_secret_key"
FLEXPRICE_AUTH_API_KEY_HEADER="x-api-key"
# Database Configuration
FLEXPRICE_POSTGRES_HOST=localhost
FLEXPRICE_POSTGRES_PORT=5432
FLEXPRICE_POSTGRES_USER=flexprice
FLEXPRICE_POSTGRES_PASSWORD=flexprice123
FLEXPRICE_POSTGRES_DBNAME=flexprice
FLEXPRICE_POSTGRES_SSLMODE=disable
FLEXPRICE_POSTGRES_AUTO_MIGRATE=true
# ClickHouse Configuration
FLEXPRICE_CLICKHOUSE_ADDRESS=localhost:9000
FLEXPRICE_CLICKHOUSE_TLS=false
FLEXPRICE_CLICKHOUSE_USERNAME=flexprice
FLEXPRICE_CLICKHOUSE_PASSWORD=flexprice123
FLEXPRICE_CLICKHOUSE_DATABASE=flexprice
# Kafka Configuration
FLEXPRICE_KAFKA_BROKERS=localhost:29092
FLEXPRICE_KAFKA_CONSUMER_GROUP=flexprice-consumer-local
FLEXPRICE_KAFKA_TOPIC=events
# Logging Configuration
FLEXPRICE_LOGGING_LEVEL=debug
```
When using Docker Compose, you can specify environment variables in your `docker-compose.yml` file or use the `.env` file directly.
## Configuration File
For more complex configurations, you can use a YAML configuration file. By default, Flexprice looks for a file named `config.yaml` in the `internal/config` directory:
```yaml theme={null}
deployment:
mode: "local" # "local", "docker", "production"
server:
address: ":8080"
auth:
provider: "flexprice" # "flexprice" or "supabase"
secret: "your_secure_secret_key"
supabase:
base_url: "http://localhost:54321"
service_key: ""
api_key:
header: "x-api-key"
keys:
"your_api_key_hash":
tenant_id: "00000000-0000-0000-0000-000000000000"
user_id: "00000000-0000-0000-0000-000000000000"
name: "Dev API Keys"
is_active: true
kafka:
brokers: "localhost:29092"
consumer_group: "flexprice-consumer-local"
topic: "events"
use_sasl: false
sasl_mechanism: ""
sasl_user: ""
sasl_password: ""
client_id: "flexprice-client-local"
clickhouse:
address: "localhost:9000"
tls: false
username: "flexprice"
password: "flexprice123"
database: "flexprice"
postgres:
host: "localhost"
port: 5432
user: "flexprice"
password: "flexprice123"
dbname: "flexprice"
sslmode: "disable"
auto_migrate: false
temporal:
address: "localhost:7233"
tls: false
namespace: "default"
task_queue: "billing-task-queue"
```
Environment variables take precedence over configuration file settings. If an environment variable is set, it will override the corresponding value in the configuration file.
## Production Best Practices
When configuring Flexprice for production, follow these best practices:
### Security
1. **Use strong, unique passwords** for all database users
2. **Enable SSL/TLS** for all external connections
3. **Set up a firewall** to restrict access to your servers
4. **Use a secure API key** for authentication
5. **Rotate secrets regularly** to maintain security
### Performance
1. **Allocate sufficient resources** to each component based on your expected load
2. **Monitor resource usage** and scale as needed
3. **Configure appropriate Kafka settings** for your message volume
4. **Adjust database connection pools** based on your workload
### High Availability
1. **Set up database replication** for PostgreSQL and ClickHouse
2. **Deploy multiple API server instances** behind a load balancer
3. **Configure Kafka with multiple brokers** in a cluster
4. **Implement automated backups** for all data stores
# Go-Live Checklist
Source: https://docs.flexprice.io/docs/getting-started/go-live-checklist
Complete this before switching to Production.
## 1. Environment & API Access
* Switch to the Production environment in the Flexprice dashboard.
* Generate a Production API key (Read & Write) and copy it immediately.
* Confirm auth header is `x-api-key: ` and base URL matches your region: `https://us.api.flexprice.io` (US) or `https://api.cloud.flexprice.io` (India).
* Revoke Sandbox API keys for any system now pointing to Production.
## 2. Product Catalog
* Clone your catalog from Sandbox via the Plans dashboard and verify it transferred correctly.
* Confirm all metered feature Event Names are active in Production and match exactly what your app sends. They are immutable after creation.
## 3. Event Ingestion
* Verify required fields on every event: `event_name` (case-sensitive), `external_customer_id`, `timestamp` (ISO 8601 UTC), `event_id` (unique).
* Send test events and confirm they appear in the Event Debugger.
An `event_name` mismatch returns HTTP 202 but is never counted toward billing. No error is thrown.
## 4. Customers & Subscriptions
* Create Production customers and confirm `external_customer_id` matches what your event pipeline sends.
* Create subscriptions on the correct Production plans.
## 5. Invoicing
* Review `subscription_config.grace_period_days` and confirm the auto-cancel behavior is correct.
* Decide auto-collect vs. manual invoicing before your first billing cycle runs.
* Generate a test invoice and verify line items match expected usage.
## 6. Webhooks
* Register Production webhook endpoints and implement signature verification.
* Subscribe to: `invoice.update.finalized`, `invoice.update.payment`, `subscription.created`, `subscription.cancelled`.
* Return 2xx for all received events. A 4xx permanently stops retries for that event.
## 7. Smoke Test
1. Send test events for a Production test customer.
2. Confirm meter aggregation matches what you sent.
3. Finalize the draft invoice and verify line items.
4. Confirm `invoice.update.finalized` fires and your handler processes it.
## 8. Post-Launch
* Subscribe to the [Flexprice changelog](/changelog) for API changes.
* Subscribe to the [status page](https://status.flexprice.io) for incident notifications.
* Join the [Slack community](https://join.slack.com/t/flexpricecommunity/shared_invite/zt-39uat51l0-n8JmSikHZP~bHJNXladeaQ) for support.
# Self-hosting on AWS
Source: https://docs.flexprice.io/docs/getting-started/self-hosting-aws
Complete guide to deploy Flexprice on AWS with ECS, Aurora PostgreSQL, MSK, EKS, and Redis
This guide provides a comprehensive, step-by-step walkthrough for self-hosting Flexprice on **AWS** in a production-ready setup. It covers VPC networking, ECS compute (EC2 with ARM64), Aurora PostgreSQL, Amazon MSK (Kafka), EKS with ClickHouse, ElastiCache Redis, DynamoDB, IAM, secrets management, and observability.
## Prerequisites
Before you begin, ensure you have the following:
An [AWS account](https://aws.amazon.com/) with administrator or equivalent
permissions to create VPCs, ECS, RDS, MSK, EKS, S3, IAM roles, and CloudWatch
resources
[AWS CLI v2](https://aws.amazon.com/cli/) installed and configured with
credentials (`aws configure`)
[Docker](https://www.docker.com/) installed (for building and pushing images
to ECR)
[kubectl](https://kubernetes.io/docs/tasks/tools/) installed (for
EKS/ClickHouse management)
[eksctl](https://eksctl.io/) installed (optional but recommended for EKS
cluster creation)
[Helm](https://helm.sh/) installed (for ClickHouse deployment)
### Region selection
Choose an AWS region that:
* Has all required services (see Cost estimation for the list)
* Is geographically close to your users for lower latency
* Meets your compliance requirements (e.g., GDPR for EU data)
This guide uses `us-east-1` as the example region. Replace with your preferred region.
### Cost estimation
We provide two configurations: a **development** setup for testing and a **production** setup for high-throughput workloads (100M+ events/month).
| Component | Configuration | Monthly Cost |
| ----------------------- | ----------------------------------------------------------------- | ------------------- |
| EC2 for ECS | 10x m6g.xlarge (ARM64/Graviton) | \~\$1,030 |
| Aurora PostgreSQL | 2x db.r8g.xlarge (Writer + Reader) | \~\$650 |
| Amazon MSK | 2 brokers, kafka.m5.large (4 vCPU, 8 GB), 1 TB storage per broker | \~\$350 |
| EKS + ClickHouse | Control plane + m5.8xlarge nodes | \~\$1,900 |
| ElastiCache Redis | Multi-node cluster (cache.r6g.large, cluster mode) | \~\$650 |
| DynamoDB | On-demand, \~100M events | \~\$50 |
| Storage (EBS) | 3,000 GB across components (gp3) | \~\$290 |
| ALB + NAT Gateway | 2x NAT for HA | \~\$130 |
| S3, CloudWatch, Secrets | Storage + logs | \~\$50 |
| **AWS Subtotal** | | **\~\$5,100** |
| Third-party services | Temporal Cloud, Supabase, Svix, Grafana | \~\$400 |
| **Total** | | **\~\$5,500/month** |
| Component | Configuration | Monthly Cost |
| --------------------- | ------------------------------ | --------------------- |
| ECS Fargate | 3 tasks (0.5 vCPU, 1 GB each) | \~\$80 |
| RDS PostgreSQL | db.t3.small, Single-AZ | \~\$30 |
| Amazon MSK | 2x kafka.t3.small, 100 GB each | \~\$90 |
| EKS + ClickHouse | 2x m5.large nodes | \~\$200 |
| ElastiCache Redis | cache.t3.micro | \~\$15 |
| NAT Gateway | 1 gateway | \~\$35 |
| ALB + S3 + CloudWatch | Standard | \~\$50 |
| **Total** | | **\~\$500-600/month** |
Costs vary by region and usage. Use the [AWS Pricing
Calculator](https://calculator.aws/) for accurate estimates. ARM64/Graviton
instances provide \~20% cost savings over x86.
### Sizing for 100M events/month
| Component | Development | Production (100M events/month) |
| ------------------- | ------------------------- | ------------------------------------------ |
| ECS API | 1 task, 0.5 vCPU, 1 GB | 6 tasks, 0.75 vCPU, 1.5 GB each |
| ECS Consumer | 1 task, 0.5 vCPU, 1 GB | 30 tasks, 1 vCPU, 1.75 GB each |
| ECS Temporal Worker | 1 task, 1 vCPU, 2 GB | 3 tasks, 2 vCPU, 4 GB each |
| Database | RDS db.t3.small | Aurora 2x db.r8g.xlarge |
| Kafka | 2x kafka.t3.small, 100 GB | 2 brokers, kafka.m5.large, 1 TB per broker |
| ClickHouse | 2x m5.large (8 GB) | m5.8xlarge node(s) |
| Redis | cache.t3.micro | cache.r6g.large, multi-node cluster mode |
**Traffic and storage estimates:**
* 100M events/month = \~38.5 events/second average
* Peak traffic: 150-200 events/second (4-5x burst)
* ClickHouse storage: \~50 GB/month growth
* DynamoDB: \~20 GB/month growth
***
## Architecture overview
Flexprice on AWS runs with the following production architecture:
**Data flow:**
* **Clients** → **Cloudflare** (DNS, WAF, rate limiting) → **ALB** → **ECS** (API, Consumer, Temporal Worker)
* **API** writes to **Aurora PostgreSQL**, publishes events to **MSK (Kafka)** and **DynamoDB**
* **Consumer** reads from Kafka and writes to **ClickHouse** (on EKS) for analytics
* **Temporal Worker** connects to **Temporal Cloud** for workflow orchestration
* **ElastiCache Redis** provides caching in cluster mode
* **S3** stores invoice PDFs; **CloudWatch** and **Grafana Cloud** collect logs and metrics
This guide uses **Temporal Cloud** (recommended for production). You can also
self-host Temporal, but it requires additional infrastructure. Cloudflare is
optional but recommended for DNS and WAF.
### Component summary
| Component | AWS Service | Purpose |
| ------------------ | ------------------ | -------------------------------------------- |
| Compute | ECS on EC2 (ARM64) | API, Consumer, Temporal Worker services |
| Primary Database | Aurora PostgreSQL | Transactional data, subscriptions, customers |
| Analytics Database | ClickHouse on EKS | Event analytics, usage aggregation |
| Message Queue | Amazon MSK | Event streaming between services |
| Cache | ElastiCache Redis | Session cache, rate limiting |
| Event Store | DynamoDB | Durable event storage |
| Object Storage | S3 | Invoice PDFs, exports |
| Workflow Engine | Temporal Cloud | Billing workflows, scheduled jobs |
| Authentication | Supabase | User authentication (optional) |
| Webhooks | Svix | Webhook delivery (optional) |
***
## Step 1: VPC and networking
Create a VPC with public and private subnets across two Availability Zones for high availability. Unless otherwise specified, create each resource in this guide via AWS Console, CLI, or IaC using the configuration described in the tables.
### VPC configuration
| Setting | Value | Purpose |
| ------------------------- | ------------------------------------ | ------------------------------ |
| VPC CIDR | `10.0.0.0/16` | 65,536 IP addresses |
| Availability Zones | 2 (e.g., `us-east-1a`, `us-east-1b`) | High availability |
| Public subnets | 2 (`10.0.1.0/24`, `10.0.2.0/24`) | ALB, NAT Gateway |
| Private subnets (compute) | 2 (`10.0.10.0/24`, `10.0.20.0/24`) | ECS tasks |
| Private subnets (data) | 2 (`10.0.100.0/24`, `10.0.200.0/24`) | RDS, MSK, EKS |
| NAT Gateway | 1 (or 2 for HA) | Private subnet internet access |
| Internet Gateway | 1 | Public subnet internet access |
### Create VPC with AWS CLI
Create the VPC, enable DNS hostnames, and attach an Internet Gateway.
### Create subnets
Create public and private subnets in two Availability Zones using the CIDRs in the VPC configuration table.
### Create NAT Gateway
Create an Elastic IP and NAT Gateway in a public subnet.
### Create route tables
Create public and private route tables and associate subnets (public: default route to Internet Gateway; private: default route to NAT Gateway).
### Create security groups
Create security groups for ALB, ECS, RDS, MSK, and EKS. Use the rules in the summary table below.
### Security group rules summary
| Security Group | Inbound | Source | Port(s) | Purpose |
| ------------------ | ------- | ----------- | ---------------- | ------------------------ |
| `flexprice-alb-sg` | HTTPS | `0.0.0.0/0` | 443 | Public API access |
| `flexprice-alb-sg` | HTTP | `0.0.0.0/0` | 80 | Redirect to HTTPS |
| `flexprice-ecs-sg` | TCP | `alb-sg` | 8080 | ALB to API |
| `flexprice-ecs-sg` | TCP | `ecs-sg` | All | Inter-task communication |
| `flexprice-rds-sg` | TCP | `ecs-sg` | 5432 | PostgreSQL access |
| `flexprice-msk-sg` | TCP | `ecs-sg` | 9092, 9094, 9096 | Kafka access |
| `flexprice-eks-sg` | TCP | `ecs-sg` | 9000, 8123 | ClickHouse access |
For production, consider restricting the ALB security group to only Cloudflare
IP ranges if you're using Cloudflare for DNS and WAF.
***
## Step 2: IAM roles and policies
Create IAM roles for ECS task execution and task runtime permissions.
### ECS Task Execution Role
This role allows ECS to pull container images and write logs. Create the role and attach the managed policy `AmazonECSTaskExecutionRolePolicy` plus an inline policy for Secrets Manager access.
### ECS Task Role
This role grants permissions for the Flexprice application at runtime (S3, CloudWatch Logs, Secrets Manager). Create the task role and attach the inline policy.
***
## Step 3: Secrets Manager
Store sensitive configuration in AWS Secrets Manager.
### Create secrets
Create secrets for PostgreSQL, ClickHouse, Kafka (SASL), auth, and Temporal Cloud. Store postgres (host, username, password, database), clickhouse (username, password), kafka (username, password), auth (64-char hex secret), and temporal (API key, key name, namespace) as needed.
Replace placeholder values with strong, unique credentials. Use a password
generator for production secrets.
***
## Step 4: Aurora PostgreSQL
Create an Aurora PostgreSQL cluster for Flexprice's primary database. Aurora provides higher availability and performance compared to standard RDS.
Create a DB subnet group, Aurora cluster (with Secrets Manager managed
credentials), writer instance (db.r8g.xlarge), and reader instance in the
other AZ. Retrieve the cluster writer and reader endpoints for application
configuration.
For development, use standard RDS PostgreSQL (e.g. db.t3.small) via AWS
Console, CLI, or IaC.
### Aurora configuration summary
| Setting | Development | Production |
| ---------------- | --------------- | ----------------------------------- |
| Engine | PostgreSQL 15.4 | Aurora PostgreSQL 17.4 |
| Instance class | `db.t3.small` | `db.r8g.xlarge` (4 vCPU, 32 GB) |
| Instances | 1 (Single-AZ) | 2 (Writer + Reader, Multi-AZ) |
| Storage | 100 GB gp3 | Aurora I/O-Optimized (auto-scaling) |
| Multi-AZ | No | Yes (2 zones) |
| Encryption | Enabled | Enabled |
| Backup retention | 7 days | 7 days |
| Monthly cost | \~\$30 | \~\$650 |
### Update Secrets Manager with Aurora endpoints
Update the postgres secret in Secrets Manager with the Aurora writer and reader endpoints and the managed master password ARN.
Aurora with Secrets Manager managed credentials automatically rotates the
master password. Use the `MasterUserSecret` ARN to retrieve the current
password.
### Run database migrations
You can run migrations using a one-off ECS task or from a bastion host. Create a migration task definition and run it via ECS (or run `flexprice migrate up` from a host with DB access) using the configuration described above.
***
## Step 5: Amazon MSK (Kafka)
Create an Amazon MSK cluster for event streaming.
### Create MSK configuration
Create an MSK configuration (server properties) and register it.
### Create MSK cluster
Create the MSK cluster with **2 brokers** (1 per AZ), **kafka.m5.large** (4 vCPU, 8 GB) instance type, and **1024 GB (1 TB) storage per broker**. Enable SASL/SCRAM, TLS, encryption at rest, and enhanced monitoring.
### Create SASL/SCRAM secret for MSK
Create a secret in Secrets Manager with the prefix `AmazonMSK_` and associate it with the MSK cluster.
### Get MSK bootstrap brokers
Retrieve the SASL/SCRAM bootstrap broker string from the MSK cluster (AWS Console or CLI) for application configuration.
### Create Kafka topics
Use a bastion host or an EC2 instance with Kafka CLI tools to create the `events` and `events-dlq` topics (e.g. 6 partitions, replication factor 2). Use SASL\_SSL and SCRAM-SHA-512 in client configuration.
### MSK configuration summary
| Setting | Development | Production |
| ------------------ | ---------------- | ------------------------------------- |
| Kafka version | 3.5.1 | 3.8.1 |
| Broker type | `kafka.t3.small` | `kafka.m5.large` (4 vCPU, 8 GB) |
| Number of brokers | 2 | 2 (1 per AZ) |
| Storage per broker | 100 GB | 1024 GB (1 TB) |
| Authentication | SASL/SCRAM | SASL/SCRAM + IAM |
| Encryption | TLS in transit | TLS in transit + at rest |
| Monitoring | Basic | Enhanced partition-level + Prometheus |
| Monthly cost | \~\$90 | \~\$350 |
For development, use `kafka.t3.small` with 100 GB storage. For production
(100M+ events/month), use **2 brokers**, **kafka.m5.large**, and **1 TB
storage per broker**.
***
## Step 6: EKS with ClickHouse
Create an EKS cluster and deploy ClickHouse for analytics storage. For production (100M+ events/month), use **m5.8xlarge** nodes for the ClickHouse node group.
### Create EKS cluster with eksctl
Create an EKS cluster with a managed node group (m5.8xlarge for production) via eksctl or IaC. Use private subnets and attach the EKS security group.
### Create gp3 StorageClass
Create a gp3 StorageClass (EBS CSI driver, encrypted, Retain, WaitForFirstConsumer) via kubectl or IaC.
### Create ClickHouse namespace and secrets
Create the `clickhouse` namespace and a Kubernetes secret with credentials from Secrets Manager via kubectl or IaC.
### Deploy ClickHouse with Helm
Add the Altinity ClickHouse Helm repo and install the ClickHouse Operator in the `clickhouse` namespace via Helm.
### Create ClickHouse cluster
Deploy a ClickHouseInstallation (Altinity operator) with the credentials secret, gp3 storage, and appropriate resources via kubectl or Helm.
### Create ClickHouse service for ECS access
Create a ClusterIP Service for ClickHouse (ports 9000, 8123) targeting the ClickHouse installation via kubectl or IaC.
### Get ClickHouse endpoint
For ECS tasks to access ClickHouse, you have several options:
1. **Internal NLB** (recommended): Create an internal Network Load Balancer pointing to the ClickHouse service
2. **VPC peering/Transit Gateway**: If ECS and EKS are in separate VPCs
3. **AWS PrivateLink**: For cross-account access
Create the internal NLB (type LoadBalancer with internal annotation) and use its DNS name as the ClickHouse endpoint (port 9000) for ECS configuration.
### Initialize ClickHouse database
Connect to ClickHouse (e.g. via port-forward or the NLB) and create the `flexprice` database using clickhouse-client.
***
## Step 7: ElastiCache Redis
Create an ElastiCache Redis cluster for caching and session management.
### Create Redis subnet group
Create a cache subnet group in the data subnets.
### Create Redis security group
Create a security group for Redis allowing TCP 6379 from the ECS security group.
### Create Redis replication group (cluster mode)
Create a Redis replication group with cache.r6g.large, cluster mode,
multi-node (e.g. multiple node groups for \~\$600/month), TLS and at-rest
encryption, and multi-AZ.
Create a single-node Redis cluster (cache.t3.micro).
### Redis configuration summary
| Setting | Development | Production (multi-node cluster) |
| ------------ | ---------------- | --------------------------------- |
| Node type | `cache.t3.micro` | `cache.r6g.large` (2 vCPU, 13 GB) |
| Cluster mode | Disabled | Enabled |
| Replicas | 0 | 1 per shard |
| Multi-AZ | No | Yes |
| Encryption | Optional | TLS in transit + at rest |
| Monthly cost | \~\$15 | \~\$600 |
***
## Step 8: DynamoDB
Create a DynamoDB table for durable event storage alongside ClickHouse.
### Create events table
Create a DynamoDB table named `events` with partition key `pk` (String) and sort key `sk` (String), on-demand billing.
### Enable Point-in-Time Recovery
Enable point-in-time recovery (continuous backups) on the events table.
### DynamoDB configuration summary
| Setting | Value | Notes |
| ------------- | ------------- | ---------------------------- |
| Billing mode | On-demand | Pay per request, auto-scales |
| Partition key | `pk` (String) | Tenant/customer ID |
| Sort key | `sk` (String) | Event timestamp |
| PITR | Enabled | Point-in-time recovery |
| Encryption | AWS managed | Default encryption |
| Monthly cost | \~\$50 | For \~100M events/month |
DynamoDB is used alongside ClickHouse for durable event storage. Events are
written to both DynamoDB (for durability) and ClickHouse (for analytics).
***
## Step 9: S3 and CloudWatch
### Create S3 bucket for invoices
Create an S3 bucket for invoice PDFs with versioning, AES256 encryption, block public access, and optional lifecycle rules (e.g. transition to STANDARD\_IA after 90 days).
### Create CloudWatch log groups
Create log groups for ECS services (api, worker, temporal-worker, migration) with a retention policy (e.g. 30 days).
### Create CloudWatch alarms
Create alarms for ECS API CPU, RDS CPU, and RDS connections (e.g. threshold 80%, 2 evaluation periods) and associate with an SNS topic for alerts.
***
## Step 10: ECR and container images
### Create ECR repositories
Create ECR repositories for api, worker, and temporal-worker with scan-on-push and AES256 encryption.
### Build and push images
Build Flexprice container images (api, worker, temporal-worker), authenticate to ECR, tag and push to your ECR repositories.
***
## Step 11: ECS cluster and services
### Create ECS cluster
For production (100M+ events/month), create an ECS cluster with EC2
capacity: launch template with **m6g.xlarge** (ARM64/Graviton), Auto Scaling
Group with **10 nodes** (min/max as needed), capacity provider with managed
scaling, and associate with the cluster.
For development, create an ECS cluster with Fargate and FARGATE\_SPOT
capacity providers.
### Create API task definition
Register an ECS task definition for the API service: production uses EC2/ARM64 (768 CPU, 1536 memory) with bridge network; development uses Fargate (1024 CPU, 2048 memory). Include environment variables and secrets from Secrets Manager (auth, postgres, clickhouse, kafka, temporal). Set FLEXPRICE\_DEPLOYMENT\_MODE=api, health check on :8080/health, and CloudWatch log group. See Step 12 for the full environment variable reference.
### Create Worker task definition
Register an ECS task definition for the Consumer (worker) service: FLEXPRICE\_DEPLOYMENT\_MODE=consumer, postgres/clickhouse/kafka secrets from Secrets Manager. For production use **30 tasks** (100M events/month). See Step 12 for environment variables.
### Create Temporal Worker task definition
Register an ECS task definition for the Temporal Worker: FLEXPRICE\_DEPLOYMENT\_MODE=temporal\_worker, postgres/clickhouse/kafka/temporal secrets. See Step 12 for environment variables.
### Create Application Load Balancer
Create an internet-facing Application Load Balancer in the public subnets, a target group (HTTP 8080, health check /health), an HTTPS listener with an ACM certificate, and an HTTP listener that redirects to HTTPS.
### Create ECS services
Create ECS services for API (desired count **6** for production), Worker/Consumer (desired count **30** for production), and Temporal Worker (e.g. 3 tasks). Attach the API service to the ALB target group. Use private subnets and the ECS security group.
### Configure Auto Scaling
Register scalable targets and target-tracking scaling policies for the API (and optionally Worker) services (e.g. min/max desired count, CPU target 70%).
***
## Step 12: Environment variables reference
Below is a complete reference of environment variables for each service. Variables marked with (secret) should be stored in AWS Secrets Manager.
### API service
| Variable | Value | Source |
| -------------------------------- | ------------------------- | ----------- |
| `FLEXPRICE_DEPLOYMENT_MODE` | `api` | Environment |
| `FLEXPRICE_SERVER_ADDRESS` | `:8080` | Environment |
| `FLEXPRICE_AUTH_SECRET` | 64-char hex | Secret |
| `FLEXPRICE_POSTGRES_HOST` | RDS endpoint | Secret |
| `FLEXPRICE_POSTGRES_PORT` | `5432` | Environment |
| `FLEXPRICE_POSTGRES_USER` | `flexprice` | Secret |
| `FLEXPRICE_POSTGRES_PASSWORD` | DB password | Secret |
| `FLEXPRICE_POSTGRES_DBNAME` | `flexprice` | Environment |
| `FLEXPRICE_POSTGRES_SSLMODE` | `require` | Environment |
| `FLEXPRICE_CLICKHOUSE_ADDRESS` | ClickHouse NLB endpoint | Environment |
| `FLEXPRICE_CLICKHOUSE_USERNAME` | `flexprice` | Secret |
| `FLEXPRICE_CLICKHOUSE_PASSWORD` | ClickHouse password | Secret |
| `FLEXPRICE_CLICKHOUSE_DATABASE` | `flexprice` | Environment |
| `FLEXPRICE_CLICKHOUSE_TLS` | `false` | Environment |
| `FLEXPRICE_KAFKA_BROKERS` | MSK bootstrap brokers | Environment |
| `FLEXPRICE_KAFKA_USE_SASL` | `true` | Environment |
| `FLEXPRICE_KAFKA_SASL_MECHANISM` | `SCRAM-SHA-512` | Environment |
| `FLEXPRICE_KAFKA_SASL_USER` | `flexprice` | Secret |
| `FLEXPRICE_KAFKA_SASL_PASSWORD` | Kafka password | Secret |
| `FLEXPRICE_KAFKA_TOPIC` | `events` | Environment |
| `FLEXPRICE_KAFKA_CONSUMER_GROUP` | `flexprice-consumer-prod` | Environment |
| `FLEXPRICE_TEMPORAL_ADDRESS` | Temporal Cloud endpoint | Environment |
| `FLEXPRICE_TEMPORAL_TLS` | `true` | Environment |
| `FLEXPRICE_TEMPORAL_NAMESPACE` | Your namespace | Environment |
| `FLEXPRICE_TEMPORAL_TASK_QUEUE` | `billing-task-queue` | Environment |
| `FLEXPRICE_TEMPORAL_API_KEY` | Temporal API key | Secret |
| `FLEXPRICE_LOGGING_LEVEL` | `info` | Environment |
### Worker and Temporal Worker services
Worker and Temporal Worker use the same variables as API, with `FLEXPRICE_DEPLOYMENT_MODE` set to `consumer` or `temporal_worker` respectively; omit `FLEXPRICE_SERVER_ADDRESS` for both.
### Additional environment variables (Production)
These variables are used in production deployments:
| Variable | Description | Example |
| ------------------------------------- | -------------------------- | -------------------------------------- |
| `FLEXPRICE_DYNAMODB_IN_USE` | Enable DynamoDB for events | `true` |
| `FLEXPRICE_DYNAMODB_REGION` | AWS region for DynamoDB | `us-west-2` |
| `FLEXPRICE_DYNAMODB_EVENT_TABLE_NAME` | DynamoDB table name | `events` |
| `FLEXPRICE_REDIS_HOST` | ElastiCache Redis endpoint | `clustercfg.xxx.cache.amazonaws.com` |
| `FLEXPRICE_REDIS_PORT` | Redis port | `6379` |
| `FLEXPRICE_REDIS_CLUSTER_MODE` | Enable cluster mode | `true` |
| `FLEXPRICE_REDIS_USE_TLS` | Enable TLS | `true` |
| `FLEXPRICE_REDIS_KEY_PREFIX` | Key prefix | `flexprice:prod` |
| `FLEXPRICE_EVENT_PUBLISH_DESTINATION` | Where to publish events | `all` (Kafka + DynamoDB) |
| `FLEXPRICE_LOGGING_FORMAT` | Log format | `json` |
| `FLEXPRICE_POSTGRES_READER_HOST` | Aurora reader endpoint | `xxx.cluster-ro-xxx.rds.amazonaws.com` |
***
## Step 13: Temporal Cloud configuration
Temporal Cloud is the recommended workflow orchestration service for production deployments.
### Sign up for Temporal Cloud
1. Go to [temporal.io/cloud](https://temporal.io/cloud)
2. Create an account and organization
3. Create a namespace (e.g., `flexprice-prod-usa`)
### Create service account and API key
1. In Temporal Cloud console, go to **Settings** > **API Keys**
2. Create a new API key with appropriate permissions
3. Note the API key and key name
### Store Temporal credentials
```bash theme={null}
aws secretsmanager create-secret \
--name flexprice/${ENV}/temporal \
--description "Flexprice Temporal Cloud credentials" \
--secret-string '{
"address": "us-west-2.aws.api.temporal.io:7233",
"namespace": "your-namespace.your-account-id",
"api_key": "YOUR_TEMPORAL_API_KEY",
"api_key_name": "your-service-account-name"
}'
```
### Temporal environment variables
| Variable | Value | Description |
| --------------------------------- | ------------------------------------ | ----------------------- |
| `FLEXPRICE_TEMPORAL_ADDRESS` | `us-west-2.aws.api.temporal.io:7233` | Temporal Cloud endpoint |
| `FLEXPRICE_TEMPORAL_NAMESPACE` | `your-namespace.account-id` | Your namespace |
| `FLEXPRICE_TEMPORAL_TLS` | `true` | TLS is required |
| `FLEXPRICE_TEMPORAL_TASK_QUEUE` | `billing-task-queue` | Task queue name |
| `FLEXPRICE_TEMPORAL_API_KEY` | (from Secrets Manager) | API key |
| `FLEXPRICE_TEMPORAL_API_KEY_NAME` | Service account name | Key identifier |
Temporal Cloud provides managed infrastructure, automatic upgrades, and 99.99%
SLA. For self-hosted Temporal, refer to the [Temporal
documentation](https://docs.temporal.io/self-hosted-guide).
***
## Step 14: Third-party integrations (Optional)
Configure optional third-party services for enhanced functionality.
### Supabase (Authentication)
If using Supabase for authentication:
```bash theme={null}
aws secretsmanager create-secret \
--name flexprice/${ENV}/supabase \
--secret-string '{
"base_url": "https://your-project.supabase.co",
"service_key": "YOUR_SUPABASE_SERVICE_KEY"
}'
```
| Variable | Value |
| ------------------------------------- | -------------------- |
| `FLEXPRICE_AUTH_PROVIDER` | `supabase` |
| `FLEXPRICE_AUTH_SUPABASE_BASE_URL` | Supabase project URL |
| `FLEXPRICE_AUTH_SUPABASE_SERVICE_KEY` | Service role key |
### Svix (Webhooks)
For webhook delivery via Svix:
```bash theme={null}
aws secretsmanager create-secret \
--name flexprice/${ENV}/svix \
--secret-string '{
"auth_token": "YOUR_SVIX_AUTH_TOKEN",
"base_url": "https://api.us.svix.com"
}'
```
| Variable | Value |
| ------------------------------------------ | ------------------------- |
| `FLEXPRICE_WEBHOOK_SVIX_CONFIG_ENABLED` | `true` |
| `FLEXPRICE_WEBHOOK_SVIX_CONFIG_AUTH_TOKEN` | Svix auth token |
| `FLEXPRICE_WEBHOOK_SVIX_CONFIG_BASE_URL` | `https://api.us.svix.com` |
### Sentry (Error Tracking)
For error tracking with Sentry:
| Variable | Value |
| ------------------------------ | ------------------- |
| `FLEXPRICE_SENTRY_ENABLED` | `true` |
| `FLEXPRICE_SENTRY_DSN` | Your Sentry DSN |
| `FLEXPRICE_SENTRY_ENVIRONMENT` | `production` |
| `FLEXPRICE_SENTRY_SAMPLE_RATE` | `1` (100% sampling) |
### Grafana Cloud (Observability)
For profiling with Pyroscope on Grafana Cloud:
| Variable | Value |
| ----------------------------------------- | --------------------------------------- |
| `FLEXPRICE_PYROSCOPE_ENABLED` | `true` |
| `FLEXPRICE_PYROSCOPE_SERVER_ADDRESS` | `https://profiles-prod-xxx.grafana.net` |
| `FLEXPRICE_PYROSCOPE_APPLICATION_NAME` | `flexprice-prod-api` |
| `FLEXPRICE_PYROSCOPE_BASIC_AUTH_USER` | Grafana user ID |
| `FLEXPRICE_PYROSCOPE_BASIC_AUTH_PASSWORD` | Grafana API key |
### FluentD (Log Aggregation)
For centralized logging with FluentD:
| Variable | Value |
| ----------------------------------- | ------------------ |
| `FLEXPRICE_LOGGING_FLUENTD_ENABLED` | `true` |
| `FLEXPRICE_LOGGING_FLUENTD_HOST` | FluentD service IP |
| `FLEXPRICE_LOGGING_FLUENTD_PORT` | `30242` |
| `FLEXPRICE_LOGGING_FORMAT` | `json` |
### Resend (Email)
For transactional emails via Resend:
| Variable | Value |
| -------------------------------- | ------------------- |
| `FLEXPRICE_EMAIL_ENABLED` | `true` |
| `FLEXPRICE_EMAIL_RESEND_API_KEY` | Your Resend API key |
| `FLEXPRICE_EMAIL_FROM_ADDRESS` | Sender email |
| `FLEXPRICE_EMAIL_REPLY_TO` | Reply-to email |
### Third-party cost summary
Breakdown below; production total is in the Cost estimation table above.
| Service | Purpose | Monthly Cost |
| -------------- | ---------------------- | --------------- |
| Temporal Cloud | Workflow orchestration | \~\$200 |
| Supabase | Authentication | \~\$25 |
| Svix | Webhooks | \~\$50 |
| Grafana Cloud | Observability | \~\$50 |
| Resend | Email | \~\$20 |
| Sentry | Error tracking | \$0-29 |
| **Total** | | **\~\$345-375** |
***
## Deployment checklist
Use this checklist to verify your deployment:
* [ ] VPC created with correct CIDR
* [ ] 2 public subnets created
* [ ] 4 private subnets created (2 compute, 2 data)
* [ ] Internet Gateway attached
* [ ] NAT Gateway(s) created and running
* [ ] Route tables configured correctly
* [ ] Security groups created with correct rules
* [ ] ECS Task Execution Role created
* [ ] ECS Task Role created
* [ ] Policies attached correctly (S3, Secrets Manager, CloudWatch, DynamoDB)
* [ ] PostgreSQL/Aurora credentials stored
* [ ] ClickHouse credentials stored
* [ ] Kafka SASL credentials stored
* [ ] Auth secret stored
* [ ] Temporal Cloud credentials stored
* [ ] Third-party credentials stored (Supabase, Svix, etc.)
* [ ] DB subnet group created
* [ ] Aurora cluster created and available
* [ ] Writer and Reader instances running
* [ ] Security group allows ECS access
* [ ] Secrets Manager updated with endpoints
* [ ] Database migrations completed
* [ ] MSK cluster created and active
* [ ] SASL/SCRAM secret associated
* [ ] Topics created (events, events\_lazy, events-dlq)
* [ ] Security group allows ECS access
* [ ] Prometheus exporters enabled
* [ ] EKS cluster created
* [ ] Node group running
* [ ] gp3 StorageClass created
* [ ] ClickHouse operator installed
* [ ] ClickHouse cluster deployed
* [ ] NLB created for ClickHouse access
* [ ] Database initialized
* [ ] Redis subnet group created
* [ ] Redis replication group created
* [ ] Cluster mode enabled (production)
* [ ] TLS encryption enabled
* [ ] Security group allows ECS access
* [ ] Events table created
* [ ] Point-in-time recovery enabled
* [ ] IAM policy allows ECS access
* [ ] S3 bucket created with encryption
* [ ] CloudWatch log groups created
* [ ] CloudWatch alarms configured
* [ ] ECR repositories created
* [ ] Container images built and pushed
* [ ] ECS cluster created
* [ ] Task definitions registered
* [ ] ALB created with HTTPS listener
* [ ] Target group configured
* [ ] Services created and healthy
* [ ] Auto Scaling configured
* [ ] API health check passing
* [ ] Worker consuming from Kafka
* [ ] Temporal workflows executing
* [ ] Logs appearing in CloudWatch
***
## Troubleshooting
### API unreachable
1. **Check ALB health checks**:
```bash theme={null}
aws elbv2 describe-target-health --target-group-arn $TG_ARN
```
2. **Check ECS task status**:
```bash theme={null}
aws ecs describe-services \
--cluster flexprice-${ENV} \
--services flexprice-api-${ENV}
```
3. **Check ECS task logs**:
```bash theme={null}
aws logs tail /ecs/flexprice-api-${ENV} --follow
```
4. **Verify security groups**:
* ALB SG allows inbound 443 from internet
* ECS SG allows inbound 8080 from ALB SG
* ECS SG allows outbound to RDS, MSK, ClickHouse
### Worker not consuming
1. **Check Kafka connectivity**:
```bash theme={null}
# From a bastion or EC2 instance with Kafka tools
kafka-consumer-groups.sh \
--bootstrap-server $MSK_BOOTSTRAP \
--command-config client.properties \
--group flexprice-consumer-${ENV} \
--describe
```
2. **Check consumer lag in MSK CloudWatch metrics**
3. **Verify SASL credentials**:
* Ensure `AmazonMSK_` prefixed secret is associated with cluster
* Verify username/password match in Secrets Manager
4. **Check security group**:
* MSK SG allows inbound 9094/9096 from ECS SG
### Temporal workflows failing
1. **Check Temporal Worker logs**:
```bash theme={null}
aws logs tail /ecs/flexprice-temporal-worker-${ENV} --follow
```
2. **Verify Temporal Cloud connection**:
* Correct `FLEXPRICE_TEMPORAL_ADDRESS`
* Valid API key and namespace
* TLS enabled
3. **Check Temporal Cloud UI** for workflow history and errors
### ClickHouse connection errors
1. **Verify ClickHouse pods are running**:
```bash theme={null}
kubectl get pods -n clickhouse
```
2. **Check ClickHouse logs**:
```bash theme={null}
kubectl logs -n clickhouse -l clickhouse.altinity.com/chi=flexprice
```
3. **Verify NLB is healthy**:
```bash theme={null}
kubectl get svc clickhouse-nlb -n clickhouse
```
4. **Test connectivity from ECS**:
* Ensure EKS SG allows inbound 9000 from ECS SG
* Verify NLB DNS resolves correctly
### RDS connection issues
1. **Verify RDS is available**:
```bash theme={null}
aws rds describe-db-instances \
--db-instance-identifier flexprice-${ENV} \
--query 'DBInstances[0].DBInstanceStatus'
```
2. **Check security group**:
* RDS SG allows inbound 5432 from ECS SG
3. **Verify credentials**:
* Check Secrets Manager values match RDS configuration
4. **Test from bastion**:
```bash theme={null}
psql -h $RDS_ENDPOINT -U flexprice -d flexprice
```
***
## Scaling guidelines
Scale when metrics exceed the thresholds below.
| Component | Metric | Threshold | Action |
| ---------- | -------------------- | ----------------- | ------------------------------------ |
| ECS | CPU utilization | > 70% sustained | Scale out |
| ECS | Memory utilization | > 80% sustained | Scale out or increase task memory |
| ECS | API latency (p99) | > 500ms | Scale out API tasks |
| ECS | Kafka consumer lag | Growing | Scale out Worker tasks |
| RDS | CPU utilization | > 80% sustained | Upgrade instance class |
| RDS | Database connections | > 80% of max | Upgrade instance or add read replica |
| RDS | Read IOPS | Hitting limits | Upgrade to gp3 with higher IOPS |
| RDS | Storage | > 80% used | Increase allocated storage |
| MSK | Broker CPU | > 60% sustained | Add brokers |
| MSK | Consumer lag | Growing over time | Add partitions and consumers |
| MSK | Storage | > 80% used | Increase broker storage |
| ClickHouse | Query latency | Degrading | Add replicas or upgrade nodes |
| ClickHouse | Disk usage | > 80% | Expand PVCs or add shards |
| ClickHouse | Memory pressure | OOM events | Increase node memory |
***
## Cost optimization
### Reserved instances
* **RDS**: Purchase Reserved Instances for 1-3 year commitment (up to 72% savings)
* **MSK**: Not available; consider Kafka on EC2 with Reserved Instances for significant savings
### Fargate Spot
Use Fargate Spot for non-critical workloads:
```bash theme={null}
# Update service to use Fargate Spot
aws ecs update-service \
--cluster flexprice-${ENV} \
--service flexprice-worker-${ENV} \
--capacity-provider-strategy capacityProvider=FARGATE_SPOT,weight=2 capacityProvider=FARGATE,weight=1
```
### S3 lifecycle policies
Already configured to transition to IA after 90 days. Consider:
* Glacier for archives > 1 year
* Intelligent-Tiering for unpredictable access patterns
### CloudWatch log retention
Set appropriate retention periods:
* Production: 30-90 days
* Development: 7-14 days
* Archive to S3 for long-term storage
***
## Additional resources
Complete list of Flexprice environment variables
Understand Flexprice's internal architecture
Set up monitoring and observability
Common issues and solutions
## Need help?
If you encounter issues during deployment:
* Check our [GitHub Issues](https://github.com/flexprice/flexprice/issues) for similar problems
* Join our [Slack community](https://join.slack.com/t/flexpricecommunity/shared_invite/zt-39uat51l0-n8JmSikHZP~bHJNXladeaQ) for real-time support
* Contact us at [support@flexprice.io](mailto:support@flexprice.io)
# Self-hosting the Frontend
Source: https://docs.flexprice.io/docs/getting-started/self-hosting-frontend
Run the Flexprice dashboard on your own infrastructure and connect it to a self-hosted backend
The [backend self-hosting guide](/docs/getting-started/self-hosting-guide) gets the Flexprice API and its infrastructure running. This guide covers the other half: running the Flexprice dashboard (`flexprice-front`) yourself and pointing it at that backend.
Complete the [backend self-hosting guide](/docs/getting-started/self-hosting-guide) first. The frontend is a static single-page app that talks to the API over HTTP; it has no database or infrastructure of its own.
## Self-hosting options
Run the dashboard in a container alongside your self-hosted backend. Best for a single server.
Build static files with `npm run build` and serve them with nginx or any static host.
Deploy the same repo to Vercel using the bundled `vercel.json`.
***
## Self-hosted environment configuration
Before you build or deploy anything, set these. They're the three variables that actually change how the app behaves in a self-hosted setup, and getting them wrong is the single most common way this deployment breaks:
| Variable | Self-hosted value | Why |
| -------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `VITE_APP_ENV` | `self-hosted` | Switches the dashboard's auth flow to call your backend's own `/auth/login` and `/auth/signup` endpoints directly, and stores the session token in the browser instead of using Supabase. Without this, the app expects a Supabase project to exist. |
| `VITE_AUTH_PROVIDER` | `flexprice` | Confirms the dashboard is using Flexprice's own auth rather than Supabase, and hides Supabase-only UI (Google sign-in, cloud announcement banners) that don't apply to a self-hosted instance. |
| `VITE_API_URL` | `http://:8080/v1` | Points the dashboard at your self-hosted API. See [Connecting to the backend](#connecting-to-the-backend) for what "reachable" means here. |
The `.env.example` file defaults `VITE_AUTH_PROVIDER` to `supabase` and leaves `VITE_APP_ENV` unset (which falls back to `local`). If you skip setting these two values, login and signup will silently try to reach Supabase instead of your backend.
With `VITE_APP_ENV=self-hosted` set, you do **not** need to fill in `VITE_SUPABASE_URL` or `VITE_SUPABASE_ANON_KEY` — leave them blank.
Vite bakes every `VITE_*` variable into the static bundle at **build time**, not at container start. Whenever you change `.env`, you must rebuild (`docker compose up -d --build`, or `npm run build` again) before the change takes effect. This trips people up more than anything else in this guide, see [Troubleshooting](#troubleshooting).
### Full environment variable reference
| Variable | Self-hosted value | Notes |
| -------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `VITE_APP_ENV` | `self-hosted` | See above. Also accepts `local`, `development`, `production` for other setups. |
| `VITE_API_URL` | Your backend's API URL, e.g. `http://localhost:8080/v1` | Must be reachable from the browser, not just the server. |
| `VITE_AUTH_ENABLED` | `true` | Set this regardless; the dashboard always requires login. Kept for forward compatibility. |
| `VITE_AUTH_PROVIDER` | `flexprice` | Use Flexprice's built-in auth instead of Supabase. |
| Variable | Self-hosted value | Notes |
| ------------------------ | ----------------- | --------------------------------------------- |
| `VITE_SUPABASE_URL` | Leave blank | Only used when `VITE_AUTH_PROVIDER=supabase`. |
| `VITE_SUPABASE_ANON_KEY` | Leave blank | Same as above. |
| Variable | Recommended for self-hosted | Notes |
| ---------------------- | --------------------------------------- | --------------------------------------------------------------- |
| `VITE_SENTRY_ENABLED` | `false` unless you run your own Sentry | Set `VITE_SENTRY_DSN` if enabled. |
| `VITE_POSTHOG_ENABLED` | `false` unless you run your own PostHog | Set `VITE_POSTHOG_KEY` / `VITE_POSTHOG_HOST` if enabled. |
| `VITE_REO_ENABLED` | `false` | Cloud session-replay integration; not relevant for self-hosted. |
| Variable | Self-hosted value | Notes |
| ----------------------- | ----------------- | -------------------------------------------------------------------------------------- |
| `VITE_PADDLE_ENABLED` | `false` | Paddle checkout is used by Flexprice Cloud billing, not applicable when you self-host. |
| `VITE_INTERCOM_ENABLED` | `false` | Flexprice Cloud support widget. |
| Variable | Self-hosted value | Notes |
| ----------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `VITE_WEBHOOK_PROVIDER` | `flexprice` | Uses the custom Flexprice webhook portal instead of hosted Svix. Set to `svix` only if you're self-hosting Svix yourself. |
| `VITE_SVIX_URL` | Public origin of your self-hosted Svix API, or leave blank | Only needed if you run Svix yourself and set `VITE_WEBHOOK_PROVIDER=svix`. |
| Variable | Self-hosted value | Notes |
| ---------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------- |
| `VITE_DASHBOARD_URL_INDIA` / `VITE_DASHBOARD_URL_US` | Leave blank | Used to route between Flexprice Cloud regions; not applicable to a single self-hosted instance. |
| `VITE_DATA_REGION_SELECTION_ENABLED` | `false` | Same as above. |
| `VITE_RESTRICTED_ENVS` | Leave blank | Used by Flexprice Cloud to suspend tenant environments. |
| `VITE_TENANT_FEATURE_ALLOWLIST` | Leave blank | Gates cloud-only UI for specific tenants; irrelevant when you're the only tenant. |
| Variable | Notes |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `VITE_GOOGLE_SHEETS_WEB_APP_URL` | Only needed if you use the Google Sheets export integration. |
| `VITE_FONT_CONFIG` | Optional JSON override for dashboard typography, e.g. `{"primary":"Inter","fallback":"ui-sans-serif, system-ui, sans-serif"}`. Omit to use the default font. |
***
## First login: create your account
A self-hosted instance starts with no dashboard users. There's no default username or password: every account, including your first one, is created through Sign Up against your own backend:
Once your container or dev server is running, visit it in a browser (e.g. `http://localhost:3000`).
On the auth screen, switch to the **Sign Up** tab and enter an email and password (6+ characters).
In self-hosted mode, sign-up skips email verification entirely: the app posts straight to your backend's `/auth/signup` endpoint and logs you in right away using the returned session token. There's no confirmation email to wait for.
If Sign Up fails with a network or CORS error instead of a validation error, that's almost always `VITE_API_URL` or backend CORS, not the auth flow itself. See [Troubleshooting](#troubleshooting).
***
## Docker Compose
### Prerequisites
[Git](https://git-scm.com/)
[Node.js](https://nodejs.org/) 20+ and npm (only needed for a manual build; skip if you're using Docker exclusively)
[Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/)
A running Flexprice backend, reachable over HTTP from wherever the frontend runs. See the [backend self-hosting guide](/docs/getting-started/self-hosting-guide).
### Quick start
```bash theme={null}
# Clone the frontend repository
git clone https://github.com/flexprice/flexprice-front
cd flexprice-front
# Copy the environment template
cp .env.example .env
```
Edit `.env` with at least the three variables from [self-hosted environment configuration](#self-hosted-environment-configuration) above, then build and start the container:
```bash theme={null}
docker compose up -d --build
```
Use `--build` here, and every time you change `.env` afterward. `docker compose up -d` alone reuses a cached image if one already exists, which silently keeps your old environment variables since Vite bakes them in at build time.
The `docker-compose.yml` in the repo builds the image from the included `Dockerfile` (a multi-stage Node 20 build) and exposes the dashboard on port 3000 with a built-in healthcheck.
```bash theme={null}
# View logs
docker compose logs -f app
# Stop
docker compose down
```
Visit `http://localhost:3000` once the container reports healthy, then follow [First login](#first-login) to create your account.
***
## Manual build and serve
If you'd rather not use Docker, build static files and serve them yourself.
```bash theme={null}
git clone https://github.com/flexprice/flexprice-front
cd flexprice-front
npm install
cp .env.example .env
# Edit .env: VITE_APP_ENV=self-hosted, VITE_AUTH_PROVIDER=flexprice,
# VITE_API_URL=http://:8080/v1
npm run build
```
This produces a static `dist/` folder. Serve it with any of the following:
Copy the repo's `nginx.conf` (SPA fallback already configured) and point it at `dist/`:
```bash theme={null}
cp nginx.conf /etc/nginx/conf.d/flexprice.conf
nginx -s reload
```
```bash theme={null}
npm run start
# Serves on 0.0.0.0:3000
```
```bash theme={null}
npx serve -s dist
```
Whatever you use to serve the built files, it must fall back to `index.html` for unknown paths (a single-page app client-side router). The bundled `nginx.conf` handles this with `try_files $uri $uri/ /index.html;`. If you configure your own web server, replicate this rule or client-side routes like `/customers/123` will 404 on a hard refresh.
***
## Deploying on Vercel
The repo includes a `vercel.json` with the SPA rewrite already configured (`/(.*) → /`), so you can deploy directly:
1. Import the `flexprice-front` repository into Vercel.
2. Set the environment variables from the [reference above](#self-hosted-environment-configuration) in the Vercel project settings (`VITE_APP_ENV=self-hosted`, `VITE_AUTH_PROVIDER=flexprice`, `VITE_API_URL`, and any optional ones you need).
3. Deploy. Vercel runs `npm run build` automatically and serves the `dist/` output.
Your backend must be reachable from the public internet (or accessible to wherever your Vercel deployment resolves `VITE_API_URL`) since Vercel doesn't proxy to a private network by default.
***
## Connecting to the backend
* `VITE_API_URL` is baked into the JavaScript bundle and fetched by the **end user's browser**, not by the Docker container or server that built it. Use an address the browser can actually reach: the host machine's real IP or domain, not a Docker-internal hostname. `http://localhost:8080/v1` only works if the person opening the dashboard is on the same machine as the backend.
* CORS: the Flexprice API needs to allow requests from the origin the dashboard is served on. If you're serving the frontend from a different domain or port than the API expects, configure the backend's CORS allow-list accordingly (see the [backend self-hosting guide](/docs/getting-started/self-hosting-guide) and [configuration reference](/docs/getting-started/configuration)).
* Login and signup (`/auth/login`, `/auth/signup`) are public endpoints: they don't require an API key. After you log in, the dashboard authenticates every other request with the session token from that login, not with a Flexprice API key. You only need an API key (e.g. for testing with `curl`) if you're calling the API directly, outside the dashboard.
***
## Troubleshooting
`VITE_APP_ENV` is not set to `self-hosted`, or `VITE_AUTH_PROVIDER` is still `supabase` (the `.env.example` default). Set both explicitly and rebuild, since Vite environment variables are baked in at build time, not read at container start.
Confirm `VITE_APP_ENV=self-hosted` was set at build time. If it wasn't, sign-up falls back to the Supabase flow and waits for an email confirmation that will never arrive on a self-hosted instance with no email provider configured.
The web server isn't falling back to `index.html` for unknown paths. Use the bundled `nginx.conf`, or add an equivalent SPA fallback rule to your own server config.
1. Confirm `VITE_API_URL` is reachable from your browser, not just from the container network:
```bash theme={null}
curl /health
```
2. Check the browser console for a CORS error. If present, add the frontend's origin to the backend's CORS configuration.
3. Remember Vite bakes `VITE_*` variables in at build time. Changing `.env` after `npm run build` requires rebuilding.
Vite environment variables are compiled into the static bundle at build time, they aren't read at runtime. Rebuild the image (`docker compose up -d --build`) or rerun `npm run build`, then restart the container or server.
```bash theme={null}
docker compose logs -f app
docker compose ps
```
The bundled healthcheck curls `http://localhost:3000` inside the container; a non-2xx response usually means the build failed or the app crashed on start. Check the logs above for the actual error.
## Need help?
* Check our [GitHub Issues](https://github.com/flexprice/flexprice-front/issues) for similar problems
* Join our [Slack community](https://join.slack.com/t/flexpricecommunity/shared_invite/zt-39uat51l0-n8JmSikHZP~bHJNXladeaQ) for real-time support
* Contact us at [support@flexprice.io](mailto:support@flexprice.io)
## Additional resources
Run the Flexprice API and infrastructure
Complete list of Flexprice environment variables
Learn how to contribute to the frontend
Visit our official website
# Self-hosting Flexprice
Source: https://docs.flexprice.io/docs/getting-started/self-hosting-guide
Learn how to set up and run Flexprice on your own infrastructure
You can run Flexprice on your own infrastructure in several ways. We organize self-hosting by **provider** so you can pick the option that fits your environment and add more providers over time.
## Self-hosting options
Run Flexprice locally or on a single server with Docker Compose. Best for development and small deployments.
Deploy Flexprice on AWS with ECS, RDS, MSK, and EKS. Production-ready, scalable setup.
This guide covers the backend (API and infrastructure). To run the dashboard UI, see [Self-hosting the Frontend](/docs/getting-started/self-hosting-frontend).
***
## Docker Compose
The fastest way to run Flexprice on your own machine or a single server is with Docker Compose.
### Prerequisites
Before you begin, make sure you have the following installed:
[Golang](https://go.dev/) 1.23+
[Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) (Docker Desktop, OrbStack, or Podman Desktop all work)
`make` (pre-installed on macOS/Linux; on Windows use WSL or install via [chocolatey](https://chocolatey.org/packages/make))
One of these supported platforms:
* Linux-based environment
* macOS (Darwin)
* WSL under Windows
You do **not** need `psql`, `kafka-cli`, or any other database client installed locally.
All database operations run inside Docker containers via `docker compose exec`.
## Quick Setup with Docker Compose
The easiest way to get started is using our automated setup command:
```bash theme={null}
# Clone the repository
git clone https://github.com/flexprice/flexprice
cd flexprice
# Set up the complete development environment
make dev-setup
```
This single command takes care of everything you need to get started:
1. Starting all required infrastructure (PostgreSQL, Kafka, ClickHouse, Temporal)
2. Building the Flexprice application image
3. Running database migrations (PostgreSQL via Ent ORM + ClickHouse) and initializing Kafka topics
4. Seeding default tenant and environment data
5. Starting all Flexprice services (API, Consumer, Worker)
### Default API Key
After setup, use the following key to authenticate local API requests:
```
sk_local_flexprice_test_key
```
Pass it in the `x-api-key` request header:
```bash theme={null}
curl -H "x-api-key: sk_local_flexprice_test_key" http://localhost:8080/v1/customers
```
The key is pre-configured in `internal/config/config.yaml`. Entity tables are created
entirely by the Ent ORM layer — the Postgres migration files only bootstrap schemas,
extensions, and stored functions.
## Accessing Services
Once setup is complete, you can access:
[http://localhost:8080](http://localhost:8080)
[http://localhost:8088](http://localhost:8088)
[http://localhost:8084](http://localhost:8084) (requires `--profile dev`)
[http://localhost:8123](http://localhost:8123)
The Kafka UI requires the `dev` profile:
```bash theme={null}
docker compose --profile dev up -d kafka-ui
```
## Useful Commands
Here are some common commands you might need during development:
```bash theme={null}
make restart-flexprice
```
```bash theme={null}
make down
```
```bash theme={null}
make clean-start
```
```bash theme={null}
make build-image && make restart-flexprice
```
```bash theme={null}
make seed-db
```
## Running Without Docker (API only)
If you prefer to run the application binary directly while keeping infrastructure in Docker:
```bash theme={null}
# Start required infrastructure
docker compose up -d postgres kafka clickhouse temporal temporal-ui
# Run migrations
make migrate-postgres migrate-clickhouse migrate-ent seed-db init-kafka
# Run the application locally
go run cmd/server/main.go
```
## Connection Details
Use these credentials to connect to the various services:
* **Host**: localhost
* **Port**: 5432
* **Database**: flexprice
* **Username**: flexprice
* **Password**: flexprice123
* **Host**: localhost
* **Port**: 9000 (native) / 8123 (HTTP)
* **Database**: flexprice
* **Username**: flexprice
* **Password**: flexprice123
* **Bootstrap Server**: localhost:29092
* **UI**: [http://localhost:8084](http://localhost:8084) (requires `--profile dev`)
## API Documentation
Flexprice provides comprehensive API documentation in OpenAPI 3.0 format.
### Setting up Postman
1. Open Postman
2. Click on **Import** in the top left
3. Select **Import File**
4. Choose `docs/swagger/swagger-3-0.json`
5. Click **Import**
6. Create a new environment for local development:
| Variable | Value |
| --------- | ----------------------------- |
| `baseUrl` | `http://localhost:8080/v1` |
| `apiKey` | `sk_local_flexprice_test_key` |
Configure the collection to send `x-api-key: {{apiKey}}` as a header on every request.
## Troubleshooting
If you encounter issues during setup or operation, try these troubleshooting steps:
Another process (e.g. a previously running local Flexprice binary) may be bound to port 8080
and intercepting requests before Docker's port mapping.
```bash theme={null}
# Find and kill the conflicting process
lsof -ti :8080 | xargs kill -9
```
Then restart the API container:
```bash theme={null}
docker compose restart flexprice-api
```
The API key in your request does not match the configured key. For local development use:
```bash theme={null}
curl -H "x-api-key: sk_local_flexprice_test_key" http://localhost:8080/v1/customers
```
If you changed the key in `config.yaml`, remember the config stores the **SHA-256 hash** of the
raw key, not the key itself. The middleware hashes the incoming key before lookup.
You may see log lines like:
```
Failed to create Redis client: dial tcp [::1]:6379: connect: connection refused
```
This is **non-fatal** for local development. Redis is optional — it powers response caching,
which falls back gracefully when unavailable. The API continues to work normally.
All entity tables (`customers`, `plans`, `subscriptions`, etc.) are managed exclusively by the
**Ent ORM** migration layer. The Postgres migration files (`migrations/postgres/`) only create:
* Schemas and the `uuid-ossp` extension (`V0__init.sql`)
* Stored functions for invoice/billing sequences (`V2_invoice_sequences.up.sql`)
If you see `relation does not exist` errors, ensure you ran `make migrate-ent` after starting
Postgres:
```bash theme={null}
make migrate-ent
make seed-db
```
1. Ensure Docker is running properly:
```bash theme={null}
docker info
```
2. Check the status of all containers:
```bash theme={null}
docker compose ps
```
3. View logs for a specific service:
```bash theme={null}
docker compose logs -f flexprice-api
docker compose logs -f flexprice-consumer
```
4. If containers are in a bad state, do a full clean restart:
```bash theme={null}
docker compose down -v
make dev-setup
```
1. Verify Kafka is running:
```bash theme={null}
docker compose logs kafka
```
2. Check that all required topics exist:
```bash theme={null}
docker compose exec kafka kafka-topics --bootstrap-server kafka:9092 --list
```
3. Re-initialize topics if any are missing:
```bash theme={null}
make init-kafka
```
ClickHouse migrations use standard `MergeTree` engine for local/single-node compatibility.
If you see engine-related errors, ensure you are running the open-source ClickHouse image
(not ClickHouse Cloud) as specified in `docker-compose.yml`.
To re-run ClickHouse migrations:
```bash theme={null}
make migrate-clickhouse
```
## Need Help?
If you're still experiencing issues after trying the troubleshooting steps, please:
* Check our [GitHub Issues](https://github.com/flexprice/flexprice/issues) for similar problems
* Join our [Slack community](https://join.slack.com/t/flexpricecommunity/shared_invite/zt-39uat51l0-n8JmSikHZP~bHJNXladeaQ) for real-time support
* Contact us at [support@flexprice.io](mailto:support@flexprice.io)
## Additional Resources
Learn how to contribute to Flexprice
Explore our API documentation
Our community guidelines
Visit our official website
# Invoice Calculation Order
Source: https://docs.flexprice.io/docs/invoices/calculation
The exact sequence flexprice uses to calculate invoice totals: line items, discounts, wallet credits, tax, and final amount.
Every invoice flexprice generates follows the same deterministic sequence. Getting this order right matters when you combine coupons at multiple levels, wallet credits, and tax rates.
## The sequence
```
1. Subtotal
= sum of all line item amounts (usage charges + flat fees)
2. Line-item coupon discounts
Each targeted line item's amount is reduced individually.
3. Invoice-level (subscription) coupon discounts
Applied to the running subtotal in association order.
Each coupon sees the total left after the previous coupon.
4. Prepaid wallet credits
Deducted after all coupon discounts.
5. Taxable amount
= MAX(subtotal - all_coupon_discounts, 0)
Floored at zero. Coupons cannot create a negative taxable base.
6. Tax
= sum of all applicable tax rates x taxable_amount
Each rate is applied independently to the same taxable amount.
7. Invoice total
= subtotal - total_coupon_discounts - wallet_credits_applied + tax
```
## Worked example
| Line item | Amount |
| ------------------------ | ------------ |
| Usage charge (API calls) | \$80.00 |
| Flat monthly fee | \$20.00 |
| **Subtotal** | **\$100.00** |
| Discount step | Change | Running total |
| ----------------------------------- | -------- | ------------- |
| Line-item coupon: 25% off API calls | -\$20.00 | \$80.00 |
| Subscription coupon: \$10 off | -\$10.00 | \$70.00 |
| Prepaid wallet credits | -\$5.00 | \$65.00 owed |
| Tax step | Amount |
| ------------------------------------------------- | ----------- |
| Taxable amount (subtotal - coupon discounts only) | \$70.00 |
| Tax at 8.25% | +\$5.78 |
| **Invoice total** | **\$70.78** |
Wallet credits reduce the amount owed but do **not** reduce the taxable base. Tax is calculated on `subtotal - coupon_discounts`, not on `subtotal - coupon_discounts - wallet_credits`.
## Coupon chaining
Multiple subscription-level coupons apply sequentially. Each coupon sees the amount remaining after the previous one, not the original subtotal.
```
Subtotal: $100.00
Coupon A (10%): - $10.00 --> running total: $90.00
Coupon B (10%): - $9.00 --> running total: $81.00
(applies to $90, not $100)
```
Two 10% coupons produce a 19% effective discount, not 20%.
## Line-item vs subscription-level coupons
| Coupon type | What it targets | When it runs |
| ------------------------- | -------------------------------------- | ------------------------------------------ |
| Line-item coupon | One specific charge within the invoice | Step 2, before any subscription-level math |
| Subscription-level coupon | The invoice subtotal | Step 3, after all line-item discounts |
A subscription-level coupon sees a subtotal that has already been reduced by any line-item coupons.
## Multiple tax rates
When multiple tax associations apply to the same invoice, each rate is applied to the same taxable amount independently. They do not compound.
```
Taxable amount: $100.00
State tax (6%): + $6.00
Federal tax (2%): + $2.00
Total tax: $8.00 (not $8.12 from compounding)
```
## Zero-floor rule
If coupons collectively exceed the subtotal, the taxable amount is clamped to zero. Excess discount is not carried forward to the next invoice period.
```
Subtotal: $50.00
Coupon A: -$30.00
Coupon B: -$30.00
-------
Taxable: $0.00 (clamped, not -$10.00)
Tax: $0.00
```
## See also
* [Coupons](/docs/product-catalogue/coupons/overview)
* [Taxes](/docs/product-catalogue/taxes/overview)
* [Wallets](/docs/wallet/create)
# Managing Invoices
Source: https://docs.flexprice.io/docs/invoices/managing
**Viewing Invoices**
* Navigate to **Customer Management > Invoices**.
* View the list of invoices with details like **Invoice ID, Amount, Invoice Status, Customer Slug, Payment Status, and Due Date**.
* Click on an invoice to view more details.
* If you want to access Invoice for individual customers, you can click on the customer and navigate to Customers section.
**Invoice Details Breakdown**
When viewing a specific invoice, you will see:
* **Invoice Number** – A unique identifier for the invoice.
* **Date of Issue** – The date the invoice was generated.
* **Due Date** – The deadline for payment.
* **Order Details** – Breakdown of subscription charges, intervals, and quantities.
* **Taxes and Discounts** – Any applied tax rates, credits, or discounts.
* **Payment Status** – Indicates whether payment is pending, successful, or failed.
* **Wallet Application** – If a customer wallet is used to partially or fully pay the invoice.
**Updating Invoice Status**
* Click the **Options (⋮)** menu on an invoice.
* Select **Update Invoice Status**.
* Choose from:
* **Draft** – Editable invoice before finalization.
* **Finalized** – Locked invoice ready for payment processing.
* **Void** – Cancels the invoice and prevents further changes.
* Click **Update** to save the changes.
### Updating Payment Status
1. Open the invoice and click the **Options (⋮)** menu.
2. Select **Update Payment Status**.
3. Choose from:
* **Pending** – Payment is awaited.
* **Successful** – Invoice is marked as paid.
* **Failed** – Payment attempt was unsuccessful.
4. Click **Update** to apply changes.
### Downloading Invoices
1. Open an invoice.
2. Click **Download** to get a PDF version.
# Overview
Source: https://docs.flexprice.io/docs/invoices/overview
Invoices in Flexprice provide a structured way to bill customers for their subscriptions, usage, and additional charges. The invoicing system supports automated generation, status management, and payment tracking, ensuring a seamless billing process for businesses.
Invoices are essential for:
* Tracking revenue and outstanding payments.
* Maintaining financial records for compliance.
* Enabling clear and transparent billing for customers.
Flexprice allows you to create, manage, and update invoices directly from the dashboard or through the API.
**Invoice Lifecycle**
Invoices progress through the following states:
* **Draft** – Created but not yet finalized; can be edited.
* **Finalized** – Locked and ready for payment processing.
* **Paid** – The customer has successfully made the payment.
* **Failed** – Payment attempt failed (e.g., insufficient funds, expired card).
* **Void** – The invoice is canceled and will not be processed further.
# Partial payments
Source: https://docs.flexprice.io/docs/invoices/partial-payments
Flexprice supports partial payments for invoices, allowing businesses to configure whether invoices can be partially settled using available credits. This ensures a flexible payment process for customers who may not have enough credits to cover the entire invoice.
By enabling partial payments, businesses can:
* Automatically deduct available credits from an invoice before charging the remaining balance.
* Allow invoices to be settled partially if the customer doesn’t have enough credits.
Important point to note is:
* Partial payments only apply to invoices where credits are eligible for that payment.
* If partial payments are disabled, invoices remain unpaid (pending) until the full amount is covered
**How to enable patial payments using Flexprice**
* **When partial payments are allowed**
* If a customer **doesn’t have enough credits** to cover the full invoice, the available credits will be **partially applied**.
* The remaining balance can be settled via the customer’s **default payment method** or remain as **outstanding**.
* The invoice will reflect **partial payment status** until fully paid.
* **When partial payments are not allowed**
* The invoice remains in **Finalized** state with a **Pending payment status** until the **full amount is paid**.
* No credits are applied unless the available credits cover the **entire invoice amount**.
# Apply Coupon to a Subscription
Source: https://docs.flexprice.io/docs/product-catalogue/coupons/apply-discount-on-subscription
Link coupons to subscriptions at creation or post-creation. Use subscription phases to schedule discount windows.
You can attach coupons at subscription creation time. For post-creation changes (add, remove, or schedule a future removal), use the subscription modification API.
## Before you start
The coupon must be:
* Status: `active`
* Within its `redeem_after` / `redeem_before` window (if set)
* Below its `max_redemptions` limit (if set)
Validation runs at association time. If any condition fails, you get a validation error before the association is created.
## At subscription creation
Pass coupons in the `subscription_coupons` array. Each entry takes a `coupon_code` (the human-readable code from the coupon object), optional `start_date`/`end_date`, and an optional `price_id` to target a specific line item.
### Subscription-level coupon
Discounts the full invoice subtotal. Omit `price_id`:
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/subscriptions \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"customer_id": "",
"plan_id": "",
"start_date": "2025-01-01T00:00:00Z",
"subscription_coupons": [
{
"coupon_code": "SUMMER15"
}
]
}'
```
`start_date` defaults to the subscription start date. `end_date` is optional: omit it to let the coupon run until its own `cadence` ends or the subscription cancels.
### Line-item coupon
Discounts one specific price in the plan only. Set `price_id` to the target price:
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/subscriptions \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"customer_id": "",
"plan_id": "",
"start_date": "2025-01-01T00:00:00Z",
"subscription_coupons": [
{
"coupon_code": "USAGE10",
"price_id": ""
}
]
}'
```
## Scheduled discounts via phases
Subscription phases are the recommended way to apply time-bounded coupons. Each phase has its own `start_date` and optional `end_date`. Coupons in a phase inherit those dates as their association window.
### Example: introductory discount for Q1 only
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/subscriptions \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"customer_id": "",
"plan_id": "",
"start_date": "2025-01-01T00:00:00Z",
"phases": [
{
"start_date": "2025-01-01T00:00:00Z",
"end_date": "2025-03-31T23:59:59Z",
"subscription_coupons": [
{ "coupon_code": "Q1PROMO" }
]
},
{
"start_date": "2025-04-01T00:00:00Z"
}
]
}'
```
The coupon is active during Phase 1 only. Phase 2 has no coupon, so invoices from April onwards are at full price.
### Example: line-item discount scoped to a phase
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/subscriptions \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"customer_id": "",
"plan_id": "",
"start_date": "2025-01-01T00:00:00Z",
"phases": [
{
"start_date": "2025-01-01T00:00:00Z",
"end_date": "2025-06-30T23:59:59Z",
"subscription_coupons": [
{
"coupon_code": "USAGE10",
"price_id": ""
}
]
},
{
"start_date": "2025-07-01T00:00:00Z"
}
]
}'
```
## Common scheduling patterns
| Goal | Phase configuration |
| ---------------------------------------- | ---------------------------------------------------------------------------------- |
| Discount for first 3 months | Phase 1: `end_date` = 3 months out, `coupons: [id]`. Phase 2: no coupon. |
| Discount starting 6 months in | Phase 1: no coupon. Phase 2: `start_date` = 6 months in, `coupons: [id]`. |
| Remove a discount on a known future date | Phase 1: today to removal date with coupon. Phase 2: from removal date, no coupon. |
| Discount on one charge only | In `subscription_coupons`, set `price_id` to the target price ID. |
## Add or remove a coupon after subscription creation
Use the subscription modification API with `type: "coupon"`.
### Add a coupon
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/subscriptions//modify/execute \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"type": "coupon",
"coupon_params": {
"action": "add",
"coupon_code": "",
"start_date": "2025-06-01T00:00:00Z",
"end_date": "2025-08-31T23:59:59Z"
}
}'
```
`start_date` defaults to now if omitted. `end_date` is optional: omit it to apply the coupon indefinitely.
To target a specific line item instead of the full invoice, add `subscription_line_item_id` (mutually exclusive with `subscription_id`):
```bash theme={null}
{
"type": "coupon",
"coupon_params": {
"action": "add",
"coupon_code": "",
"start_date": "2025-06-01T00:00:00Z",
"subscription_line_item_id": ""
}
}
```
### Remove a coupon
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/subscriptions//modify/execute \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"type": "coupon",
"coupon_params": {
"action": "remove",
"coupon_association_id": ""
}
}'
```
Get the `coupon_association_id` from the subscription response with `expand=coupon_associations`.
Removal affects only invoices generated after the request. Previously issued invoices are not changed.
### Preview before executing
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/subscriptions//modify/preview \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"type": "coupon",
"coupon_params": {
"action": "add",
"coupon_code": "",
"start_date": "2025-06-01T00:00:00Z"
}
}'
```
## subscription\_coupons fields (at creation)
| Field | Type | Required | Description |
| ------------- | --------- | -------- | ------------------------------------------------------------------------------ |
| `coupon_code` | string | Yes | The coupon's `code` value (case-insensitive) |
| `start_date` | timestamp | No | When the association starts. Defaults to the subscription or phase start date. |
| `end_date` | timestamp | No | When the association ends. Omit for indefinite. |
| `price_id` | string | No | Target a specific line item. Omit for subscription-level. |
## coupon\_params fields (post-creation modification)
| Field | Type | Required | Description |
| --------------------------- | --------- | -------------------- | ----------------------------------------------------------------------- |
| `action` | string | Yes | `add` or `remove` |
| `coupon_code` | string | When `action=add` | Code of the coupon to attach |
| `coupon_association_id` | string | When `action=remove` | ID of the coupon association to detach |
| `start_date` | timestamp | No | When the association starts. Defaults to now. |
| `end_date` | timestamp | No | When the association ends. Omit for indefinite. |
| `subscription_line_item_id` | string | No | Target a specific line item. Mutually exclusive with `subscription_id`. |
## How discounts appear on invoices
Discount amounts are tracked at two levels on each line item:
| Field | Description |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| `line_item_discount` | Discount applied directly to this line item (from a line-item coupon targeting this price) |
| `invoice_level_discount` | Invoice-wide coupon discount prorated to this line item |
At the invoice level, `total_discount` is the sum of all coupon discounts across all line items.
The discount terms are snapshotted when the coupon is applied, so they are preserved even if the coupon definition is later updated.
## Check active coupons on a subscription
```bash theme={null}
curl "https://us.api.flexprice.io/v1/subscriptions/?expand=coupon_associations" \
-H "x-api-key: "
```
## Validation errors
| Error | Cause |
| ------------------------- | -------------------------------------- |
| `coupon_not_active` | Coupon is not in `active` status |
| `coupon_expired` | Current time is after `redeem_before` |
| `coupon_not_yet_valid` | Current time is before `redeem_after` |
| `max_redemptions_reached` | `total_redemptions >= max_redemptions` |
# Create a Coupon
Source: https://docs.flexprice.io/docs/product-catalogue/coupons/create
Create percentage or fixed-amount discount coupons via API or dashboard.
## Via API
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/coupons \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"name": "Summer Sale",
"type": "percentage",
"percentage_off": "15.00",
"cadence": "once",
"coupon_code": "SUMMER15",
"redeem_after": "2025-07-01T00:00:00Z",
"redeem_before": "2025-08-31T23:59:59Z",
"max_redemptions": 1000
}'
```
**Response**
```json theme={null}
{
"id": "coup_01abc123",
"name": "Summer Sale",
"type": "percentage",
"percentage_off": "15.00",
"cadence": "once",
"coupon_code": "SUMMER15",
"redeem_after": "2025-07-01T00:00:00Z",
"redeem_before": "2025-08-31T23:59:59Z",
"max_redemptions": 1000,
"total_redemptions": 0,
"status": "active"
}
```
## Request fields
| Field | Type | Required | Description |
| --------------------- | -------------- | ----------------------- | ------------------------------------------------------------------------------------------------- |
| `name` | string | Yes | Display name shown in the dashboard and invoice details |
| `type` | string | Yes | `percentage` or `fixed` |
| `cadence` | string | Yes | `once`, `repeated`, or `forever` |
| `percentage_off` | decimal string | When `type=percentage` | Discount as a percent, e.g. `"15.00"` for 15% |
| `amount_off` | decimal string | When `type=fixed` | Flat discount amount, e.g. `"50.00"` |
| `currency` | string | When `type=fixed` | ISO currency code, e.g. `"USD"` |
| `duration_in_periods` | int | When `cadence=repeated` | Number of billing cycles the discount applies. Must not be set for other cadences. |
| `coupon_code` | string | No | Human-readable code customers can enter to redeem (e.g. `"SUMMER15"`). Auto-generated if omitted. |
| `redeem_after` | timestamp | No | Earliest time a new association can be created |
| `redeem_before` | timestamp | No | Expiry. New associations cannot be created after this time. |
| `max_redemptions` | int | No | Cap on total associations across all subscriptions |
| `metadata` | object | No | Key-value pairs for your own tracking |
## Cadence behaviour
| Cadence | What happens |
| ---------- | --------------------------------------------------------------------- |
| `once` | Discount applies to the first invoice in the association window only |
| `repeated` | Discount applies for `duration_in_periods` consecutive billing cycles |
| `forever` | Discount applies to every invoice for the life of the subscription |
## Fixed-amount coupon
For a fixed dollar amount off, set `type: fixed` and provide both `amount_off` and `currency`:
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/coupons \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"name": "New Customer Credit",
"type": "fixed",
"amount_off": "50.00",
"currency": "USD",
"cadence": "once"
}'
```
## Recurring coupon
To discount multiple billing cycles, use `cadence: repeated` with `duration_in_periods`:
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/coupons \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"name": "Quarterly Promo",
"type": "percentage",
"percentage_off": "20.00",
"cadence": "repeated",
"duration_in_periods": 3
}'
```
This applies a 20% discount for the first 3 billing cycles of each association.
## Manage existing coupons
```bash theme={null}
# List coupons
curl https://us.api.flexprice.io/v1/coupons \
-H "x-api-key: "
# Get a coupon
curl https://us.api.flexprice.io/v1/coupons/ \
-H "x-api-key: "
# Search coupons by status
curl -X POST https://us.api.flexprice.io/v1/coupons/search \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{ "status": "active" }'
# Update a coupon (only name and metadata are updatable)
curl -X PUT https://us.api.flexprice.io/v1/coupons/ \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{ "name": "Summer Sale 2025" }'
# Delete a coupon
curl -X DELETE https://us.api.flexprice.io/v1/coupons/ \
-H "x-api-key: "
```
## Via dashboard
Go to **Product Catalog** > **Coupons** > **Create Coupon** and fill in the same fields described above.
## Next step
[Apply the coupon to a subscription](/docs/product-catalogue/coupons/apply-discount-on-subscription) at creation time or post-creation via the subscription modification API.
# Coupons Overview
Source: https://docs.flexprice.io/docs/product-catalogue/coupons/overview
Create percentage or fixed-amount discounts and apply them to subscriptions at the invoice or line-item level.
A coupon is a reusable discount definition. You create it once, then link it to one or more subscriptions via an **association**. Each association can be scoped to a time window and optionally targeted at a specific line item rather than the whole invoice.
## Coupon types
| Type | Discount field | Currency required | Example |
| ------------ | ---------------- | ----------------- | ------------------- |
| `percentage` | `percentage_off` | No | 15% off the invoice |
| `fixed` | `amount_off` | Yes | \$50 off |
## Cadence
Cadence controls how many billing cycles the discount repeats within an association's active window.
| Cadence | Behavior |
| ---------- | ------------------------------------------------------------ |
| `once` | Applied to the first invoice of the association period only |
| `repeated` | Applied for `duration_in_periods` consecutive billing cycles |
| `forever` | Applied to every invoice for the life of the subscription |
## Validity controls
These fields live on the coupon itself and gate whether a new association can be created.
| Field | Purpose |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `redeem_after` | Earliest timestamp a coupon can be associated to a subscription |
| `redeem_before` | Expiry timestamp. Associations cannot be created after this date. Existing associations created before expiry remain active. |
| `max_redemptions` | Maximum total associations allowed across all subscriptions |
The `total_redemptions` counter increments when the association is **created**, not when an invoice is generated.
## Coupon scope
**Subscription level**: discount applies to the invoice subtotal (or the running subtotal after earlier coupons have applied).
**Line-item level**: discount applies to a specific price within the invoice before any subscription-level coupons run.
## How multiple coupons apply
When an invoice has both line-item and subscription-level coupons:
1. Line-item coupons apply first, reducing each targeted line item's amount individually.
2. Subscription-level coupons apply next, in association order, each seeing the subtotal left after the previous coupon.
3. Wallet credits are deducted after all coupon discounts.
4. Tax is calculated on `MAX(subtotal - all_discounts, 0)`. Tax is never charged on discounted-away amounts.
Two 20% subscription-level coupons do not produce a 40% total discount. The second coupon applies to the amount remaining after the first. See [Billing Calculation Order](/docs/invoices/billing-calculation-order) for the full worked example.
## Coupon statuses
| Status | Meaning |
| ---------- | ------------------------------------- |
| `draft` | Created but not yet usable |
| `active` | Can be associated to subscriptions |
| `archived` | No longer usable for new associations |
Only `active` coupons pass association validation.
## Quick start
1. [Create a coupon](/docs/product-catalogue/coupons/create) with a type, value, and optional validity window.
2. [Apply it to a subscription](/docs/product-catalogue/coupons/apply-discount-on-subscription) at creation time or via subscription phases for scheduled windows.
# Custom currency
Source: https://docs.flexprice.io/docs/product-catalogue/custom-pricing/custom-currency
Define your own currencies, price your catalogue in them, and let Flexprice settle invoices in fiat
Custom currency lets you price your catalogue in a unit of your own, such as credits or tokens. You configure prices and send usage in your currency, and Flexprice issues the invoice in a fiat settlement currency.
**Benefits:**
* **No conversion logic on your side**: price, meter, and report in your own unit. Flexprice converts once, at invoicing.
* **Unchanged settlement**: invoices are denominated in fiat, so payment providers, accounting exports, and ledger integrations are unaffected.
* **Defined once, used everywhere**: a currency is configured in one place and available to **prices**, **subscriptions**, **wallets**, **addons**, and **coupons**.
Custom currency and [pricing units](/docs/product-catalogue/custom-pricing/pricing-unit) address different problems. A pricing unit applies to a single price and is converted when that price is created. A custom currency is defined once and applies to every entity created in it, including subscriptions and wallets.
## Configure
Define the currencies you need for your environment in the `custom_currency_config` setting. See [Settings](/docs/settings/settings#custom-currency-configuration) for the full schema and validation rules.
```json theme={null}
{
"custom_currencies": {
"fpc": {
"name": "FinePrint Credits",
"symbol": "FPC",
"fiat_conversion_factors": { "usd": "0.10", "inr": "8.50" }
}
},
"default_fiat_currency": "usd"
}
```
## Conversion factors
A conversion factor states how much fiat **one unit** of your currency is worth.
```text theme={null}
fiat amount = custom amount × conversion factor
```
With `"usd": "0.10"`, one credit is worth \$0.10, so 150 credits invoice as \$15.00.
## What to make sure
Give every custom currency a factor for the same fiat currencies. If one currency defines `inr` and another does not, the configuration is rejected.
* **Currency codes are immutable.** Prices, subscriptions, and wallets store the code as their currency. Supplying a different code adds a currency rather than renaming the existing one.
* **A subscription bills in one currency.** A plan can carry charges in both a custom currency and fiat. A subscription bills only the charges matching its own currency.
* **Names and symbols are safe to change.** They are read at display time and are not stored on any entity.
## Invoicing
Invoices are denominated in the `default_fiat_currency` you configure. The amounts in your currency are retained on the invoice and on each line item, so both are available on the API response and in the dashboard.
```json theme={null}
{
"currency": "usd",
"subtotal": "15",
"total": "15",
"amount_due": "15",
"custom_currency": {
"code": "fpc",
"rate": "0.1",
"subtotal": "150",
"total_discount": "0",
"total_tax": "0",
"total_prepaid_credits_applied": "0",
"total": "150",
"amount_due": "150"
}
}
```
The conversion factor is recorded when the invoice is finalized. Changing a factor afterwards does not affect invoices that are already finalized.
## Use cases
If you already track consumption internally in credits, configure that as your currency and send usage in it directly, no exchange logic on your side, no conversion before sending it to Flexprice. Pricing, metering, entitlements, and reporting all stay in the unit you already use. Flexprice handles conversion and settlement.
You can also use it to sell credits to customers: publish prices in credits so customers see one unit across your catalogue, while invoices and payments settle in the currency you collect and report in.
# Pricing unit
Source: https://docs.flexprice.io/docs/product-catalogue/custom-pricing/pricing-unit
This feature allows you to create and use your own currency in Flexprice. With create Price Units, add them to plans and wallets in custom currency.
**Custom pricing** allows you to price features using your own unit (such as credits) instead of only standard currencies like USD or EUR.
### What is custom currency?
**Custom currency** is pricing in your own unit (such as credits) instead of a standard money currency like USD or EUR. You set and show prices in that unit-for example, "10 credits per month"-and **Flexprice converts those amounts to your base currency (e.g. USD) for billing**, This ensures a consistent currency for accounting, even though the feature is shown in credits.
***
## How it works internally
All billing calculations are performed using the pricing unit’s base currency (for example, USD). Amounts are defined and displayed in the **pricing unit** (e.g. credits), Conversion happens only at price or wallet creation; Flexprice uses the base currency for all calculations. In other words: input and display use the pricing unit; computation, persistence, and invoicing use base currency.
Conversion uses the formula **amount in your unit × conversion\_rate = amount in base currency**. Rounding and precision (e.g. two decimals for USD, zero for JPY) are applied only when **invoice charges are computed**, not at conversion or when creating prices or price units.
Wallet balance is always shown in **base currency and credits**, whether the wallet was created with fiat or a custom price unit. Flexprice **stores and bills** everything in the base currency (e.g. USD).
***
## Using custom currency
1. **Create** a Price Unit (dashboard or API, below).
2. **Use in plans** — add charges or create prices in your unit; see [Use cases](/docs/product-catalogue/custom-pricing/use-cases#use-custom-currency-in-plans-and-prices).
3. **Use in wallets** — create a wallet in your unit; see [Use cases](/docs/product-catalogue/custom-pricing/use-cases).
***
## Use cases
* **Credits or tokens** - Sell in credits or tokens; show balance and charges in that unit while billing and storing in base currency (e.g. USD).
* **Multi-currency display, single-currency billing** - Present prices in a custom unit per region or product while retaining one base currency for accounting and payouts.
***
## Concepts and terminology
**Custom currency / Price Unit** - Your own pricing unit (e.g. credits) used instead of fiat currency. Defined once as a Price Unit and reused across plans, prices, and wallets, with automatic conversion to the base currency for billing.
| Field | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **code** | **3-character** identifier (e.g. `CRD`, `TOK`, `fpc`) used when referencing the unit in a price or wallet. Must be unique per environment when Active. |
| **symbol** | Display symbol shown in the UI and on invoices (e.g. ¢, ==). |
| **base\_currency** | The standard currency the unit is pegged to (e.g. `usd`). All billing and storage use this currency. |
| **conversion\_rate** | Multiplier from the custom unit to base currency. |
**Conversion formula**
```text theme={null}
amount in your unit × conversion_rate = amount in base currency
```
**Example:** If 1 credit = 0.01 USD, set `conversion_rate = "0.01"`. Then 100 credits = 1.00 USD.
**Price unit types: FIAT vs CUSTOM**
* **FIAT** - Amounts are in a standard currency (USD, EUR, etc.). No conversion is applied.
* **CUSTOM** - Amounts are in a Price Unit. Flexprice converts to base currency for billing; you configure and display values in the custom unit.
***
## Create a Price Unit (dashboard)
Go to **Product Catalog** → **Price Units** → **Add** (or **Create Price Unit**).
Enter **Name**, **Code**, **Symbol**, **Base currency**, and **Conversion rate**.\
Example: 100 units = 1 USD → use conversion rate **0.01**.
***
## Create a Price Unit (API)
Send `POST /v1/prices/units` with a JSON body (you can see example below).
Include **name**, **code**, **symbol**, **base\_currency**, and **conversion\_rate**. Optionally **metadata**.
**Example request**
```json theme={null}
{
"name": "Credits",
"code": "CRD",
"symbol": "¢",
"base_currency": "usd",
"conversion_rate": "0.01",
"metadata": {}
}
```
Conversion, 100 CRD = 1.00 USD. Base URL options: `https://us.api.flexprice.io/v1` or `https://api.cloud.flexprice.io/v1` (check out the [Create a price unit](/api-reference/price-units/create-price-unit)).
***
## Use custom currency in plans and prices
You can add charges or create prices in your custom unit from the **dashboard** or via the **API**. In both cases you choose your Price Unit and enter amounts in that unit; Flexprice converts to base currency for billing.
### Dashboard
Go to **Product Catalog** → **Plans** → open the plan you want to edit.
In **Charges**, click **+ Add** (or **Edit** then add). In the **Currency** dropdown choose **Custom** and select your Price Unit. Add recurring or usage-based charges and enter amounts in your unit (e.g. 15 credits/month). Click **Save**.
For more detail and screenshots, see [Use cases: add charges and wallet](/docs/product-catalogue/custom-pricing/use-cases).
### API
In the create-price request (`POST /v1/prices`), set **price\_unit\_type** to **CUSTOM** and include **price\_unit\_config**.
In **price\_unit\_config** set **price\_unit** (your unit’s code, e.g. `CRD`). For a fixed amount use **amount**; for tiered pricing use **price\_unit\_tiers** only (not the top-level **tiers** field).
**Example: fixed monthly price in credits**
The request must also include **type** (e.g. `FIXED`), **billing\_cadence** (e.g. `RECURRING`), and **invoice\_cadence** (e.g. `ADVANCE` or `ARREAR`). Below shows the custom-pricing–relevant fields:
```json theme={null}
{
"currency": "usd",
"price_unit_type": "CUSTOM",
"type": "FIXED",
"billing_model": "FLAT_FEE",
"billing_period": "MONTHLY",
"billing_cadence": "RECURRING",
"invoice_cadence": "ADVANCE",
"entity_type": "PLAN",
"entity_id": "your-plan-id",
"price_unit_config": {
"price_unit": "CRD",
"amount": "100.00"
}
}
```
With `conversion_rate = "0.01"`, 100 CRD = 1.00 USD. Flexprice stores both the custom amount and the base-currency amount and uses base currency for billing.
**Billing models**
| Model | In price\_unit\_config | Notes |
| --------- | --------------------------------------- | -------------------------------------------------------------------------------------- |
| FLAT\_FEE | **price\_unit**, **amount** | Fixed price in your unit. |
| PACKAGE | **price\_unit**, **amount** | **transform\_quantity** is required; for usage add **meter\_id**. |
| TIERED | **price\_unit**, **price\_unit\_tiers** | Use **price\_unit\_tiers** only, not top-level **tiers**. For usage add **meter\_id**. |
***
Refer to the API Reference and Price Unit documentation. check out the [API Reference](/api-reference/introduction) and the [Price Unit](/api-reference/price-units/list-price-units).
# Use cases
Source: https://docs.flexprice.io/docs/product-catalogue/custom-pricing/use-cases
Add charges and create wallets using custom price units. All billing calculations use base currency.
## Add charges in custom currency
**Product Catalog** → **Plans** → select a plan (e.g. Pro).
In the **Charges** section click **+ Add** (or **Edit** then add charges). You’ll see **Add Charges to \[plan]**.
In the **Currency** dropdown choose **Custom**, then select your price unit (e.g. **fpc (==)** — 1 fpc = 1.27 USD). Standard options (USD, INR, etc.) stay under **Standard**.
**Add Recurring Charges** — fixed amount per period (e.g. 15 fpc/month). **Add Usage Based Charges** — charge by usage in the custom unit. Set **Display Name** for the charge (e.g. feature name). Enter the **value** in the custom unit.
***
## Use custom currency in plans and prices
You can add charges from the **dashboard** (see above) or create prices via the **API** below. In both cases you choose your Price Unit and enter amounts in that unit; Flexprice converts to base currency for billing.
### API
In the create-price request (`POST /v1/prices`), set **price\_unit\_type** to **CUSTOM** and include **price\_unit\_config**.
In **price\_unit\_config** set **price\_unit** (your unit's code, e.g. `CRD`). For a fixed amount use **amount**; for tiered pricing use **price\_unit\_tiers** only (not the top-level **tiers** field).
**Example: fixed monthly price in credits**
The request must also include **type** (e.g. `FIXED`), **billing\_cadence** (e.g. `RECURRING`), and **invoice\_cadence** (e.g. `ADVANCE` or `ARREAR`). Below shows the custom-pricing–relevant fields:
```json theme={null}
{
"currency": "usd",
"price_unit_type": "CUSTOM",
"type": "FIXED",
"billing_model": "FLAT_FEE",
"billing_period": "MONTHLY",
"billing_cadence": "RECURRING",
"invoice_cadence": "ADVANCE",
"entity_type": "PLAN",
"entity_id": "your-plan-id",
"price_unit_config": {
"price_unit": "CRD",
"amount": "100.00"
}
}
```
With `conversion_rate = "0.01"`, 100 CRD = 1.00 USD. Flexprice stores both the custom amount and the base-currency amount and uses base currency for billing.
**Billing models**
| Model | In price\_unit\_config | Notes |
| --------- | --------------------------------------- | -------------------------------------------------------------------------------------- |
| FLAT\_FEE | **price\_unit**, **amount** | Fixed price in your unit. |
| PACKAGE | **price\_unit**, **amount** | **transform\_quantity** is required; for usage add **meter\_id**. |
| TIERED | **price\_unit**, **price\_unit\_tiers** | Use **price\_unit\_tiers** only, not top-level **tiers**. For usage add **meter\_id**. |
For more on fields and endpoints, see the [API Reference](/api-reference/introduction) and [Price Unit](/api-reference/price-units/list-price-units).
***
## Create Wallet using custom pricing unit
For a wallet that uses a **custom price unit**, Flexprice does not perform any conversion at wallet creation: it uses the **price unit’s conversion rate** as-is. If you pass **conversion\_rate** in the create-wallet API request, it is **ignored** when **price\_unit** is provided; the price unit’s conversion rate is always used. The wallet’s currency and conversion rate come only from the selected price unit.
Go to **Customers** → select the customer → open the **Wallets** tab (or the section where you manage wallets).
Click **+ Add** or **Create Wallet** to open the Create Wallet dialog.
In the **Currency** dropdown, choose **Custom** (not Standard), then select your price unit (e.g. **fpc (==)** or **CRD (¢)**). Standard options (USD, INR, etc.) are listed under **Standard**.
When you select a custom price unit, **Currency** and **Conversion rate** are set automatically from that unit’s base currency and conversion rate. You don’t need to enter them manually. Optional: set **Initial credits to load**, **Wallet type** (Pre-Paid / Post-Paid), or **Alert** settings.
**API:** To create a wallet in custom currency via the API, include **price\_unit** (the unit’s **code**, e.g. `fpc` or `CRD`) in the request body. Flexprice sets the wallet’s currency to the price unit’s base currency and its conversion rate from the price unit. The price unit must be **Active**; otherwise the request fails validation.
***
## Summary
* **Create** a price unit first (Product Catalog → Price Units) with a conversion rate to base currency, and ensure it’s **Active**.
* **Use** it in a plan: open the plan → **Charges** → **+ Add** → **Currency** → **Custom** → select your unit → add recurring or usage-based charges → **Save**.
* **Use** it in a wallet: open the customer → **Wallets** → **Create Wallet** → **Currency** → **Custom** → select your unit → create. Balance is always shown in **base currency and credits** only, irrespective of whether the wallet was created with fiat currency or a price unit.
# AVERAGE
Source: https://docs.flexprice.io/docs/product-catalogue/features/aggregation/average
Calculates the average value of a specified property across all matching events.
Step-by-Step Setup when creating an AVERAGE-based metered feature:
1. **Navigate to Features**
* Go to Product Catalog → Features
* Click "Add Feature"
2. **Basic Information**
* **Name**: "Response Time" (or descriptive name)
* **Type**: Select "Metered"
3. **Event Configuration**
* **Event Name**: `api.response` (must match your event data)
* **Aggregation Function**: Average
* **Aggregation Field**: `response_time_ms` (the property to average)
4. **Usage Settings**
* **Usage Reset**: Periodic (for monthly averages) or Cumulative
* **Unit Name**: `ms` (milliseconds)
5. **Save Feature**
## Calculation Example
### Event Data
```json theme={null}
[
{
"event_id": "evt_001",
"event_name": "api.response",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:00:00Z",
"properties": {
"response_time_ms": 10
}
},
{
"event_id": "evt_002",
"event_name": "api.response",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:05:00Z",
"properties": {
"response_time_ms": 20
}
},
{
"event_id": "evt_003",
"event_name": "api.response",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:10:00Z",
"properties": {
"response_time_ms": 30
}
},
{
"event_id": "evt_004",
"event_name": "api.response",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:15:00Z",
"properties": {
"response_time_ms": 40
}
},
{
"event_id": "evt_005",
"event_name": "api.response",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:20:00Z",
"properties": {
"response_time_ms": -1
}
},
{
"event_id": "evt_006",
"event_name": "api.response",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:25:00Z",
"properties": {
"response_time_ms": 0
}
}
]
```
### Calculation Process
1. **Event Matching**: All events with `event_name = "api.response"`
2. **Deduplication**: Remove duplicate event IDs using `anyLast()` (each event has unique ID in this case)
* `evt_001` → `10 ms`
* `evt_002` → `20 ms`
* `evt_003` → `30 ms`
* `evt_004` → `40 ms`
* `evt_005` → `-1 ms`
* `evt_006` → `0 ms`
3. **Average Calculation**: `(10 + 20 + 30 + 40 + (-1) + 0) / 6 = 99 / 6 = 16.5 ms`
**Result**: `16.5 ms`
## Use Cases
### Response Time Monitoring
**Perfect for**: API response times, page load times, query performance
```json theme={null}
{
"event_name": "api.response",
"external_customer_id": "acme_corp",
"properties": {
"response_time_ms": 185
}
}
```
### Performance Metrics
**Perfect for**: Processing duration, execution time, latency measurements
```json theme={null}
{
"event_name": "job.processing",
"external_customer_id": "user_456",
"properties": {
"duration_seconds": 45.2
}
}
```
### Business Analytics
**Perfect for**: Order values, session lengths, transaction amounts
```json theme={null}
{
"event_name": "order.placed",
"external_customer_id": "merchant_789",
"properties": {
"order_value": 125.50
}
}
```
### System Monitoring
**Perfect for**: CPU usage, memory consumption, bandwidth utilization
```json theme={null}
{
"event_name": "system.metrics",
"external_customer_id": "customer_101",
"properties": {
"cpu_percent": 65.8
}
}
```
### File Size Analysis
**Perfect for**: Average file sizes, document lengths, data volumes
```json theme={null}
{
"event_name": "file.processed",
"external_customer_id": "data_user",
"properties": {
"file_size_mb": 12.7
}
}
```
### When to Use AVERAGE
✅ **Use AVERAGE when:**
* Need mean values over time periods
* Tracking performance or quality metrics
* Measuring typical behavior or usage patterns
* Billing based on average consumption levels
## Next Steps
* **[Creating a Metered Feature](/docs/event-ingestion/creating-a-metered-feature)** - Complete setup guide
* **[Sending Events](/docs/event-ingestion/sending-events)** - How to transmit AVERAGE events
# COUNT
Source: https://docs.flexprice.io/docs/product-catalogue/features/aggregation/count
Counts the number of distinct events (by ID) that match the criteria.
Step-by-Step Setup when creating a COUNT-based metered feature:
1. **Navigate to Features**
* Go to Product Catalog → Features
* Click "Add Feature"
2. **Basic Information**
* **Name**: "API Calls" (or descriptive name)
* **Type**: Select "Metered"
3. **Event Configuration**
* **Event Name**: `api.calls` (must match your event data)
* **Aggregation Function**: Count
* **Aggregation Field**: Leave empty (not required for COUNT)
4. **Usage Settings**
* **Usage Reset**: Periodic (for monthly limits) or Cumulative
* **Unit Name**: `API call / API calls`
5. **Save Feature**
## Calculation Example
### Event Data
```json theme={null}
[
{
"event_id": "evt_001",
"event_name": "api.calls",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:00:00Z"
},
{
"event_id": "evt_002",
"event_name": "api.calls",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:05:00Z"
},
{
"event_id": "evt_001", // Duplicate event ID
"event_name": "api.calls",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:10:00Z"
}
]
```
### Calculation Process
1. **Event Matching**: All events with `event_name = "api.calls"`
2. **Deduplication**: Remove duplicate event IDs
* `evt_001` (first occurrence)
* `evt_002`
* `evt_001` (duplicate - ignored)
3. **Count Result**: `2` (distinct events)
**Result**: `2 API calls`
## Use Cases
### API Request Tracking
**Perfect for**: RESTful APIs, GraphQL queries, webhook calls
```json theme={null}
{
"event_name": "api.calls",
"external_customer_id": "acme_corp"
}
```
### File Operations
**Perfect for**: File uploads, downloads, processing tasks
```json theme={null}
{
"event_name": "file.uploads",
"external_customer_id": "user_456"
}
```
### Transaction Counting
**Perfect for**: Payment transactions, database operations, user actions
```json theme={null}
{
"event_name": "transactions",
"external_customer_id": "merchant_789"
}
```
### Feature Usage
**Perfect for**: Feature activations, button clicks, page views
```json theme={null}
{
"event_name": "feature.usage",
"external_customer_id": "customer_101"
}
```
### When to Use COUNT
✅ **Use COUNT when:**
* Tracking discrete events or actions
* You don't need to measure quantity/volume
* Simple occurrence-based billing
* High-volume event streams (best performance)
## Next Steps
* **[Creating a Metered Feature](/docs/event-ingestion/creating-a-metered-feature)** - Complete setup guide
* **[Sending Events](/docs/event-ingestion/sending-events)** - How to transmit COUNT events
# COUNT UNIQUE
Source: https://docs.flexprice.io/docs/product-catalogue/features/aggregation/count-unique
Counts the number of distinct values for a specified property.
Step-by-Step Setup when creating a COUNT UNIQUE-based metered feature:
1. **Navigate to Features**
* Go to Product Catalog → Features
* Click "Add Feature"
2. **Basic Information**
* **Name**: "Active Users" (or descriptive name)
* **Type**: Select "Metered"
3. **Event Configuration**
* **Event Name**: `user.activity` (must match your event data)
* **Aggregation Function**: Count Unique
* **Aggregation Field**: `user_id` (the property to count unique values for)
4. **Usage Settings**
* **Usage Reset**: Periodic (for monthly limits) or Cumulative
* **Unit Name**: `user / users`
5. **Save Feature**
## Calculation Example
### Event Data
```json theme={null}
[
{
"event_id": "evt_001",
"event_name": "user.activity",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:00:00Z",
"properties": {
"user_id": "user_alice"
}
},
{
"event_id": "evt_002",
"event_name": "user.activity",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:05:00Z",
"properties": {
"user_id": "user_bob"
}
},
{
"event_id": "evt_003",
"event_name": "user.activity",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:10:00Z",
"properties": {
"user_id": "user_alice"
}
},
{
"event_id": "evt_004",
"event_name": "user.activity",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:15:00Z",
"properties": {
"user_id": "user_charlie"
}
}
]
```
### Calculation Process
1. **Event Matching**: All events with `event_name = "user.activity"`
2. **Property Extraction**: Extract `user_id` values from each event
* `evt_001` → `"user_alice"`
* `evt_002` → `"user_bob"`
* `evt_003` → `"user_alice"` (duplicate)
* `evt_004` → `"user_charlie"`
3. **Count Unique Result**: `3` distinct users (`user_alice`, `user_bob`, `user_charlie`)
**Result**: `3 users`
## Use Cases
### Active Users Tracking
**Perfect for**: Monthly active users, daily active users, unique visitors
```json theme={null}
{
"event_name": "user.activity",
"external_customer_id": "acme_corp",
"properties": {
"user_id": "user_12345"
}
}
```
### Unique Sessions
**Perfect for**: Session tracking, device identification, unique connections
```json theme={null}
{
"event_name": "session.activity",
"external_customer_id": "user_456",
"properties": {
"session_id": "sess_abc123"
}
}
```
### Feature Usage
**Perfect for**: Unique feature users, distinct tool usage, unique resource access
```json theme={null}
{
"event_name": "feature.usage",
"external_customer_id": "merchant_789",
"properties": {
"feature_name": "advanced_analytics"
}
}
```
### IP Address Tracking
**Perfect for**: Unique IP addresses, geographic distribution, security monitoring
```json theme={null}
{
"event_name": "api.request",
"external_customer_id": "customer_101",
"properties": {
"ip_address": "192.168.1.100"
}
}
```
### Device Tracking
**Perfect for**: Unique devices, platform analytics, hardware usage
```json theme={null}
{
"event_name": "app.usage",
"external_customer_id": "mobile_user",
"properties": {
"device_id": "device_xyz789"
}
}
```
### When to Use COUNT UNIQUE
✅ **Use COUNT UNIQUE when:**
* Tracking distinct users, sessions, or resources
* Need to count unique values, not occurrences
* Measuring reach or adoption metrics
* Eliminating duplicates for billing purposes
## Next Steps
* **[Creating a Metered Feature](/docs/event-ingestion/creating-a-metered-feature)** - Complete setup guide
* **[Sending Events](/docs/event-ingestion/sending-events)** - How to transmit COUNT UNIQUE events
# LATEST
Source: https://docs.flexprice.io/docs/product-catalogue/features/aggregation/latest
Returns the most recent value of a property based on event timestamp.
Step-by-Step Setup when creating a LATEST-based metered feature:
1. **Navigate to Features**
* Go to Product Catalog → Features
* Click "Add Feature"
2. **Basic Information**
* **Name**: "Current Tier" (or descriptive name)
* **Type**: Select "Metered"
3. **Event Configuration**
* **Event Name**: `subscription.tier` (must match your event data)
* **Aggregation Function**: Latest
* **Aggregation Field**: `tier_level` (the property to get latest value for)
4. **Usage Settings**
* **Usage Reset**: Periodic (for current period values) or Cumulative
* **Unit Name**: `tier` (descriptive unit)
5. **Save Feature**
## Calculation Example
### Event Data
```json theme={null}
[
{
"event_id": "evt_001",
"event_name": "subscription.tier",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:00:00Z",
"properties": {
"tier_level": 1
}
},
{
"event_id": "evt_002",
"event_name": "subscription.tier",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T12:00:00Z",
"properties": {
"tier_level": 2
}
},
{
"event_id": "evt_003",
"event_name": "subscription.tier",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T09:00:00Z",
"properties": {
"tier_level": 3
}
},
{
"event_id": "evt_004",
"event_name": "subscription.tier",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T14:00:00Z",
"properties": {
"tier_level": 4
}
}
]
```
### Calculation Process
1. **Event Matching**: All events with `event_name = "subscription.tier"`
2. **Timestamp Ordering**: Find the event with the latest timestamp
* `evt_003` → `09:00:00Z` (tier\_level: 3)
* `evt_001` → `10:00:00Z` (tier\_level: 1)
* `evt_002` → `12:00:00Z` (tier\_level: 2)
* `evt_004` → `14:00:00Z` (tier\_level: 4) **← Latest**
3. **Latest Result**: `4` (most recent tier\_level value)
**Result**: `4 tier`
## Use Cases
### Current Subscription Tier
**Perfect for**: Subscription levels, plan tiers, service grades
```json theme={null}
{
"event_name": "subscription.tier",
"external_customer_id": "acme_corp",
"properties": {
"tier_level": 3
}
}
```
### Configuration Settings
**Perfect for**: Feature flags, settings values, configuration states
```json theme={null}
{
"event_name": "config.update",
"external_customer_id": "user_456",
"properties": {
"max_users": 50
}
}
```
### Status Tracking
**Perfect for**: Account status, service health, operational states
```json theme={null}
{
"event_name": "account.status",
"external_customer_id": "merchant_789",
"properties": {
"status_code": 2
}
}
```
### Resource Limits
**Perfect for**: Current quotas, capacity limits, threshold values
```json theme={null}
{
"event_name": "quota.update",
"external_customer_id": "customer_101",
"properties": {
"storage_gb_limit": 1000
}
}
```
### Version Tracking
**Perfect for**: Software versions, API versions, schema versions
```json theme={null}
{
"event_name": "version.update",
"external_customer_id": "api_user",
"properties": {
"api_version": 3
}
}
```
### When to Use LATEST
✅ **Use LATEST when:**
* Need current state or most recent configuration
* Tracking subscription tiers or plan levels
* Billing based on current settings or status
* Want the final value in a sequence of changes
## Next Steps
* **[Creating a Metered Feature](/docs/event-ingestion/creating-a-metered-feature)** - Complete setup guide
* **[Sending Events](/docs/event-ingestion/sending-events)** - How to transmit LATEST events
# MAX
Source: https://docs.flexprice.io/docs/product-catalogue/features/aggregation/max
Get the maximum value of a property with optional bucketed or group-by calculations.
Step-by-Step Setup when creating a MAX-based metered feature:
1. **Navigate to Features**
* Go to Product Catalog → Features
* Click "Add Feature"
2. **Basic Information**
* **Name**: "Peak Concurrent Users" (or descriptive name)
* **Type**: Select "Metered"
3. **Event Configuration**
* **Event Name**: `concurrent.users` (must match your event data)
* **Aggregation Function**: Max
* **Aggregation Field**: `user_count` (the property to find maximum for)
* **Bucket Size**: Optional - Leave empty for standard MAX, or select (HOUR, DAY, etc.) for bucketed MAX
* **Group By**: Optional - Only used with bucketed MAX. When Bucket Size is set, specify a single property name (e.g. `resource_id`) to compute MAX per group within each time bucket, then sum (per bucket and across buckets)
4. **Usage Settings**
* **Usage Reset**: Periodic (for peak tracking per period)
* **Unit Name**: `user / users`
5. **Save Feature**
## MAX - Modes
MAX aggregation can operate in two modes depending on whether `bucket_size` is specified. **`group_by` is only applied when using bucketed MAX** (i.e. when `bucket_size` is set).
### Mode 1: Standard MAX (Non-Bucketed)
**When:** `bucket_size` is NOT specified (group\_by is not used)\
**Returns:** Overall maximum value across all events\
**Use for:** Simple peak detection
### Mode 2: Bucketed MAX (Windowed)
**When:** `bucket_size` IS specified\
**Returns:** Sum of maximum values from each time bucket (see formula below)\
**Use for:** Cumulative peak billing, tiered capacity models
**Optional: Bucketed MAX with group\_by**\
When **Group By** is also set (a single property name string), within **each time bucket** events are grouped by that property. For each bucket: MAX is computed per group, then those group maximums are summed. The final result is the sum of these per-bucket totals. Use this for per-resource or per-entity peak billing within time windows (e.g. per resource, per project, per region). **group\_by has no effect without bucket\_size.**
## Calculation Examples
### Standard MAX Example
#### Event Data
```json theme={null}
[
{
"event_id": "evt_001",
"event_name": "concurrent.users",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:00:00Z",
"properties": {
"user_count": 25
}
},
{
"event_id": "evt_002",
"event_name": "concurrent.users",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T11:30:00Z",
"properties": {
"user_count": 40
}
},
{
"event_id": "evt_003",
"event_name": "concurrent.users",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T14:00:00Z",
"properties": {
"user_count": 35
}
}
]
```
#### Standard MAX Calculation
**Process:** Find the highest value across all events\
**Result:** `40 users` (maximum from all events)
***
### Bucketed MAX Example
#### Configuration
* **Bucket Size:** HOUR
* **Slab Pricing:**
* **0-5 GB:** 0 Rs (free tier)
* **5-10 GB:** 2 Rs per GB
* **10+ GB:** 3 Rs per GB
#### Event Data
```json theme={null}
[
{
"event_id": "evt_001",
"event_name": "storage.usage",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T07:30:00Z",
"properties": {
"gb_used": 8
}
},
{
"event_id": "evt_002",
"event_name": "storage.usage",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T07:45:00Z",
"properties": {
"gb_used": 4
}
},
{
"event_id": "evt_003",
"event_name": "storage.usage",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T08:15:00Z",
"properties": {
"gb_used": 10
}
},
{
"event_id": "evt_004",
"event_name": "storage.usage",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T08:30:00Z",
"properties": {
"gb_used": 5
}
},
{
"event_id": "evt_005",
"event_name": "storage.usage",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T08:45:00Z",
"properties": {
"gb_used": 9
}
}
]
```
### Bucketed MAX Formula
```
total_result = MAX(bucket_1) + MAX(bucket_2) + ... + MAX(bucket_n)
```
Where each bucket represents a time window defined by `bucket_size`.
#### Calculation Process
**Hour 1 (7:00-8:00 UTC):**
* Events: 8 GB, 4 GB
* **Bucket Maximum:** 8 GB
**Hour 2 (8:00-9:00 UTC):**
* Events: 10 GB, 5 GB, 9 GB
* **Bucket Maximum:** 10 GB
**Total Bucketed MAX:** 8 GB + 10 GB = **18 GB**
#### Tiered Billing Calculation
**Billing Calculation for 18 GB total:**
* First 5 GB: 0 Rs
* Next 5 GB (5-10): 5 × 2 = 10 Rs
* Remaining 8 GB (10-18): 8 × 3 = 24 Rs
* **Total: 34 Rs**
***
### Bucketed MAX with group\_by Example
**Group By** is only available with bucketed MAX. When set, within **each time bucket** events are grouped by the chosen property; MAX is computed per group in that bucket, then those group maximums are summed. The final result is the sum of these per-bucket totals. Useful for per-resource or per-entity peak billing within time windows.
#### Configuration
* **Aggregation Field:** `data`
* **Bucket Size:** HOUR (required for group\_by to apply)
* **Group By:** `resource_id` (single property name; must exist on event properties)
*Screenshot: Metered feature with Aggregation Function = Max, Aggregation Field = data, Bucket Size = HOUR, and Group By = resource\_id.*
#### Event Data
```json theme={null}
[
{
"event_id": "evt_A",
"event_name": "resource.usage",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:00:00Z",
"properties": {
"data": 10,
"resource_id": "resource_a"
}
},
{
"event_id": "evt_B",
"event_name": "resource.usage",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:30:00Z",
"properties": {
"data": 20,
"resource_id": "resource_b"
}
},
{
"event_id": "evt_C",
"event_name": "resource.usage",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T11:15:00Z",
"properties": {
"data": 15,
"resource_id": "resource_a"
}
}
]
```
#### Bucketed MAX with group\_by Formula
```
total_result = sum over buckets of ( MAX(group_1 in bucket) + MAX(group_2 in bucket) + ... + MAX(group_n in bucket) )
```
Within each time bucket, events are grouped by the `group_by` property; each group’s maximum is taken, then summed. Those per-bucket sums are then added together.
#### Calculation Process
**Hour 1 (10:00–11:00 UTC):**
* **resource\_a:** 10 → MAX = 10
* **resource\_b:** 20 → MAX = 20
* **Bucket 1 total:** 10 + 20 = **30**
**Hour 2 (11:00–12:00 UTC):**
* **resource\_a:** 15 → MAX = 15
* **Bucket 2 total:** **15**
**Total (Bucketed MAX with group\_by):** 30 + 15 = **45**
Without `group_by`, bucketed MAX would give 20 + 15 = 35 (max per hour). With `group_by: "resource_id"`, within each hour we sum the max per resource, then sum across hours: **45**.
## Use Cases
### Standard MAX Use Cases
#### Peak Concurrent Users
**Perfect for**: Maximum simultaneous users, peak connections
```json theme={null}
{
"event_name": "concurrent.users",
"external_customer_id": "acme_corp",
"properties": {
"active_users": 150
}
}
```
#### Peak Resource Usage
**Perfect for**: Maximum CPU, peak memory, highest bandwidth
```json theme={null}
{
"event_name": "resource.peak",
"external_customer_id": "user_456",
"properties": {
"cpu_percent": 85
}
}
```
### Bucketed MAX Use Cases
#### Cumulative Peak Billing
**Perfect for**: Pay-per-peak hour models, capacity-based pricing
```json theme={null}
{
"event_name": "capacity.usage",
"external_customer_id": "merchant_789",
"properties": {
"gb_capacity": 50
}
}
```
#### Infrastructure Peak Tracking
**Perfect for**: Peak detection across time windows for billing
```json theme={null}
{
"event_name": "server.load",
"external_customer_id": "customer_101",
"properties": {
"peak_instances": 12
}
}
```
### Bucketed MAX with group\_by Use Cases
*Requires Bucket Size to be set; group\_by is only applied with bucketed MAX.*
#### Per-Resource or Per-Entity Peak Billing
**Perfect for**: Billing on the sum of peak usage per resource within each time window (e.g. per resource ID, per project ID, per region)
```json theme={null}
{
"event_name": "resource.usage",
"external_customer_id": "customer_123",
"properties": {
"data": 50,
"resource_id": "res_abc"
}
}
```
#### Multi-Tenant or Multi-Project Peak
**Perfect for**: Sum of each tenant’s or project’s maximum usage
```json theme={null}
{
"event_name": "capacity.usage",
"external_customer_id": "platform_customer",
"properties": {
"gb_used": 100,
"project_id": "proj_xyz"
}
}
```
### When to Use Each Mode
✅ **Use Standard MAX when:**
* Need simple peak detection
* Billing based on overall maximum
* Single highest value matters
✅ **Use Bucketed MAX when:**
* Cumulative peak billing models
* Time-series peak analysis
* Tiered capacity pricing
* Sum of peaks across time windows
✅ **Use Bucketed MAX with group\_by when:**
* You already use bucketed MAX (Bucket Size is set) and want per-resource or per-entity breakdown
* Billing per resource, per project, or per entity **within each time bucket**
* `group_by` is a single property name string on your events
* **Note:** group\_by is only applied when bucket\_size is set
### Key Differences
| Aspect | Standard MAX | Bucketed MAX | Bucketed MAX + group\_by |
| ----------------- | ---------------------- | ------------------------ | ---------------------------------------------------------- |
| **Configuration** | No bucket\_size | Requires bucket\_size | Requires bucket\_size + group\_by (single field) |
| **Calculation** | Single maximum value | Sum of bucket maximums | Per bucket: sum of group maximums; then sum across buckets |
| **Use Case** | Overall peak detection | Cumulative peak billing | Per-resource / per-entity peak within time windows |
| **Result** | Simple max value | Sum of time-window peaks | Sum of (per-bucket sum of each group’s max) |
### ⚠️ Critical Notes
* **bucket\_size can ONLY be used with MAX aggregation** - No other aggregation supports this
* **Bucketed MAX sums the maximums** - Does NOT return max of maximums
* **group\_by is only applied with bucketed MAX** - You must set Bucket Size for group\_by to take effect. Within each time bucket, events are grouped by the `group_by` property; MAX is computed per group, then those group maximums are summed (per bucket), and bucket results are summed
## Next Steps
* **[Creating a Metered Feature](/docs/event-ingestion/creating-a-metered-feature)** - Complete setup guide
* **[Sending Events](/docs/event-ingestion/sending-events)** - How to transmit MAX events
# Overview
Source: https://docs.flexprice.io/docs/product-catalogue/features/aggregation/overview
**Aggregation types** define how usage is measured from incoming events. They are used to **calculate billable consumption** and form the foundation of usage-based billing.
**Flexprice** supports the following **aggregation types:**
| Aggregation | Description | Transcription |
| ----------------------- | -------------------------------------------------------- | --------------------------------------------------- |
| **COUNT** | Counts event occurrences for basic usage tracking | COUNT(DISTINCT events.id) |
| **SUM** | Aggregates numeric property values across events | SUM(events.properties.property\_name) |
| **AVERAGE** | Computes mean values from event properties | AVG(events.properties.property\_name) |
| **COUNT UNIQUE** | Tracks distinct property values for unique metrics | COUNT\_DISTINCT(events.properties.property\_name) |
| **LATEST** | Captures the most recent property value by timestamp | argMax(events.properties.property\_name, timestamp) |
| **SUM WITH MULTIPLIER** | Applies configurable rate multipliers to summed values | SUM(events.properties.property\_name) \* multiplier |
| **MAX** | Identifies peak values with optional bucketing support | MAX(events.properties.property\_name) |
| **WEIGHTED SUM** | Time-proportional aggregation for capacity-based billing | Weighted calculation based on event duration |
All aggregation types except **COUNT** operate on event properties. **The result of this aggregation will be used to calculate charges.**
## Next Steps
* [COUNT Aggregation](/docs/product-catalogue/features/aggregation/count)
* [SUM Aggregation](/docs/product-catalogue/features/aggregation/sum)
* [AVERAGE Aggregation](/docs/product-catalogue/features/aggregation/average)
* [COUNT UNIQUE Aggregation](/docs/product-catalogue/features/aggregation/count-unique)
* [LATEST Aggregation](/docs/product-catalogue/features/aggregation/latest)
* [SUM WITH MULTIPLIER Aggregation](/docs/product-catalogue/features/aggregation/sum-with-multiplier)
* [MAX Aggregation](/docs/product-catalogue/features/aggregation/max)
* [WEIGHTED SUM Aggregation](/docs/product-catalogue/features/aggregation/weighted-sum)
# SUM
Source: https://docs.flexprice.io/docs/product-catalogue/features/aggregation/sum
Sums the values of a specified property across all matching events.
Step-by-Step Setup when creating a SUM-based metered feature:
1. **Navigate to Features**
* Go to Product Catalog → Features
* Click "Add Feature"
2. **Basic Information**
* **Name**: "Data Transfer" (or descriptive name)
* **Type**: Select "Metered"
3. **Event Configuration**
* **Event Name**: `data.transfer` (must match your event data)
* **Aggregation Function**: Sum
* **Aggregation Field**: `gb` (the property to sum)
4. **Usage Settings**
* **Usage Reset**: Periodic (for monthly limits) or Cumulative
* **Unit Name**: `GB / GBs`
5. **Save Feature**
## Calculation Example
### Event Data
```json theme={null}
[
{
"event_id": "evt_001",
"event_name": "data.transfer",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:00:00Z",
"properties": {
"gb": 5.2
}
},
{
"event_id": "evt_002",
"event_name": "data.transfer",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:05:00Z",
"properties": {
"gb": 3.8
}
},
{
"event_id": "evt_001", // Duplicate event ID
"event_name": "data.transfer",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:10:00Z",
"properties": {
"gb": 7.1
}
}
]
```
### Calculation Process
1. **Event Matching**: All events with `event_name = "data.transfer"`
2. **Deduplication**: Remove duplicate event IDs, keeping latest value
* `evt_001` → `7.1 GB` (latest value for this ID)
* `evt_002` → `3.8 GB`
3. **Sum Result**: `7.1 + 3.8 = 10.9 GB`
**Result**: `10.9 GBs`
## Use Cases
### Data Transfer Tracking
**Perfect for**: Bandwidth usage, file downloads, API data transfer
```json theme={null}
{
"event_name": "data.transfer",
"external_customer_id": "acme_corp",
"properties": {
"gb": 2.5
}
}
```
### Compute Time
**Perfect for**: CPU hours, GPU usage, processing time
```json theme={null}
{
"event_name": "compute.usage",
"external_customer_id": "user_456",
"properties": {
"hours": 1.5
}
}
```
### Storage Usage
**Perfect for**: Disk space, database storage, file storage
```json theme={null}
{
"event_name": "storage.usage",
"external_customer_id": "merchant_789",
"properties": {
"mb": 512
}
}
```
### Credits Consumed
**Perfect for**: AI model usage, API credits, processing credits
```json theme={null}
{
"event_name": "model.usage",
"external_customer_id": "customer_101",
"properties": {
"credits": 25
}
}
```
### When to Use SUM
✅ **Use SUM when:**
* Tracking quantities or volumes
* Measuring resource consumption
* Accumulating usage values
* Need precise numeric totals
## Next Steps
* **[Creating a Metered Feature](/docs/event-ingestion/creating-a-metered-feature)** - Complete setup guide
* **[Sending Events](/docs/event-ingestion/sending-events)** - How to transmit SUM events
# SUM WITH MULTIPLIER
Source: https://docs.flexprice.io/docs/product-catalogue/features/aggregation/sum-with-multiplier
Sums property values and applies a configurable multiplier.
Step-by-Step Setup when creating a SUM WITH MULTIPLIER-based metered feature:
1. **Navigate to Features**
* Go to Product Catalog → Features
* Click "Add Feature"
2. **Basic Information**
* **Name**: "API Credits (USD)" (or descriptive name)
* **Type**: Select "Metered"
3. **Event Configuration**
* **Event Name**: `api.usage` (must match your event data)
* **Aggregation Function**: Sum with Multiplier
* **Aggregation Field**: `credits` (the property to sum)
* **Multiplier**: `0.001` (rate to apply - e.g., \$0.001 per credit)
4. **Usage Settings**
* **Usage Reset**: Periodic (for monthly billing) or Cumulative
* **Unit Name**: `USD` (currency or unit after multiplier)
5. **Save Feature**
## Calculation Example
### Event Data
```json theme={null}
[
{
"event_id": "evt_001",
"event_name": "api.usage",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:00:00Z",
"properties": {
"credits": 1000
}
},
{
"event_id": "evt_002",
"event_name": "api.usage",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:05:00Z",
"properties": {
"credits": 2500
}
},
{
"event_id": "evt_003",
"event_name": "api.usage",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:10:00Z",
"properties": {
"credits": 1500
}
},
{
"event_id": "evt_001", // Duplicate event ID
"event_name": "api.usage",
"external_customer_id": "customer_123",
"timestamp": "2024-01-15T10:15:00Z",
"properties": {
"credits": 800
}
}
]
```
### Configuration
* **Multiplier**: `0.001` (converts credits to USD at \$0.001 per credit)
### Sum with Multiplier Formula
```
final_result = SUM(values) × multiplier
```
### Calculation Process
1. **Event Matching**: All events with `event_name = "api.usage"`
2. **Deduplication**: Remove duplicate event IDs, keeping latest value
* `evt_001` → `800 credits` (latest value for this ID)
* `evt_002` → `2500 credits`
* `evt_003` → `1500 credits`
3. **Sum Calculation**: `800 + 2500 + 1500 = 4800 credits`
4. **Apply Multiplier**: `4800 × 0.001 = $4.80`
**Result**: `$4.80 USD`
## Use Cases
### Currency Conversion
**Perfect for**: Converting between currencies, unit price calculations
```json theme={null}
{
"event_name": "sales.transaction",
"external_customer_id": "acme_corp",
"properties": {
"amount_eur": 100
}
}
```
*Multiplier: `1.10` (EUR to USD conversion)*
### Credit-Based Pricing
**Perfect for**: API credits, processing credits, usage tokens
```json theme={null}
{
"event_name": "api.credits",
"external_customer_id": "user_456",
"properties": {
"credits_used": 250
}
}
```
*Multiplier: `0.01` (converts credits to dollars)*
### Tiered Rate Calculations
**Perfect for**: Volume discounts, tier-based pricing, rate adjustments
```json theme={null}
{
"event_name": "data.processing",
"external_customer_id": "merchant_789",
"properties": {
"gb_processed": 500
}
}
```
*Multiplier: `0.05` (premium tier rate: \$0.05/GB)*
### Unit Conversions
**Perfect for**: Converting units, standardizing measurements
```json theme={null}
{
"event_name": "storage.usage",
"external_customer_id": "customer_101",
"properties": {
"mb_stored": 1024
}
}
```
*Multiplier: `0.001` (converts MB to GB)*
### Commission Calculations
**Perfect for**: Revenue sharing, affiliate commissions, partner payouts
```json theme={null}
{
"event_name": "transaction.revenue",
"external_customer_id": "partner_user",
"properties": {
"gross_revenue": 1000
}
}
```
*Multiplier: `0.15` (15% commission rate)*
### When to Use SUM WITH MULTIPLIER
✅ **Use SUM WITH MULTIPLIER when:**
* Need to apply consistent rate conversions
* Converting between units or currencies
* Implementing tiered or variable pricing
* Calculating commissions or percentage-based fees
* Standardizing different measurement units
### Configuration Requirements
⚠️ **Critical Notes:**
* **Multiplier must be > 0** - Cannot use negative or zero values
* **Multiplier is immutable** - Cannot be changed after feature creation
* **High precision** - Stored as decimal for accurate calculations
* **Applied after summing** - Sum first, then multiply (not per-event)
## Next Steps
* **[Creating a Metered Feature](/docs/event-ingestion/creating-a-metered-feature)** - Complete setup guide
* **[Sending Events](/docs/event-ingestion/sending-events)** - How to transmit SUM WITH MULTIPLIER events
# WEIGHTED SUM
Source: https://docs.flexprice.io/docs/product-catalogue/features/aggregation/weighted-sum
Time-weighted sum calculation where values are prorated based on duration.
Step-by-Step Setup when creating a WEIGHTED SUM-based metered feature:
1. **Navigate to Features**
* Go to Product Catalog → Features
* Click "Add Feature"
2. **Basic Information**
* **Name**: "Reserved Storage" (or descriptive name)
* **Type**: Select "Metered"
3. **Event Configuration**
* **Event Name**: `storage.reserved` (must match your event data)
* **Aggregation Function**: Weighted Sum
* **Aggregation Field**: `gb_reserved` (the property to weight by time)
4. **Usage Settings**
* **Usage Reset**: Periodic (for capacity-based billing)
* **Unit Name**: `GB-time` (time-weighted unit)
5. **Save Feature**
## Calculation Example
### Billing Period
**Period:** July 31, 2025 18:30:00 UTC to August 31, 2025 18:30:00 UTC\
**Total period duration:** 31 days = 2,678,400 seconds
### Event Data
```json theme={null}
[
{
"event_id": "evt_001",
"event_name": "storage.reserved",
"external_customer_id": "customer_123",
"timestamp": "2025-08-16T00:00:00Z",
"properties": {
"gb_reserved": 20
}
},
{
"event_id": "evt_002",
"event_name": "storage.reserved",
"external_customer_id": "customer_123",
"timestamp": "2025-08-18T00:00:00Z",
"properties": {
"gb_reserved": 10
}
},
{
"event_id": "evt_003",
"event_name": "storage.reserved",
"external_customer_id": "customer_123",
"timestamp": "2025-08-20T00:00:00Z",
"properties": {
"gb_reserved": 10
}
},
{
"event_id": "evt_004",
"event_name": "storage.reserved",
"external_customer_id": "customer_123",
"timestamp": "2025-08-25T00:00:00Z",
"properties": {
"gb_reserved": 5
}
}
]
```
### Weighted Sum Formula
```
weighted_value = (property_value / total_period_seconds) × seconds_from_event_to_period_end
```
### Calculation Process
#### Event 1: August 16, 2025 - 20 GB
* **Event timestamp:** August 16, 2025 00:00:00 UTC
* **Seconds until period end:** August 31 18:30:00 - August 16 00:00:00 = 15 days, 18.5 hours = 1,365,300 seconds
* **Weighted contribution:** (20 / 2,678,400) × 1,365,300 = (20 × 1,365,300) / 2,678,400 = 27,306,000 / 2,678,400 = **10.198660714285714 GB**
#### Event 2: August 18, 2025 - 10 GB
* **Event timestamp:** August 18, 2025 00:00:00 UTC
* **Seconds until period end:** August 31 18:30:00 - August 18 00:00:00 = 13 days, 18.5 hours = 1,197,300 seconds
* **Weighted contribution:** (10 / 2,678,400) × 1,197,300 = (10 × 1,197,300) / 2,678,400 = 11,973,000 / 2,678,400 = **4.470982142857143 GB**
#### Event 3: August 20, 2025 - 10 GB
* **Event timestamp:** August 20, 2025 00:00:00 UTC
* **Seconds until period end:** August 31 18:30:00 - August 20 00:00:00 = 11 days, 18.5 hours = 1,029,300 seconds
* **Weighted contribution:** (10 / 2,678,400) × 1,029,300 = (10 × 1,029,300) / 2,678,400 = 10,293,000 / 2,678,400 = **3.843303571428571 GB**
#### Event 4: August 25, 2025 - 5 GB
* **Event timestamp:** August 25, 2025 00:00:00 UTC
* **Seconds until period end:** August 31 18:30:00 - August 25 00:00:00 = 6 days, 18.5 hours = 573,300 seconds
* **Weighted contribution:** (5 / 2,678,400) × 573,300 = (5 × 573,300) / 2,678,400 = 2,866,500 / 2,678,400 = **1.070200892857143 GB**
### Total Weighted Sum
**10.198660714285714 + 4.470982142857143 + 3.843303571428571 + 1.070200892857143 = 19.583147321428571 GB**
**Result**: `19.583147321428571 GB-time`
## Use Cases
### Reserved Capacity Billing
**Perfect for**: Cloud storage reservations, compute capacity, bandwidth reservations
```json theme={null}
{
"event_name": "capacity.reserved",
"external_customer_id": "acme_corp",
"properties": {
"cpu_cores": 8
}
}
```
### Time-Based Resource Usage
**Perfect for**: Database connections, server instances, license usage
```json theme={null}
{
"event_name": "database.connection",
"external_customer_id": "user_456",
"properties": {
"connection_count": 5
}
}
```
### Subscription Changes
**Perfect for**: Mid-cycle plan changes, prorated upgrades/downgrades
```json theme={null}
{
"event_name": "plan.upgrade",
"external_customer_id": "merchant_789",
"properties": {
"new_tier_level": 3
}
}
```
### Infrastructure Provisioning
**Perfect for**: VM instances, container resources, network bandwidth
```json theme={null}
{
"event_name": "vm.provisioned",
"external_customer_id": "customer_101",
"properties": {
"memory_gb": 16
}
}
```
### License Allocation
**Perfect for**: Software seats, user licenses, feature entitlements
```json theme={null}
{
"event_name": "license.allocated",
"external_customer_id": "enterprise_user",
"properties": {
"seat_count": 25
}
}
```
### When to Use WEIGHTED SUM
✅ **Use WEIGHTED SUM when:**
* Billing for reserved or allocated capacity
* Need time-proportional calculations
* Handling mid-cycle subscription changes
* Measuring sustained resource usage over time
* Events earlier in the period have more time remaining and get higher weight
### Key Characteristics
* **Time-weighted calculation** - Events earlier in the period have higher impact
* **Perfect for capacity billing** - Bills based on how long resources were allocated
* **Handles subscription changes** - Automatically prorates usage changes
* **Complex formula** - More computationally intensive than other aggregations
## Next Steps
* **[Creating a Metered Feature](/docs/event-ingestion/creating-a-metered-feature)** - Complete setup guide
* **[Sending Events](/docs/event-ingestion/sending-events)** - How to transmit WEIGHTED SUM events
# Creating a feature
Source: https://docs.flexprice.io/docs/product-catalogue/features/create
Follow the steps below to create a feature in Flexprice:
1. In the main navigation menu, select **Features** from **Product Catalog**.
2. At the top-right of the Feature dashboard, select **Add Feature** to create a new feature.
3. Fill in the feature details:
* **Feature Name:** Enter a unique and descriptive name for the feature (e.g., "Premium API Access").
* **Feature Type:** Select the type of feature:
* **Metered:** Tracks quantifiable usage (e.g., number of API calls)
* **Boolean:** Indicates a simple on/off state (e.g., access to a premium dashboard)
* **Static:** Represents fixed attributes or entitlements (e.g., priority support)
* **Config:** Delivers a custom JSON configuration to your application through the customer's entitlements (e.g., feature flags, rate limits, tenant settings)
## How to create a Boolean feature
Select **Boolean** as Type which indicates a simple on/off state (e.g., access to a premium dashboard).
## How to create a Static feature
Select **Static** as feature type which represents fixed attributes or entitlements (e.g., priority support)
## How to create a Config feature
A Config feature lets you attach a JSON object to an entitlement as a `config_value`. Flexprice stores and delivers this payload through plans and subscriptions, giving your application full control over how the configuration is applied.
In the **Add Feature** form, choose **Config** as the Feature Type.
The `config_value` is set at the entitlement level, not on the feature itself. You define it when attaching this feature to a plan.
Navigate to the target plan, open the **Entitlements** tab, and select **Add Entitlement**. Choose the Config feature you created.
A JSON editor appears. Enter a valid JSON object as the `config_value`. The editor validates the input and blocks submission if the value is not valid JSON.
Click **Add** to save the entitlement. The plan's Entitlements tab displays a truncated JSON preview in the **Value** column. Click the preview to open the full stored config payload.
The `config_value` is available on the customer's entitlements as soon as they subscribe to a plan that includes this feature. Use the [Get Customer Entitlements](/api-reference/customers/get-customer-entitlements) endpoint to retrieve it.
## How to create a Metered feature
1. Select **Metered** as Type which tracks quantifiable usage (e.g., number of API calls).
2. Optionally edit the singular and plural unit names, for example: "token" and "tokens".
### Define Event Details
#### Event Name
For example, suppose you want to track how many tokens users consume while calling your AI model:
* **Event Name**: `tokens_total`
* This is the unique identifier you'll use to send events that match the metric
* Must be unique across your Flexprice account to avoid confusion with other metrics
#### Filters (Optional)
It is important to note that filters cannot be edited later, so decide up front if you'll need them for billing or reporting.
Filters let you **narrow down** which events get counted or summed in this metric. You do not have to add filters if you want to aggregate all events of a given type. However, filters become crucial if you need more granular control, such as billing for only certain AI models or distinguishing usage by environment.
**Example**: If your system supports multiple AI models (`gpt 3`, `llama3.2`, `gpt 4`), and you only want to track usage from GPT models for specialized billing, you can add a filter:
* **Key**: `model`
* **Values**: `gpt 3`, `gpt 4`
This means Flexprice will only aggregate token usage events where `model` is either `gpt 3` or `gpt 4`.
### Define Aggregation
The aggregation settings tell Flexprice how to measure your events.
#### Function
Choose one of these aggregation functions (see [Aggregation Overview](/docs/product-catalogue/features/aggregation/overview) for detailed explanations):
* **COUNT**: Counts the total number of matching events, regardless of any numeric value in the event data
* **SUM**: Adds a numeric property (like tokens, price, etc.) across all matching events
* **AVERAGE**: Calculates the mean value of a specified property across all matching events
* **COUNT UNIQUE**: Counts only the unique values of a specified event property
* **LATEST**: Returns the most recent value of a specified property based on event timestamp
* **SUM WITH MULTIPLIER**: Sums a property and applies a configurable multiplier for rate conversions
* **MAX**: Returns the maximum value of a specified property (supports bucketed calculations)
* **WEIGHTED SUM**: Time-weighted sum for capacity-based billing where values are prorated by duration
#### Field
* The property that Flexprice aggregates. Required for all aggregation functions except `COUNT`
* For numeric aggregations (`SUM`, `AVERAGE`, `SUM WITH MULTIPLIER`, `MAX`, `WEIGHTED SUM`): specify a numeric property (e.g., `tokens`, `gb`, `response_time`)
* For `COUNT UNIQUE`: specify any property whose distinct values you want to count (e.g., `user_id`, `session_id`)
* For `LATEST`: specify the property whose most recent value you want to track (e.g., `tier_level`, `status`)
* For `COUNT`: no field required (counts event occurrences only)
#### Usage Reset
* **Cumulative**: The meter never resets. Usage keeps accumulating across billing cycles, providing a running total. This is useful for features like storage.
* **Periodic**: Usage resets at each billing period (e.g., monthly). This is ideal for most subscription-based models where usage resets for every billing period
**Example Configuration**:
* **Aggregation** = `SUM`
* **Aggregation Value** = `tokens`
* **Aggregation Type** = `Periodic` (if you invoice monthly and want usage to reset each month)
# Custom Expression
Source: https://docs.flexprice.io/docs/product-catalogue/features/custom-expression
Compute a per-event quantity from event properties using a CEL formula, then aggregate the result.
Custom Expression lets you write a small formula that runs on every event to compute the quantity that gets aggregated. Instead of aggregating a single property field directly, the aggregation runs on the result of the expression. Use it when the billable quantity is derived from multiple properties (e.g. `input_tokens + output_tokens`), needs a unit conversion (e.g. `duration_ms / 1000`), or needs to be rounded, clamped, or floored per event.
Expressions are written in **CEL** (Common Expression Language). They are validated when the feature is saved, so syntax and type errors surface up front rather than at ingestion time.
**Benefits:**
* **Multi-property quantities** — Combine several event fields into one billable number (`input_tokens + output_tokens`, `cores * hours`)
* **Per-event transforms** — Round, floor, ceil, or clamp before aggregation (`ceil(duration_ms / 1000)`, `min(usage, quota)`)
* **Unit conversions** — Convert between units per event (`bytes / 1024`, `duration_ms / 1000`)
* **Validated at save time** — Syntax errors, missing operators, and non-numeric expressions are rejected when the feature is created, not per event
* **No code deploys** — Change the pricing formula from the dashboard when your metering model evolves
Custom Expression is only available for **SUM**, **AVG**, **MAX**, and **LATEST** aggregations. It is not supported for COUNT, COUNT UNIQUE, SUM WITH MULTIPLIER, or WEIGHTED SUM.
## How It Works
For each event, Flexprice reads the top-level properties in `event.properties`, evaluates your expression against them, and treats the result as that event's quantity. The chosen aggregation (SUM / AVG / MAX / LATEST) then runs over those per-event quantities.
```
per_event_quantity = evaluate(expression, event.properties)
feature_value = aggregate(per_event_quantity across events)
```
* **Variables** are top-level property names on your events (e.g. `tokens`, `duration_ms`, `input_tokens`). Nested paths are not supported — flatten the value to a top-level property.
* **Missing properties** are treated as `0`.
* **Numeric strings** (e.g. `"2"`, `"5.5"`) are coerced to numbers so events don't fail just because a field was serialized as a string.
* **Non-numeric strings**, `NaN`, and `±Inf` are rejected — the event's quantity fails to compute and the error is surfaced.
* **All arithmetic is real-valued.** Integer literals are promoted to doubles, so `total / 4` with `total = 10` evaluates to `2.5`, not `2`.
## Step-by-Step Setup
Go to **Product Catalog → Features** and click **Add Feature**.
* **Name**: descriptive name (e.g. `Phone Call Seconds`)
* **Type**: Select **Metered**
* **Event Name**: the event you send (e.g. `phone_call`)
* **Aggregation Function**: pick **Sum**, **Average**, **Max**, or **Latest**
* **Aggregation Field**: the property that would be used if no expression is set (e.g. `tokens`). This is still required as a fallback / label.
Click **+ Custom expression** below the aggregation section.
Enter a CEL formula in the **Custom Expression** field. Variables are the top-level property names on your events.
On save, the expression is compiled and validated. If it has a syntax error, references no variables, or evaluates to a non-numeric type, saving fails with the error.
## Supported Functions
The following math helpers are available inside expressions. All take and return numbers.
| Function | Signature | Description |
| ----------- | --------------------------- | ------------------------------------ |
| `max(a, b)` | `(number, number) → number` | Returns the larger of `a` and `b` |
| `min(a, b)` | `(number, number) → number` | Returns the smaller of `a` and `b` |
| `pow(x, y)` | `(number, number) → number` | Raises `x` to the power `y` |
| `abs(x)` | `(number) → number` | Absolute value |
| `ceil(x)` | `(number) → number` | Rounds up to the nearest integer |
| `floor(x)` | `(number) → number` | Rounds down to the nearest integer |
| `round(x)` | `(number) → number` | Rounds half away from zero |
| `sqrt(x)` | `(number) → number` | Square root; errors if `x < 0` |
| `log(x)` | `(number) → number` | Natural logarithm; errors if `x ≤ 0` |
Standard CEL operators are also available: `+`, `-`, `*`, `/`, `<`, `<=`, `>`, `>=`, `==`, `!=`, `&&`, `||`, `!`, and the ternary `cond ? a : b`.
Modulo (`%`) is not available because all values are typed as doubles. Use `x - floor(x / y) * y` if you need a remainder.
## Calculation Example
### Configuration
* **Event Name**: `phone_call`
* **Aggregation Function**: Sum
* **Custom Expression**: `ceil(duration_ms / 1000)`
* **Unit Name**: `second / seconds`
### Event Data
```json theme={null}
[
{
"event_id": "evt_001",
"event_name": "phone_call",
"external_customer_id": "customer_123",
"timestamp": "2026-07-09T10:00:00Z",
"properties": {
"duration_ms": 4200
}
},
{
"event_id": "evt_002",
"event_name": "phone_call",
"external_customer_id": "customer_123",
"timestamp": "2026-07-09T10:05:00Z",
"properties": {
"duration_ms": 8100
}
},
{
"event_id": "evt_003",
"event_name": "phone_call",
"external_customer_id": "customer_123",
"timestamp": "2026-07-09T10:10:00Z",
"properties": {
"duration_ms": 500
}
}
]
```
### Calculation Process
1. **Per-event quantity** — Evaluate `ceil(duration_ms / 1000)` on each event
* `evt_001` → `ceil(4200 / 1000)` = `5`
* `evt_002` → `ceil(8100 / 1000)` = `9`
* `evt_003` → `ceil(500 / 1000)` = `1`
2. **Aggregation** — SUM across events: `5 + 9 + 1 = 15`
**Result**: `15 seconds`
Without the expression, a plain `SUM(duration_ms)` would have produced `12,800` — the expression bills whole seconds with per-call rounding, which is what the pricing model requires.
## Use Cases
### Token-based AI billing
**Perfect for**: LLM APIs that charge different rates for input vs. output tokens.
**Expression**: `input_tokens + output_tokens * 3`
```json theme={null}
{
"event_name": "llm.completion",
"external_customer_id": "acme_corp",
"properties": {
"input_tokens": 500,
"output_tokens": 120,
"model": "gpt-4"
}
}
```
*Aggregation: SUM. Each output token is billed as 3 units to reflect the higher output cost.*
### Rounded call duration (telephony)
**Perfect for**: Voice APIs billed by the whole second or whole minute, with per-call rounding.
**Expression**: `ceil(duration_ms / 1000)`
```json theme={null}
{
"event_name": "phone_call",
"external_customer_id": "customer_123",
"properties": {
"duration_ms": 8100
}
}
```
*Aggregation: SUM. A 8.1s call is billed as 9 seconds; the sum reflects total billable seconds.*
### Compute cost per event
**Perfect for**: Infrastructure billing where quantity depends on multiple dimensions per job.
**Expression**: `cores * hours * rate`
```json theme={null}
{
"event_name": "compute.job",
"external_customer_id": "user_456",
"properties": {
"cores": 8,
"hours": 2.5,
"rate": 0.05
}
}
```
*Aggregation: SUM. Each job contributes its own cost; the feature value is total spend.*
### Quota-clamped usage
**Perfect for**: Fair-use plans that cap what a single event can contribute.
**Expression**: `min(api_calls, 1000)`
```json theme={null}
{
"event_name": "api.batch",
"external_customer_id": "merchant_789",
"properties": {
"api_calls": 4200
}
}
```
*Aggregation: SUM. A batch of 4,200 calls contributes at most 1,000 to the metered total.*
### Pulse pricing (minimum billable unit)
**Perfect for**: Charging in fixed pulses (e.g. 15-second SMS pulses, 6-second call pulses).
**Expression**: `duration <= 10 ? 0 : ceil(duration / 15) * 15`
```json theme={null}
{
"event_name": "sms.send",
"external_customer_id": "customer_101",
"properties": {
"duration": 18
}
}
```
*Aggregation: SUM. Short pulses under 10 units are free; longer pulses round up to the next 15-unit block.*
### Data transfer with unit conversion
**Perfect for**: Events emitted in bytes but billed in GB.
**Expression**: `bytes / 1073741824`
```json theme={null}
{
"event_name": "data.transfer",
"external_customer_id": "acme_corp",
"properties": {
"bytes": 5368709120
}
}
```
*Aggregation: SUM. Each event converts bytes to GB; the feature value is total GB transferred.*
## Error Handling
Expressions can fail either at **save time** (compile) or **event time** (evaluate). Compile errors block feature creation; evaluate errors cause the specific event's quantity to fail.
### Compile-time errors
Surfaced when you save the feature. Fix the expression before saving.
| Error | Cause | Example |
| ------------------ | ----------------------------------------- | ------------- |
| Syntax error | Malformed CEL | `a + b +` |
| No variables | Expression has only literals | `1 + 2` |
| Non-numeric result | Result is not a number | `a + "hello"` |
| Unknown function | Function is not in the supported list | `tan(theta)` |
| Wrong arity | Function called with wrong number of args | `max(tokens)` |
### Event-time errors
Surfaced per event during ingestion.
| Error | Cause |
| ----------------------- | ---------------------------------------------------------------------------------- |
| Property is not numeric | An identifier resolves to a non-numeric string (e.g. `"abc"`) |
| Non-finite property | Property value is `NaN`, `+Inf`, or `-Inf` |
| Non-finite result | Expression produced `NaN` or `±Inf` (division by zero, `sqrt(-1)`, `pow` overflow) |
Test your expression against a few real payloads in a scratch event before switching production traffic. A quick way is to enable the feature on a test plan and confirm the aggregated value matches what you expect.
## Critical Notes
* **Only SUM, AVG, MAX, and LATEST** support custom expressions today.
* **Top-level properties only** — nested fields (e.g. `payload.tokens`) are not addressable; flatten them at ingestion.
* **Missing properties default to `0`**, not an error. If a required property is missing on the event, the expression will still evaluate but the quantity will reflect that zero.
* **CEL reserved keywords** cannot be property names inside expressions: `as`, `break`, `const`, `continue`, `else`, `false`, `for`, `function`, `if`, `import`, `in`, `let`, `loop`, `package`, `namespace`, `null`, `return`, `true`, `var`, `void`, `while`. Rename the property on your event if it clashes.
* **Real division** — Integer literals are promoted to doubles, so `total / 1000` returns a real number, not a truncated integer.
* **Expression changes apply to new events only** — Historical events are not re-evaluated when you edit the expression.
## Next Steps
* **[SUM Aggregation](/docs/product-catalogue/features/aggregation/sum)** — The most common aggregation used with custom expressions
* **[Creating a Metered Feature](/docs/event-ingestion/creating-a-metered-feature)** — Full feature setup walkthrough
* **[Sending Events](/docs/event-ingestion/sending-events)** — How to structure `event.properties` for expression variables
# Entitlement Grants
Source: https://docs.flexprice.io/docs/product-catalogue/features/entitlement-grants
Configure time-boxed usage quotas on entitlements — rolling windows, parallel limits, and amount-based caps with exhaustion alerts and overage billing
Entitlement grants add time-boxed quotas on top of [entitlements](/docs/product-catalogue/features/linking-to-plans). A regular entitlement gives a customer a usage limit per billing period; a grant config turns it into a quota per window — "1M tokens per 5 hours" or "\$50 of compute per day" — independent of the billing cycle. Flexprice opens a concrete grant window when usage starts, tracks consumption against the quota, fires a webhook when the quota is exhausted, and bills usage beyond the quota as overage on the invoice.
**Benefits:**
* **Rolling limits** — Quotas measured in hours, days, or weeks instead of the billing period
* **Parallel limits** — Stack independent windows on one feature, such as a 5-hour limit and a weekly limit
* **Amount-based quotas** — Cap spend in currency, priced through your existing billing pipeline
* **Exhaustion webhooks** — `entitlement.grant.exhausted` fires when a window's quota is used up
* **Overage billing** — Usage beyond the quota lands on the arrears invoice automatically
## Concepts
| Term | Meaning |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Entitlement config** | An entitlement that carries a grant config (`grant_measure`, `grant_duration_value`, `grant_duration_unit`, `grant_quota`, `aggregation_mode`). Setting these fields is the opt-in — there is no separate type flag. |
| **Entitlement grant** | One concrete window instantiated from the config: a `valid_from`/`valid_to` range with an immutable quota and a usage snapshot. |
| **Measure** | What the quota counts: `quantity` (raw meter units) or `amount` (currency, priced through the billing pipeline). |
| **Aggregation mode** | How multiple grant configs on the same feature combine: `additive` merges quotas into one bucket, `parallel` keeps each config as its own independent bucket. |
## Configuring a Grant
Add the grant fields to a [create entitlement](/api-reference/entitlements/create-entitlement) or [update entitlement](/api-reference/entitlements/update-entitlement) request. The feature must be metered.
```bash cURL theme={null}
curl -X POST "https://us.api.flexprice.io/v1/entitlements" \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"feature_id": "feat_tokens",
"feature_type": "metered",
"entity_type": "plan",
"entity_id": "plan_pro",
"is_enabled": true,
"grant_measure": "quantity",
"grant_quota": "1000000",
"grant_duration_value": 5,
"grant_duration_unit": "hour",
"aggregation_mode": "parallel"
}'
```
### Grant Config Fields
| Field | Description |
| ---------------------- | ----------------------------------------------- |
| `grant_measure` | `quantity` (meter units) or `amount` (currency) |
| `grant_quota` | Quota per window, as a decimal string |
| `grant_duration_value` | Window length, a positive integer |
| `grant_duration_unit` | `hour`, `day`, or `week` |
| `aggregation_mode` | `additive` (default) or `parallel` |
The grant config is all-or-nothing: set every field or none. On update, `clear_grant_config: true` removes the config and returns the entitlement to legacy behavior.
Grant configs are validated at write time:
* The feature must be metered. Meters with `MAX` aggregation or bucketed aggregation are not supported.
* All grant configs on one feature must share the same `grant_measure` and the same `aggregation_mode`. Additive configs must also share the same duration.
* `amount` grants require flat per-unit pricing on the meter — tiered prices are rejected.
A grant config whose duration is the billing period or longer is ignored at runtime — a window spanning the whole period is just the period quota. Use `usage_limit` with `usage_reset_period` for that case.
## How Grant Windows Work
Windows are usage-anchored. Flexprice does not open a window on a fixed schedule; it opens one when usage arrives:
1. The first usage event past the covered range opens a window at that event's timestamp. An event at 2:00 opens a window starting at 2:00, even if evaluation runs at 2:07.
2. The window runs for the configured duration: `valid_to = valid_from + duration`.
3. When the window ends, nothing happens until the next usage event — idle periods open no windows, so an inactive customer accumulates no grant rows.
4. Windows never straddle billing cycles. A window that would end within an hour of the cycle boundary stretches to the boundary instead of leaving a short stub.
Each grant tracks two states in `grant_status`:
* `active` — usage is below the quota
* `exhausted` — usage reached the quota (`usage >= quota`)
Expiry is not a status. A grant is closed when `valid_to` has passed, regardless of how much quota was used.
### Grant Object
| Field | Description |
| -------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `id` | Grant identifier (`eg_` prefix) |
| `entitlement_config_id` | The entitlement this window was instantiated from |
| `customer_id`, `subscription_id` | Who the window belongs to |
| `scope_entity_type`, `scope_entity_id` | What the grant meters — `feature` and the feature ID |
| `measure` | `quantity` or `amount` |
| `quota` | The window's quota (immutable; the summed quota for additive groups) |
| `usage` | Usage consumed in this window, refreshed on each evaluation |
| `valid_from`, `valid_to` | The window boundaries |
| `grant_status` | `active` or `exhausted` |
| `quota_crossed_at` | Set once, when evaluation first sees `usage >= quota`. Overage billing starts from this timestamp. |
## Aggregation Modes
When a customer holds multiple grant configs on the same feature — a plan entitlement plus an addon entitlement, for example — `aggregation_mode` decides how they combine.
### Additive
All configs merge into **one** window with `quota = sum of quotas`. Use this when an addon should top up the base allowance.
**Example**: The Pro plan grants 100K tokens per day. A booster addon grants another 50K per day. The customer gets one daily window with a 150K quota.
### Parallel
Each config opens its **own** independent window with its own duration. All windows meter the same usage stream. A request is within limits only while every applicable window has quota left; exhausting any one window puts the feature into overage for the remainder of that window.
**Example**: 1M tokens per 5 hours *and* 5M tokens per week, as two parallel configs. Heavy use exhausts the 5-hour window first; sustained use over days exhausts the weekly window even though no single 5-hour burst did.
## Use Cases
### Rolling Rate Limits for an AI Product
Cap token consumption in a rolling window that has nothing to do with the monthly invoice — the pattern used by AI assistants that advertise "resets every 5 hours".
```json theme={null}
{
"grant_measure": "quantity",
"grant_quota": "1000000",
"grant_duration_value": 5,
"grant_duration_unit": "hour",
"aggregation_mode": "parallel"
}
```
Add a second parallel config with `"grant_duration_unit": "week"` to layer a weekly ceiling on top of the burst limit.
### Daily Spend Cap
Cap what a customer's usage *costs* rather than how many units they consume. The `amount` measure prices usage through your existing meter prices, so a mid-window price change is respected.
```json theme={null}
{
"grant_measure": "amount",
"grant_quota": "50",
"grant_duration_value": 1,
"grant_duration_unit": "day",
"aggregation_mode": "additive"
}
```
The customer gets \$50 of compute per day. Alert on exhaustion, or let usage continue and bill the excess as overage.
### Plan Plus Booster Addon
Sell top-ups without new plumbing. The plan carries a grant config; the addon carries another on the same feature with `additive` mode. Buying the addon raises the shared window's quota by the addon's amount. With `parallel` mode instead, the addon grants a separate window — useful when the addon's allowance runs on its own clock.
### Soft Limits With Billed Overage
Grants implement soft limits: usage is never blocked. When a window's quota is crossed, usage recorded after the crossing is billed as overage on the subscription's arrears invoice — quantity overage re-enters the pricer as billable units, amount overage lands directly as a charge. With parallel windows, overlapping overage periods are merged so a unit of usage is never billed twice.
Overage folding skips line items with tiered prices, commitments, or true-up — those price against the full cycle, so grant overage on them falls back to the standard billing calculation.
## Exhaustion Alerts
Evaluation runs after usage events arrive (debounced, \~5m30s by default). When a window's usage reaches its quota, the grant flips to `exhausted`, one alert log is written, and a webhook fires. Recovery is not a state transition — a fresh window with a fresh quota simply opens when the next one starts.
### Webhook Payload
**Event type:** `entitlement.grant.exhausted`
```json theme={null}
{
"event_type": "entitlement.grant.exhausted",
"alert_type": "entitlement_grant_exhausted",
"alert_status": "in_alarm",
"usage_ratio": "1.15",
"triggered_at": "2026-07-25T20:20:23Z",
"subscription": {
"id": "subs_01KYARKSC3GVX64QEF4T4ABS9X",
"customer_id": "cust_01KYARB3TVZ0RZPDGC5P40NHHC",
"plan_id": "plan_pro",
"subscription_status": "active",
"currency": "usd",
"billing_period": "MONTHLY",
"current_period_start": "2026-07-24T19:11:06Z",
"current_period_end": "2026-08-24T19:11:06Z"
},
"customer": {
"id": "cust_01KYARB3TVZ0RZPDGC5P40NHHC",
"external_id": "cust-new",
"name": "Acme Corp",
"email": "billing@acme.com"
},
"entitlement": {
"id": "ent_01KYB5ESPNNB8DWFVQAFJN6KKZ",
"feature_id": "feat_tokens",
"feature_type": "metered",
"grant_measure": "quantity",
"grant_quota": "1000000",
"grant_duration_value": 5,
"grant_duration_unit": "hour",
"aggregation_mode": "parallel"
},
"entitlement_grant": {
"id": "eg_01KYDEZ86JA78AM7F7Y2SMTRWE",
"entitlement_config_id": "ent_01KYB5ESPNNB8DWFVQAFJN6KKZ",
"customer_id": "cust_01KYARB3TVZ0RZPDGC5P40NHHC",
"subscription_id": "subs_01KYARKSC3GVX64QEF4T4ABS9X",
"scope_entity_type": "feature",
"scope_entity_id": "feat_tokens",
"measure": "quantity",
"quota": "1000000",
"usage": "1150000",
"valid_from": "2026-07-25T15:20:00Z",
"valid_to": "2026-07-25T20:20:00Z",
"grant_status": "exhausted",
"quota_crossed_at": "2026-07-25T20:20:23Z"
}
}
```
### Webhook Fields
| Field | Description |
| ------------------- | --------------------------------------------------------------------------- |
| `alert_type` | Always `entitlement_grant_exhausted` |
| `alert_status` | `in_alarm` when the quota is exhausted |
| `usage_ratio` | `usage / quota` at evaluation time — `1` or more when exhausted |
| `triggered_at` | When the exhaustion was detected |
| `subscription` | The owning subscription, without line items or plan expansions |
| `customer` | The customer the grant belongs to |
| `entitlement` | The entitlement config the window was instantiated from |
| `entitlement_grant` | The exhausted window, including `quota`, `usage`, and the window boundaries |
# Link features to plans
Source: https://docs.flexprice.io/docs/product-catalogue/features/linking-to-plans
In **Flexprice**, managing customer access to features is done through **entitlements**. An **entitlement** defines which features a customer can access based on their subscription plan. This ensures that each customer receives the correct level of access, allowing precise control over feature availability and usage limits.
**Linking Features to Plans through Entitlements**
* Choose the plan to which you want to link features.
* Navigate to the **Entitlements** section within the selected plan.
* Click **Add** to add the desired features to the plan.
* Configure access parameters, including usage limits, permissions, and other relevant settings.
* Click **Save**, and the entitlements will be applied to all customers on this plan.
**Configuring Entitlements for Different Feature Types**
* **Boolean Features**
* Selecting a Boolean feature **grants or restricts** access for customers on this plan.
* No further configuration is required—just **Add** or **Cancel**.
* **Metered features**
* **Setting the Value**
* Enter the maximum usage limit a customer is allowed before restrictions apply.
* If there is no cap, select Set to Infinite.
* **Usage reset**
* By default, metered features reset at the start of each billing period.
* If needed, you can configure a separate reset interval, independent of the billing period.
* When enabled, usage resets to zero at the specified interval, regardless of when the billing cycle occurs.
💡 **Example:** A plan is billed **annually**, but API call limits **reset every month**. Setting a **monthly reset** ensures that users get a fresh allocation of calls each month.
* **Soft Limit vs. Hard Limit**
Once a customer reaches their allocated limit, there are two ways to handle additional usage:
* Soft Limit (Overage Allowed)
* Customers can exceed their limit.
* Excess usage may be charged separately (based on pricing rules).
* Common for pay-as-you-go models.
💡 Example: A GPT API plan allows 50,000 tokens/month, but if exceeded, additional tokens are charged \$0.10 per 1,000 tokens.
* Hard Limit (Strict Cap)
* Customers cannot exceed the limit.
* Once they reach their limit, requests are blocked.
* Typically used for free tiers or strict allowances.
* **Static Features**
* Instead of setting limits, define a custom value.
* Example: 24/7 support, premium customer service, AI model access.
* Customers will see these benefits listed as part of their subscription.
The linked features are now associated with the plan, and subscribers will have access according to the configured entitlements.
# Overview
Source: https://docs.flexprice.io/docs/product-catalogue/features/overview
A Feature in Flexprice represents a unit of functionality that customers can access, use, or be billed for.
They form the foundational building blocks of your product offering. Features define what customers can do, how much they can use, and how they are charged—like API endpoints, storage limits, premium dashboards, priority support, or advanced analytics.
**Types of features**
Flexprice allows you to define three types of features:
* **Metered Features**
* **Boolean Features**
* **Static Features**
**Boolean Features**
Boolean Features represent capabilities or functionalities that customers either have access to or don't—essentially a simple on/off switch.
For example, in a SaaS platform offering AI models shown above, a Boolean Feature like **SAML SSO Authentication** might be available only in higher-tier plans. In this case, customers on Plan 1 would not have access to SAML authentication, while those on Plan 2 would.
**Metered Features**
Metered Features track quantifiable usage by customers, allowing you to bill them based on how much they use. These features capture dynamic, variable usage over time. It forms the backbone for accurate invoicing, usage analysis, and flexible pricing plans.
In the above example, a metered feature tracks the number of **GPT tokens** processed per month. A customer on Plan 1 may have a limit of 10,000 tokens per month, while a customer on Plan 2 could have 1,000,000 tokens per month.
**Static Features**
Static Features represent fixed attributes or configurations of your product that don't dynamically change based on usage. They are generally consistent and predefined per pricing plan or tier. Unlike Metered Features or Boolean Features, Static Features remain constant and serve as descriptors of a plan’s benefits.
In this example, the Available Models is a static feature.
# Use cases
Source: https://docs.flexprice.io/docs/product-catalogue/features/use-cases
Metered features in Flexprice allow you to **track usage-based metrics** like API requests, AI token consumption, and storage usage. Below are some **common real-world examples**, along with **why they are structured a certain way** and how to configure them in Flexprice.
**AI Token Usage Tracking**
In most AI applications, token usage needs to be tracked for billing and cost control. Since a single API interaction may consume multiple tokens (input, output, and system prompts), tracking token usage accurately is critical.
Additionally, different LLM models (e.g., GPT-4, Claude, Llama) charge different rates per token, and token pricing varies based on input vs. output tokens. To capture and price these variations differently, we must store model type and prompt type as filters.
```jsx theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/meters \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"aggregation": {
"field": "tokens",
"type": "SUM"
},
"event_name": "tokens_total",
"name": "ai_tokens_used",
"filters": [
{
"key": "model_type",
"values": [
"gpt-4"
]
},
{
"key": "prompt_type",
"values": [
"input"
]
}
],
"reset_usage": "Periodic"
}'
```
💡 Outcome:
Flexprice will sum up all tokens used within a billing cycle, while also tracking usage by model and prompt type. This allows you to bill differently for input vs. output tokens and adjust pricing per model.
**GPU Compute Time Billing**
Cloud infrastructure providers charge customers based on GPU time used for model training and inference. Since different GPUs (e.g., NVIDIA A100, H100) have different hourly costs, tracking GPU usage by type is necessary for accurate billing.
Additionally, compute time is measured in seconds, but billing is often done in hourly increments. To ensure proper charge alignment, we track time in seconds and aggregate usage before billing.
```jsx theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/meters \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"aggregation": {
"field": "time_seconds",
"type": "SUM"
},
"event_name": "gpu_time",
"name": "gpu_time_used",
"filters": [
{
"key": "gpu_type",
"values": [
"nvidia_a100"
]
}
],
"reset_usage": "Periodic"
}'
```
💡 Outcome:
This setup enables per-second tracking of GPU usage, aggregated hourly for billing. Filtering by GPU type ensures each machine is billed at the correct rate.
**API Request Counting**
Many SaaS products with API-based pricing offer a fixed number of free requests per month, with overage charges for additional usage.
For accurate billing:
* Every API request should be counted.
* Different endpoints may have different billing weights (e.g., a basic GET request vs. an expensive AI inference call).
```jsx theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/meters \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"aggregation": {
"field": "",
"type": "COUNT"
},
"event_name": "api_calls",
"name": "api_requests",
"filters": [],
"reset_usage": "Periodic"
}'
```
💡 Outcome:
Every API call is counted and attributed to the customer, with overages billed at the correct rate. You can also filter by endpoint to charge differently for high-compute calls.
# Feature-Wallet Balance Alert
Source: https://docs.flexprice.io/docs/product-catalogue/features/wallet-balance-alert
Set up automated alerts to monitor feature wallet balances and receive notifications when thresholds are crossed
The Feature Wallet Balance Alert system enables real-time monitoring of wallet balances for specific features and triggers automated notifications when configured thresholds are breached. This powerful monitoring tool helps prevent service interruptions, manage usage limits, and track spending across your feature offerings.
## Overview
Feature Wallet Balance Alerts provide a flexible, multi-level alerting system that monitors wallet balances associated with specific features. When a wallet balance crosses defined thresholds, the system automatically triggers alerts and sends webhook notifications, allowing you to take proactive action before critical issues arise.
**Key Benefits:**
* **Prevent Service Interruptions**: Get notified before wallet balance reaches critical levels
* **Multi-Level Alerting**: Configure up to three independent alert levels (Critical, Warning, Info)
* **Real-Time Monitoring**: Instant notifications via webhooks when thresholds are breached
* **Flexible Configuration**: Set thresholds for "above" or "below" conditions based on your use case
* **Automated Tracking**: No manual checking required—continuous background monitoring
## Alert Levels
The system supports three independent alert levels, each with its own priority and purpose:
### 1. Critical (In Alarm) - Highest Priority
**When to Use**: When immediate action is required to prevent service disruption or critical issues.
**Example**: Balance reaches \$0 or less, meaning service may be interrupted within hours.
### 2. Warning - Medium Priority
**When to Use**: When attention is needed soon, but there's still time to take action.
**Example**: Balance reaches \$10 or less, indicating low balance that needs replenishment soon.
### 3. Info - Lowest Priority
**When to Use**: For informational tracking and usage milestones.
**Example**: Balance reaches \$20 or less, serving as an early tracking indicator.
### 4. OK - No Alert
**Example**: Balance is healthy and above all configured thresholds.
## Alert States
The system evaluates wallet balance against configured thresholds and determines the current alert state:
* **OK**: Balance is healthy, no action needed
* **Info**: Balance crossed info threshold, informational tracking only
* **Warning**: Balance crossed warning threshold, attention needed
* **In Alarm**: Balance crossed critical threshold, immediate action required
## How It Works
### Threshold Configuration
Each threshold consists of two components:
1. **Threshold Value**: The numeric value to monitor (e.g., 100.00)
2. **Condition**: The direction that triggers an alert
* **"below"**: Alert when balance is **less than or equal to** (≤) the threshold
* **"above"**: Alert when balance is **greater than or equal to** (≥) the threshold
**Important**: Alerts trigger on **equality**, not just when crossing the threshold. For example, if your critical threshold is set to \$0 with "below" condition, the alert triggers when balance is exactly \$0 **or** negative.
### Threshold Ordering Rules
The system enforces proper ordering of thresholds based on the condition:
**For "below" condition** (monitoring real-time ongoing-balance depletion):
```
Critical < Warning < Info
```
Example: Critical: \$0, Warning: \$10, Info: \$20
**For "above" condition** (monitoring usage/spending):
```
Critical > Warning > Info
```
Example: Critical: \$1000, Warning: \$500, Info: \$100
### Configuration Flexibility
The system supports multiple configuration patterns:
* **Critical Only**: Critical threshold without warning or info
* **Critical + Warning**: Critical and warning thresholds without info
* **Critical + Info**: Critical and info thresholds without warning
* **Info Only**: Standalone info threshold without critical or warning
* **Full Stack**: All three thresholds (critical, warning, and info)
Warning threshold **requires** a critical threshold. You cannot configure a warning threshold without also configuring a critical threshold.
## Real-World Use Cases
### Use Case 1: Prepaid Wallet Balance Monitoring
**Scenario**: Track prepaid wallet balance to prevent service interruption
**Configuration**:
```json theme={null}
{
"critical": {
"threshold": "0.00",
"condition": "below"
},
"warning": {
"threshold": "10.00",
"condition": "below"
},
"info": {
"threshold": "20.00",
"condition": "below"
},
"alert_enabled": true
}
```
**How it works**: Customer has a prepaid wallet starting with \$100. As balance depletes:
* Balance reaches \$20 or less → Info alert (informational milestone, balance getting low)
* Balance reaches \$10 or less → Warning alert (low balance, needs top-up soon)
* Balance reaches \$0 or less → Critical alert (urgent, service will be interrupted)
### Use Case 2: Monthly Spending Limit Monitoring
**Scenario**: Monitor spending against monthly spending limit
**Configuration**:
```json theme={null}
{
"info": {
"threshold": "100.00",
"condition": "above"
},
"warning": {
"threshold": "500.00",
"condition": "above"
},
"critical": {
"threshold": "1000.00",
"condition": "above"
},
"alert_enabled": true
}
```
**How it works**: Monthly spending limit of \$1,000 set. As spending increases:
* Spending reaches \$100 or more → Info alert (spending milestone reached)
* Spending reaches \$500 or more → Warning alert (halfway to spending limit)
* Spending reaches \$1,000 or more → Critical alert (spending limit reached, action required)
## Configuring Alerts via Dashboard
Follow these steps to configure feature wallet balance alerts:
### Step 1: Navigate to Features
1. In the main navigation menu, select **Features** from **Product Catalogue**
2. Locate and select the feature you want to configure alerts for from the features list
### Step 2: Access Alert Settings
1. In the feature details page, click the **three-dot menu (⋮)** in the top right corner
2. Select **Alert Settings** from the dropdown menu
### Step 3: Configure Alert Thresholds
A dialog box titled "Feature Alert Settings" will appear with the following configuration options:
#### Configuration Options
**Enable Alerts**
* Toggle the switch to activate balance monitoring for this feature
* When disabled, no alerts will be triggered regardless of threshold settings
**Alert Condition**
* **Below**: Alert when balance is **less than or equal to** (≤) the threshold (for monitoring balance depletion)
* **Above**: Alert when balance is **greater than or equal to** (≥) the threshold (for monitoring spending/usage)
**Critical Threshold** (Required when Warning is set)
* Enter the balance amount that triggers a critical alert
* This is the highest priority alert level
* Example for "below": \$0 (service will be interrupted)
* Example for "above": \$1000 (spending limit reached)
**Warning Threshold** (Optional, requires Critical)
* Enter the balance amount that triggers a warning alert
* Medium priority alert level
* Example for "below": \$10 (low balance, needs attention)
* Example for "above": \$500 (approaching spending limit)
**Info Threshold** (Optional, can be standalone)
* Enter the balance amount that triggers an info alert
* Lowest priority alert level, informational only
* Example for "below": \$20 (early warning indicator)
* Example for "above": \$100 (usage milestone)
**Save Changes**
* Click to apply the alert configuration
* Alerts will begin monitoring immediately after saving
**Cancel**
* Click to discard changes and close the dialog
### Step 4: Verify Alert Configuration
After saving:
1. The feature will display an alert indicator showing alerts are enabled
2. Webhook notifications will be sent when thresholds are breached
3. You can view alert history in the feature's activity log
## State Transitions
The alert system evaluates balance changes and transitions between states automatically:
### State Transition Examples
**Scenario 1: Balance decreasing from \$50 (below condition)**
Starting balance: \$50 (above all thresholds)
1. Balance reaches \$20 or less → **OK → Info**
2. Balance reaches \$10 or less → **Info → Warning**
3. Balance reaches \$0 or less → **Warning → In Alarm**
4. Balance increases to \$50 (top-up) → **In Alarm → OK**
**Scenario 2: Rapid balance decrease (below condition)**
Starting balance: \$100 (above all thresholds)
1. Large transaction: Balance reaches \$5 or less → **OK → Warning** (skips info, as \$5 is ≤ \$10 but > \$0)
2. Another transaction: Balance reaches -\$2 → **Warning → In Alarm** (balance is ≤ \$0)
**Scenario 3: Usage increase (above condition)**
Starting balance/usage: \$0 (below all thresholds)
1. Usage reaches \$100 or more → **OK → Info**
2. Usage reaches \$500 or more → **Info → Warning**
3. Usage reaches \$1000 or more → **Warning → In Alarm**
4. New billing cycle resets usage to \$0 → **In Alarm → OK**
State transitions are unidirectional based on threshold crossings. The system never transitions "backwards" (e.g., Warning → Info) unless the balance crosses back over the threshold.
## Webhook Integration
### Webhook Event Type
When an alert threshold is breached, the system sends a webhook with the following event type:
```
feature.wallet_balance.alert
```
### Webhook Payload Structure
```json theme={null}
{
"alert_status": "in_alarm",
"alert_type": "feature_wallet_balance",
"event_type": "feature.wallet_balance.alert",
"feature": {
"id": "feat_01K8DBMBN87CY36DEFY62PEXBT",
"name": "Alert Feat 1",
"type": "metered",
"alert_settings": {
"alert_enabled": true,
"critical": {
"condition": "below",
"threshold": "-15"
},
"warning": {
"condition": "below",
"threshold": "-5"
},
"info": {
"condition": "below",
"threshold": "0"
}
},
"status": "published",
"meter_id": "meter_01K8DBMBN5NRFNYBMXN1G3WDWY",
"created_at": "2025-10-25T09:36:40.616655Z",
"updated_at": "2025-10-25T11:14:15.925198Z"
},
"wallet": {
"id": "wallet_01K8DC3ZAH6R55GTPFTWSR2EHF",
"name": "Prepaid Wallet",
"wallet_type": "PRE_PAID",
"balance": "10",
"credit_balance": "10",
"currency": "usd",
"alert_enabled": true,
"alert_state": "in_alarm",
"wallet_status": "active",
"customer_id": "cust_01K8DBYJ0ST5GD57EB6EATEACE",
"created_at": "2025-10-25T09:45:12.273036Z",
"updated_at": "2025-10-25T09:58:22.259833Z"
}
}
```
### Webhook Fields Explained
| Field | Description |
| ------------------------ | ----------------------------------------------------------- |
| `alert_status` | Current alert state: `ok`, `info`, `warning`, or `in_alarm` |
| `alert_type` | Type of alert: `feature_wallet_balance` |
| `event_type` | Webhook event identifier: `feature.wallet_balance.alert` |
| `feature.id` | Unique identifier of the feature |
| `feature.name` | Name of the feature being monitored |
| `feature.alert_settings` | Complete alert configuration including all thresholds |
| `wallet.id` | Unique identifier of the wallet |
| `wallet.balance` | Current wallet balance (total balance) |
| `wallet.credit_balance` | Current credit balance |
| `wallet.alert_state` | Current alert state of the wallet |
| `wallet.currency` | Currency of the wallet |
### Webhook Behavior
* **All alert states trigger webhooks**: Info, Warning, and Critical all send notifications
* **Immediate delivery**: Webhooks are sent instantly when state transitions occur
* **Retry logic**: Failed webhook deliveries are automatically retried (up to 3 attempts)
* **Rate limiting**: Webhook delivery is rate-limited to prevent spam (10 webhooks/second max)
## Monitoring Implementation
The system uses two complementary monitoring approaches:
### 1. Real-Time Monitoring
* **Trigger**: Wallet transactions (credit/debit operations)
* **Behavior**: Immediate state evaluation on balance changes
* **Notifications**: Instant webhook delivery
* **Use case**: Primary monitoring mechanism for active wallets
### 2. Cron-Based Monitoring
* **Frequency**: Every 5 minutes
* **Behavior**: Periodic balance checks across all feature wallets
* **Purpose**: Catches edge cases and ensures no alert is missed
* **Use case**: Backup monitoring for reliability
## Validation Rules
When configuring alerts, the system enforces the following validation rules:
**Required Fields**
When `alert_enabled` is `true`, at least one threshold (critical, warning, or info) must be provided.
### Validation Error Examples
**Error 1: Warning without Critical**
```json theme={null}
{
"warning": {
"threshold": "10.00",
"condition": "below"
},
"alert_enabled": true
}
```
❌ **Error**: "critical threshold is required when warning threshold is provided"
**Error 2: Incorrect Threshold Ordering**
```json theme={null}
{
"critical": {
"threshold": "20.00",
"condition": "below"
},
"warning": {
"threshold": "10.00",
"condition": "below"
},
"info": {
"threshold": "0.00",
"condition": "below"
},
"alert_enabled": true
}
```
❌ **Error**: "info threshold must be greater than warning threshold for 'below' condition"
**Error 3: No Thresholds Provided**
```json theme={null}
{
"alert_enabled": true
}
```
❌ **Error**: "at least one threshold (critical, warning, or info) is required when alert\_enabled is true"
## Best Practices
**Set Critical Thresholds at Service Interruption Points**
Configure critical thresholds at the exact point where service would be interrupted or severely degraded.
**Use 2-5x Multiplier for Warning Thresholds**
Set warning thresholds at 2-5 times the critical threshold to provide adequate warning time.
**Use 5-10x Multiplier for Info Thresholds**
Set info thresholds at 5-10 times the critical threshold for early tracking indicators.
**Test Alert Thresholds in Staging**
Always test alert configurations in a staging environment before deploying to production.
**Document Threshold Decisions**
Document why each threshold value was chosen for future reference and optimization.
**Use "below" for Credit Depletion**
Use the "below" condition when monitoring balance depletion or credit consumption.
**Use "above" for Usage Limits**
Use the "above" condition when monitoring spending, usage limits, or consumption milestones.
## Troubleshooting
### Alerts Not Triggering
**Possible causes**:
* `alert_enabled` is set to `false`
* Threshold configuration is incorrect
* Balance has not crossed the configured threshold
* Webhook endpoint is not configured
**Solution**:
1. Verify `alert_enabled` is `true`
2. Check threshold values and conditions
3. Review current wallet balance
4. Confirm webhook endpoint is configured correctly
### Wrong Alert State
**Possible causes**:
* Threshold ordering is incorrect for the condition
* Multiple thresholds using different conditions
* Balance calculation is incorrect
**Solution**:
1. Verify threshold ordering matches condition (below: ascending, above: descending)
2. Ensure all thresholds use the same condition
3. Check wallet balance calculation logic
## Summary
Feature Wallet Balance Alerts provide a comprehensive, flexible monitoring solution for tracking wallet balances across your feature offerings. Key takeaways:
* **Three independent alert levels**: Critical, Warning, and Info
* **Flexible configurations**: Standalone info, critical+info, or full stack
* **Real-time monitoring**: Instant webhook notifications on threshold breaches
* **Multiple monitoring approaches**: Real-time transaction monitoring + cron-based backup
* **Comprehensive error handling**: Robust validation and error messages
* **Production-ready**: Extensive logging, retry logic, and rate limiting
By implementing Feature Wallet Balance Alerts, you can proactively monitor usage, prevent service interruptions, and maintain a superior customer experience.
# Charges grouping
Source: https://docs.flexprice.io/docs/product-catalogue/groups/charges-grouping
Assign prices (charges) to a group when adding or editing a charge.
Create a group with **Entity type** **Price** first ([Creating a group](/docs/product-catalogue/groups/create)). Then assign charges to that group using the steps below.
## Workflow
* When adding a charge to a plan, use the **Group** field: search or select a group (e.g. "GPU Usage")
* Save the charge; it is stored with that group
* Open the plan → **Charges** → **Edit Details**
* In **Edit Price Details**, set **Group** to the desired group or **None** to ungroup
* Save
## API (prices)
| Action | Method | Endpoint |
| --------- | ------ | ------------------- |
| Create | POST | `/v1/prices` |
| Update | PUT | `/v1/prices/:id` |
| Get price | GET | `/v1/prices/:id` |
| Search | POST | `/v1/prices/search` |
**Create:** Include `group_id` (group `id` from `POST /v1/groups`).
**Update:** Send `group_id: "grp_..."` to assign, `group_id: ""` to ungroup. Omit to leave unchanged.
**View:** `GET /v1/prices/:id?expand=groups` or `POST /v1/prices/search` with `expand: "groups"` for full `group` on each price.
**Analytics:** `POST /v1/events/analytics` with `expand: ["price"]`. Group appears under each item's `price.group` when set.
**Invoices:** Line items use the price's `group_id` to fetch group names.
**Plan cloning** preserves groups. Cloned plan's prices keep the same group assignments as the source.
# Creating a group
Source: https://docs.flexprice.io/docs/product-catalogue/groups/create
Create and list groups. Entity type is set once at creation and cannot be changed.
A group has a single **Entity type** (Price or Feature), chosen when you create it. That type cannot be changed later. Price groups are used for plan charges. Feature groups are used for features.
## Steps to create a group
* Go to **Product Catalog** → **Groups**
* Use **Filter** and **Sort** as needed. **+ Add** creates a new group
* Click **+ Add**
* In the **Create Group** dialog, enter **Group Name**, **Lookup Key** (unique per tenant and environment), and select **Entity Type** (**Price** or **Feature**)
* Click **Create Group**
The list shows **Type** (Price or Feature) for each group.
## API
| Action | Method | Endpoint |
| ----------- | ------ | ------------------- |
| Create | POST | `/v1/groups` |
| Get one | GET | `/v1/groups/:id` |
| List/search | POST | `/v1/groups/search` |
| Delete | DELETE | `/v1/groups/:id` |
**Create:** Body must include `name`, `entity_type` (`"price"` or `"feature"`), `lookup_key`. Response returns `id`. Use it as `group_id` on prices or features.
**List/search:** Filter by `entity_type`, `name`, `lookup_key`, and other supported fields.
**Get one:** Returns the group with `entity_ids` (IDs of prices or features in that group).
**Delete:** Soft-delete. Every entity in that group has its `group_id` cleared.
When you delete a group, every price or feature in that group has its `group_id` cleared (no orphaned references).
## Validation
| Scope | Rule |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Group** | `entity_type` must be `"price"` or `"feature"`. `lookup_key` is required and must be unique per tenant and environment (published groups). |
# Feature grouping
Source: https://docs.flexprice.io/docs/product-catalogue/groups/feature-grouping
Assign features to a group when creating or editing a feature.
Create a group with **Entity type** **Feature** first ([Creating a group](/docs/product-catalogue/groups/create)). Then assign features to that group using the steps below.
## Workflow
* Go to **Product Catalog** → **Features** → **Create Feature**
* In the **Group** field, search or select a group (e.g. "Usage Pricing")
* Save; the feature is stored with that group
* Open the feature → **Edit**
* In **Group**, set the desired group or **None** to ungroup
* Save
## API (features)
| Action | Method | Endpoint |
| ----------- | ------ | --------------------- |
| Create | POST | `/v1/features` |
| Update | PUT | `/v1/features/:id` |
| Get feature | GET | `/v1/features/:id` |
| Search | POST | `/v1/features/search` |
**Create:** Optional `group_id`. Group must exist and have `entity_type: "feature"`. Response includes `group_id` and `group`.
**Update:** Send `group_id: "group_..."` to assign, `group_id: ""` to ungroup. Omit to leave unchanged.
**Get:** Response includes `group_id` and `group` when the feature is in a group.
**Search:** Each item has `group_id` and `group` when set. Filter by group: `field: "group_id", operator: "eq", value: { "string": "" }` (or `operator: "in"` with array).
### Where reflected
**Usage Breakdown** in the analytics dashboard and [Customer Portal](/docs/customers/customer-portal#usage-tab) **Usage** tab shows usage and cost by group.
# Overview
Source: https://docs.flexprice.io/docs/product-catalogue/groups/overview
Create named groups (e.g. Usage Pricing, Add-on Charges) for features and prices, and see grouped invoice line-items, usage and cost broken down by group in analytics and the customer portal.
Groups in Flexprice let you organize **prices** and **features** into named buckets—e.g. by product line, region, or tier. You can then filter and display them by group in invoices, catalogs, analytics, and the [customer portal](/docs/customers/customer-portal#usage-tab) Usage tab.
Flexprice supports grouping for **prices** and **features**. Each group holds one entity type only (`entity_type: "price"` or `"feature"`).
**Benefits:**
* **Organize** — Bucket prices or features by product line, region, tier, or any dimension
* **Filter** — Search groups by `lookup_key`, `name`, `entity_type`. List entities with group on each
* **Charges (prices)** — Group charges in a plan. Grouped invoice line-items
* **Features** — Group features for lists and filters. Feature search can filter by `group_id`. Usage breakdown in analytics and [customer portal](/docs/customers/customer-portal#usage-tab) reflects feature usage and cost by group
## API reference
| Action | Method | Endpoint |
| -------------------------------- | ------ | ------------------- |
| Create group | POST | `/v1/groups` |
| Get group | GET | `/v1/groups/:id` |
| Query groups | POST | `/v1/groups/search` |
| Delete group | DELETE | `/v1/groups/:id` |
| Create price (with group) | POST | `/v1/prices` |
| Update price (set/clear group) | PUT | `/v1/prices/:id` |
| Create feature (with group) | POST | `/v1/features` |
| Update feature (set/clear group) | PUT | `/v1/features/:id` |
**Create group:** Body: `name`, `entity_type` (`"price"` or `"feature"`), `lookup_key`. Response returns `id` — use as `group_id` on prices or features.
**Delete group:** Soft-delete. Every entity in that group has `group_id` cleared.
When you delete a group, every price or feature in that group has its `group_id` cleared (no orphaned references).
## Validation
| Scope | Rule |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------- |
| **Group** | `entity_type` must be `"price"` or `"feature"`. `lookup_key` required; unique per tenant and environment (published groups). |
| **Price** | If `group_id` is set, the group must exist, be published, and have `entity_type` `"price"`. |
| **Feature** | If `group_id` is set, the group must exist, be published, and have `entity_type` `"feature"`. |
## What's next
* [Create a group](/docs/product-catalogue/groups/create) — Choose Entity Type (Price or Feature), then assign entities
* [Charges grouping](/docs/product-catalogue/groups/charges-grouping) — Assign prices to a group when adding or editing a charge
* [Feature grouping](/docs/product-catalogue/groups/feature-grouping) — Assign features to a group when creating or editing a feature
# Archiving a plan
Source: https://docs.flexprice.io/docs/product-catalogue/plans/archive
Over time, businesses may need to modify or discontinue certain pricing plans due to changes in product offerings, evolving customer needs, or strategic shifts. Instead of deleting plans, archiving allows you to retain historical data while preventing new customers from subscribing. This ensures that existing customers remain unaffected while new sign-ups are restricted.
**Steps to archive a plan**
* Navigate to the Plans Section in Product Catalog section
* Click on the three-dot menu next to the plan you want to archive
* Select Archive from the dropdown options
* A confirmation dialog will appear asking if you want to archive the plan. Make sure that you can no longer modify the plan details once it is archived.
* Click **Archive** to proceed, or **Cancel** if you change your mind.
Customers already subscribed to the plan will continue using it until the end of their current billing cycle, after which they will need to transition to an active plan.
# Flat fee
Source: https://docs.flexprice.io/docs/product-catalogue/plans/billing-models/flat-fee
The **Flat Fee** model charges a consistent rate for each unit of usage. This straightforward approach ensures predictability for both the business and the customer.
**Example:**
An API service charges **\$0.05 per API call**. If a customer makes **1,000 API calls** during a billing period, their total charge would be:
`1,000 API calls × $0.05 = $50`
This model is ideal when the cost per unit remains constant, regardless of the total usage volume.
# Package
Source: https://docs.flexprice.io/docs/product-catalogue/plans/billing-models/package
The **Package-Based** model applies a **fixed price to a defined range of usage**. Instead of charging per unit, the entire **range of units falls under a single fixed price**.
**Example:**
A **SMS service provider** applies the following package-based pricing:
* **1 – 1,000 messages** → \$50
* **1,001 – 5,000 messages** → \$200
* **5,001 – 10,000 messages** → \$350
If a customer sends **4,500 messages**, they fall within the **1,001 – 5,000 messages package**, meaning they are charged **\$200**, regardless of the exact count.
Even if the customer only sent 1,500 messages, they would still pay the full **\$200** for the 1,001 – 5,000 package.
💡 It is Best for Businesses that want to charge customers based on predefined consumption ranges rather than individual units.
# Volume tiered
Source: https://docs.flexprice.io/docs/product-catalogue/plans/billing-models/volume-tiered
In Flexprice, the **Volume-Tiered** model allows you to define multiple pricing tiers, where a single unit price is applied based on the total volume of usage. This means that the unit price is determined by the highest tier reached by the customer's total consumption.
**Example:**
Consider the following pricing tiers for an API service:
| Tier | Usage Range (API Calls) | Unit Price (\$) |
| ---------- | ----------------------- | --------------- |
| **Tier 1** | 0 – 10,000 | 0.0010 |
| **Tier 2** | 10,001 – 50,000 | 0.0008 |
| **Tier 3** | 50,001 – 100,000 | 0.0006 |
| **Tier 4** | 100,001 and above | 0.0004 |
If a customer makes **65,000 API calls** in a billing period, their total usage falls within **Tier 3** (50,001 – 100,000 calls) with a unit price of **\$0.0006**. The total charge would be:
`65,000 API calls × $0.0006 = $39`
This model ensures that customers are charged a consistent rate for all units based on their total usage volume, which can either increase or decrease depending on the defined tiers.
# Advance vs Arrear
Source: https://docs.flexprice.io/docs/product-catalogue/plans/charges/advance-vs-arrear
Billing is a fundamental aspect of any business, determining when and how customers are charged for the products or services they use. The timing of billing can significantly impact cash flow, customer experience, and overall financial operations.
Broadly, businesses charge customers in one of two ways:
* **Advance Billing**: Customers pay before they receive a service.
* **Arrear Billing**: Customers pay after they have used a service.
Both approaches are widely used across industries, and neither is tied exclusively to a particular pricing model. Understanding when to use advance or arrear billing is key to designing a flexible and efficient billing system. This section explores how each method applies to **recurring pricing** and **usage-based pricing.**
**Recurring charges**\
Recurring pricing follows a fixed billing cycle—monthly, quarterly, or more. Here, businesses decide whether to charge before or after each cycle.
* Advance billing
* **In advance billing**, customers pay before the billing cycle starts, similar to a prepaid mobile plan where you recharge upfront before making calls or using data.In advance billing, customers **pay before** the billing cycle starts, similar to a prepaid mobile plan where you **recharge upfront** before making calls or using data.\
\
**Example:**
* A SaaS company charges **\$100 on the 1st of every month** for access throughout the month.
* A B2B software provider offers an **annual plan** where customers **pay for 12 months in advance**.
* **Why businesses use advance billing?**
* **Predictable revenue** – Ensures cash flow before delivering the service.
* **No unpaid invoices** – Customers pay upfront, reducing collection risks.
* **Customers expect it** – Just like prepaid plans, SaaS subscriptions are commonly prepaid.
* Arrear billing
* In arrear billing, customers use the service first and pay later, just like postpaid mobile plans where you get an invoice at the end of the month.
**Example:**
* A company provides **access to a software platform** and invoices customers at the **end of the month** for that period’s access.
* An enterprise SaaS provider allows customers to **use the service for a quarter** and then **pays via Net-30 terms**.
* **Why businesses use arrear billing?**
* **Common in enterprise sales** – B2B customers often expect Net-30/Net-60 payment terms.
* **Supports flexible contracts** – Customers commit upfront but settle payments later.
* **Better for relationship-driven sales** – Businesses may delay billing to remove friction in customer acquisition.
Usage-based charges\
Usage-based pricing follows a **metered** model, where charges depend on **how much of a service is consumed**. The common assumption is that **usage must always be billed in arrears**, but that’s not necessarily the case.
* Advance billing
* Advance billing is also used in usage-based pricing, especially in **credit-based or commit-based models**, similar to how users **buy a data pack upfront before using mobile internet**.
**Example:**
* **Prepaid Credits**: An AI API charges **\$500 upfront for 1M tokens**, which customers use over time.
* **Commit-Based Pricing**: A cloud provider requires a **minimum spend of \$10,000/month**, paid upfront, regardless of actual usage.
* **Hybrid Prepaid Model**: A SaaS tool sells **prepaid API credits**, but if the user exceeds the limit, additional usage is billed in arrears.
* **Why businesses use advance billing for usage?**
* **Guaranteed cash flow** – Payments are received before service usage.
* **Lowers default risk** – Eliminates unpaid bills from overconsumption.
* **Encourages commitment** – Customers commit to volume usage, reducing churn.
* Arrear billing
* This is the standard choice for pay-as-you-go models, where customers **use the service first and are billed later**, just like a **postpaid mobile data plan**.
**Example:**
* A cloud provider (AWS, Azure) charges at the **end of each month** based on **actual compute/storage usage**.
* A telecom provider bills customers at the end of the month for **call duration, data usage, and SMS count**.
* **Why businesses use arrear billing?**
* **Customers pay only for what they use** – No need to estimate usage in advance.
* **Works well for unpredictable usage** – Suitable for cloud services and pay-as-you-go models.
* **Preferred by enterprise clients** – Many businesses expect to be billed after consumption
###
# Cloning a plan
Source: https://docs.flexprice.io/docs/product-catalogue/plans/clone
Create a duplicate of an existing plan with a new name and lookup key, including its active prices, published entitlements, and plan-scoped credit grants.
Cloning creates a new plan that is a duplicate of an existing plan. Use it to version a plan (e.g. "Pro 2024" → "Pro 2025"), or experiment with a duplicate before changing the original. The new plan has a **distinct name**, and a **unique lookup key**; subscriptions and historical data are not copied.
## What gets copied
* **Plan** — New plan in the same environment with the name and lookup key you provide; description, display order, and metadata can be overridden or copied from the source.
* **Prices** — Only **active** (published and non-expired) plan prices.
* **Entitlements** — Only **published** plan entitlements.
* **Credit grants** — Only **published**, **plan-scoped** credit grants.
## What does not get copied
* Subscriptions or line items
* Expired prices
* Draft or archived prices, entitlements, or grants
Plan cloning is supported **within the same environment** only. The cloned plan is created in the same environment as the source plan.
## Steps to clone a plan
* Go to **Product Catalog** → **Plans**
* Open the plan you want to duplicate
* Click the **three-dot menu (⋮)** in the top right corner
* Select **Duplicate** from the dropdown menu
* Enter **Plan Name** (required)
* Enter **Lookup Key** (required, must be unique). It is auto-generated by default.
* Optionally enter **Description** and **Metadata**
* Click **Duplicate**
If a published plan already exists with the lookup key you entered, the system will return an error and ask you to choose a different lookup key.
A new plan is created with the same charges, entitlements, and credit grants as the source.
You are directed to the cloned plan after plan cloning is successful.
Flexprice always adds **`source_plan_id`** to the cloned plan's metadata set to the source plan ID, so you can tell which plan it was duplicated from.
## Use cases
### Plan versioning
* **Annual updates**: Clone "Pro 2024" to "Pro 2025" and adjust prices or features on the new plan while keeping the same structure.
* **Product evolution**: Duplicate a plan before changing entitlements or credit grants so existing subscribers stay on the original.
### Experimentation and rollback
* **Safe experimentation**: Clone a plan, change the duplicate, and only move customers over when ready. The source plan stays unchanged.
* **Rollback**: Keep a copy of a plan before major changes so you can reference or re-use the previous configuration.
## Best practices
💡 **Use a unique lookup key**: Every cloned plan must have a lookup key that does not match any existing published plan. Use a naming pattern (e.g. `pro_2025`, `pro_plan_v2`) to avoid conflicts.
💡 **Set a clear name and description**: Give the cloned plan a descriptive name and, if needed, a description so your team can tell it apart from the source (e.g. "Pro 2024 v2 – cloned from Pro 2024").
💡 **Use `source_plan_id` in metadata**: The cloned plan’s metadata always includes **`source_plan_id`**. Use it in reporting or automation to track plan lineage and which plans were duplicated from which.
💡 **Clone instead of editing live plans**: When you want to change structure (prices, entitlements, credit grants), clone the plan and edit the copy. Existing subscribers stay on the original; new sign-ups can use the new plan.
💡 **Review what gets copied**: Only active (non-expired) prices and published entitlements and plan-scoped credit grants are copied. Draft or archived items and subscriptions are not. Confirm the source plan has everything published before cloning.
# Creating a plan
Source: https://docs.flexprice.io/docs/product-catalogue/plans/create
Follow the steps below to create a pricing plan in Flexprice:
* In the main navigation menu, select **Plans** from *Product Catalog.*
* Click **“Add”** at the top-right of the screen.
This opens a **two-step form** to configure your plan.
**Step 1: Enter plan details**
This step captures key identifiers for your pricing plan.
* **Plan Name**: A descriptive name for the plan. This name will be visible to users.
* **Lookup Key**: A unique identifier used in API calls and system integrations.
* **Plan Description** *(Optional)*: Provides internal context about the plan.
**Step 2: Define plan charges**
It defines how customers will be charged under this plan. You can add **one or multiple** charges while creating a plan.
* **Setting-up a recurring charge:**
Recurring charges are fixed charges that your customers are billed at regular intervals (e.g., \$10/month subscription fee).
* **Select billing currency**
Choose the currency in which you want to bill your customers. **Flexprice supports all currencies**, enabling businesses to charge customers globally.
* **Choose a billable period**
The **billing period** determines when invoices are generated. The **billing period impacts invoice generation, feature limits, and renewals**. You can select from the following plan intervals:
* **Weekly** → Customers are billed once every week.
* **Monthly** → The most common cycle, where customers are charged every month.
* **Quarterly** → Charges occur every 3 months.
* **Half-Yearly** → Customers are billed twice a year (every 6 months).
* **Yearly** → Customers are billed once per year.
* **Set the Price**
Define the **amount** customers will be charged per billing period. The price is set based on the **selected currency and interval**.
* **Choose billing timing**:Decide when the charge is applied within the billing cycle:
* **Advance Billing** → Customers are charged **at the start** of each billing period.
* **Arrears Billing** → Customers are charged **at the end** of each billing period.
* **Adding trial period** *(Optional)*:
* Toggle **on** to enable a **free trial** before billing starts.
* Enter the **trial period duration** (in days).
* After the trial ends, the first charge is applied based on the **billing timing** selected.
* Click **Add** to save the recurring charge settings and apply them to the pricing plan.
* **Setting-up a usage-based charge:**
To incorporate usage-based charges into a plan, you can utilize existing metered features. For instance, you can create charges based on the number of API calls, the number of active users, transactions, compute time, etc.
* **Select a Metered Feature**
Choose a metered feature that will determine how usage is measured and billed. Features must be pre-configured in your product catalog
💡 *Only metered features can be linked to usage-based charges.*
* **Choose Billing Currency**
Select the currency in which you want to bill your customers. **Flexprice supports all currencies**, enabling businesses to charge customers globally.
* **Define the Billing Period**
The **billing period** determines how frequently usage is calculated and charged. Available intervals include:💡 *The billing period must align with the subscription plan to ensure accurate invoicing.*
* **Weekly** → Usage is tracked and billed every week.
* **Monthly** → The most common cycle, where customers are charged based on monthly usage.
* **Quarterly** → Charges occur every 3 months based on accumulated usage.
* **Half-Yearly** → Customers are billed twice a year (every 6 months).
* **Yearly** → Customers are billed once per year for total usage over the year.
* **Select a Billing Model**
Flexprice allows different pricing models for usage-based billing:💡 *The correct billing model ensures flexibility in pricing strategy and revenue optimization. To read more about different types of charges read in the [Billing Models](/docs/product-catalogue/plans/billing-models/flat-fee) section.*
* **Flat Fee** → A fixed price per unit of usage (e.g., \$0.01 per API call).
* **Volume-Based Pricing** → Price changes depending on the volume of usage (e.g., $0.01 per API call for the first 100K calls, then $0.008 for additional calls).
* **Package-Based Pricing** → Customers purchase usage in predefined bundles (e.g., \$50 for 100K API calls).
* **Set the Price per Unit**
Define how much you want to charge **per unit of usage**. The unit is determined by the **metered feature selected** (e.g., per API call, per GB stored, per transaction).
* **Choose Billing Timing**
Select when the customer should be charged for usage:💡 *Arrears billing is common for metered services like cloud computing and APIs*
* **Advance Billing** → Customers will be invoiced as soon as they use a particular feature
* **Arrears Billing** → Customers are billed at the end of the cycle based on actual consumption.
* Once all configurations are set, click **Add** to apply the usage-based charge to the pricing plan.
* Click **Save** to finalize the pricing plan.
# Localisation
Source: https://docs.flexprice.io/docs/product-catalogue/plans/localisation
As businesses scale globally, **pricing localization becomes critical** for acquiring and retaining customers across different regions. A **one-size-fits-all pricing strategy doesn’t work** when:
* **Customers expect to pay in their local currency** rather than USD or EUR.
* **Exchange rate fluctuations impact affordability** and purchasing decisions.
* **Regional taxes and compliance requirements** require billing in local currencies.
* **Enterprise customers negotiate contracts in multiple currencies**, making flexible invoicing essential.
Flexprice provides **full multi-currency support** across both **recurring and usage-based charges**, allowing businesses to price their products flexibly across global markets.
**Creating Plans with Multiple Currencies**\
With Flexprice, you don't need to create separate plans for each currency. When setting up pricing plans, you can define a**single plan with multiple currency options** → e.g., `$99 USD/month`, `€89 EUR/month`, `₹7,999 INR/month`.
**Assigning Subscriptions in a Specific Currency**\
While a single pricing plan can support multiple currencies, a customer’s subscription must be assigned in one currency.\
This is because:
* **Invoices are generated per subscription.** Since invoices are currency-specific, each **subscription must have a single currency** for accurate billing, tax calculation, and revenue reporting.
* To offer **multiple currencies to a customer, assign multiple subscriptions.**
# Overview
Source: https://docs.flexprice.io/docs/product-catalogue/plans/overview
A **Pricing Plan** in Flexprice defines **how your product or service is priced and structured for customers**. Plans are a structured set of pricing rules that determine the cost and access of features for a customer. Whenever a customers subscribes to a plan, it governs their usage limits, billing terms, and available functionality.
**Plans in Flexprice allow you to:**
* **Monetise your product effectively:** Create any pricing model whether it is recurring, usage-based or hybrid based effortlessly.
* **Define entitlements:** Control which Metered, Boolean, or Static Features are available in each plan.
* **Enable pricing experimentation:** Iterate on pricing and entitlements without major code changes.
* **Handle multi-currency billing:** Create pricing plan in any currency
* **Automate trial management:** Offer trial periods before transitioning to paid plans.
* **Configure advanced charges:** Supports flat fees, package-based pricing, and volume-based pricing.
* **Granular plan management:** Easily edit, duplicate, and manage pricing plans as your product evolves.
With Flexprice, you have full control over how your product is packaged, ensuring **transparent, scalable, and adaptable billing** for your customers.
# Price Overrides
Source: https://docs.flexprice.io/docs/product-catalogue/plans/price-overrides
Learn how to customize pricing for individual customers by overriding plan charges during subscription creation.
**Price Overrides** in Flexprice allow you to **customize pricing for specific customers** by modifying individual charges within a plan during subscription creation. This feature is particularly useful for offering special pricing, discounts, or custom rates for enterprise customers, early adopters, or promotional campaigns.
## When to Use Price Overrides
Price overrides are ideal for scenarios such as:
* **Enterprise deals** with custom pricing arrangements
* **Promotional pricing** for specific customers or campaigns
* **Volume discounts** applied at the customer level
* **Trial or beta pricing** for early adopters
* **Negotiated rates** for strategic partnerships
## How to Override Plan Prices
### Step 1: Navigate to Customer Subscription Creation
* Go to **Billing** in the main navigation
* Select **Customers** from the billing section
* Select the customer you want to create a subscription for
* Click **Add** to create a new subscription
### Step 2: Select a Plan
* Choose the pricing plan from the dropdown menu
* The system will display the subscription preview with all charges
### Step 3: Access the Charges Table
* In the subscription preview, you'll see a **table of charges** showing all recurring and usage-based charges for the selected plan
* Each charge row displays the original pricing from the plan
### Step 4: Override Individual Charges
* Click the **three dots (⋮)** menu on any charge row
* Select **"Override Charge"** from the dropdown menu
### Step 5: Set the Override Amount
* A dialog titled **"Override Price"** will open
* The dialog shows:
* **Original Price**: The default price from the plan (read-only)
* **Override Amount (USD)**: Input field where you can enter the new price
* **Cancel** and **Override Price** buttons
* Enter the **new price** in the Override Amount field
* Click **"Override Price"** to save the change
### Step 6: Review and Save
* The charges table will update to show the **overridden prices**
* You can override multiple charges within the same subscription
* Review the **total subscription cost** with all overrides applied
* Click **"Add Subscription"** to finalize the subscription with custom pricing
## Important Notes
* **Plan Integrity**: Price overrides only affect the specific subscription being created. The original plan pricing remains unchanged for other customers.
* **Billing Consistency**: Overridden prices will be used for all future billing cycles unless the subscription is modified.
* **Audit Trail**: All price overrides are tracked and can be reviewed in the subscription history.
* **Currency Support**: Price overrides work with all supported currencies in your Flexprice account.
## Best Practices
* **Document Overrides**: Keep records of why specific overrides were applied for business continuity.
* **Regular Reviews**: Periodically review overridden subscriptions to ensure pricing remains appropriate.
* **Clear Communication**: Ensure your team understands when and how to use price overrides.
* **Validation**: Double-check override amounts before saving to avoid billing errors.
Price overrides provide the flexibility needed for complex pricing scenarios while maintaining the structure and consistency of your pricing plans.
# Pricing Plans
Source: https://docs.flexprice.io/docs/product-catalogue/plans/pricing
Learn how to create and manage pricing plans in Flexprice, including step-by-step creation, billing models, and price termination management.
# Pricing Plans
A **pricing plan** defines how customers are billed for using your product or service. Flexprice provides a **comprehensive and customizable** pricing system that enables businesses to design billing structures that fit their monetization strategies. With **recurring, usage-based, and hybrid** pricing models, companies can launch new pricing plans seamlessly and iterate quickly.
## **Key Features of the Pricing System**
* **Flexible Pricing Models**: Supports recurring subscriptions, metered billing, and hybrid plans.
* **Multiple Billing Periods**: Options for daily, weekly, monthly, or yearly cycles.
* **Multi-Currency Support**: Currently supports INR and USD.
* **Automated Trial Management**: Enables businesses to offer trial periods before converting to paid plans.
* **Advanced Charge Configurations**: Supports flat fees, package-based pricing, and volume-based pricing.
* **Scalable & API-Driven**: Seamless integration with business operations and customer billing workflows.
* **Granular Control Over Plans**: Edit, duplicate, and manage pricing plans with ease.
## **Use Cases**
Flexprice is designed for a wide range of SaaS businesses, AI infrastructure providers, and digital platforms that need:
* **Subscription-based billing** for SaaS products.
* **Usage-based billing** for AI services, cloud resources, or API metering.
* **Hybrid billing models** combining subscriptions with consumption-based charges.
***
## **Creating a Pricing Plan**
Follow the steps below to create a pricing plan in Flexprice:
### **Navigate to Pricing Plans**
1. Log in to your **Flexprice Dashboard**.
2. In the left sidebar, go to **Customer Management > Pricing Plan**.
3. Click **"Add Pricing Plan"** at the top-right of the screen.
This opens a **three-step form** to configure your plan.
***
## **Step 1: Enter plan details**
This step captures key identifiers for your pricing plan.
* **Plan Name**: A descriptive name for the plan. This name will be visible to users.
* **Plan Slug**: A unique identifier used in API calls and system integrations.
* **Plan Description** *(Optional)*: Provides internal context about the plan.
## **Step 2: Configure Billing Preferences**
Billing preferences define when and how the customer will be billed. It includes:
* **Choose billing timing**:
* **Advance:** Customers are billed at the beginning of the billing period.
* **Arrears:** Customers are billed at the end of the billing period based on actual usage.
* **Adding trial period** *(Optional)*:
* Toggle **on** to enable a free trial.
* Enter the **trial period duration** in days.
* Click **Next** to continue.
## **Step 3: Define plan charges**
It defines how customers will be charged under this plan. You can add **one or multiple** charges while creating a plan.
* **Select Subscription Type:**
* **Recurring Charges**: Fixed charges billed at regular intervals (e.g., \$10/month subscription fee).
* **Usage-Based Charges**: Charges based on actual consumption (e.g., $0.01 per API call, $5 per 1,000 tokens).
You can create a plan by adding multiple usage-based charges in the same plan as well
### Setting-up a recurring-based charge:
Select subscription type as Recurring to charge your customers a recurring fee based on the defined billing period. Whenever user purchases this plan, it will be charged a flat fee and given access to the features that will be part of that plan.
* **Select billing currency**
We currently support 2 currencies which includes:
* INR (Indian Rupees)
* USD (United States Dollars)
* **Choose a billable period**
The plan interval corresponds to the billing period and defines when invoices are generated. There are several plan intervals:
* **Weekly**: subscription fees and charges are billed on a weekly basis (Monday to Sunday);
* **Monthly**: subscription fees and charges are billed on a monthly basis;
* **Quarterly**: subscription fees and charges are billed on a quarterly basis (every 3 months); and
* **Yearly**: subscription fees are billed on a yearly basis and charges can be billed monthly or annually.
* Add relevant price you want to charge your customer based on the selected billing period and then click on **Add** button.
### Setting-up a usage-based charge:
To incorporate usage-based charges into a plan, you can utilize existing billable metrics. This enables you to offer "pay-as-you-go" features. For instance, you can create charges based on the number of API calls, the number of active users, transactions, compute time, etc.
* Select the billable metric on which you've created earlier.
* Define relevant currency and billing period for your plan
* Select the pricing model.
Flexprice currently support 3 types of pricing models:
* **Flat fee**
Select the Flat fee pricing model if you want to charge the **same price for each unit** consumed.
**Example:** \$0.01 per API call.
* **Package pricing**
Select the package charge model if you want to apply a **fixed price to a range of units**.
**Example:** \$5 per 100 API calls.
* **Volume tiered based pricing**
Select the volume charge model if you want to define several price tiers but want to apply a **single unit price based on the total volume**. You can also apply a **flat fee** in addition to the unit price.
**Example:**- 0 – 10,000 calls → \$0.001 per call
* 10,001 – ∞ calls → \$0.0008 per call
* Click on **Add** to save the charges
### Setting-up hybrid charges:
You can create a single pricing plan which includes multiple charges - Recurring as well usage-based charges.
### **Review & Save**
* Double-check all entered details.
* Click **Save** to finalize the pricing plan.
***
## **Managing Plan Prices After Creation**
### **Terminating Plan Prices**
Once a pricing plan is created and has active subscriptions, you may need to terminate specific prices for various business reasons such as:
* **Price increases or decreases**
* **Feature deprecation**
* **Plan restructuring**
* **Regulatory compliance**
* **Business model changes**
#### **Immediate Price Termination**
To immediately terminate a price:
1. **Navigate to the specific price** in your plan
2. **Use the DELETE API endpoint**: `DELETE /prices/{price_id}`
3. **The price will be terminated immediately** and marked with the current timestamp as the end date
**API Example:**
```bash theme={null}
curl -X DELETE "https://us.api.flexprice.io/v1/prices/{price_id}" \
-H "x-api-key: YOUR_API_KEY"
```
#### **Scheduled Price Termination**
For better business continuity, you can schedule price termination in the future:
1. **Use the DELETE API endpoint** with an `end_date` parameter
2. **Set the end\_date to a future date** when you want the price to terminate
3. **Existing subscriptions will continue to use the price** until the specified end date
**API Example with Future End Date:**
```bash theme={null}
curl -X DELETE "https://us.api.flexprice.io/v1/prices/{price_id}" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"end_date": "2025-12-31T23:59:59Z"
}'
```
**Benefits of Scheduled Termination:**
* **Customer notification time** - Gives customers advance notice of price changes
* **Billing continuity** - Prevents disruption to existing subscriptions
* **Compliance** - Meets regulatory requirements for price change notifications
* **Customer experience** - Allows customers to plan for changes
#### **Price Termination Behavior**
When a price is terminated:
* **Active subscriptions** continue to use the terminated price until the end date
* **New subscriptions** cannot be created with the terminated price
* **Billing continues normally** until the termination date
* **Line items are automatically ended** when the price expires
### **Synchronizing Plan Prices with Existing Subscriptions**
**⚠️ Important: After terminating prices, you must run the Plan Price Sync API to ensure existing subscriptions reflect the changes.**
#### **Why Plan Price Sync is Required**
When you terminate a price in a plan:
1. **Existing subscriptions still reference the old price**
2. **Line items need to be updated** to reflect the termination
3. **Billing calculations must be adjusted** for the new pricing structure
4. **Override prices and customizations** need to be preserved
#### **Running Plan Price Sync**
Use the Plan Price Sync API to synchronize terminated prices across all existing subscriptions:
**API Endpoint:** `POST /plans/{plan_id}/sync/subscriptions`
**Example:**
```bash theme={null}
curl -X POST "https://us.api.flexprice.io/v1/plans/{plan_id}/sync/subscriptions" \
-H "x-api-key: YOUR_API_KEY"
```
#### **What Plan Price Sync Does**
The sync process:
1. **Identifies all active subscriptions** using the plan
2. **Processes each subscription** to update line items
3. **Ends line items for terminated prices** (unless overridden)
4. **Creates line items for new active prices** (if applicable)
5. **Preserves subscription-specific overrides** and customizations
6. **Maintains billing continuity** throughout the transition
#### **Best Practices for Price Termination**
1. **Plan Ahead**
* Schedule terminations well in advance
* Consider customer notification periods
* Plan for business continuity
2. **Use Scheduled Termination**
* Avoid immediate termination when possible
* Give customers time to adjust
* Maintain billing stability
3. **Always Run Plan Price Sync**
* Run sync after any price changes
* Ensure existing subscriptions are updated
* Verify billing accuracy
4. **Monitor the Process**
* Check sync results and statistics
* Verify line item updates
* Test billing calculations
5. **Preserve Customer Relationships**
* Communicate changes clearly
* Honor existing commitments
* Provide migration paths
#### **Monitoring Sync Results**
The Plan Price Sync API returns detailed statistics:
```json theme={null}
{
"message": "Plan prices synchronized successfully",
"plan_id": "plan_123",
"plan_name": "Premium Plan",
"synchronization_summary": {
"subscriptions_processed": 150,
"prices_added": 25,
"prices_removed": 10,
"prices_skipped": 115
}
}
```
**Key Metrics:**
* **Subscriptions Processed**: Total number of subscriptions updated
* **Prices Added**: New line items created for active prices
* **Prices Removed**: Line items ended for terminated prices
* **Prices Skipped**: Line items that didn't require changes
#### **Common Scenarios**
**Scenario 1: Price Increase**
1. Terminate old price with future end date
2. Create new price with higher amount
3. Run plan price sync to update subscriptions
4. Customers continue with old price until end date
5. New billing period uses new price
**Scenario 2: Feature Deprecation**
1. Terminate price for deprecated feature
2. Run plan price sync to end related line items
3. Existing subscriptions lose access to feature
4. New subscriptions cannot include deprecated feature
**Scenario 3: Plan Restructuring**
1. Terminate multiple prices simultaneously
2. Create new pricing structure
3. Run plan price sync to update all subscriptions
4. Ensure billing continuity during transition
### **Troubleshooting Price Termination**
#### **Common Issues**
1. **Sync Failures**
* Check plan ID validity
* Verify API permissions
* Review error logs for specific issues
2. **Incomplete Updates**
* Verify all prices are properly terminated
* Check for override prices that may interfere
* Ensure sync completed successfully
3. **Billing Discrepancies**
* Review sync statistics
* Check individual subscription line items
* Verify price end dates are set correctly
#### **Validation Rules**
* **End date must be in the future** for scheduled termination
* **Price must exist** and be accessible
* **Plan must have active subscriptions** for sync to be meaningful
* **API key must have appropriate permissions**
### **API Reference**
**Delete Price:**
* **Endpoint:** `DELETE /prices/{price_id}`
* **Body:** `{"end_date": "2025-12-31T23:59:59Z"}` (optional)
* **Response:** `{"message": "price deleted successfully"}`
**Plan Price Sync:**
* **Endpoint:** `POST /plans/{plan_id}/sync/subscriptions`
* **Response:** Detailed synchronization summary with statistics
**Get Plan:**
* **Endpoint:** `GET /plans/{plan_id}`
* **Use:** To verify plan structure before and after changes
***
*For technical implementation details and advanced use cases, refer to the [API Reference](/api-reference//plans/create-plan).*
# Updating a Plan Price
Source: https://docs.flexprice.io/docs/product-catalogue/plans/update-price
Update a plan price's amount, tiers, or metadata — and safely propagate the change to all active subscribers using the price sync workflow.
Updating a plan price changes how it is charged going forward. Depending on what you change, the system either mutates the price in place or creates a new price version. After updating pricing fields, you need to run the price sync workflow to propagate the change to existing subscribers.
## The Update Price API
```
PUT /prices/{id}
```
All fields are optional. Only include the fields you want to change.
### Available Fields
| Field | Type | Notes |
| -------------------- | --------- | ------------------------------------------------------- |
| `amount` | decimal | New unit price (FIAT prices) |
| `billing_model` | string | `FLAT_FEE`, `TIERED`, or `PACKAGE` |
| `tier_mode` | string | `VOLUME` or `SLAB` — for TIERED model |
| `tiers` | array | New tier breakpoints (TIERED/FIAT) |
| `transform_quantity` | object | Package billing: `divide_by` and `round` |
| `price_unit_amount` | decimal | New unit price (CUSTOM price unit) |
| `price_unit_tiers` | array | New tiers (CUSTOM price unit + TIERED) |
| `display_name` | string | Display label shown on invoices |
| `description` | string | Internal description |
| `lookup_key` | string | Unique lookup key |
| `metadata` | object | Key-value metadata |
| `group_id` | string | Assign/reassign to a price group; `""` clears the group |
| `effective_from` | timestamp | When the new price version takes effect (default: now) |
### Fields You Cannot Change
These are immutable after a price is created:
| Field | Why |
| ----------------------------------------- | ---------------------------------- |
| `type` | Cannot switch FIXED ↔ USAGE |
| `currency` | Currency is locked at creation |
| `billing_period` / `billing_period_count` | Billing cycle is fixed |
| `billing_cadence` | Cannot switch recurring ↔ one-time |
| `invoice_cadence` | ARREAR/ADVANCE is fixed |
| `meter_id` | The usage metric cannot be swapped |
| `price_unit_type` | Cannot switch FIAT ↔ CUSTOM |
| `entity_type` / `entity_id` | Price ownership is fixed |
## Two Update Paths
### Path A — Metadata-only update (in-place)
If you only update `display_name`, `description`, `lookup_key`, `metadata`, or `group_id`, the price is updated **in place**. No new price version is created. No sync is needed.
```json theme={null}
PUT /prices/price_abc
{
"display_name": "API Calls (v2)",
"metadata": { "tier": "enterprise" }
}
```
Response: the updated price object. Done.
### Path B — Pricing field update (new version created)
If you update any pricing field (`amount`, `billing_model`, `tiers`, `tier_mode`, `transform_quantity`, `price_unit_amount`, `price_unit_tiers`), the system:
1. **Terminates the current price** — sets its `end_date` to `effective_from` (or now if not provided)
2. **Creates a new price** — starts exactly at `effective_from`, with the updated values
3. Returns the **new price** in the response (with a new `id`)
```json theme={null}
PUT /prices/price_abc
{
"amount": "49.99",
"effective_from": "2026-04-01T00:00:00Z"
}
```
Response: the new price object (`id` will be different from the original).
Save the new price `id` from the response. Existing subscription line items still reference the old price ID. The sync workflow creates new line items referencing the new price.
## Syncing the Change to Existing Subscribers
After a pricing field update, existing subscriptions are **not automatically updated**. You must run the price sync workflow to propagate the new price to all active subscribers on the plan.
### Why Manual Sync?
The sync workflow can affect large numbers of subscriptions. Triggering it automatically on every price update would be unpredictable. Doing it explicitly gives you control over timing.
### Safe Sync Sequence
**Step 1 — Update the price**
```json theme={null}
PUT /prices/{price_id}
{
"amount": "49.99",
"effective_from": "2026-04-01T00:00:00Z"
}
```
Note the plan ID associated with this price (needed for sync).
**Step 2 — Check for a running sync**
```json theme={null}
POST /workflows/search
{
"workflow_type": "PriceSyncWorkflow",
"entity_id": "",
"workflow_status": "Running"
}
```
* If `pagination.total > 0` — a sync is already running. Do not trigger another. Poll `GET /workflows/{workflow_id}/{run_id}` every 30 seconds until `status` is `Completed` or `Failed`, then continue to Step 3.
* If `pagination.total == 0` — proceed to Step 3.
**Step 3 — Trigger sync**
```
POST /plans/{plan_id}/sync/subscriptions
```
Response:
```json theme={null}
{
"workflow_id": "PriceSyncWorkflow-plan_xxx",
"run_id": "run_abc123",
"message": "price sync workflow started successfully"
}
```
**Step 4 — Poll until complete**
```
GET /workflows/{workflow_id}/{run_id}
```
Poll every 30 seconds. Terminal states:
| Status | Action |
| -------------------------------------- | ----------------------------------------------- |
| `Completed` | Done — all subscribers updated |
| `Failed` | Check error details, fix root cause, re-trigger |
| `Canceled` / `Terminated` / `TimedOut` | Re-trigger sync |
**Step 5 — Verify (optional)**
Check the sync summary in the completed workflow response:
```json theme={null}
{
"summary": {
"line_items_found_for_creation": 120,
"line_items_created": 120,
"line_items_terminated": 120
}
}
```
`line_items_terminated` = old price line items closed. `line_items_created` = new price line items added.
## What Sync Does After a Price Update
When you update a price, the old price gets an `end_date` and a new price is created. The sync workflow:
1. **Terminates** existing line items that reference the old price (their `end_date` is set to match the old price's `end_date`)
2. **Creates** new line items for the new price on all subscriptions that don't have one yet
Subscriptions with **overridden** line items (custom pricing) are **not affected** — their line items point to subscription-scoped prices, not the plan price you just updated.
## Using `effective_from`
`effective_from` controls when the old price expires and the new one starts.
| Scenario | Recommendation |
| --------------------------------------------------- | ---------------------------------------------------------------- |
| Change takes effect immediately | Omit `effective_from` (defaults to now) |
| Change takes effect at start of next billing period | Set `effective_from` to the next period start |
| Scheduled future price change | Set `effective_from` to the future date, trigger sync in advance |
The sync workflow creates new line items with `start_date = price.start_date`. If you set `effective_from: "2026-04-01"`, the new line items will start on April 1 and the old ones will end on April 1.
## Examples
### Update a flat fee amount
```json theme={null}
PUT /prices/price_monthly_base
{
"amount": "79.00",
"effective_from": "2026-04-01T00:00:00Z"
}
```
Then sync: `POST /plans/plan_growth/sync/subscriptions`
### Update usage tiers
```json theme={null}
PUT /prices/price_api_calls
{
"billing_model": "TIERED",
"tier_mode": "VOLUME",
"tiers": [
{ "up_to": 50000, "unit_amount": "0.002" },
{ "up_to": 200000, "unit_amount": "0.001" },
{ "up_to": null, "unit_amount": "0.0005" }
]
}
```
### Update metadata only (no sync needed)
```json theme={null}
PUT /prices/price_api_calls
{
"display_name": "API Calls — Updated Tiers",
"metadata": { "updated_by": "billing-team" }
}
```
No sync required.
## Related Topics
* [Subscription Line Item Overrides](/docs/subscriptions/subscription-line-item-overrides)
* [Plan Price Overrides](/docs/subscriptions/plan-price-overrides)
* [Plan Pricing](/docs/product-catalogue/plans/pricing)
* [Billing Models](/docs/product-catalogue/plans/billing-models/flat-fee)
# Clone Cursor pricing
Source: https://docs.flexprice.io/docs/product-catalogue/plans/use-cases/clone-cursor-pricing
Cursor’s pricing model combines subscription tiers with usage-based metering, allowing developers and teams to scale AI-powered assistance effectively.
Cursor charges users based on their selected plan (Free, Pro, or Team) and additional metered usage for AI completions, codebase indexing, and customization. By setting this up in Flexprice, you can provide users with transparent billing while managing limits dynamically. This guide walks you through setting up Cursor’s pricing model using a structured approach, ensuring clarity in billing and monetization.
**Use Cases**
* AI-powered development tools
* Code completion and AI assistance platforms
* Team-based coding environments with AI-driven automation
Whether you're a solo developer or a growing team, replicating Cursor’s pricing in Flexprice ensures seamless billing automation and monetization. This guide walks you through the setup process step by step.
**Cursor’s pricing model**
Cursor provides three pricing tiers:
| Plan Name | Price per Month | Features Included |
| --------- | --------------- | --------------------------------------------------------------------------------- |
| Hobby | \$0 | 2,000 completions, 50 slow premium requests |
| Pro | \$20 | Unlimited completions, 500 fast premium requests, unlimited slow premium requests |
| Business | \$40 per user | Pro features + privacy mode, centralized billing, admin dashboard, SAML/OIDC SSO |
In addition, Cursor provides usage-based metering for:
* AI completions
* Premium requests (fast and slow)
**Configuring Cursor’s Pricing in Flexprice**
1. **Define **[**Features**](/docs/product-catalogue/features/create)** in Flexprice**\
Flexprice allows you to define different types of features to enable precise billing and usage tracking for Cursor's pricing model.\
\
**Metered Features**
These features track usage over time and are billed based on consumption.
| Feature Name | Feature Type | Aggregation Method | Key Filters |
| --------------------- | ------------ | ------------------ | --------------------- |
| AI Completions | Metered | SUM | Feature: AI Queries |
| Fast Premium Requests | Metered | SUM | Feature: Premium Fast |
| Slow Premium Requests | Metered | SUM | Feature: Premium Slow |
**Boolean Features (On/Off Toggle)**\
These features enable or disable specific functionality without tracking usage.
| Feature Name | Feature Type | Description |
| --------------- | ------------ | -------------------------------------- |
| Privacy Mode | Boolean | Enables privacy mode for organizations |
| Admin Dashboard | Boolean | Grants access to admin-level analytics |
| SAML/OIDC SSO | Boolean | Enables single sign-on authentication |
**Static Features**\
These features are always included in a specific plan and do not change based on usage.
| Feature Name | Feature Type | Description |
| ------------- | ------------ | ------------------------------------- |
| SAML/OIDC SSO | Static | Enables single sign-on authentication |
2. **Create \*\* [**Plans**](/docs/product-catalogue/plans/create)** for Subscription Tiers\*\*
1. Each of Cursor’s plans can be created as a **recurring charges** in Flexprice:
| Plan Name | Billing Type | Base Price |
| --------- | ------------ | --------------- |
| Hobby | Subscription | \$0 |
| Pro | Subscription | \$20/month |
| Business | Subscription | \$40/user/month |
2. Define usage-based charges along with recurring charges\\
# Clone OpenAI pricing
Source: https://docs.flexprice.io/docs/product-catalogue/plans/use-cases/clone-openai-pricing
Manually managing per-token pricing can be complex, but with Flexprice, you can automate and scale your billing effortlessly. This guide walks you through setting up OpenAI's O1 pricing model using a **package-based pricing approach**. This method ensures clarity in billing, making it ideal for AI APIs, generative models, and machine learning services.
**Use Cases**
* AI APIs (LLMs like OpenAI, Anthropic, Mistral)
* Machine learning inference services
* Text-to-Speech or Speech-to-Text APIs
**OpenAI** charges users based on the number of input, output and cached tokens processed by their models. You can view the official pricing details here: [OpenAI Pricing](https://openai.com/api/pricing/).
The O1 model has the following pricing structure:
| **Token Type** | **Price per Million Tokens** |
| ------------------- | ---------------------------- |
| Input Tokens | \$15.00 per million tokens |
| Output Tokens | \$60.00 per million tokens |
| Cached Input Tokens | \$7.50 per million tokens |
For example, if a user processes:
* 5 million input tokens → $75.00 ($15.00 x 5)
* 2 million output tokens → $120.00 ($60.00 x 2)
* 1 million cached input tokens → $7.50 ($7.50 x 1)
* Total Cost = \$202.50
Now, let’s configure the pricing for the O1 model using **Flexprice**.
**Configuring Pricing of o1 model in Flexprice**
1. **Create** [Metered Features](/docs/product-catalogue/features/create) **for Token Usage**
Since token usage is metered, we first define three separate Metered Features in Flexprice for input tokens, output tokens, and cached input tokens.
| **Feature Name** | **Feature Type** | **Aggregation Method** | **Key** | **Filters** |
| ------------------- | ---------------- | ---------------------- | ----------- | --------------------------------------------- |
| Input Tokens | Metered | SUM | model\_name | model: OpenAI O1, prompt\_type: input |
| Output Tokens | Metered | SUM | model\_name | model: OpenAI O1, prompt\_type: output |
| Cached Input Tokens | Metered | SUM | model\_name | model: OpenAI O1, prompt\_type: cached\_input |
2. **Create a Plan with** [Package-Based Pricing](/docs/product-catalogue/plans/billing-models/package)
Once the metered features are created, we define a **Plan** that charges users per million tokens rather than per individual token.
| **Metered Feature** | **Billing Model** | Charges |
| ------------------- | ----------------- | -------------------------- |
| Input Tokens | Package Charge | \$15.00 per million tokens |
| Output Tokens | Package Charge | \$60.00 per million tokens |
| Cached Input Tokens | Package Charge | \$7.50 per million tokens |
Now, whenever a customer **purchases this plan and starts using it**, they will:
* See **real-time usage events** for token consumption.
* Get a **dynamically generated proposed invoice** based on their usage.
* Have full transparency in billing, ensuring clarity on costs.
This process ensures that **AI companies can charge users fairly based on actual usage** while providing predictable and scalable billing.
# Apply Tax to Customers and Subscriptions
Source: https://docs.flexprice.io/docs/product-catalogue/taxes/apply
Link tax rates to tenants, customers and subscriptions, and choose whether each is added to the price or included in it
A **tax association** is what makes a tax rate collect: it associates the rate with an entity, and carries the two decisions that matter, which rate applies and whether that rate is added on top of your price (`exclusive`) or already inside it (`inclusive`). Flexprice supports associations at three scopes: tenant (default for all customers), customer (overrides tenant for that customer), and subscription (overrides customer for that subscription's invoices).
| Level | Use it when |
| ------------ | ------------------------------------------------------------------------------ |
| Tenant | Every customer you bill pays this tax by default |
| Customer | This customer pays a tax the rest do not, or is in a different jurisdiction |
| Subscription | One subscription needs different tax treatment from the rest of the customer's |
## Association fields
| Field | Type | Required | Description |
| ---------------------- | --------- | -------- | ----------------------------------------------------------------------------------- |
| `tax_rate_code` | string | Yes | The `code` of the tax rate to link |
| `entity_type` | string | No | `tenant`, `customer`, `subscription`, or `invoice` |
| `entity_id` | string | No | ID of the entity to link |
| `external_customer_id` | string | No | Your own customer ID, usable instead of `entity_id` for customer-level associations |
| `auto_apply` | bool | No | Set to `true` to apply this tax automatically on every new invoice |
| `tax_behavior` | string | No | `inclusive` or `exclusive`. Resolved from the subscription's currency if omitted. |
| `priority` | int | No | Lower number fires first. Default: `0` |
| `currency` | string | No | Scopes this association to a specific currency, e.g. `"USD"` |
| `start_date` | timestamp | No | When the association becomes active. Defaults to now. |
| `end_date` | timestamp | No | When the association expires. Omit for indefinite. |
## Apply to a tenant (default for all customers)
A tenant-level association acts as the default tax for every customer that has no association of its own.
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/taxes/associations \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"tax_rate_code": "TAX_US_CA",
"entity_type": "tenant",
"entity_id": "",
"auto_apply": true,
"currency": "USD",
"tax_behavior": "inclusive",
"priority": 0
}'
```
**Response**
```json theme={null}
{
"id": "txa_01abc123",
"tax_rate_id": "txr_01abc456",
"entity_type": "tenant",
"entity_id": "",
"auto_apply": true,
"currency": "USD",
"tax_behavior": "inclusive",
"priority": 0
}
```
## Apply to a specific customer
Customer-level associations override tenant-level ones for that customer only.
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/taxes/associations \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"tax_rate_code": "TAX_EU_VAT",
"entity_type": "customer",
"external_customer_id": "your-customer-id",
"auto_apply": true,
"currency": "EUR",
"tax_behavior": "exclusive",
"priority": 0
}'
```
## Apply at subscription creation
Pass `tax_rate_overrides` in the create subscription request to link rates directly to that subscription. These override any customer or tenant associations for that subscription's invoices.
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/subscriptions \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"customer_id": "",
"plan_id": "",
"start_date": "2025-01-01T00:00:00Z",
"tax_rate_overrides": [
{
"tax_rate_code": "TAX_US_CA",
"currency": "USD",
"tax_behavior": "exclusive"
}
]
}'
```
When `tax_rate_overrides` is set, flexprice uses only those rates for the
subscription's invoices. Customer and tenant associations are not inherited.
Omit `tax_rate_overrides` entirely to inherit from the customer.
**`tax_rate_overrides` fields**
| Field | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------------------------------------------------------- |
| `tax_rate_code` | string | Yes | The `code` of the tax rate to link |
| `currency` | string | Yes | ISO currency code, e.g. `"USD"` |
| `tax_behavior` | string | No | `inclusive` or `exclusive`. Resolved from the subscription's currency if omitted. |
| `auto_apply` | bool | No | Defaults to `true` |
| `priority` | int | No | Lower number fires first. Default: `0` |
| `metadata` | object | No | Key-value pairs for your own tracking |
## Add or remove tax after subscription creation
Use the subscription modification API with `type: "tax"`.
### Add a tax rate
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/subscriptions//modify/execute \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"type": "tax",
"tax_params": {
"action": "add",
"tax_rate_id": "",
"effective_date": "2025-06-01T00:00:00Z"
}
}'
```
`effective_date` is optional. Omit it to apply the change immediately.
### Remove a tax rate
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/subscriptions//modify/execute \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"type": "tax",
"tax_params": {
"action": "remove",
"tax_association_id": ""
}
}'
```
Get the `tax_association_id` from `GET /v1/taxes/associations?entity_type=subscription&entity_id=`.
Removing a tax association affects only invoices generated after the change. Previously issued invoices are not changed.
### Preview before executing
Swap `/modify/execute` for `/modify/preview` to see the effect without committing:
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/subscriptions//modify/preview \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"type": "tax",
"tax_params": {
"action": "add",
"tax_rate_id": ""
}
}'
```
## tax\_params fields
| Field | Type | Required | Description |
| -------------------- | --------- | -------------------- | ---------------------------------------------- |
| `action` | string | Yes | `add` or `remove` |
| `tax_rate_id` | string | When `action=add` | ID of the tax rate to attach |
| `tax_association_id` | string | When `action=remove` | ID of the tax association to detach |
| `effective_date` | timestamp | No | When the change takes effect. Defaults to now. |
## Multiple tax rates on the same entity
Create one association per rate. Each rate is applied independently to the same taxable base and does not compound.
```bash theme={null}
# State tax (higher priority)
curl -X POST https://us.api.flexprice.io/v1/taxes/associations \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"tax_rate_code": "TAX_STATE",
"entity_type": "customer",
"entity_id": "",
"auto_apply": true,
"currency": "USD",
"priority": 0
}'
# Federal tax (lower priority)
curl -X POST https://us.api.flexprice.io/v1/taxes/associations \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"tax_rate_code": "TAX_FEDERAL",
"entity_type": "customer",
"entity_id": "",
"auto_apply": true,
"currency": "USD",
"priority": 1
}'
```
If the taxable amount is \$100, state at 6% adds \$6 and federal at 2% adds \$2 for a total tax of \$8. They do not compound.
## List associations
```bash theme={null}
curl "https://us.api.flexprice.io/v1/taxes/associations?entity_type=customer&entity_id=" \
-H "x-api-key: "
```
## Update an association
You can update `priority`, `auto_apply`, `tax_behavior`, and `metadata` without deleting and recreating.
```bash theme={null}
curl -X PUT https://us.api.flexprice.io/v1/taxes/associations/ \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"auto_apply": false
}'
```
Setting `auto_apply: false` pauses the tax without removing the record. Set it back to `true` to re-enable.
# Configure Tax Rates
Source: https://docs.flexprice.io/docs/product-catalogue/taxes/configure
Create and manage reusable tax rate definitions in flexprice.
A tax rate is a reusable definition of a percentage you collect. You create it once and reference it by `code` wherever you apply it.
## Create a tax rate
```bash theme={null}
curl -X POST https://us.api.flexprice.io/v1/taxes/rates \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"name": "US Sales Tax",
"code": "TAX_US_CA",
"tax_rate_type": "percentage",
"percentage_value": "8.25",
"description": "California state sales tax"
}'
```
**Response**
```json theme={null}
{
"id": "txr_01abc123",
"name": "US Sales Tax",
"code": "TAX_US_CA",
"tax_rate_type": "percentage",
"percentage_value": "8.25",
"tax_rate_status": "active",
"description": "California state sales tax",
"created_at": "2025-01-15T10:00:00Z"
}
```
Required properties:
* The `name` appears on your customer's invoice. Use a short label for the kind of tax, such as `Sales Tax`, `VAT`, or `GST`.
* The `code` is your unique identifier for the rate. You pass this `code`, not the ID, whenever you link the rate to a customer or subscription.
* The `percentage_value` is the percentage to collect, between `0` and `100`. Pass it as a string, for example `"8.25"` for 8.25%.
* The `tax_rate_type` is always `percentage`.
Optional properties:
* The `description` stores notes for your own reference. Your customers do not see it.
* The `metadata` object stores your own key-value pairs on the rate.
The `code` field is immutable. Choose it carefully since it is the key used in
all association requests. To rename a code, create a new tax rate and migrate
your associations to it.
A rate on its own collects nothing. It starts applying once you [associate it with a customer or subscription](/docs/product-catalogue/taxes/apply), which is also where you decide whether it is inclusive or exclusive.
## List tax rates
```bash theme={null}
curl https://us.api.flexprice.io/v1/taxes/rates \
-H "x-api-key: "
```
## Get a tax rate
```bash theme={null}
curl https://us.api.flexprice.io/v1/taxes/rates/ \
-H "x-api-key: "
```
## Update a tax rate
You can update `name`, `description`, and `metadata`. The `code` and `tax_rate_type` fields are immutable.
```bash theme={null}
curl -X PUT https://us.api.flexprice.io/v1/taxes/rates/ \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"description": "Updated for 2025 rate change"
}'
```
## Delete a tax rate
A tax rate can only be deleted if it has no active associations and no applied records on existing invoices. To stop applying a tax without losing audit history, delete the association instead of the rate.
```bash theme={null}
curl -X DELETE https://us.api.flexprice.io/v1/taxes/rates/ \
-H "x-api-key: "
```
**Error: rate has active associations**
```json theme={null}
{
"error": "tax_rate_in_use",
"message": "Cannot delete a tax rate with active associations. Delete associations first."
}
```
## Next step