# Create addon
Source: https://docs.flexprice.io/api-reference/addons/create-addon
/api-reference/openapi.json post /addons
Use when defining an optional purchasable item (e.g. extra storage or support tier). Ideal for add-ons that customers can attach to a subscription.
# Delete addon
Source: https://docs.flexprice.io/api-reference/addons/delete-addon
/api-reference/openapi.json delete /addons/{id}
Use when retiring an addon (e.g. end-of-life). Returns 200 with success message.
# Get addon
Source: https://docs.flexprice.io/api-reference/addons/get-addon
/api-reference/openapi.json get /addons/{id}
Use when you need to load a single addon (e.g. for display or to attach to a subscription).
# Get addon by lookup key
Source: https://docs.flexprice.io/api-reference/addons/get-addon-by-lookup-key
/api-reference/openapi.json get /addons/lookup/{lookup_key}
Use when resolving an addon by external id (e.g. from your product catalog). Ideal for integrations.
# Query addons
Source: https://docs.flexprice.io/api-reference/addons/query-addons
/api-reference/openapi.json post /addons/search
Use when listing or searching addons (e.g. catalog or subscription builder). Returns a paginated list; supports filtering and sorting.
# Update addon
Source: https://docs.flexprice.io/api-reference/addons/update-addon
/api-reference/openapi.json put /addons/{id}
Use when changing addon details (e.g. name, pricing, or metadata).
# Query alert logs
Source: https://docs.flexprice.io/api-reference/alerts/query-alert-logs
/api-reference/openapi.json post /alerts/search
Use when viewing or searching alert history (e.g. support triage or customer-facing alert log). Returns a paginated list; supports filtering by type, customer, subscription.
# Create checkout session
Source: https://docs.flexprice.io/api-reference/checkout/create-checkout-session
/api-reference/openapi.json post /checkout/sessions
# Delete checkout session
Source: https://docs.flexprice.io/api-reference/checkout/delete-checkout-session
/api-reference/openapi.json delete /checkout/sessions/{id}
# Get checkout session
Source: https://docs.flexprice.io/api-reference/checkout/get-checkout-session
/api-reference/openapi.json get /checkout/sessions/{id}
# Create costsheet
Source: https://docs.flexprice.io/api-reference/costs/create-costsheet
/api-reference/openapi.json post /costs
Use when setting up a new pricing configuration (e.g. a new product or region). Costsheets group prices and define the default for the environment.
# Delete costsheet
Source: https://docs.flexprice.io/api-reference/costs/delete-costsheet
/api-reference/openapi.json delete /costs/{id}
Use when retiring a costsheet (e.g. end-of-life product). Soft-deletes; status set to deleted.
# Get active costsheet
Source: https://docs.flexprice.io/api-reference/costs/get-active-costsheet
/api-reference/openapi.json get /costs/active
Use when you need the tenant's default pricing configuration (e.g. for checkout or plan display). Returns the active costsheet for the environment.
# Get combined revenue and cost analytics
Source: https://docs.flexprice.io/api-reference/costs/get-combined-revenue-and-cost-analytics
/api-reference/openapi.json post /costs/analytics
Use when building dashboards or reports that need revenue vs cost, ROI, and margin over a time period (e.g. finance views or executive summaries).
# Get combined revenue and cost analytics (V2)
Source: https://docs.flexprice.io/api-reference/costs/get-combined-revenue-and-cost-analytics-v2
/api-reference/openapi.json post /costs/analytics-v2
Use when you need the same revenue/cost/ROI analytics but computed from the costsheet usage-tracking pipeline (e.g. for consistency with usage-based cost data).
# Get costsheet
Source: https://docs.flexprice.io/api-reference/costs/get-costsheet
/api-reference/openapi.json get /costs/{id}
Use when you need to load a single costsheet (e.g. for editing or display). Supports optional expand for related prices.
# Query costsheets
Source: https://docs.flexprice.io/api-reference/costs/query-costsheets
/api-reference/openapi.json post /costs/search
Use when listing or searching costsheets (e.g. admin catalog). Returns a paginated list; supports filtering and sorting.
# Update costsheet
Source: https://docs.flexprice.io/api-reference/costs/update-costsheet
/api-reference/openapi.json put /costs/{id}
Use when changing costsheet name or metadata.
# Get coupon association
Source: https://docs.flexprice.io/api-reference/coupon-associations/get-coupon-association
/api-reference/openapi.json get /coupons/associations/{id}
Get a single coupon association by ID. Coupon associations are created and removed via the subscription modify API.
# List coupon associations
Source: https://docs.flexprice.io/api-reference/coupon-associations/list-coupon-associations
/api-reference/openapi.json get /coupons/associations
List coupon associations with optional filters. Coupon associations are created and removed via the subscription modify API.
# Create coupon
Source: https://docs.flexprice.io/api-reference/coupons/create-coupon
/api-reference/openapi.json post /coupons
Use when creating a discount (e.g. promo code or referral). Ideal for percent or fixed value, with optional validity and usage limits.
# Delete coupon
Source: https://docs.flexprice.io/api-reference/coupons/delete-coupon
/api-reference/openapi.json delete /coupons/{id}
Use when retiring a coupon (e.g. campaign ended). Returns 200 with success message.
# Get coupon
Source: https://docs.flexprice.io/api-reference/coupons/get-coupon
/api-reference/openapi.json get /coupons/{id}
Use when you need to load a single coupon (e.g. for display or to validate a code).
# Get coupon by code
Source: https://docs.flexprice.io/api-reference/coupons/get-coupon-by-code
/api-reference/openapi.json get /coupons/code/{code}
Use when resolving a coupon by promo code (e.g. checkout or validation).
# Query coupons
Source: https://docs.flexprice.io/api-reference/coupons/query-coupons
/api-reference/openapi.json post /coupons/search
Use when listing or searching coupons (e.g. promo management). Returns a paginated list; supports filtering and sorting.
# Update coupon
Source: https://docs.flexprice.io/api-reference/coupons/update-coupon
/api-reference/openapi.json put /coupons/{id}
Use when changing coupon config (e.g. value, validity, or usage limits).
# Create credit grant
Source: https://docs.flexprice.io/api-reference/credit-grants/create-credit-grant
/api-reference/openapi.json post /creditgrants
Use when giving a customer or plan credits (e.g. prepaid balance or promotional credits). Scope can be plan or subscription; supports start/end dates.
# Delete credit grant
Source: https://docs.flexprice.io/api-reference/credit-grants/delete-credit-grant
/api-reference/openapi.json delete /creditgrants/{id}
Use when removing or ending a credit grant (e.g. revoke promo or close prepaid). Plan-scoped grants are archived; subscription-scoped supports optional effective_date in body.
# Get credit grant
Source: https://docs.flexprice.io/api-reference/credit-grants/get-credit-grant
/api-reference/openapi.json get /creditgrants/{id}
Use when you need to load a single credit grant (e.g. for display or to check balance).
# Get plan credit grants
Source: https://docs.flexprice.io/api-reference/credit-grants/get-plan-credit-grants
/api-reference/openapi.json get /plans/{id}/creditgrants
Use when listing credits attached to a plan (e.g. included prepaid or promo credits).
# Update credit grant
Source: https://docs.flexprice.io/api-reference/credit-grants/update-credit-grant
/api-reference/openapi.json put /creditgrants/{id}
Use when changing a credit grant (e.g. amount or end date). Request body contains the fields to update.
# Create credit note
Source: https://docs.flexprice.io/api-reference/credit-notes/create-credit-note
/api-reference/openapi.json post /creditnotes
Use when issuing a refund or adjustment (e.g. customer dispute or proration). Links to an invoice; create as draft then finalize.
# Finalize credit note
Source: https://docs.flexprice.io/api-reference/credit-notes/finalize-credit-note
/api-reference/openapi.json post /creditnotes/{id}/finalize
Use when locking a draft credit note and applying the credit (e.g. after approval). Once finalized, applied per billing provider.
# Get credit note
Source: https://docs.flexprice.io/api-reference/credit-notes/get-credit-note
/api-reference/openapi.json get /creditnotes/{id}
Use when you need to load a single credit note (e.g. for display or reconciliation).
# Void credit note
Source: https://docs.flexprice.io/api-reference/credit-notes/void-credit-note
/api-reference/openapi.json post /creditnotes/{id}/void
Use when cancelling a draft credit note (e.g. created by mistake). Only draft credit notes can be voided.
# Create customer
Source: https://docs.flexprice.io/api-reference/customers/create-customer
/api-reference/openapi.json post /customers
Use when onboarding a new billing customer (e.g. sign-up or CRM sync). Ideal for linking via external_customer_id to your app's user id.
# Delete customer
Source: https://docs.flexprice.io/api-reference/customers/delete-customer
/api-reference/openapi.json delete /customers/{id}
Use when removing a customer (e.g. GDPR or churn). Returns 204 No Content on success.
# Get customer
Source: https://docs.flexprice.io/api-reference/customers/get-customer
/api-reference/openapi.json get /customers/{id}
Use when you need to load a single customer (e.g. for a billing portal or to attach a subscription).
# Get customer by external ID
Source: https://docs.flexprice.io/api-reference/customers/get-customer-by-external-id
/api-reference/openapi.json get /customers/external/{external_id}
Use when resolving a customer by your app's id (e.g. from your user table). Ideal for integrations that key by external id.
# Get customer entitlements
Source: https://docs.flexprice.io/api-reference/customers/get-customer-entitlements
/api-reference/openapi.json get /customers/{id}/entitlements
Use when checking what a customer can access (e.g. feature gating or usage limits). Supports optional filters (feature_ids, subscription_ids).
# Get customer entitlements by external ID
Source: https://docs.flexprice.io/api-reference/customers/get-customer-entitlements-by-external-id
/api-reference/openapi.json get /customers/external/{external_id}/entitlements
Use when checking entitlements by your app's customer id (e.g. feature gating at the edge). Supports optional filters (feature_ids, subscription_ids).
# Get customer usage summary
Source: https://docs.flexprice.io/api-reference/customers/get-customer-usage-summary
/api-reference/openapi.json get /customers/usage
Use when showing a customer's usage (e.g. portal or overage alerts). Identify by customer_id or customer_lookup_key; supports filters.
# Get upcoming credit grant applications
Source: https://docs.flexprice.io/api-reference/customers/get-upcoming-credit-grant-applications
/api-reference/openapi.json get /customers/{id}/grants/upcoming
Use when showing upcoming or pending credits for a customer (e.g. in a portal or for forecasting).
# Query customers
Source: https://docs.flexprice.io/api-reference/customers/query-customers
/api-reference/openapi.json post /customers/search
Use when listing or searching customers (e.g. admin CRM or reporting). Returns a paginated list; supports filtering and sorting.
# Update customer
Source: https://docs.flexprice.io/api-reference/customers/update-customer
/api-reference/openapi.json put /customers
Use when updating customer details (e.g. name, email, or metadata). Identify by id or external_customer_id.
# Create entitlement
Source: https://docs.flexprice.io/api-reference/entitlements/create-entitlement
/api-reference/openapi.json post /entitlements
Use when attaching a feature (and its limit) to a plan or addon (e.g. "10 seats" or "1000 API calls"). Defines what the plan/addon includes.
# Create entitlements in bulk
Source: https://docs.flexprice.io/api-reference/entitlements/create-entitlements-in-bulk
/api-reference/openapi.json post /entitlements/bulk
Use when attaching many features to a plan or addon at once (e.g. initial plan setup or import). Bulk version of create entitlement.
# Delete entitlement
Source: https://docs.flexprice.io/api-reference/entitlements/delete-entitlement
/api-reference/openapi.json delete /entitlements/{id}
Use when removing a feature from a plan or addon (e.g. deprecating a capability). Returns 200 with success message.
# Get addon entitlements
Source: https://docs.flexprice.io/api-reference/entitlements/get-addon-entitlements
/api-reference/openapi.json get /addons/{id}/entitlements
Use when checking what features or limits an addon grants (e.g. for display or entitlement logic).
# Get entitlement
Source: https://docs.flexprice.io/api-reference/entitlements/get-entitlement
/api-reference/openapi.json get /entitlements/{id}
Use when you need to load a single entitlement (e.g. to display or edit a feature limit).
# Get plan entitlements
Source: https://docs.flexprice.io/api-reference/entitlements/get-plan-entitlements
/api-reference/openapi.json get /plans/{id}/entitlements
Use when checking what a plan includes (e.g. feature list or limits for display or gating).
# Query entitlements
Source: https://docs.flexprice.io/api-reference/entitlements/query-entitlements
/api-reference/openapi.json post /entitlements/search
Use when listing or searching entitlements (e.g. plan editor or audit). Returns a paginated list; supports filtering by plan, addon, feature.
# Update entitlement
Source: https://docs.flexprice.io/api-reference/entitlements/update-entitlement
/api-reference/openapi.json put /entitlements/{id}
Use when changing an entitlement (e.g. increasing or decreasing a limit). Request body contains the fields to update.
# Clone an environment
Source: https://docs.flexprice.io/api-reference/environments/clone-an-environment
/api-reference/openapi.json post /environments/{id}/clone
Clone all published features and plans from the source environment into a target environment. If target_environment_id is provided, entities are cloned into that existing environment. Otherwise a new environment is created from name and type first.
# Error responses
Source: https://docs.flexprice.io/api-reference/error-responses
Understand Flexprice API error response formats and error codes
# Error responses
All Flexprice API errors follow a consistent structure. Each error response includes:
* `success`: Boolean flag indicating whether the request was successful (always `false` for errors)
* `error`: Object containing error details with:
* `message`: Human-readable description of the error
* `internal_error`: Internal error details (may be empty for client errors)
Error responses may also include additional fields specific to the error type.
```json theme={null} theme={null}
{
"success": false,
"error": {
"message": "Could not find a resource of type Plan with identifier: DBhwNcMNfYgeBpn.",
"internal_error": ""
}
}
```
## Common error types
The table below lists common error types. When processing errors programmatically, use the HTTP status code along with the error message.
| Title | Status code | Description |
| --------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Authentication failure | 401 | Your request did not provide a valid or well-formatted API key. Ensure you're including the `x-api-key` header with a valid API key. |
| Request validation error | 400 | A request that fails validation will return human-readable information about what went wrong. The error response includes a `message` field describing the validation issues. |
| Duplicate resource creation | 400 | Returned when attempting to create a resource that already exists. |
| Resource not found | 404 | This is returned when a provided identifier does not map to a resource that is accessible by the current account. |
| Feature not available | 404 | Returned when the endpoint or feature is behind a feature flag on the server-side and is unavailable for current use. |
| URL not found | 404 | Returned when the requested URL was not found on the server. |
| Resource conflict | 409 | Returned when the request conflicts with another concurrent request. This may occur when multiple requests attempt to modify the same resource simultaneously (e.g., the credits ledger). You can safely retry these requests. |
| Too many requests | 429 | Returned when the request rate exceeds the limit. |
| Internal Server error | 500 | This is an internal server error. The Flexprice team has been notified! |
# Bulk ingest events
Source: https://docs.flexprice.io/api-reference/events/bulk-ingest-events
/api-reference/openapi.json post /events/bulk
Use when batching usage events (e.g. backfill or high-volume ingestion). More efficient than single event calls; returns 202 when accepted.
# Get event
Source: https://docs.flexprice.io/api-reference/events/get-event
/api-reference/openapi.json get /events/{id}
Use when debugging a specific event (e.g. why it failed or how it was aggregated). Includes processing status and debug info.
# Get Hugging Face inference data
Source: https://docs.flexprice.io/api-reference/events/get-hugging-face-inference-data
/api-reference/openapi.json post /events/huggingface-inference
Use when fetching Hugging Face inference usage or billing data (e.g. for HF-specific reporting or reconciliation).
# Get usage analytics
Source: https://docs.flexprice.io/api-reference/events/get-usage-analytics
/api-reference/openapi.json post /events/analytics
Use when building analytics views (e.g. usage by feature or customer over time). Supports filtering, grouping, and time-series breakdown.
# Get usage by meter
Source: https://docs.flexprice.io/api-reference/events/get-usage-by-meter
/api-reference/openapi.json post /events/usage/meter
Use when showing usage for a specific meter (e.g. dashboard or overage check). Supports time range, filters, and grouping by customer or subscription.
# Get usage statistics
Source: https://docs.flexprice.io/api-reference/events/get-usage-statistics
/api-reference/openapi.json post /events/usage
Use when building usage reports or dashboards across events. Supports filters and grouping; defaults to last 7 days if no range provided.
# Ingest event
Source: https://docs.flexprice.io/api-reference/events/ingest-event
/api-reference/openapi.json post /events
Use when sending a single usage event from your app (e.g. one API call or one GB stored). Events are processed asynchronously; returns 202 with event_id.
# List raw events
Source: https://docs.flexprice.io/api-reference/events/list-raw-events
/api-reference/openapi.json post /events/query
Use when debugging ingestion or exporting raw event data (e.g. support or audit). Returns a paginated list; supports time range and sorting.
# Clone a feature
Source: https://docs.flexprice.io/api-reference/features/clone-a-feature
/api-reference/openapi.json post /features/{id}/clone
Clone an existing feature
# Create feature
Source: https://docs.flexprice.io/api-reference/features/create-feature
/api-reference/openapi.json post /features
Use when defining a new feature or capability to gate or meter (e.g. feature flags or usage-based limits). Ideal for boolean or usage features.
# Delete feature
Source: https://docs.flexprice.io/api-reference/features/delete-feature
/api-reference/openapi.json delete /features/{id}
Use when retiring a feature (e.g. deprecated capability). Returns 200 with success message.
# Query features
Source: https://docs.flexprice.io/api-reference/features/query-features
/api-reference/openapi.json post /features/search
Use when listing or searching features (e.g. catalog or entitlement setup). Returns a paginated list; supports filtering and sorting.
# Update feature
Source: https://docs.flexprice.io/api-reference/features/update-feature
/api-reference/openapi.json put /features/{id}
Use when changing feature definition (e.g. name, type, or meter). Request body contains the fields to update.
# Create group
Source: https://docs.flexprice.io/api-reference/groups/create-group
/api-reference/openapi.json post /groups
Use when organizing entities into a group (e.g. for filtering prices or plans by product line or region).
# Delete group
Source: https://docs.flexprice.io/api-reference/groups/delete-group
/api-reference/openapi.json delete /groups/{id}
Use when removing a group and clearing its entity associations (e.g. retiring a product line). Returns 204 or 200 on success.
# Get group
Source: https://docs.flexprice.io/api-reference/groups/get-group
/api-reference/openapi.json get /groups/{id}
Use when you need to load a single group (e.g. for display or to assign entities).
# Query groups
Source: https://docs.flexprice.io/api-reference/groups/query-groups
/api-reference/openapi.json post /groups/search
Use when listing or searching groups (e.g. admin catalog). Returns a paginated list; supports filtering and sorting.
# Delink integration mapping
Source: https://docs.flexprice.io/api-reference/integrations/delink-integration-mapping
/api-reference/openapi.json delete /integrations/link
Soft-delete (archive) the mapping between a FlexPrice entity and a provider entity.
# Get entity integration mappings
Source: https://docs.flexprice.io/api-reference/integrations/get-entity-integration-mappings
/api-reference/openapi.json get /integrations/mappings
Get integration mappings for a specific entity by entity type and entity ID.
# Get integration configurations
Source: https://docs.flexprice.io/api-reference/integrations/get-integration-configurations
/api-reference/openapi.json get /integrations/config
Returns the base capabilities and current sync configuration for all connected providers.
# Link integration mapping
Source: https://docs.flexprice.io/api-reference/integrations/link-integration-mapping
/api-reference/openapi.json post /integrations/link
Link a FlexPrice entity to provider entity with provider-specific side effects.
# Introduction
Source: https://docs.flexprice.io/api-reference/introduction
# Welcome to the Flexprice API
The Flexprice API lets you programmatically manage pricing, subscriptions, and billing for your SaaS application.
## Base URL
All API requests must use one of the following base URLs:
* **US Region**: `https://us.api.flexprice.io/v1`
* **India Region**: `https://api.cloud.flexprice.io/v1`
Select your preferred region using the server selector in the API playground:
## Authentication
All API endpoints require authentication using an API key. Include your API key in the `x-api-key` header with every request.
```bash theme={null}
x-api-key:
```
Create API keys in the Flexprice Dashboard. If you need help, email us at [support@flexprice.io](mailto:support@flexprice.io).
## Response Format
Successful API responses return JSON data directly. The response structure varies by endpoint—see individual endpoint documentation for details.
## Error Handling
The API uses standard HTTP status codes:
* `200` - Success
* `201` - Created
* `400` - Bad Request
* `401` - Unauthorized
* `404` - Not Found
* `409` - Conflict
* `429` - Too Many Requests
* `500` - Internal Server Error
For detailed error response formats and error codes, see the [Error Responses](/api-reference/error-responses) documentation.
## Need Help?
* Email: [support@flexprice.io](mailto:support@flexprice.io)
* Documentation Updates: [tech@flexprice.io](mailto:tech@flexprice.io)
# Attempt invoice payment
Source: https://docs.flexprice.io/api-reference/invoices/attempt-invoice-payment
/api-reference/openapi.json post /invoices/{id}/payment/attempt
Use when paying an invoice with the customer's wallet balance (e.g. prepaid credits or balance applied at checkout).
# Create one-off invoice
Source: https://docs.flexprice.io/api-reference/invoices/create-one-off-invoice
/api-reference/openapi.json post /invoices
Use when creating a manual or one-off invoice (e.g. custom charge or non-recurring billing). Invoice is created in draft; finalize when ready.
# Finalize invoice
Source: https://docs.flexprice.io/api-reference/invoices/finalize-invoice
/api-reference/openapi.json post /invoices/{id}/finalize
Use when locking an invoice for payment (e.g. after review). Once finalized, line items are locked; invoice can be paid or voided.
# Get customer invoice summary
Source: https://docs.flexprice.io/api-reference/invoices/get-customer-invoice-summary
/api-reference/openapi.json get /customers/{id}/invoices/summary
Use when showing a customer's invoice overview (e.g. billing portal or balance summary). Includes totals and multi-currency breakdown.
# Get invoice
Source: https://docs.flexprice.io/api-reference/invoices/get-invoice
/api-reference/openapi.json get /invoices/{id}
Use when loading an invoice for display or editing (e.g. portal or reconciliation). Supports group_by for usage breakdown and force_runtime_recalculation.
# Get invoice PDF
Source: https://docs.flexprice.io/api-reference/invoices/get-invoice-pdf
/api-reference/openapi.json get /invoices/{id}/pdf
Use when delivering an invoice PDF to the customer (e.g. email attachment or download). Use url=true for a presigned URL instead of binary. Use force_generate=true to regenerate and re-upload the PDF even if one already exists in S3.
# Get invoice preview
Source: https://docs.flexprice.io/api-reference/invoices/get-invoice-preview
/api-reference/openapi.json post /invoices/preview
Use when showing a customer what they will be charged (e.g. preview before checkout or plan change). No invoice is created.
# Query invoices
Source: https://docs.flexprice.io/api-reference/invoices/query-invoices
/api-reference/openapi.json post /invoices/search
Use when listing or searching invoices (e.g. admin view or customer history). Returns a paginated list; supports filtering by customer, status, date range.
# Recalculate draft invoice (v2)
Source: https://docs.flexprice.io/api-reference/invoices/recalculate-draft-invoice-v2
/api-reference/openapi.json post /invoices/{id}/recalculate-v2
Recalculates a draft SUBSCRIPTION invoice in-place (replaces line items, reapplies credits/coupons/taxes). Use when subscription or usage data changed before finalizing.
# Recalculate invoice (voided invoice)
Source: https://docs.flexprice.io/api-reference/invoices/recalculate-invoice-voided-invoice
/api-reference/openapi.json post /invoices/{id}/recalculate
Starts an async workflow that creates a fresh replacement invoice for a voided SUBSCRIPTION invoice (same billing period). Returns workflow_id and run_id; poll workflow status or GET the new invoice via recalculated_invoice_id after completion.
# Trigger invoice communication webhook
Source: https://docs.flexprice.io/api-reference/invoices/trigger-invoice-communication-webhook
/api-reference/openapi.json post /invoices/{id}/comms/trigger
Use when sending an invoice to the customer (e.g. trigger email or Slack). Payload includes full invoice details for your integration.
# Update invoice
Source: https://docs.flexprice.io/api-reference/invoices/update-invoice
/api-reference/openapi.json put /invoices/{id}
Use when updating invoice metadata or due date (e.g. PDF URL, net terms). For paid invoices only safe fields can be updated.
# Update invoice payment status
Source: https://docs.flexprice.io/api-reference/invoices/update-invoice-payment-status
/api-reference/openapi.json put /invoices/{id}/payment
Use when reconciling payment status from an external gateway or manual entry (e.g. mark paid after bank confirmation).
# Void invoice
Source: https://docs.flexprice.io/api-reference/invoices/void-invoice
/api-reference/openapi.json post /invoices/{id}/void
Use when cancelling an invoice (e.g. order cancelled or duplicate). Only unpaid invoices can be voided.
# Pagination
Source: https://docs.flexprice.io/api-reference/pagination
Learn how to paginate through API responses using offset-based pagination
# Pagination
The Flexprice API uses offset-based pagination for endpoints that list resources, such as the [List Customers](/api-reference/customers/query-customers) endpoint. This lets you load resources incrementally without transferring large amounts of data in a single request.
Use the `limit` and `offset` query parameters to control how many items are returned and where to start in the result set. For the first request, you typically use `offset=0`. The API then returns an updated `offset` in the response that you can reuse directly for the next page.
## Paginated API responses
List endpoints return a standardized format with the requested items in an `items` array and pagination metadata in a `pagination` object.
The response format is:
```json theme={null} theme={null}
{
"items": [
...
],
"pagination": {
"limit": 50,
"offset": 50,
"total": 150
}
}
```
**Response fields:**
* `limit`: Maximum Number of items returned in this page (default: 50).
* `offset`: The offset you should use when requesting the **next** page. For the first request, you typically pass `offset=0`; the API returns `offset = request_offset + limit` in the response.
* `total`: Total number of items in the database that match your query, not just the items in the current response.
**Fetching the next page:**
To get the next page, reuse the `offset` value returned in the previous response instead of computing it yourself. For example:
* First request: `limit=50&offset=0` returns items 1-50 and a `pagination.offset` value of `50`.
* Second request: `limit=50&offset=50` (using the previous `pagination.offset`) returns items 51-100 and a `pagination.offset` value of `100`.
* Third request: `limit=50&offset=100` returns items 101-150, and so on.
**Sorting:**
By default, resources are sorted by `created_at` in descending order (newest first). Use the `sort` and `order` query parameters to change the sorting behavior.
## Limits
The default page size is 50 items. You can request up to 1000 items per page using the `limit` query parameter.
# Create payment
Source: https://docs.flexprice.io/api-reference/payments/create-payment
/api-reference/openapi.json post /payments
Use when recording a payment against an invoice (e.g. after receiving funds via a gateway or manual entry).
# Delete payment
Source: https://docs.flexprice.io/api-reference/payments/delete-payment
/api-reference/openapi.json delete /payments/{id}
Use when removing or voiding a payment record (e.g. correcting erroneous entries). Returns 200 with success message.
# Get payment
Source: https://docs.flexprice.io/api-reference/payments/get-payment
/api-reference/openapi.json get /payments/{id}
Use when you need to load a single payment (e.g. for a receipt view or reconciliation).
# List payments
Source: https://docs.flexprice.io/api-reference/payments/list-payments
/api-reference/openapi.json get /payments
Use when listing or searching payments (e.g. reconciliation UI or customer payment history). Returns a paginated list; supports filtering by customer, invoice, status.
# Process payment
Source: https://docs.flexprice.io/api-reference/payments/process-payment
/api-reference/openapi.json post /payments/{id}/process
Use when you need to charge or process a payment (e.g. trigger the payment provider to capture funds). Returns updated payment with status.
# Update payment
Source: https://docs.flexprice.io/api-reference/payments/update-payment
/api-reference/openapi.json put /payments/{id}
Use when updating payment status or metadata (e.g. after reconciliation or adding a reference).
# Clone a plan
Source: https://docs.flexprice.io/api-reference/plans/clone-a-plan
/api-reference/openapi.json post /plans/{id}/clone
Clone an existing plan, copying its active prices, published entitlements, and published credit grants
# Create plan
Source: https://docs.flexprice.io/api-reference/plans/create-plan
/api-reference/openapi.json post /plans
Use when defining a new pricing plan (e.g. Free, Pro, Enterprise). Attach prices and entitlements; customers subscribe to plans.
# Delete plan
Source: https://docs.flexprice.io/api-reference/plans/delete-plan
/api-reference/openapi.json delete /plans/{id}
Use when retiring a plan (e.g. end-of-life). Existing subscriptions may be affected. Returns 200 with success message.
# Get plan
Source: https://docs.flexprice.io/api-reference/plans/get-plan
/api-reference/openapi.json get /plans/{id}
Use when you need to load a single plan (e.g. for display or to create a subscription).
# Query plans
Source: https://docs.flexprice.io/api-reference/plans/query-plans
/api-reference/openapi.json post /plans/search
Use when listing or searching plans (e.g. plan picker or admin catalog). Returns a paginated list; supports filtering and sorting.
# Synchronize plan prices
Source: https://docs.flexprice.io/api-reference/plans/synchronize-plan-prices
/api-reference/openapi.json post /plans/{id}/sync/subscriptions
Use when you have changed plan prices and need to push them to all active subscriptions (e.g. global price update). Returns workflow ID.
# Update plan
Source: https://docs.flexprice.io/api-reference/plans/update-plan
/api-reference/openapi.json put /plans/{id}
Use when changing plan details (e.g. name, interval, or metadata). Partial update supported.
# Create price unit
Source: https://docs.flexprice.io/api-reference/price-units/create-price-unit
/api-reference/openapi.json post /prices/units
Use when defining a new unit of measure for pricing (e.g. GB, API call, seat). Ideal for metered or usage-based prices.
# Delete price unit
Source: https://docs.flexprice.io/api-reference/price-units/delete-price-unit
/api-reference/openapi.json delete /prices/units/{id}
Use when removing a price unit that is no longer needed. Fails if any price references this unit.
# Get price unit
Source: https://docs.flexprice.io/api-reference/price-units/get-price-unit
/api-reference/openapi.json get /prices/units/{id}
Use when you need to load a single price unit (e.g. for display or when creating a price).
# Get price unit by code
Source: https://docs.flexprice.io/api-reference/price-units/get-price-unit-by-code
/api-reference/openapi.json get /prices/units/code/{code}
Use when resolving a price unit by code (e.g. from an external catalog or config). Ideal for integrations.
# List price units
Source: https://docs.flexprice.io/api-reference/price-units/list-price-units
/api-reference/openapi.json get /prices/units
Use when listing price units (e.g. in a catalog or when creating prices). Returns a paginated list; supports status, sort, and pagination.
# Query price units
Source: https://docs.flexprice.io/api-reference/price-units/query-price-units
/api-reference/openapi.json post /prices/units/search
Use when searching or listing price units (e.g. admin catalog). Returns a paginated list; supports filtering and sorting.
# Update price unit
Source: https://docs.flexprice.io/api-reference/price-units/update-price-unit
/api-reference/openapi.json put /prices/units/{id}
Use when renaming or updating metadata for a price unit. Code is immutable once created.
# Create price
Source: https://docs.flexprice.io/api-reference/prices/create-price
/api-reference/openapi.json post /prices
Use when adding a new price to a plan or catalog (e.g. per-seat, flat, or metered). Ideal for both simple and usage-based pricing.
# Create prices in bulk
Source: https://docs.flexprice.io/api-reference/prices/create-prices-in-bulk
/api-reference/openapi.json post /prices/bulk
Use when creating many prices at once (e.g. importing a catalog or setting up a plan with multiple tiers).
# Delete price
Source: https://docs.flexprice.io/api-reference/prices/delete-price
/api-reference/openapi.json delete /prices/{id}
Use when retiring a price (e.g. end-of-life or replacement). Optional effective date or cascade for subscriptions.
# Get price
Source: https://docs.flexprice.io/api-reference/prices/get-price
/api-reference/openapi.json get /prices/{id}
Use when you need to load a single price (e.g. for display or editing). Response includes expanded meter and price unit when applicable.
# Get price by lookup key
Source: https://docs.flexprice.io/api-reference/prices/get-price-by-lookup-key
/api-reference/openapi.json get /prices/lookup/{lookup_key}
Use when resolving a price by external id (e.g. from your catalog or CMS). Ideal for integrations.
# Query prices
Source: https://docs.flexprice.io/api-reference/prices/query-prices
/api-reference/openapi.json post /prices/search
Use when listing or searching prices (e.g. plan builder or catalog). Returns a paginated list; supports filtering and sorting.
# Update price
Source: https://docs.flexprice.io/api-reference/prices/update-price
/api-reference/openapi.json put /prices/{id}
Use when changing price configuration (e.g. amount, billing scheme, or metadata).
# Get a specific RBAC role
Source: https://docs.flexprice.io/api-reference/rbac/get-a-specific-rbac-role
/api-reference/openapi.json get /rbac/roles/{id}
Use when you need to show or edit a single role (e.g. role detail page). Includes permissions, name, and description.
# List all RBAC roles
Source: https://docs.flexprice.io/api-reference/rbac/list-all-rbac-roles
/api-reference/openapi.json get /rbac/roles
Use when building role pickers or permission UIs. Returns all roles with permissions and descriptions.
# Create scheduled task
Source: https://docs.flexprice.io/api-reference/scheduled-tasks/create-scheduled-task
/api-reference/openapi.json post /tasks/scheduled
Use when setting up recurring data exports or other scheduled jobs. Ideal for report generation or syncing data on a schedule.
# Delete a scheduled task
Source: https://docs.flexprice.io/api-reference/scheduled-tasks/delete-a-scheduled-task
/api-reference/openapi.json delete /tasks/scheduled/{id}
Use when removing a scheduled task from the active roster. Archives the task and removes it from the scheduler (soft delete).
# Get scheduled task
Source: https://docs.flexprice.io/api-reference/scheduled-tasks/get-scheduled-task
/api-reference/openapi.json get /tasks/scheduled/{id}
Use when you need to load a single scheduled task (e.g. to show details in a UI or check its configuration).
# List scheduled tasks
Source: https://docs.flexprice.io/api-reference/scheduled-tasks/list-scheduled-tasks
/api-reference/openapi.json get /tasks/scheduled
Use when listing or managing scheduled tasks in an admin UI. Returns a list; supports filtering by status, type, and pagination.
# Schedule draft finalization
Source: https://docs.flexprice.io/api-reference/scheduled-tasks/schedule-draft-finalization
/api-reference/openapi.json post /tasks/scheduled/schedule-draft-finalization
Triggers the draft invoice finalization workflow that scans computed draft invoices whose finalization delay has elapsed and finalizes them (assign invoice number, sync to vendors, attempt payment).
# Schedule update billing period
Source: https://docs.flexprice.io/api-reference/scheduled-tasks/schedule-update-billing-period
/api-reference/openapi.json post /tasks/scheduled/schedule-update-billing-period
Use when you need to trigger a billing-period update workflow (e.g. to recalculate or sync billing windows).
# Trigger force run
Source: https://docs.flexprice.io/api-reference/scheduled-tasks/trigger-force-run
/api-reference/openapi.json post /tasks/scheduled/{id}/run
Use when you need to run a scheduled export immediately (e.g. on-demand report or catch-up). Supports optional custom time range.
# Update a scheduled task
Source: https://docs.flexprice.io/api-reference/scheduled-tasks/update-a-scheduled-task
/api-reference/openapi.json put /tasks/scheduled/{id}
Use when pausing or resuming a scheduled task. Only the enabled field can be changed.
# Create a new API key
Source: https://docs.flexprice.io/api-reference/secrets/create-a-new-api-key
/api-reference/openapi.json post /secrets/api/keys
Use when issuing a new API key (e.g. for a service account or for the current user). Provide service_account_id to create for a service account.
# Delete an API key
Source: https://docs.flexprice.io/api-reference/secrets/delete-an-api-key
/api-reference/openapi.json delete /secrets/api/keys/{id}
Use when revoking an API key (e.g. rotation or compromise). Permanently invalidates the key.
# List API keys
Source: https://docs.flexprice.io/api-reference/secrets/list-api-keys
/api-reference/openapi.json get /secrets/api/keys
Use when listing API keys (e.g. admin view or rotating keys). Returns a paginated list.
# Activate draft subscription
Source: https://docs.flexprice.io/api-reference/subscriptions/activate-draft-subscription
/api-reference/openapi.json post /subscriptions/{id}/activate
Use when turning a draft subscription live (e.g. after collecting payment or completing setup). Once activated, billing and entitlements apply.
# Add addon to subscription
Source: https://docs.flexprice.io/api-reference/subscriptions/add-addon-to-subscription
/api-reference/openapi.json post /subscriptions/addon
Use when adding an optional product or add-on to an existing subscription (e.g. extra storage or support tier).
# Cancel subscription
Source: https://docs.flexprice.io/api-reference/subscriptions/cancel-subscription
/api-reference/openapi.json post /subscriptions/{id}/cancel
Use when a customer churns or downgrades. Supports immediate or end-of-period cancellation and proration. Ideal for self-serve or support-driven cancellations.
# Cancel subscription schedule
Source: https://docs.flexprice.io/api-reference/subscriptions/cancel-subscription-schedule
/api-reference/openapi.json post /v1/subscriptions/schedules/{schedule_id}/cancel
Use when cancelling a scheduled change (e.g. customer changed mind). Identify by schedule ID in path or by subscription ID + schedule type in body.
# Create subscription
Source: https://docs.flexprice.io/api-reference/subscriptions/create-subscription
/api-reference/openapi.json post /subscriptions
Use when onboarding a customer to a plan or starting a new subscription. Ideal for draft subscriptions (activate later) or active from start.
# Create subscription line item
Source: https://docs.flexprice.io/api-reference/subscriptions/create-subscription-line-item
/api-reference/openapi.json post /subscriptions/{id}/lineitems
Use when adding a new charge or seat to a subscription (e.g. extra seat or one-time add). Supports price_id or inline price.
# Delete subscription line item
Source: https://docs.flexprice.io/api-reference/subscriptions/delete-subscription-line-item
/api-reference/openapi.json delete /subscriptions/lineitems/{id}
Use when removing a charge or seat from a subscription (e.g. downgrade). Line item ends; retained for history but no longer billed.
# Execute subscription modification
Source: https://docs.flexprice.io/api-reference/subscriptions/execute-subscription-modification
/api-reference/openapi.json post /subscriptions/{id}/modify/execute
Execute a mid-cycle subscription modification (inheritance, quantity change, grouped invoicing, trial end, coupon, or tax).
# Execute subscription plan change
Source: https://docs.flexprice.io/api-reference/subscriptions/execute-subscription-plan-change
/api-reference/openapi.json post /subscriptions/{id}/change/execute
Use when applying a plan change (e.g. upgrade or downgrade). Executes proration and generates invoice or credit as needed.
# Get active addon associations
Source: https://docs.flexprice.io/api-reference/subscriptions/get-active-addon-associations
/api-reference/openapi.json get /subscriptions/{id}/addons/associations
Use when listing which add-ons are currently attached to a subscription (e.g. for display or editing).
# Get subscription
Source: https://docs.flexprice.io/api-reference/subscriptions/get-subscription
/api-reference/openapi.json get /subscriptions/{id}
Use when you need to load a single subscription (e.g. for a billing portal or to check status).
# Get subscription entitlements
Source: https://docs.flexprice.io/api-reference/subscriptions/get-subscription-entitlements
/api-reference/openapi.json get /subscriptions/{id}/entitlements
Use when checking what features or limits a subscription has (e.g. entitlement checks or feature gating). Optional feature_ids to filter.
# Get subscription schedule
Source: https://docs.flexprice.io/api-reference/subscriptions/get-subscription-schedule
/api-reference/openapi.json get /v1/subscription-schedules/{id}
Use when you need to load a single scheduled change (e.g. to show when a plan change or renewal takes effect).
# Get subscription (V2)
Source: https://docs.flexprice.io/api-reference/subscriptions/get-subscription-v2
/api-reference/openapi.json get /subscriptions/{id}/v2
Use when you need a subscription with related data (line items, prices, plan). Supports expand for detailed payloads without extra round-trips.
# Get upcoming credit grant applications
Source: https://docs.flexprice.io/api-reference/subscriptions/get-upcoming-credit-grant-applications
/api-reference/openapi.json get /subscriptions/{id}/grants/upcoming
Use when showing upcoming or pending credits for a subscription (e.g. in a portal or for forecasting).
# Get usage by subscription
Source: https://docs.flexprice.io/api-reference/subscriptions/get-usage-by-subscription
/api-reference/openapi.json post /subscriptions/usage
Use when showing usage for a subscription (e.g. in a portal or for overage checks). Supports time range and filters.
# List all subscription schedules
Source: https://docs.flexprice.io/api-reference/subscriptions/list-all-subscription-schedules
/api-reference/openapi.json get /v1/subscription-schedules
Use when listing or searching scheduled changes across subscriptions (e.g. admin view). Returns schedules with optional filtering.
# List subscription schedules
Source: https://docs.flexprice.io/api-reference/subscriptions/list-subscription-schedules
/api-reference/openapi.json get /v1/subscriptions/{subscription_id}/schedules
Use when listing scheduled changes for a subscription (e.g. upcoming plan change or renewal). Returns all schedules for that subscription.
# Preview subscription modification
Source: https://docs.flexprice.io/api-reference/subscriptions/preview-subscription-modification
/api-reference/openapi.json post /subscriptions/{id}/modify/preview
Preview the impact of a mid-cycle subscription modification (inheritance, quantity change, grouped invoicing, trial end, coupon, or tax) without committing changes.
# Preview subscription plan change
Source: https://docs.flexprice.io/api-reference/subscriptions/preview-subscription-plan-change
/api-reference/openapi.json post /subscriptions/{id}/change/preview
Use when showing a customer the cost of a plan change before they confirm (e.g. upgrade/downgrade preview with proration).
# Query subscriptions
Source: https://docs.flexprice.io/api-reference/subscriptions/query-subscriptions
/api-reference/openapi.json post /subscriptions/search
Use when listing or searching subscriptions (e.g. admin view or customer subscription list). Returns a paginated list; supports filtering by customer, plan, status.
# Remove addon from subscription
Source: https://docs.flexprice.io/api-reference/subscriptions/remove-addon-from-subscription
/api-reference/openapi.json delete /subscriptions/addon
Use when removing an add-on from a subscription (e.g. downgrade or opt-out).
# Search subscription line items
Source: https://docs.flexprice.io/api-reference/subscriptions/search-subscription-line-items
/api-reference/openapi.json post /subscriptions/lineitems/search
List subscription line items with a JSON filter (subscription, customer, price, pagination, expand=prices, etc.).
# Update subscription
Source: https://docs.flexprice.io/api-reference/subscriptions/update-subscription
/api-reference/openapi.json put /subscriptions/{id}
Use when changing subscription details (e.g. quantity, billing anchor, or parent). Supports partial update; send "" to clear parent_subscription_id.
# Update subscription line item
Source: https://docs.flexprice.io/api-reference/subscriptions/update-subscription-line-item
/api-reference/openapi.json put /subscriptions/lineitems/{id}
Use when changing a subscription line item (e.g. quantity or price). Implemented by ending the current line and creating a new one for clean billing.
# Create a new task
Source: https://docs.flexprice.io/api-reference/tasks/create-a-new-task
/api-reference/openapi.json post /tasks
Use when submitting a file or job for async processing (e.g. export or import). Returns task ID to poll for status and result.
# Download task export file
Source: https://docs.flexprice.io/api-reference/tasks/download-task-export-file
/api-reference/openapi.json get /tasks/{id}/download
Use when letting a user download an exported file (e.g. report or data export). Returns a presigned URL; supports FlexPrice or customer-owned S3.
# Get a task
Source: https://docs.flexprice.io/api-reference/tasks/get-a-task
/api-reference/openapi.json get /tasks/{id}
Use when checking task status or progress (e.g. polling after create). Returns task by ID.
# Get task processing result
Source: https://docs.flexprice.io/api-reference/tasks/get-task-processing-result
/api-reference/openapi.json get /tasks/result
Use when fetching the outcome of a completed task (e.g. export URL or error message). Call after task status is complete.
# List tasks
Source: https://docs.flexprice.io/api-reference/tasks/list-tasks
/api-reference/openapi.json get /tasks
Use when listing or searching async tasks (e.g. admin queue view). Returns list with optional filtering.
# Update task status
Source: https://docs.flexprice.io/api-reference/tasks/update-task-status
/api-reference/openapi.json put /tasks/{id}/status
Use when updating task status (e.g. marking complete or failed from a worker). Typically called by backend processors.
# Create Tax Association
Source: https://docs.flexprice.io/api-reference/tax-associations/create-tax-association
/api-reference/openapi.json post /taxes/associations
Use when linking a tax rate to an entity (e.g. customer, product, or region) so that rate applies on invoices.
# Delete tax association
Source: https://docs.flexprice.io/api-reference/tax-associations/delete-tax-association
/api-reference/openapi.json delete /taxes/associations/{id}
Use when removing a tax association (e.g. entity no longer subject to that rate).
# Get Tax Association
Source: https://docs.flexprice.io/api-reference/tax-associations/get-tax-association
/api-reference/openapi.json get /taxes/associations/{id}
Use when you need to load a single tax association (e.g. for display or editing).
# List tax associations
Source: https://docs.flexprice.io/api-reference/tax-associations/list-tax-associations
/api-reference/openapi.json get /taxes/associations
Use when listing tax associations (e.g. tax config or audit). Returns list with optional filtering.
# Update tax association
Source: https://docs.flexprice.io/api-reference/tax-associations/update-tax-association
/api-reference/openapi.json put /taxes/associations/{id}
Use when changing a tax association (e.g. switch rate or entity). Request body contains the fields to update.
# Create a tax rate
Source: https://docs.flexprice.io/api-reference/tax-rates/create-a-tax-rate
/api-reference/openapi.json post /taxes/rates
Use when defining a new tax rate (e.g. VAT or sales tax) for use in invoices. Attach to customers or products via tax associations.
# Delete a tax rate
Source: https://docs.flexprice.io/api-reference/tax-rates/delete-a-tax-rate
/api-reference/openapi.json delete /taxes/rates/{id}
Use when retiring a tax rate (e.g. no longer applicable). Fails if still referenced by associations.
# Get a tax rate
Source: https://docs.flexprice.io/api-reference/tax-rates/get-a-tax-rate
/api-reference/openapi.json get /taxes/rates/{id}
Use when you need to load a single tax rate (e.g. for display or when creating an association).
# Get tax rates
Source: https://docs.flexprice.io/api-reference/tax-rates/get-tax-rates
/api-reference/openapi.json get /taxes/rates
Use when listing tax rates (e.g. tax config UI). Returns tax rates with optional filters.
# Update a tax rate
Source: https://docs.flexprice.io/api-reference/tax-rates/update-a-tax-rate
/api-reference/openapi.json put /taxes/rates/{id}
Use when changing a tax rate (e.g. rate value or name). Request body contains the fields to update.
# Get billing usage for the current tenant
Source: https://docs.flexprice.io/api-reference/tenants/get-billing-usage-for-the-current-tenant
/api-reference/openapi.json get /tenant/billing
Use when showing the current tenant's billing usage (e.g. admin billing page or usage caps). Returns subscription and usage for the tenant.
# Get tenant by ID
Source: https://docs.flexprice.io/api-reference/tenants/get-tenant-by-id
/api-reference/openapi.json get /tenants/{id}
Get tenant by ID
# Update a tenant
Source: https://docs.flexprice.io/api-reference/tenants/update-a-tenant
/api-reference/openapi.json put /tenants/update
Use when changing tenant details (e.g. name or billing info). Request body contains the fields to update.
# Create user or service account
Source: https://docs.flexprice.io/api-reference/users/create-user-or-service-account
/api-reference/openapi.json post /users
Create a user account (type=user, email required; returns user + password for login) or a service account (type=service_account, roles required) for API/automation access.
# Delete service account
Source: https://docs.flexprice.io/api-reference/users/delete-service-account
/api-reference/openapi.json delete /users/{id}
Soft-delete (archive) a service account by ID.
# Get current user
Source: https://docs.flexprice.io/api-reference/users/get-current-user
/api-reference/openapi.json get /users/me
Use to show the logged-in user's profile in the UI or to check permissions and roles for the current session.
# Query users
Source: https://docs.flexprice.io/api-reference/users/query-users
/api-reference/openapi.json post /users/search
Use when listing or searching service accounts in an admin UI, or when auditing who has API access and which roles they have.
# Update current user
Source: https://docs.flexprice.io/api-reference/users/update-current-user
/api-reference/openapi.json put /users/me
Update the current authenticated user. Supports name and metadata updates.
# Update service account
Source: https://docs.flexprice.io/api-reference/users/update-service-account
/api-reference/openapi.json put /users/{id}
Update a service account by ID (name only).
# Create a new wallet
Source: https://docs.flexprice.io/api-reference/wallets/create-a-new-wallet
/api-reference/openapi.json post /wallets
Use when giving a customer a prepaid or credit balance (e.g. prepaid plans or promotional credits).
# Get Customer Wallets
Source: https://docs.flexprice.io/api-reference/wallets/get-customer-wallets
/api-reference/openapi.json get /customers/wallets
Use when resolving wallets by external customer id or lookup key (e.g. from your app's user id). Supports optional real-time balance and expand.
# Get wallet
Source: https://docs.flexprice.io/api-reference/wallets/get-wallet
/api-reference/openapi.json get /wallets/{id}
Use when you need to load a single wallet (e.g. for a balance or settings view).
# Get wallet balance
Source: https://docs.flexprice.io/api-reference/wallets/get-wallet-balance
/api-reference/openapi.json get /wallets/{id}/balance/real-time
Use when displaying or checking current wallet balance (e.g. before charging or in a portal). Supports optional expand for credits breakdown and from_cache.
# Get wallet transactions
Source: https://docs.flexprice.io/api-reference/wallets/get-wallet-transactions
/api-reference/openapi.json get /wallets/{id}/transactions
Use when showing transaction history for a wallet (e.g. credit/debit log or audit). Returns a paginated list; supports limit, offset, and filters.
# Get wallets by customer ID
Source: https://docs.flexprice.io/api-reference/wallets/get-wallets-by-customer-id
/api-reference/openapi.json get /customers/{id}/wallets
Use when showing a customer's wallets (e.g. balance overview by currency or in a billing portal). Supports optional expand for balance breakdown.
# Query wallet transactions
Source: https://docs.flexprice.io/api-reference/wallets/query-wallet-transactions
/api-reference/openapi.json post /wallets/transactions/search
Use when searching or reporting on wallet transactions (e.g. cross-wallet history or reconciliation). Returns a paginated list; supports filtering by wallet, customer, type, date range.
# Query wallets
Source: https://docs.flexprice.io/api-reference/wallets/query-wallets
/api-reference/openapi.json post /wallets/search
Use when listing or searching wallets (e.g. admin view or reporting). Returns a paginated list; supports filtering by customer and status.
# Terminate a wallet
Source: https://docs.flexprice.io/api-reference/wallets/terminate-a-wallet
/api-reference/openapi.json post /wallets/{id}/terminate
Use when closing a customer wallet (e.g. churn or migration). Closes the wallet and applies remaining balance per policy (refund or forfeit).
# Top up wallet
Source: https://docs.flexprice.io/api-reference/wallets/top-up-wallet
/api-reference/openapi.json post /wallets/{id}/top-up
Use when adding funds to a wallet (e.g. top-up, refund, or manual credit). Supports optional idempotency via reference.
# Update a wallet
Source: https://docs.flexprice.io/api-reference/wallets/update-a-wallet
/api-reference/openapi.json put /wallets/{id}
Use when changing wallet settings (e.g. enabling or updating auto top-up thresholds).
# credit_note.created
Source: https://docs.flexprice.io/api-reference/webhook-events/credit_notecreated
/api-reference/openapi.json post /webhook-events/credit_note.created
Fired when a new credit note is created. Doc-only for parsing.
# credit_note.updated
Source: https://docs.flexprice.io/api-reference/webhook-events/credit_noteupdated
/api-reference/openapi.json post /webhook-events/credit_note.updated
Fired when a credit note is updated. Doc-only for parsing.
# customer.created
Source: https://docs.flexprice.io/api-reference/webhook-events/customercreated
/api-reference/openapi.json post /webhook-events/customer.created
Fired when a new customer is created. Doc-only for parsing.
# customer.deleted
Source: https://docs.flexprice.io/api-reference/webhook-events/customerdeleted
/api-reference/openapi.json post /webhook-events/customer.deleted
Fired when a customer is deleted. Doc-only for parsing.
# customer.updated
Source: https://docs.flexprice.io/api-reference/webhook-events/customerupdated
/api-reference/openapi.json post /webhook-events/customer.updated
Fired when a customer is updated. Doc-only for parsing.
# entitlement.created
Source: https://docs.flexprice.io/api-reference/webhook-events/entitlementcreated
/api-reference/openapi.json post /webhook-events/entitlement.created
Fired when a new entitlement is created. Doc-only for parsing.
# entitlement.deleted
Source: https://docs.flexprice.io/api-reference/webhook-events/entitlementdeleted
/api-reference/openapi.json post /webhook-events/entitlement.deleted
Fired when an entitlement is deleted. Doc-only for parsing.
# entitlement.updated
Source: https://docs.flexprice.io/api-reference/webhook-events/entitlementupdated
/api-reference/openapi.json post /webhook-events/entitlement.updated
Fired when an entitlement is updated. Doc-only for parsing.
# feature.created
Source: https://docs.flexprice.io/api-reference/webhook-events/featurecreated
/api-reference/openapi.json post /webhook-events/feature.created
Fired when a new feature is created. Doc-only for parsing.
# feature.deleted
Source: https://docs.flexprice.io/api-reference/webhook-events/featuredeleted
/api-reference/openapi.json post /webhook-events/feature.deleted
Fired when a feature is deleted. Doc-only for parsing.
# feature.updated
Source: https://docs.flexprice.io/api-reference/webhook-events/featureupdated
/api-reference/openapi.json post /webhook-events/feature.updated
Fired when a feature is updated. Doc-only for parsing.
# feature.wallet_balance.alert
Source: https://docs.flexprice.io/api-reference/webhook-events/featurewallet_balancealert
/api-reference/openapi.json post /webhook-events/feature.wallet_balance.alert
Fired when a feature's associated wallet balance crosses an alert threshold. Doc-only for parsing.
# invoice.communication.triggered
Source: https://docs.flexprice.io/api-reference/webhook-events/invoicecommunicationtriggered
/api-reference/openapi.json post /webhook-events/invoice.communication.triggered
Fired when an invoice communication (e.g. email notification) is triggered. Doc-only for parsing.
# invoice.payment.overdue
Source: https://docs.flexprice.io/api-reference/webhook-events/invoicepaymentoverdue
/api-reference/openapi.json post /webhook-events/invoice.payment.overdue
Fired when an invoice payment is overdue past the due date. Doc-only for parsing.
# invoice.update
Source: https://docs.flexprice.io/api-reference/webhook-events/invoiceupdate
/api-reference/openapi.json post /webhook-events/invoice.update
Fired when an invoice is updated. Doc-only for parsing.
# invoice.update.finalized
Source: https://docs.flexprice.io/api-reference/webhook-events/invoiceupdatefinalized
/api-reference/openapi.json post /webhook-events/invoice.update.finalized
Fired when an invoice is finalized and locked for payment. Doc-only for parsing.
# invoice.update.payment
Source: https://docs.flexprice.io/api-reference/webhook-events/invoiceupdatepayment
/api-reference/openapi.json post /webhook-events/invoice.update.payment
Fired when an invoice payment status changes. Doc-only for parsing.
# invoice.update.voided
Source: https://docs.flexprice.io/api-reference/webhook-events/invoiceupdatevoided
/api-reference/openapi.json post /webhook-events/invoice.update.voided
Fired when an invoice is voided (e.g. order cancelled or duplicate). Doc-only for parsing.
# payment.created
Source: https://docs.flexprice.io/api-reference/webhook-events/paymentcreated
/api-reference/openapi.json post /webhook-events/payment.created
Fired when a new payment is created. Doc-only for parsing.
# payment.failed
Source: https://docs.flexprice.io/api-reference/webhook-events/paymentfailed
/api-reference/openapi.json post /webhook-events/payment.failed
Fired when a payment fails. Doc-only for parsing.
# payment.pending
Source: https://docs.flexprice.io/api-reference/webhook-events/paymentpending
/api-reference/openapi.json post /webhook-events/payment.pending
Fired when a payment is pending processing. Doc-only for parsing.
# payment.success
Source: https://docs.flexprice.io/api-reference/webhook-events/paymentsuccess
/api-reference/openapi.json post /webhook-events/payment.success
Fired when a payment succeeds. Doc-only for parsing.
# payment.updated
Source: https://docs.flexprice.io/api-reference/webhook-events/paymentupdated
/api-reference/openapi.json post /webhook-events/payment.updated
Fired when a payment is updated. Doc-only for parsing.
# subscription.activated
Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptionactivated
/api-reference/openapi.json post /webhook-events/subscription.activated
Fired when a draft subscription is activated. Doc-only for parsing.
# subscription.cancelled
Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptioncancelled
/api-reference/openapi.json post /webhook-events/subscription.cancelled
Fired when a subscription is cancelled (immediately or end-of-period). Doc-only for parsing.
# subscription.created
Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptioncreated
/api-reference/openapi.json post /webhook-events/subscription.created
Fired when a new subscription is created. Doc-only for parsing.
# subscription.draft.created
Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptiondraftcreated
/api-reference/openapi.json post /webhook-events/subscription.draft.created
Fired when a new draft subscription is created (not yet activated). Doc-only for parsing.
# subscription.paused
Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptionpaused
/api-reference/openapi.json post /webhook-events/subscription.paused
Fired when a subscription is paused. Doc-only for parsing.
# subscription.phase.created
Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptionphasecreated
/api-reference/openapi.json post /webhook-events/subscription.phase.created
Fired when a new subscription phase is created. Doc-only for parsing.
# subscription.phase.deleted
Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptionphasedeleted
/api-reference/openapi.json post /webhook-events/subscription.phase.deleted
Fired when a subscription phase is deleted. Doc-only for parsing.
# subscription.phase.updated
Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptionphaseupdated
/api-reference/openapi.json post /webhook-events/subscription.phase.updated
Fired when a subscription phase is updated. Doc-only for parsing.
# subscription.renewal.due
Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptionrenewaldue
/api-reference/openapi.json post /webhook-events/subscription.renewal.due
Fired when a subscription renewal is upcoming (cron-driven). Doc-only for parsing.
# subscription.resumed
Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptionresumed
/api-reference/openapi.json post /webhook-events/subscription.resumed
Fired when a paused subscription is resumed. Doc-only for parsing.
# subscription.updated
Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptionupdated
/api-reference/openapi.json post /webhook-events/subscription.updated
Fired when a subscription is updated (e.g. quantity, billing anchor, or metadata changes). Doc-only for parsing.
# wallet.created
Source: https://docs.flexprice.io/api-reference/webhook-events/walletcreated
/api-reference/openapi.json post /webhook-events/wallet.created
Fired when a new wallet is created. Doc-only for parsing.
# wallet.credit_balance.dropped
Source: https://docs.flexprice.io/api-reference/webhook-events/walletcredit_balancedropped
/api-reference/openapi.json post /webhook-events/wallet.credit_balance.dropped
Fired when a wallet's credit balance drops below an alert threshold. Doc-only for parsing.
# wallet.credit_balance.recovered
Source: https://docs.flexprice.io/api-reference/webhook-events/walletcredit_balancerecovered
/api-reference/openapi.json post /webhook-events/wallet.credit_balance.recovered
Fired when a wallet's credit balance recovers above an alert threshold. Doc-only for parsing.
# wallet.ongoing_balance.dropped
Source: https://docs.flexprice.io/api-reference/webhook-events/walletongoing_balancedropped
/api-reference/openapi.json post /webhook-events/wallet.ongoing_balance.dropped
Fired when a wallet's ongoing balance drops below an alert threshold. Doc-only for parsing.
# wallet.ongoing_balance.recovered
Source: https://docs.flexprice.io/api-reference/webhook-events/walletongoing_balancerecovered
/api-reference/openapi.json post /webhook-events/wallet.ongoing_balance.recovered
Fired when a wallet's ongoing balance recovers above an alert threshold. Doc-only for parsing.
# wallet.terminated
Source: https://docs.flexprice.io/api-reference/webhook-events/walletterminated
/api-reference/openapi.json post /webhook-events/wallet.terminated
Fired when a wallet is terminated. Doc-only for parsing.
# wallet.transaction.created
Source: https://docs.flexprice.io/api-reference/webhook-events/wallettransactioncreated
/api-reference/openapi.json post /webhook-events/wallet.transaction.created
Fired when a new wallet transaction is created (top-up, deduction, etc.). Doc-only for parsing.
# wallet.updated
Source: https://docs.flexprice.io/api-reference/webhook-events/walletupdated
/api-reference/openapi.json post /webhook-events/wallet.updated
Fired when a wallet is updated. Doc-only for parsing.
# Query workflows
Source: https://docs.flexprice.io/api-reference/workflows/query-workflows
/api-reference/openapi.json post /workflows/search
Use when listing or auditing workflow runs (e.g. ops dashboard or debugging). Returns a paginated list; supports filtering by workflow type and status.
# Ingesting AI Usage
Source: https://docs.flexprice.io/docs/ai-cost-tracking/ingesting-ai-usage
A technical look at how Flexprice normalises usage from any AI gateway or provider into a single standardised event, attaches cost, and auto-provisions metering — across real-time push, managed pull, and aggregate sources.
This page extends the [architecture overview](/docs/ai-cost-tracking/overview#architecture) with the mechanics of ingestion: the standardised event every source maps to, the connector types, how cost is attached, and how metering is provisioned for you.
The design goal is **zero manual setup**. You connect a source; Flexprice handles normalisation, pricing, and the creation of meters and features — you do not hand-write transforms, build a model price list, or define meters per model.
## The standardised AI usage event
Every source — a LiteLLM webhook, a Langfuse trace, a Bedrock log, a Databricks usage row — is normalised to one canonical shape before anything else happens. It rides on Flexprice's standard [event](/docs/event-ingestion/overview) (`event_name`, `external_customer_id`, `properties`, `timestamp`, `source`), with a fixed convention for AI properties.
```json theme={null}
{
"event_name": "ai.usage",
"external_customer_id": "team_platform",
"timestamp": "2026-06-14T10:30:00Z",
"source": "litellm",
"properties": {
"provider": "anthropic",
"model": "claude-opus-4-8",
"operation": "chat",
"input_tokens": "1840",
"output_tokens": "320",
"cached_tokens": "1024",
"reasoning_tokens": "0",
"reported_cost": "0.041",
"request_id": "req_abc123",
"raw_user": "u_91",
"raw_team": "team_platform",
"agent_id": "agent_support_bot",
"fidelity": "per_request"
}
}
```
Key fields:
| Field | Purpose |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `provider`, `model`, `operation` | Drive model pricing and let analytics break down by provider/model/token type. |
| `input_tokens`, `output_tokens`, `cached_tokens`, `reasoning_tokens` | Token-level usage; `unit`/`quantity` are used for non-token sources (characters, seconds, requests, credits). |
| `reported_cost` | The source's own computed cost in USD, when it provides one (most gateways do). |
| `raw_user`, `raw_team`, `agent_id`, `tags` | Source identifiers used to resolve the billing entity and hierarchy. |
| `fidelity` | `per_request` or `aggregate` — records the granularity a source can offer, so analytics stays honest. |
Following Flexprice's [event format](/docs/collectors/how-it-works#data-transformation), all `properties` values are sent as strings; Flexprice interprets numeric fields during aggregation.
## Connector types
Sources differ in whether they can push in real time, must be polled, or only expose aggregate totals. Flexprice covers all three with the same downstream pipeline.
| Type | How it works | Best for | Latency |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ---------------- |
| **Push (edge)** | The source POSTs each request to a [Flexprice Collector](/docs/collectors/overview) running in your infra, which transforms and forwards it. | Real-time sources and data that must stay in-network. | Real-time |
| **Managed pull** | Flexprice runs a scheduled job that reads the source over API, SQL, or object storage, transforms server-side, and ingests. | API/SQL/log-reachable SaaS and clouds — **no deployment**. | Seconds to hours |
| **Aggregate** | Flexprice ingests periodic exports (dashboard/email/usage views) and converts native units to cost. | Sources with no per-request API. | Daily |
### Push flow (e.g. LiteLLM)
The collector is the existing [Bento-based](/docs/collectors/how-it-works) pipeline — inputs → processors → outputs — with a ready-made transform for the source, so you are not writing Bloblang by hand.
### Managed pull flow (e.g. Langfuse, Databricks, Bedrock)
You provide read-only credentials once; Flexprice stores them securely and runs the schedule. A per-source **watermark** plus `request_id` **dedupe** guarantees no double counting across overlapping pulls.
## Integrating LiteLLM
LiteLLM is a real-time **push** source: it fires a logging callback on every completion, already carrying token counts, USD cost, model, and identity. There are three ways to wire it into Flexprice — from drop-in to fully custom — and **all three emit the same standardised `ai.usage` event**. You never define meters, features, or prices for this; enabling the LiteLLM connector with a [template](#auto-provisioning-metering) provisions them.
### Option 1 — Flexprice SDK callback (recommended)
Register the Flexprice callback and you're done — no payload mapping, no transform to maintain. Every `completion()` call you already make is metered.
```python theme={null}
import litellm
from flexprice.litellm import FlexpriceLogger
litellm.callbacks = [
FlexpriceLogger(
api_key="",
# which LiteLLM identity becomes the Flexprice billing entity
entity_from="metadata.user_api_key_team_id",
)
]
# nothing else changes — usage now flows to Flexprice on every call
litellm.completion(
model="anthropic/claude-opus-4-8",
messages=[{"role": "user", "content": "Summarise this ticket"}],
metadata={"user_api_key_team_id": "team_platform", "user_api_key_user_id": "u_91"},
)
```
The callback reads LiteLLM's per-request record, maps it to `ai.usage`, and ships it — one event per request, attributed to the team (with the user kept as a child for per-user and per-agent visibility).
### Option 2 — LiteLLM Proxy (no code)
If you run the LiteLLM **Proxy**, enable the Flexprice callback in `config.yaml`. Every request routed through the proxy — from any app, agent, or MCP server — is metered and attributed to the virtual key's team/user, with nothing to deploy in your services.
```yaml theme={null}
litellm_settings:
callbacks: ["flexprice"]
environment_variables:
FLEXPRICE_API_KEY: ""
# map a LiteLLM virtual-key dimension to the billing entity
FLEXPRICE_ENTITY_FROM: "metadata.user_api_key_team_id"
```
When usage must stay inside your network, point LiteLLM's logging webhook at your in-infra [Flexprice Collector](/docs/collectors/overview) instead — the collector applies the same transform before anything leaves your VPC.
### Option 3 — Custom callback (full control)
When you want to shape the event yourself — add tags, override the entity, or filter calls — implement a LiteLLM `CustomLogger` and send the event with the Flexprice SDK. This is exactly what Option 1 does under the hood, exposed for you to customise.
```python theme={null}
from litellm.integrations.custom_logger import CustomLogger
import litellm
from flexprice import Flexprice
fp = Flexprice(api_key="")
class FlexpriceLogger(CustomLogger):
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
# LiteLLM's normalised, per-request usage + cost record
slp = kwargs["standard_logging_object"]
meta = slp.get("metadata", {})
fp.events.ingest(
event_name="ai.usage",
external_customer_id=meta.get("user_api_key_team_id", "unknown"),
source="litellm",
properties={
"provider": slp["custom_llm_provider"],
"model": slp["model"],
"operation": "chat",
"input_tokens": slp["prompt_tokens"],
"output_tokens": slp["completion_tokens"],
"reported_cost": slp["response_cost"], # USD, computed by LiteLLM
"request_id": slp["id"],
"raw_user": meta.get("user_api_key_user_id"),
"raw_team": meta.get("user_api_key_team_id"),
},
)
litellm.callbacks = [FlexpriceLogger()]
```
### The event Flexprice receives
Whichever option you pick, the same canonical event lands — already mapped, ready to be priced and attributed:
```json theme={null}
{
"event_name": "ai.usage",
"external_customer_id": "team_platform",
"source": "litellm",
"properties": {
"provider": "anthropic",
"model": "claude-opus-4-8",
"operation": "chat",
"input_tokens": "1840",
"output_tokens": "320",
"reported_cost": "0.041",
"request_id": "req_abc123",
"raw_user": "u_91",
"raw_team": "team_platform"
}
}
```
If you'd rather not use an SDK at all, the same event can be posted directly to the [events API](/docs/event-ingestion/sending-events):
```bash theme={null}
curl -X POST https://api.cloud.flexprice.io/v1/events \
-H 'x-api-key: ' \
-H 'Content-Type: application/json' \
-d '{
"event_name": "ai.usage",
"external_customer_id": "team_platform",
"source": "litellm",
"properties": {
"provider": "anthropic", "model": "claude-opus-4-8",
"input_tokens": "1840", "output_tokens": "320",
"reported_cost": "0.041", "request_id": "req_abc123"
}
}'
```
### Closing the loop — enforcement
Because Flexprice now holds the source of truth for spend, a budget breach can act **back** on LiteLLM (the dashed return path in the push-flow diagram). On a `warning`/`critical` [alert](/docs/customers/threshold-notifications), Flexprice calls LiteLLM's management API with your master key to throttle the offending key or team:
```bash theme={null}
# soft: zero out the remaining budget on a key
curl -X POST https:///key/update \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/json' \
-d '{ "key": "", "max_budget": 0 }'
# hard: block the key outright
curl -X POST https:///key/block \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/json' \
-d '{ "key": "" }'
```
Start with soft alerts (webhooks/notifications) and graduate to hard enforcement once you trust the numbers.
## How cost is attached
Most modern gateways compute USD cost themselves, so Flexprice uses that by default and re-prices only when it needs to.
* **Trust the source cost (default)** — when `reported_cost` is present (LiteLLM, OpenRouter, Portkey, Langfuse, Helicone, Cloudflare, Vercel), Flexprice stores it as-is. This correctly captures negotiated and BYOK rates.
* **Re-price from the catalog** — when a source provides **tokens only** (Bedrock logs, Databricks token usage) or you want **markup/margin** or a **single normalised price** across gateways, Flexprice prices from its model pricing repository.
The **public model pricing repository is refreshed daily**, so popular provider/model rates are available out of the box. Override any rate per provider, model, or customer for committed pricing, and apply a markup factor when reselling.
## Auto-provisioning metering
Enabling a connector and choosing a **template** creates the metering graph for you — no manual meters, features, or prices.
* **`cost_tracking`** — meters for input/output/cached/reasoning tokens and request count, features per dimension, and catalog pricing at cost. For internal showback.
* **`team_budget`** — the above plus a wallet, a recurring monthly credit grant, and `info`/`warning`/`critical` [alerts](/docs/customers/threshold-notifications) wired to gateway enforcement. For team and agent budgets.
* **`resale_markup`** — catalog re-pricing with a configurable margin, plus margin analytics. For AI features you bill customers for.
Provider and model breakdowns come from filters and `group_by` on the `provider` and `model` properties, so there is no meter-per-model explosion — a handful of generic meters cover every model.
## Identity and hierarchy resolution
Each connector maps source identifiers to a Flexprice billing entity and, optionally, builds the hierarchy:
* A primary field (for example `raw_team`) resolves to the [customer](/docs/customers/overview) being metered.
* Additional fields (`raw_user`, `agent_id`) create child entities under it, using [Customer Hierarchy](/docs/subscriptions/customer-hierarchy) for individual visibility with consolidated rollups and shared wallets.
* Unrecognised identifiers can auto-create entities or map to an existing `external_id`.
This is what lets one wallet span an entire team while you still see each user's and agent's usage separately — and what powers per-agent ROI.
## Source coverage
Phase one deliberately spans every connector type so the model is proven against the hard cases:
| Source | Type | Cost basis | Notes |
| ------------------------- | --------------------------------- | ------------------- | ----------------------------------------------------------------- |
| **LiteLLM** | Push (or hosted webhook) | Source cost | Richest identity (key/user/team/org/tags); real-time. |
| **Langfuse** | Managed pull (observations API) | Source cost | No usage webhook upstream, so polled; near-real-time. |
| **AWS Bedrock** | Managed pull (S3 logs + CUR) | Catalog re-price | Per-request token logs; reconcile \$ against Cost & Usage Report. |
| **Databricks** | Managed pull (system tables, SQL) | Catalog re-price | DBU/token usage joined to list prices; hourly grain. |
| **Salesforce Agentforce** | Aggregate | Catalog (rate card) | Native units (credits/conversations); `fidelity: aggregate`. |
Coverage expands continuously — Helicone, OpenRouter, Portkey, Snowflake Cortex, TrueFoundry, Cloudflare, Vercel, and SAP Joule follow the same connector model. Each new source is a connector definition plus pricing entries, not a change to your setup.
Aggregate sources (such as Agentforce and SAP Joule) report coarse, native-unit totals rather than per-request tokens. Flexprice labels these with `fidelity: aggregate` so dashboards and alerts reflect the real granularity — treat per-agent ROI from these sources as approximate.
## Related
The problem, the solution, and the high-level architecture.
The Bento-based collector used for push and in-infra sources.
The underlying event pipeline AI usage rides on.
Spend thresholds, states, and webhook delivery.
# Overview
Source: https://docs.flexprice.io/docs/ai-cost-tracking/overview
Turn fragmented AI spend across gateways, clouds, and SaaS into one metered, priced, and governed view — internal cost by team, user, and application, plus customer-level cost and margin.
AI Cost Tracking makes Flexprice the single source of truth for AI usage. You stream usage from any AI gateway or provider, Flexprice attaches cost, and you get token-level visibility, spend alerts and limits, wallets, and margin analytics — at every level of your organization and for every customer you bill.
## Problem statement
AI spend is sprawling and hard to attribute. A single company typically consumes models through many disconnected paths at once:
* **Gateways** — LiteLLM, OpenRouter, Portkey
* **Observability layers** — Langfuse, Helicone
* **Clouds and platforms** — AWS Bedrock, Databricks, Snowflake Cortex
* **Packaged SaaS AI** — Salesforce Agentforce, SAP Joule
Each path has its own usage format, its own identifiers, and its own price list. That makes three questions surprisingly hard to answer:
1. **Internal cost** — what is each **team**, **user**, **application**, or **agent** spending on AI, and is it within budget?
2. **Customer cost** — for the AI features you resell, how much does serving each **customer** actually cost?
3. **Margin** — what is your AI **margin by revenue** once provider cost is subtracted from what you charge?
Without a common layer, teams stitch this together by hand — exporting CSVs, reconciling token counts, and guessing at cost. Flexprice replaces that with one ingestion and metering layer.
## Solution
Flexprice acts as a single point of AI usage ingestion. Once usage flows in, everything else is built on top of the same metered data:
* **Feature usage at the customer level** — every request is attributed to a billing entity (customer, team, user, or agent), so usage rolls up cleanly for showback, entitlements, and billing.
* **Total AI cost and token-level visibility** — break spend down by provider, model, and token type (input, output, cached, reasoning) for any entity and any time window.
* **Start from base model pricing** — Flexprice maintains a **public model pricing repository, refreshed daily**, so cost is computed out of the box for popular providers and models with no manual price setup.
* **Or customise costing for negotiated pricing** — override the base catalog with your committed or BYOK rates per provider, model, or customer, and apply markup when you resell.
* **Set spend alerts and limits, and optionally fund wallets with preloaded credits** — configure `info` → `warning` → `critical` thresholds on balances and usage, and give teams or customers a prepaid credit balance to draw down. See [Alerts and Notifications](/docs/customers/threshold-notifications).
* **Model your full org hierarchy** — represent organizations, workspaces, teams, and users with both individual and consolidated limits and rollups, sharing wallets where you need to. See [Customer Hierarchy](/docs/subscriptions/customer-hierarchy).
AI Cost Tracking builds directly on Flexprice's core metering primitives. Usage arrives through the same [event ingestion](/docs/event-ingestion/overview) pipeline, is measured with [features and aggregations](/docs/product-catalogue/features/overview), and is governed with [wallets](/docs/wallet/prepaid-and-promotional) and [alerts](/docs/customers/threshold-notifications) — nothing new to learn if you already use Flexprice.
## Architecture
At a high level, third-party sources send usage into Flexprice through one of two ingestion paths, and Flexprice turns that usage into cost, governance, and analytics.
Two ingestion paths cover every source:
* **Flexprice Collector** — a lightweight, [Bento](/docs/collectors/overview)-based collector you can run in your own infrastructure. Best for **real-time push** sources (such as LiteLLM webhooks) and for data that must stay inside your network.
* **Managed pull** — Flexprice runs scheduled, credential-based pulls for sources reachable by API, SQL, or object storage (such as Langfuse, Databricks, Snowflake, and Bedrock logs). You paste credentials; there is **nothing to deploy**.
Either way, every source is normalised to the same standardised AI usage event, so metering, pricing, alerts, and analytics work identically regardless of where usage originated. The next page, [Ingesting AI Usage](/docs/ai-cost-tracking/ingesting-ai-usage), covers this in depth.
### Communication back to your systems
Cost tracking is not only inbound. When a budget threshold is crossed, Flexprice can push signals **back** to your systems so limits are actually enforced:
* **Webhooks** — every alert state change (`info` → `warning` → `critical`) is delivered as a webhook, so your automation can react however you choose. See [Alerts and Notifications](/docs/customers/threshold-notifications).
* **Native enforcement at the gateway** — for gateways that expose programmatic key controls, Flexprice can act on a breach directly:
* **LiteLLM** — using your LiteLLM master key, Flexprice can call the proxy's management API to lower or zero a key/team budget, or block a key, for a true hard cutoff. (LiteLLM also emits its own budget alerts to Slack and webhooks independently.)
* **OpenRouter** — Flexprice can use the Provisioning API to set a per-key credit limit or disable a key.
* **Portkey** — budgets are enforced at the gateway but configured in the Portkey dashboard, so enforcement there is alert-driven rather than API-driven.
Start with **soft** alerting (webhooks and notifications) to build confidence, then graduate the same thresholds to **hard** enforcement at the gateway once you trust the numbers.
The standardised event, connector types, and per-source mechanics.
Configure spend thresholds and webhook delivery.
Model orgs, teams, and users with shared and consolidated limits.
Fund teams or customers with prepaid credit balances.
# Build Credit-based Pricing like Apollo.io
Source: https://docs.flexprice.io/docs/boiler-plate/credit-based-pricing
Credit-based pricing is a usage-based model where customers prepay for a set number of credits and spend them as they use features, with each action deducting a defined credit amount.
## Overview
Apollo.io uses a credit-based pricing model where users spend credits to access contact data (like emails, phone numbers, and exports), with higher plans offering more monthly credits. This guide shows how to build the same model in Flexprice.
## Pricing Breakdown
## 1. Key Chargeable Features in Apollo
In this model, credits are deducted when the user does an action (e.g. unlock email, export). In Flexprice you will map each action to a credit cost and debit the wallet.
### A. Contact Data Unlock & Enrichment
#### How Credits Are Applied
**Prepaid allocation:** When a user subscribes, they receive credits up to the plan limit. You configure this as recurring grants on the plan. Plans can be billed annually with **recurring grants** (e.g. credits per month).
* **Professional plan → 48,000 credits/year** (or e.g. 4,000/month as recurring grants)
* **Basic → 30,000 credits/year** (or e.g. 2,500/month as recurring grants)
**Example:** For a Basic plan billed annually, give 2,500 credits per month via recurring grants. When you create your plan, mirror these as recurring grants.
**What you’ll do:** Assign a credit cost to each action and debit that amount from the wallet when the user does it.
| Action | Credit Cost |
| --------------------- | ------------------- |
| Unlock verified email | 1 credit |
| Unlock mobile number | 5–10 credits |
| Export contact | 1 credit |
| API enrichment | 1 credit per record |
### B. Email Sending & AI Outreach Credits
#### How Credits Are Applied
**Email Sending Model:** Depending on plan, choose one: **daily send limits per mailbox** or **credits per email sent**. When you set up features, create a metered feature (e.g. Emails Sent) and set the credit cost per use.
| Action | Credit Cost |
| ---------------------------- | ------------------- |
| Send 1 email | 1 credit |
| AI-generated personalization | 1 additional credit |
| Email verification | 1 credit per email |
## 2. Apollo-Style Credit Model
### Key Behaviors
Define a credit cost for each action (e.g. 1 for search, 5 for export) in your app or feature config.
Let users purchase credits (packages or add-ons) by topping up their wallet.
If balance is zero, block the action and prompt the user to buy more credits or upgrade.
### Example: Credit Cost per Action
| Action | Credit Cost |
| ---------------- | ----------- |
| Contact Search | 1 credit |
| Contact Export | 5 credits |
| Email Enrichment | 2 credits |
| Phone Reveal | 10 credits |
### Example: Credit Packages (Prepaid)
| Plan | Credits | Price |
| ------- | ------- | ----- |
| Starter | 1,000 | \$29 |
| Growth | 5,000 | \$99 |
| Pro | 20,000 | \$299 |
**You decide:** Credits can reset monthly, never expire, or expire after N days. Configure this in your plan or billing logic.
**To charge overage:** In Flexprice, configure overage on the plan or charge (e.g. 1.00 USD per extra credit). Usage above the included grant is then billed automatically.
***
## 3. When to Use Credit-Based Pricing
Use credit-based pricing when each action has a different cost (e.g. 1 credit to unlock an email vs 10 credits to reveal a phone number).
Use it when usage varies a lot per customer and a fixed plan doesn’t fit.
Use it when you want flexible consumption and optional add-ons or top-ups.
***
## 4. Complete Flow Summary
**High level, you will:**
Create **metered features** for each action that costs credits. Link each feature to plans via **entitlements**; there you set which features each plan gets and optional free usage (e.g. 50 free, then deduct from credits). See [Linking features to plans](/docs/product-catalogue/features/linking-to-plans).
Create a **Plan** with credit packages or usage charges (e.g. annually billed with recurring credit grants per month).
Create a **Customer** (with External ID for your system).
For **plan-based credits:** Add Subscription → choose the plan. The customer gets the plan’s credit grants and feature access. For **direct purchase only**, skip this and top up the wallet in the next step.
***
## Flexprice Set-up
To implement Apollo-style credit-based pricing in Flexprice, follow these steps in order.
Navigate to **Flexprice Dashboard** → **Product Catalog** → **Features**
For each action that costs credits (e.g. Contact Export, Email Enrichment): click **Create Feature**, choose **Metered**, and set **Event Name** and **Aggregation**. Later you’ll link these to the plan and set entitlements (e.g. 50 free, then deduct from credits).
For more, see [Creating a feature](/docs/product-catalogue/features/create) and [Linking features to plans](/docs/product-catalogue/features/linking-to-plans).
Navigate to **Flexprice Dashboard** → **Product Catalog** → **Plans**
**Do this:** (1) Click **Add** and enter plan name and billing (e.g. Basic, annually). (2) Add charges that provide credits (e.g. recurring credit grant per month) or link metered features with pricing. (3) In **Entitlements**, link features so subscribers get the right access and optional free usage. When a customer is assigned this plan, they receive the credit grants you configured (e.g. monthly). For more, see [Creating a Plan](/docs/product-catalogue/plans/create) and [Linking features to plans](/docs/product-catalogue/features/linking-to-plans).
**Do this:** Open the customer → **Add Subscription** → select your plan. **Result:** The customer gets the plan’s credit grants (e.g. monthly) and access to the features you linked to that plan (including any free usage you set).
For **direct purchase only**, skip this step and top up the wallet in Step 6 (Top Up the Wallet). For more, see [Plans](/docs/product-catalogue/plans/create) and [Linking features to plans](/docs/product-catalogue/features/linking-to-plans).
Navigate to **Flexprice Dashboard** → **Customer Management** → **Customers**
**Enter at least:** Customer Name (e.g. `Flex Corporation`). Add **External ID** if you use it for API lookups.
For details, see [Customer Management](/docs/customers/create).
Before allowing the user to perform a credit-consuming action, call the **Get Wallet Balance** API (e.g. with customer identifier or wallet ID). If balance is available and sufficient for the action cost, proceed. If not, do not proceed—block the action or prompt the user to top up, then leave.
On the customer’s **Wallet** tab, click **Top-up Wallet**.
* Choose the credit type that matches your Price Unit (**Custom** → Credits).
* Enter the amount in credits (e.g. 1,000).
* Complete the top-up (e.g. **Add Credits** or link to payment).
**Use this step for:** First-time credit purchase or add-on packs.
For more, see [Top-up wallet](/docs/wallet/top-up) and [Auto top-up](/docs/wallet/auto-top-up).
Each time a user does an action (search, export, enrichment), do the following in your app or backend:
**Check entitlement:** Does this customer’s plan allow this feature? (You set this when [linking features to plans](/docs/product-catalogue/features/linking-to-plans).) If not, block the action.
**Look up credit cost:** Use your mapping (e.g. the tables in **Key Chargeable Features in Apollo** above) and get the cost for this action.
**Check balance:** Call the Flexprice API or open the dashboard and read the customer's wallet balance.
If **balance ≥ cost**, call the Flexprice API to **debit** the wallet by that amount, then perform the action.
If **balance \< cost**, return an error or prompt to purchase more credits.
**Debit via API:** Call the wallet debit endpoint with the customer identifier (e.g. External ID) and the amount in credits.
**Debit via Dashboard:** **Customer** → **Wallet** → **Manual Debit**; enter the amount in credits and reason (e.g. “Contact export – 5 credits”).
For more, see [Manual Debit](/docs/wallet/manual-debit).
***
## Next Steps
**After setup, do this:**
Map actions to credit costs in your application (e.g. 1 credit per search, 5 per export) and call the debit API (or Manual Debit) after each action.
**Optional:** Set low-balance alerts; see [Low balance alert](/docs/product-catalogue/features/wallet-balance-alert).
Check that invoices show credit top-ups and usage correctly in base currency.
Add more plans with credit packages (recurring or one-time) using the same Price Unit; see [Custom pricing use cases](/docs/product-catalogue/custom-pricing/use-cases).
For custom pricing concepts and API details, see [Custom Pricing](/docs/product-catalogue/custom-pricing/pricing-unit) and the [API Reference](/api-reference/introduction).
# Build Hybrid Pricing like Resend
Source: https://docs.flexprice.io/docs/boiler-plate/hybrid-pricing
**Hybrid pricing** combines fixed **subscription** fees (recurring base charge) with usage-based metering (pay-per-use overages), plus **entitlements** that gate features and set limits per plan.
## Overview
Resend does this with tiers (Free, Pro, Scale, Enterprise): each tier has a recurring fee, usage charges (emails sent or contacts stored), and feature gating (retention, domains, support, analytics). In Flexprice you use one plan with recurring + usage-based charges and features linked via entitlements.
## Pricing Breakdown
## 1. Key Chargeable Features in Resend
Resend charges by **usage** (emails sent or contacts stored) and **gates features by plan** (data retention, domains, support, analytics, dedicated IP). Two tracks run in parallel; this cookbook uses the **Transactional** track for the Flexprice setup.
### A. Transactional Emails
**How pricing is applied:** You are charged by **emails sent per month**. Plans scale by volume; unit cost drops at higher tiers. Each tier also defines how many domains you can use, data retention for logs, support channel, and whether dedicated IP is available.
**Pricing (volume tiers):**
* **Transactional Scale:** 100,000 emails = $90 → $0.90 per 1,000 emails
* **Transactional Scale:** 1,000,000 emails/month = $650 → $0.65 per 1,000 emails
* **Transactional Pro:** e.g. \$20/mo for 50k emails (common entry point)
| Tier | Example volume | Effective rate |
| ----- | -------------- | ----------------- |
| Pro | 50k emails | \$0.90/1k overage |
| Scale | 100k emails | \$0.90/1k |
| Scale | 1M emails | \$0.65/1k |
Transactional emails are API-triggered (password resets, verification, order confirmations). In Flexprice you model this as a **metered feature** (e.g. Emails Sent) with event name and aggregation.
### B. Marketing Emails
**How pricing is applied:** You are charged by **contacts stored** (not by emails sent). Sends can be unlimited; cost scales with contact list size. Tiers control number of audiences, analytics, and dedicated IP (e.g. included on Enterprise for Marketing).
| Dimension | Unit | Notes |
| ----------- | ----------- | -------------------- |
| Contacts | count | Billed by list size |
| Emails sent | unlimited\* | \*Within plan/limits |
Marketing use cases include newsletters, onboarding campaigns, and promotions. For Flexprice, add a second metered feature (e.g. **Contacts Stored**) with its own event name if you model the Marketing track.
***
## 2. Subscription and Entitlement Model
Plan tiers (Free, Pro, Scale, Enterprise) control both **volume/price** and **which features** the customer can use. Features and limits vary by tier:
| Feature | Free | Pro | Scale | Enterprise |
| ---------------- | ------ | ------ | -------------- | --------------- |
| Data retention | 1 day | 3 days | 7 days | Custom |
| Domains (Tx) | 1 | 10 | 1,000 | Flexible |
| Support | Ticket | Ticket | Slack + Ticket | Priority |
| Analytics (Mktg) | No | Yes | Yes | Yes |
| Dedicated IP | — | Add-on | Add-on | Included (Mktg) |
In Flexprice you control this by **linking features to plans** (entitlements): set included quantity, usage reset, and soft or hard limits for metered features; grant or restrict boolean/static features per plan. For more, see [Linking features to plans](/docs/product-catalogue/features/linking-to-plans).
***
## 3. Overage and Upgrade Logic
Resend does **not** automatically charge for excess usage. When you exceed your monthly quota, you are notified and prompted to upgrade; if you repeatedly exceed limits without upgrading, Resend may temporarily pause sending.
In Flexprice you can choose **soft limit** (overage allowed, with or without extra charge) or **hard limit** (strict cap). Configure limits and overage per plan when [linking features to plans](/docs/product-catalogue/features/linking-to-plans).
***
## Concepts and Terms
* **Subscription:** A customer attached to a plan. The subscription determines billing and which entitlements the customer has.
* **Plan:** A set of pricing rules: **recurring charges** (flat fee per billing period) and optionally **usage-based charges** (metered). A plan that has both is **hybrid** in Flexprice.
* **Feature:** Something you track or gate—**Metered** (usage, e.g. emails sent), **Boolean** (on/off access), or **Static** (fixed attribute).
* **Entitlement:** A feature linked to a plan with access rules: for metered features, included quantity, reset period, and soft or hard limit; for boolean/static, grant or restrict.
**Entitlements are free.**
For more detail, check-out [Linking features to plans](/docs/product-catalogue/features/linking-to-plans) and [Plans overview](/docs/product-catalogue/plans/overview).
## Complete Flow Summary
The end-to-end flow for hybrid (subscription + entitlement) pricing in Flexprice:
```mermaid theme={null}
flowchart LR
Features --> Plan
Plan --> Entitlements
Plan --> Subscription
Customer --> Subscription
Subscription --> Events
Events --> Invoices
```
Define the usage dimensions (e.g. Emails Sent) as **metered features** with event names and aggregation.
Create a **plan** (e.g. Transactional Pro). Add a **recurring charge** (e.g. \$20/month) and **usage-based charge(s)** for the metered feature(s) (e.g. included 50k emails, then overage or tiered pricing).
**Link features to the plan.** Set included quantity, usage reset (e.g. monthly), and soft or hard limit for metered features. Add boolean/static features if you gate capabilities by tier.
Create a **customer** (name, External ID) in Customer Management.
**Add subscription** for the customer and select the plan. Start date determines billing and when entitlements apply.
Send **usage events** that match the metered feature event names. Usage is billed per the plan’s usage-based charges and respects entitlements. Invoices are generated per billing cycle.
## Flexprice Set-up
To implement Resend-style hybrid pricing in Flexprice (Transactional track), follow these steps in order.
Navigate to **Flexprice Dashboard** → **Product Catalog** → **Features**
Click **Create Feature** (or **Add**) to create a metered feature. Enter **Name** and **Lookup Key**; the Lookup Key is immutable and unique.
**Feature 1: Emails Sent (Transactional)**
* **Feature Name**: `Emails Sent`
* **Feature Lookup Key**: `emails_sent` (immutable, unique)
* **Feature Type**: `Metered`
* **Event Name**: e.g. `emails.sent` (must match exactly when sending events)
* **Aggregation**: `Count` (e.g. over `Sum` or `emails`)
* **Unit**: `email` or `emails`
> **Note:** The Event Name must match exactly when you send usage events via API. Save it for later use.
For a second track (Marketing), add a feature such as **Contacts Stored** with its own event name and aggregation. This guide uses one metered feature: **Emails Sent**.
For more, check-out [Creating a feature](/docs/product-catalogue/features/create).
Navigate to **Flexprice Dashboard** → **Product Catalog** → **Plans**
Click **Add** at the top-right to create a new plan. Enter plan details, then click **Next** to configure charges.
**Plan details:**
* **Plan Name**: `Pro`
* **Lookup Key**: e.g. `plan-pro` (Auto-generated)
* **Billing period**: e.g. Monthly
**Add hybrid charges:**
1. **Recurring charge:** Add a **recurring** charge (e.g. \$20/month). This is the base subscription fee for the tier. Click **Add** to save.
2. **Usage-based charge:** Add a **usage-based** charge for the metered feature **Emails Sent**. Set pricing (e.g. flat fee per email, or package/tiered with included quantity and overage). Example: first 50,000 emails included, then \$0.2 per 1,000 emails. Click **Add**, then **Save** to finalize the plan.
For more, check-out [Creating a Plan](/docs/product-catalogue/plans/create) and [Plans pricing](/docs/product-catalogue/plans/pricing) (recurring and hybrid charges).
Open the plan you created → **Entitlements** section. Click **Add** to link features to the plan.
**Link the metered feature** (Emails Sent) to the plan:
* Set **included quantity** (e.g. 50,000 emails per month) or leave uncapped.
* Set **usage reset** (e.g. monthly) so limits reset each billing period.
* Choose **soft limit** (overage allowed, with billing per plan) or **hard limit** (strict cap).
If you created **boolean or static features** (e.g. Analytics, Slack support), add them to the plan and set grant/restrict so higher tiers get access. Click **Save**.
For more, check-out [Linking features to plans](/docs/product-catalogue/features/linking-to-plans).
Navigate to **Flexprice Dashboard** → **Customer Management** → **Customers**
Click **Add Customer**. Enter **Customer Name** and **External ID** (your system’s identifier; use this in APIs for usage and lookups).
**Required:**
* **Customer Name**: e.g. `flexprice organisation`
* **External ID**: e.g. `cust-flexprice-organisation`
Click **Save** to create the customer. For more, check-out [Customer Management](/docs/customers/create).
Open the customer you created, then click **Add Subscription**.
* **Select plan**: Choose **Transactional Pro** (or the plan you created).
* **Start date**: Set subscription start (billing and entitlements apply from this date).
Click **Save** (or **Add Subscription**). The customer now has the plan’s recurring charge and entitlement to the linked features and usage limits. For more, check-out [Create Subscription](/docs/subscriptions/customers-create-subscription).
Send **usage events** to Flexprice with the same **event name** as the metered feature (e.g. `emails.sent`). Usage is aggregated and billed according to the plan’s usage-based charges and respects the entitlements (included quantity, soft/hard limit).
* **Verify events:** Use the Event Debugger or usage views to confirm events are received and attributed to the customer.
* **Invoices:** Generated at the end of each billing cycle; they include the recurring fee and usage-based line items.
For more, check-out [Connecting to Billing](/docs/event-ingestion/connecting-to-billing) and your API reference for sending usage events.
## Next Steps
* **Map your app to Flexprice:** Emit usage events that match the metered feature event names (e.g. `emails.sent`) and associate them with the customer (e.g. via External ID).
* **Configure overage and limits:** Use [Linking features to plans](/docs/product-catalogue/features/linking-to-plans) to set soft vs hard limits and overage pricing per plan.
* **Add more plans:** Create Free, Scale, and Enterprise plans with different recurring fees, usage tiers, and entitlements (e.g. analytics, retention) to mirror Resend’s tiers.
For event ingestion and billing flow, check-out [Connecting to Billing](/docs/event-ingestion/connecting-to-billing). For plan and charge configuration, check-out [Creating a Plan](/docs/product-catalogue/plans/create) and [Linking features to plans](/docs/product-catalogue/features/linking-to-plans).
# Build Usage-based Pricing like Vapi.ai
Source: https://docs.flexprice.io/docs/boiler-plate/usage-based-pricing
Vapi is a developer platform for building, testing, and deploying realistic, human-like voice AI agents.
## Overview
Vapi uses a modular, usage-based pricing model where you pay for distinct components.
## Pricing Breakdown
## 1. Key Chargeable Features in Vapi
* Vapi charges customers primarily for **hosting and orchestration**, not for the underlying AI or telecom providers directly.
### A. Hosting Fee for Call Minutes
It charges a **fixed hosting fee per call minute.**
**Pricing (Pay As You Go):**
* **\$0.05 per minute**
***
### B. Hosting Cost for SMS / Chat Messages
It also charges a **fixed hosting fee per message.**
**Pricing (Pay As You Go):**
* **\$0.005 per message**
***
## 2. Fixed Fees vs Usage-Based Charges
Pricing consists of **fixed per-unit fees**, not bundled plans or feature-based tiers.
### Fixed Fees (Platform Hosting Costs)
These fees are **fixed per unit of usage**:
| Feature | Pricing Model | Cost |
| ------------------ | ------------- | ------------- |
| Call Hosting | Per minute | \$0.05 / min |
| SMS / Chat Hosting | Per message | \$0.005 / msg |
These fees remain the same regardless of scale (Pay As You Go).
***
### Usage-Based Scaling
| Usage Dimension | Pay As You Go | Enterprise |
| ----------------- | --------------------------------- | ------------ |
| Call Minutes | Usage based | Custom |
| SMS / Chat Volume | Usage based | Volume based |
| Call Concurrency | 10 included + \$10 / line / month | Custom |
***
## 3. Call Concurrency Pricing
Call concurrency determines **how many simultaneous calls** your system can handle.
### Pay As You Go
* **10 concurrent calls included**
* **\$10 per additional line per month**
### Enterprise
* Custom concurrency limits
* Volume-based pricing
* SLA-backed infrastructure
***
## 4. Enterprise Pricing Model
The **Enterprise plan** operates under an **annual contract** with custom pricing.
**Enterprise features include:**
* Custom call minute pricing
* Volume-based SMS and chat pricing
* Priority support and SLAs
> Enterprise pricing is negotiated directly with the Vapi sales team.
## Complete Flow Summary
The complete subscription flow for Vapi usage-based pricing:
1. Create Features (Call Minutes, SMS/Chat)
↓
2. Create Plans (Pay As You Go with usage charges)
↓
3. Create Customer (with External ID)
↓
4. Create Subscription (link customer to plan)
↓
5. Send Usage Events (via API)
↓
6. Verify Events (using Event Debugger)
↓
7. Billing (automatic at end of billing cycle)
↓
8. Review Invoices (Customer Info)
## Flexprice Set-up
To configure Vapi's usage-based pricing in Flexprice, follow these steps in order:
Navigate to **Flexprice Dashboard** → **Product Catalog** → **Features**
Click **"Create Feature"** to start creating features. You need to create two metered features:
**Feature 1: SMS / Chat Hosting**
* **Feature Name**: `SMS / Chat Hosting`
* **Feature Lookup\_key**: `sms_chat_hosting` (immutable, unique identifier)
* **Feature Type**: `Metered`
* **Event Name**: `sms.chat.message` (must match exactly when sending events)
* **Aggregation Method**: `Sum`
* **Aggregation Field**: `messages`
* **Unit**: `message` or `msg`
* **Description**: `Vapi hosting cost for SMS and chat messages - covers message delivery, session handling, and conversational state management`
> **Note**: The Event Name must match exactly when you send usage events via API. Save these event names for later use.
For comprehensive information on creating features, see [Features](/docs/product-catalogue/features/create).
Navigate to **Flexprice Dashboard** → **Product Catalog** → **Plans**
Click **"Add"** at the top-right to create a new plan.
**Create Pay As You Go Plan:**
**Step 2.1: Plan Details**
* **Plan Name**: `Pay As You Go`
* **Lookup Key**: `pay_as_you_go` (immutable, unique identifier)
* **Plan Description**: `Usage-based pricing plan with flat fees for call minutes and SMS/chat messages`
* Click **Next** to proceed to charge configuration
**Step 2.2: Define Usage-Based Charges**
**Charge 1: SMS / Chat Hosting**
* **Select Metered Feature**: `SMS / Chat Hosting` (sms\_chat\_hosting)
* **Billing Currency**: `USD`
* **Billing Period**: `Monthly`
* **Billing Model**: `Flat Fee`
* **Price per Unit**: `0.005` (per message)
* **Billing Timing**: `Arrears Billing` (bill at end of cycle)
* Click **Add** to save this charge
Click **Save** to finalize the Pay As You Go plan.
For comprehensive information on creating plans, see [Creating a Plan](/docs/product-catalogue/plans/create).
Navigate to **Flexprice Dashboard** → **Customer Management** → **Customers**
Click **"Add Customer"** to create a new customer.
**Required Fields:**
* **Customer Name**: `Acme Corporation` (or your customer's name)
* **External ID**: `cust_acme_001` (unique identifier from your system)
**Optional Fields:**
* **Email**: Customer's email address
* **Phone**: Customer's phone number
* **Address**: Customer's billing address
> **Important**: The External ID is what you'll use when sending usage events. Make sure it matches exactly.
Click **Save** to create the customer.
For comprehensive information on creating customers, see [Customer Management](/docs/customers/create).
Navigate to **Flexprice Dashboard** → **Customer Management** → **Customers**
Click on the customer you created to open their details page.
Click **"Add Subscription"** button.
**Subscription Configuration:**
* **Select Plan**: Choose `Pay As You Go` from the dropdown
* **Subscription Start Date**: Select the start date for billing (e.g., today's date)
* **Subscription End Date**: Leave as "Forever" for ongoing subscriptions, or set a specific end date
* **Billing Cycle**: This determines when billing cycles start (usually the start date)
The subscription preview will show:
* All recurring charges (if any)
* Usage-based charges that will be billed based on consumption
* Estimated costs for the billing period
Click **Add Subscription** to save.
The subscription will now appear under the customer's details with status "Active".
For comprehensive information on creating subscriptions, see [Create Subscription](/docs/subscriptions/customers-create-subscription).
Once a customer has an active subscription, you can start sending usage events to Flexprice via API.
**API Endpoint:**
```
POST https://api.cloud.flexprice.io/v1/events
```
**Authentication:**
Include your API key in the request header:
```
x-api-key:
```
**Example 1: Send SMS/Chat Message Event**
```bash theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/events \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"event_name": "sms.chat.message",
"external_customer_id": "cust_acme_001",
"properties": {
"messages": 1
},
"event_id": "evt_sms_67890",
"timestamp": "2025-01-15T10:35:00.000Z",
"source": "vapi-platform"
}'
```
**Example 2: Bulk Events (Multiple Events at Once)**
```bash theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/events/bulk \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"events": [
{
"event_name": "call.minutes",
"external_customer_id": "cust_acme_001",
"properties": { "minutes": 10 },
"source": "vapi-platform"
},
{
"event_name": "sms.chat.message",
"external_customer_id": "cust_acme_001",
"properties": { "messages": 3 },
"source": "vapi-platform"
}
]
}'
```
**Event Payload Fields:**
* **event\_name** (required): Must match your feature's Event Name exactly (`call.minutes` or `sms.chat.message`)
* **external\_customer\_id** (required): Must match the customer's External ID (`cust_acme_001`)
* **properties** (required): Contains the aggregation field value
* For Call Minutes: `{"minutes": }`
* For SMS/Chat: `{"messages": }`
* **event\_id** (optional): Your unique identifier for the event
* **timestamp** (optional): ISO 8601 UTC timestamp. If omitted, server time is used
* **source** (optional): Origin of the event (e.g., "vapi-platform", "api")
**Response:**
```json theme={null}
{
"event_id": "event_01K389J4M1F1NZG6XP0AMD6J52",
"message": "Event accepted for processing"
}
```
For comprehensive information on sending events, see [Sending Events](/docs/event-ingestion/sending-events).
After sending usage events, verify they are being processed correctly using the Event Debugger.
**Navigate to Event Debugger:**
1. Go to **Flexprice Dashboard** → **Usage Tracking** → **Event Debugger**
**View Events:**
The Event Debugger displays a real-time stream of events with:
* **Event ID**: Unique identifier for each event
* **Event Name**: The feature being tracked (`call.minutes` or `sms.chat.message`)
* **Customer Lookup Key**: Customer's External ID
* **Source**: Origin of the event
* **Timestamp**: When the event occurred
**View Event Details:**
Click on any event to view the complete payload:
```json theme={null}
{
"id": "event_01",
"external_customer_id": "cust_acme_001",
"event_name": "call.minutes",
"timestamp": "2025-01-15T10:30:00.000Z",
"properties": {
"minutes": 5.5
},
"source": "vapi-platform"
}
```
**Filter Events:**
Use filters to find specific events:
* **Filter by Event Name**: `event_name contains "call.minutes"`
* **Filter by Customer**: `external_customer_id contains "cust_acme_001"`
* **Filter by Time Range**: Set Start Time and End Time
* **Filter by Source**: `source contains "vapi-platform"`
**Verify Event Processing:**
1. Check that events appear in the Event Debugger shortly after sending
2. Verify the event payload matches what you sent
3. Confirm the `external_customer_id` matches your customer
4. Ensure `event_name` matches your feature configuration exactly
5. Check that `properties` contain the correct aggregation field values
For comprehensive information on event debugging, see [Event Debugger](/docs/event-ingestion/event-debugger) and [Validating Events](/docs/event-ingestion/validating-events).
## Next Steps
After completing the setup:
1. **Monitor Usage**: Regularly check the Event Debugger to ensure events are being processed
2. **Review Invoices**: Check upcoming invoices to verify billing calculations
3. **Track Usage**: Use the Query tool to view aggregated usage by customer and feature
4. **Scale**: Add more customers and subscriptions as your business grows
For troubleshooting, see [Troubleshooting Events](/docs/event-ingestion/troubleshooting).
# Changelog
Source: https://docs.flexprice.io/docs/changelog
What we’ve been shipping at Flexprice lately
## Service accounts
Service accounts are now first-class users — create them, give them a display name, update them, and archive them through dedicated endpoints, with API keys scoped to the owning service account for clean audit trails.
* **Update endpoint**: New `PUT /v1/users/{id}` lets you rename a service account without touching its API keys
* **Soft-delete endpoint**: New `DELETE /v1/users/{id}` archives a service account — blocked while it still has active API keys to prevent orphaned credentials
* **User association on secrets**: API keys now record the `user_id` of the service account that owns them and can be filtered by user and expiry
* **`super_admin` role**: New built-in RBAC role with wildcard permissions for tenant-wide administrative access
* **Dashboard support**: Manage service accounts end-to-end from the dashboard — create, edit, and revoke from the settings area
## Customer hierarchy safeguards
Subscription inheritance now enforces a strict one-parent-per-child rule across both create and modify flows — preventing the same customer from being inherited under two different parents.
* **Standalone child rejected**: Attempting to inherit a customer that already has a standalone subscription returns a validation error instead of silently creating conflicting hierarchies
* **Different-parent inheritance blocked**: A child already inherited under one parent cannot be re-inherited under a different parent
* **Same-parent re-inheritance allowed**: Re-running inheritance for an existing parent–child pair continues to work for adding new subscriptions to the relationship
## Signup metadata
User signup now accepts a `metadata` object — attach attribution, referral source, or any custom signup context at account creation time and access it later through the user profile.
* **Signup form**: The dashboard signup flow captures structured metadata and forwards it to the auth API
* **API field**: The signup request accepts a `metadata` map alongside the existing identity fields
**Other changes**
* Event debugger now surfaces an `attributed-to-customer` step in the `meter_usage` pipeline view — making it easier to see which customer an event was attributed to and why
* Commitment time bucket editor adds validation and refined configuration controls for setting up multi-window commitments from the dashboard
* Subscription status filter expanded to include `DRAFT` and `TRIALING` states alongside published and active filters
* Optional display `name` field added to users — set it on create, edit it later, and see it returned in user responses
* Wallet credit operations now return `409 Conflict` on duplicate idempotency keys instead of `500 Internal Server Error`
* Subscription inheritance no longer admits child customers with parent-type subscriptions, preventing nested parent relationships
* New `PUT /v1/users/{id}` endpoint to update a service account's name
* New `DELETE /v1/users/{id}` endpoint to soft-delete a service account; blocked while active API keys exist
* `metadata` field accepted on the user signup request
* `breakdown_bucket` flag added to the meter usage detailed analytics request to opt into per-bucket breakdowns in the response
* Secret list filters now support `user_id` and `not_expired_at` for scoping API key queries to a specific service account or expiry window
* `name` field added to the user create, update, and response payloads
## Commitment time buckets
Windowed commitments can now specify time-of-day billing windows — define multiple time buckets with start and end times to apply different commitment rates during peak and off-peak hours.
* **Time bucket editor**: `CommitmentTimeBucketsEditor` component lets you add, configure, and reorder time buckets directly from the `CommitmentConfigDialog`
* **Hour and minute precision**: Bucket granularity adapts to the meter's bucket size — hour-only for coarse meters, hour+minute for fine-grained meters
* **Validation and normalization**: Buckets are validated for completeness and overlap before saving; normalization strips the minute component when the meter doesn't support it
* **Price override storage**: Time bucket configuration is stored on the price override, not as separate line items
## Wallet enhancements
Wallets gain two new capabilities: prepaid wallets can be converted to postpaid mode via API, and debit transactions now record which credit transactions were consumed.
* **Wallet modification**: New `POST /v1/wallets/{id}/modify` endpoint accepts `modification_type: "prepaid_to_postpaid"` to convert a prepaid wallet to postpaid — original and new wallet state are returned in the response
* **Consumed credits tracking**: Debit transactions now store the IDs of all credit transactions consumed during the debit in `metadata.consumed_credit_tx_ids` — enabling full credit audit trails
* **Conversion rate applied**: Wallet debit operations now apply the configured conversion rate when calculating the credit amount
## Backdated subscription cancellation
Immediate cancellations now accept a `cancel_at` timestamp in the past to record a backdated effective date — useful for correcting cancellation records after the fact.
* **Past `cancel_at` on immediate cancellation**: Pass a past timestamp with `cancellation_type: immediate` to set a backdated effective cancellation date
* **Future timestamps rejected**: Passing a future `cancel_at` with immediate cancellation returns a validation error directing you to use `scheduled_date` cancellation instead
* **Scheduled date unchanged**: Forward-dated cancellations continue to require `cancellation_type: scheduled_date` and reject past timestamps
## `subscription.cancelled` event
The `subscription.cancelled` system event is now published whenever a subscription ends — covering immediate cancellation, scheduled-date cancellation, and period-end termination.
* **Cascade propagation**: When a parent subscription is cancelled, the event is also published for each inherited child subscription
* **Temporal activity support**: `PublishSubscriptionEvent` is exposed on the subscription service interface so Temporal billing-period activities can fire lifecycle events
## Paddle inbound invoice sync
Paddle webhook processing now handles inbound invoice state changes — invoice updates arriving from Paddle are reflected back into Flexprice without waiting for the next outbound sync cycle.
* **Inbound update handling**: `PullAndUpdateInvoice` is wired as the inbound path in Paddle's sync service; the sync type toggle enables or disables inbound processing per connection
* **Child subscription guard**: Paddle subscription sync workflow no longer fires on child subscriptions to prevent duplicate checkout URL generation
**Other changes**
* Go runtime upgraded to 1.25.0
* Meter usage consumer is now lazy — initialized on first use rather than at startup, reducing cold-start pressure
* Subscription billing scheduler interval reduced from 15 minutes to 2 minutes; stale webhook scheduler avoids system events older than 48 hours and prioritizes newly scheduled events
* Environment management: new `add-environment` script creates an environment for an existing tenant and wires in-memory cache
* Invoice pull sync cron configured; \$0 invoices are skipped from outbound sync
* Meter usage queries now support `group_by` on properties
* Helm chart: `onboarding_events` config block now rendered so the Kafka topic value is non-empty
* Paddle webhook handler now returns an error response on processing failure so Paddle retries the delivery
* Tax rate filter in subscription creation now excludes archived rates — only published rates are shown (frontend)
* Cancelled subscription meter usage benchmark fixed to exclude post-cancellation usage windows
* Meter usage `group_by feature_id` aggregation corrected
* `POST /v1/wallets/{id}/modify` endpoint added — accepts `modification_type` for wallet mode conversion
* Swagger documentation updated with new integration sync endpoints
## OTel tracing migration
Tracing is now OpenTelemetry-native across the entire backend — all span helpers (`StartDBSpan`, `StartClickHouseSpan`, `StartRepositorySpan`, `StartMonitoringSpan`) keep their call-site API but are backed by `go.opentelemetry.io/otel`. Traces route to SigNoz or any OTLP backend; Sentry is retained only for error capture.
* **OTel spans**: All existing span helpers now emit OTel spans — no call-site changes required
* **SigNoz routing**: The unified `otel:` config block exposes per-signal endpoints and headers so traces and logs can target different backends independently
* **Trace-log correlation**: `logger.WithContext` injects `trace_id` and `span_id` into every log line for cross-signal correlation
* **Temporal interceptor**: Rewritten to use the OTel tracer for activity spans; Sentry scope binding preserved for error attribution
* **Config migration**: Legacy `logging.otel_*` keys remain valid with a deprecation warning; the new `otel:` block is the canonical path
## White-label SDK pipeline
A new CI pipeline automates generation and publication of white-label SDKs for partner deployments — covering Go, TypeScript, Python, and MCP with per-partner branding applied at build time.
* **`generate-wl-sdks.yml`**: GitHub Actions workflow triggers on `sdk-v*` tags and manual dispatch; builds all four SDK flavors and publishes to partner repositories
* **Config templates**: Per-partner `gen-config` files parameterized with `WL_*` environment variables for name, module path, and author metadata
* **Branding scripts**: `apply-wl-custom-branding.sh` replaces all Flexprice identifiers in source and docs; `verify-sdk-builds.sh` confirms each SDK compiles after substitution
* **Template drift detection**: `check-wl-templates` Makefile target and a CI check fail if the canonical SDK drifts from the white-label template baseline
## GCP Managed Kafka
Kafka now supports SASL/OAUTHBEARER authentication for GCP Managed Kafka — no username or password required. Workload Identity handles token acquisition on GKE; Application Default Credentials are used locally.
* **OAUTHBEARER mechanism**: Set `saslMechanism: OAUTHBEARER` in the Kafka config; tokens are sourced via `google.DefaultTokenSource` and cached with `oauth2.ReuseTokenSource` to avoid metadata server round-trips on each broker connect
* **Existing SCRAM/PLAIN unchanged**: Sarama config output is byte-identical for existing configurations — no migration required
## Invoice pull sync
Integrations now support a `pull` sync method — trigger a sync with `method: "pull"` to fetch and update an invoice's current status directly from the payment provider.
* **`PullAndUpdateInvoice`**: Paddle fetches the latest invoice state and applies it back to Flexprice; all other integrations return a clear unsupported error
* **Factory wiring**: `IntegrationFactory.SetServices` injects `PaymentService` and `InvoiceService` post-construction to break the service dependency cycle
**Other changes**
* Meter usage pipeline: wallet balance, cost analytics, and usage export now query the meter usage store — reducing direct ClickHouse event queries in these paths
* User model gains a JSONB `metadata` field with full CRUD support via the user update API
* Subscription metadata is now persisted on subscription create and update
* JWT middleware now extracts `environment_id` from JWT claims before falling back to the request header
* Trial subscription end dates can now be modified, and trialing subscriptions can be cancelled with a backdated `cancel_at` effective date
* Revenue dashboard now shows a currency filter populated from available invoice summaries (frontend)
* Price sync: parent price ID now sends empty string instead of nil, preventing null dereference during plan-change price linking
* White-label branding scripts hardened — `sed` substitutions properly quoted; standard builds guarded against accidental branding replacement
* New integration sync service exposes `method: "pull"` and `method: "push"` for on-demand provider sync
* Subscription create and response DTOs now include the `metadata` field
* User update API now accepts and returns `metadata` key-value pairs
## Whop integration
Flexprice invoices can now be synced to Whop — creating hosted invoices or automatically charging customers with a saved payment method, with two-way reconciliation via Whop webhooks.
* **Invoice sync**: Finalized Flexprice invoices are pushed to Whop as one-time invoices, with checkout URLs stored back on the invoice metadata for customer-facing payment links
* **Automatic charge collection**: When a customer has a saved Whop payment method, invoices are charged automatically instead of sending a hosted checkout link
* **Two-way payment reconciliation**: The `invoice.paid` webhook marks the corresponding Flexprice invoice as paid; the `payment.succeeded` webhook creates a customer→member mapping that enables automatic charging on future invoices
* **Product auto-provisioning**: A Whop product is created automatically if one isn't already configured on the connection, with the product ID persisted back to the connection config
* **Dashboard UI**: Connect and configure Whop from the integrations page — set API key, company ID, and enable invoice sync; copy your unique webhook URL directly from the drawer
## Paddle subscription sync
Paddle-connected subscriptions now trigger a Temporal sync workflow that bootstraps a Paddle subscription for card capture — enabling the checkout URL flow without requiring a paid transaction.
* **Bootstrap workflow**: `PaddleSubscriptionSyncWorkflow` creates a \$0 Paddle transaction at subscription creation, stores the checkout URL in subscription metadata, and syncs all line item prices to Paddle catalog products
* **`subscription.activated` webhook**: Paddle's activation webhook is now handled — Flexprice activates subscriptions in an incomplete state when Paddle signals the subscription as active
* **Customer metadata sync**: `paddle_customer_id` is written to the Flexprice customer metadata record on first sync, enabling lookup without an additional API call
* **Pending invoice resync**: After a `subscription.activated` event, any pending Flexprice invoices are resynced via `PaddleInvoiceSyncWorkflow` to ensure payment status is up to date
## Wallet auto-topup invoice deduplication
Auto-topup now guards against duplicate invoices — a pending unpaid topup invoice blocks a new topup from being triggered, and the alert re-fires automatically once the existing invoice is resolved.
* **`WALLET_AUTO_TOPUP` billing reason**: Invoices created by auto-topup are now tagged with a dedicated billing reason, enabling accurate filtering and deduplication
* **Pending invoice guard**: Before triggering a new topup in invoiced mode, `hasPendingAutoTopupInvoice` checks for an existing finalized, unpaid auto-topup invoice — and skips if one is found
* **Re-trigger on payment**: When a purchased credit payment completes, the wallet balance alert is re-published so `triggerAutoTopup` can fire a fresh invoice for the next topup cycle
## Subscription plan price out-of-sync flag
The subscription v2 API response now includes a `plan_prices_out_of_sync` flag indicating whether the subscription's line items are behind the plan's current price configuration.
* **Out-of-sync detection**: Compares the subscription's `synced_price_sequence` against the plan's current max sequence — returns `true` when plan-price changes have not yet been reconciled into the subscription's line items
**Other changes**
* Zoho line item descriptions now include the child customer name for clearer invoice attribution in multi-tenant setups
* Zoho customer fetch logic moved from billing service to Zoho service for better separation of concerns
* Zoho customer lookup now filters by `PUBLISHED` status to exclude inactive records
* Price sync creates new subscriptions using the latest price sequence, avoiding stale sequence issues after plan updates
* Paddle webhook signature verification hardened to handle both snake\_case and camelCase event payload formats
* Paddle invoice resync after `subscription.activated` replaced goroutine+sleep with `PaddleInvoiceSyncWorkflow` for reliable, retryable execution
* Paddle checkout URL now falls back to connection config when Paddle omits it from the transaction response
* Paddle `ListTransactions` filtered by `origin=subscription_charge` to exclude unrelated transactions
* Entity integration mapping update now merges metadata instead of replacing it, preserving existing fields on update
* Wallet auto-topup invoice guard payment status filter corrected to include failed and initiated statuses alongside processing
* Meter usage window size points aggregation corrected
* Meter usage zero-events line item commitment now handled correctly without producing incorrect charges
* New `POST /webhooks/whop/{tenant_id}/{env_id}` endpoint for inbound Whop webhook events — handles `invoice.paid` and `payment.succeeded` event types
* `plan_prices_out_of_sync` boolean field added to the subscription v2 response
## Auto invoice threshold billing
Set `auto_invoice_threshold` on any usage-based subscription and Flexprice will automatically generate a mid-period invoice whenever accumulated usage charges cross that amount — no manual intervention required.
* **`auto_invoice_threshold` field**: Currency-denominated decimal on subscription creation and modification; must be zero or greater; rejected for inherited subscriptions
* **Threshold billing workflow**: `AutoInvoiceThresholdBillingWorkflow` runs on a 5-minute Temporal schedule, scans qualifying subscriptions, and fires mid-period invoices tagged with the `THRESHOLD_BILLING` billing reason
* **Validation guards**: Only valid for standalone subscriptions attached to usage-based plans; the API rejects threshold configuration on inherited or delegated-invoicing subscriptions
* **Dashboard support**: Set `auto_invoice_threshold` during subscription creation; the current threshold value is shown on the subscription detail page
## White-label dashboard & localization
The Flexprice dashboard now supports full white-label customization and RTL localization — deploy it under your own brand with your logo, colors, and language without code changes.
* **White-label branding**: Configure logo, tagline, landing page colors, and auth page layout per deployment via config — no frontend code changes required
* **RTL layout**: The entire dashboard adapts to right-to-left locales — sidebar, drawers, breadcrumbs, tables, and all directional elements reorient automatically
* **Locale switching**: Users can switch their display language at runtime; the selection persists across sessions and the layout direction updates immediately
## Zoho Books item price sync
Flexprice prices are now synced to Zoho Books as catalog items and item prices before invoice sync, ensuring Zoho always reflects the correct pricing for every line item.
* **`ItemSyncService`**: Normalizes and upserts item prices in Zoho Books' format before attaching them to invoice lines — eliminates mismatches between Flexprice prices and Zoho catalog entries
* **`TaxService`**: Dedicated Zoho tax service injected into the invoice sync pipeline for structured tax handling
* **OAuth scopes**: Zoho OAuth scopes expanded with `ZohoBooks.settings.READ` and item management permissions for full catalog access
## Helm chart v1.0.0 GA
The Flexprice Kubernetes Helm chart reaches v1.0.0 GA with production defaults for high availability, data protection, and frontend scalability, and is now published to GHCR.
* **Frontend as first-class workload**: HPA and PDB added for the frontend deployment; multi-AZ topology spread constraints enabled by default for all workloads
* **Data protection**: In-cluster Postgres, Redis, Kafka, and ClickHouse StatefulSets ship with keep annotations on by default — data survives chart uninstall
* **Redis authentication**: Readiness probe now authenticates via `REDISCLI_AUTH` for clusters with password auth enabled
* **GHCR publishing**: Chart published to GitHub Container Registry on `chart-v*` tags and manual workflow dispatch
**Other changes**
* Redis client updated to use `ClusterClient` for improved connection management and topology awareness
* Price sync now initializes with the latest price sequence to prevent stale sequence issues after plan updates
* Invoice `issue_date` now takes priority over `finalized_at` when computing the PDF issuance date
* Error codes throughout the codebase replaced with typed `ErrorCode` constants for improved type safety
* Commitment true-up: new `GetDistinctCustomerIDsWithCommitmentTrueUp` query for targeted true-up processing
* `monthsBetween` now correctly handles end-of-month boundary dates (e.g., Jan 31 → Feb 28)
* Nil check added for `ExportMetadataFields` in scheduled task validation to prevent panics
* Voided and deleted invoices excluded from invoice idempotency checks
* `auto_invoice_threshold` field added to subscription create and modify DTOs
* `issue_date` field added to invoice create and update requests — overrides the user-facing invoice date
## Grouped invoicing
A new `grouped_invoicing` subscription type lets a parent subscription collect usage from multiple child subscriptions and consolidate them into a single clubbed invoice on the parent's billing cycle.
* **`grouped_invoicing` type**: New subscription type alongside `STANDALONE`, `PARENT`, `INHERITED`, and `DELEGATED_INVOICING` — the parent owns billing and the children contribute usage
* **Bulk conversion at creation**: Pass `subscriptions_ids_for_grouped_invoicing` in `SubscriptionInheritanceConfig` to convert existing standalone subscriptions to `grouped_invoicing` children under a new parent in a single API call
* **Runtime membership management**: Add or remove grouped invoicing members via the subscription modification API using `grouped_invoicing_add` and `grouped_invoicing_remove` change types
* **Clubbed invoice preview**: The parent subscription invoice preview endpoint returns a merged view of all child line items, letting you inspect consolidated charges before finalization
* **Standalone promotion**: An existing standalone subscription can be promoted to a grouped invoicing parent without data loss
## Trial start invoice
When a subscription activates into a trial, Flexprice now automatically creates a \$0 invoice — enabling card capture via the payment processor's checkout flow without charging the customer.
* **`SUBSCRIPTION_TRIAL_START` billing reason**: New billing reason triggers a \$0 invoice at trial activation; billing period spans `trial_start` → `trial_end`
* **`ZeroOutAmounts()`**: Zeros all monetary amounts on the invoice request while preserving line item structure — used exclusively for trial-start invoices
* **Collection-method-aware payment status**: Manual collection marks the trial invoice as PAID immediately; automatic collection generates a checkout URL for card capture
* **`SKIPPED` invoice status**: Zero-dollar invoices that require no payment action are marked `SKIPPED` — no invoice number assigned, no finalization, no vendor sync
**Other changes**
* Usage analytics export now includes customer name alongside external customer ID in report output
* `AppPrefetcher` component prefetches queries on active environment load; `WebhookDashboard` now lazy-loaded for faster initial render
* Export drawer supports dynamic metadata field columns with per-field validation
* Environment display name can now be updated from the dashboard settings
* Invoice line item table: null amounts handled gracefully; filtered data sorted correctly
* `mergeAnalyticsData`: fixed nil-check base selection and `PriceResponses` merge from child accounts
* Sum-with-multiplier precision corrected for event values with more than 6 decimal digits
* Inherited subscription guard restored on nil-inheritance create path to prevent invalid subscription states
* `grouped_invoicing_add` and `grouped_invoicing_remove` change types added to the subscription modification API
* `subscriptions_ids_for_grouped_invoicing` added to subscription creation DTO under `SubscriptionInheritanceConfig`
* `SUBSCRIPTION_TRIAL_START` billing reason added to the invoice billing reason taxonomy
## Scheduled subscription cancellation
Subscriptions can now be cancelled at a specific future date — set `cancel_at` on the cancel request and the subscription stays active until that date, then terminates automatically.
* **`cancel_at` parameter**: Pass a future timestamp to the cancel endpoint to schedule termination at a precise point in time
* **Validation**: The API enforces that `cancel_at` must be strictly in the future, returning a clear error if the date has already passed
* **Existing modes preserved**: Immediate and end-of-period cancellation continue to work unchanged alongside scheduled cancellation
## Metadata filtering
Customers and plans can now be filtered by arbitrary metadata key-value pairs — useful for multi-tenant setups, account segmentation, and custom tagging.
* **JSONB GIN indexes**: Metadata columns are now indexed with PostgreSQL GIN indexes for performant key-value lookups at scale
* **Customer and plan APIs**: Both the customer list and plan list endpoints accept `metadata` key-value filters via a shared `MetadataFilter` struct
* **Dashboard query builder**: The filter popover now supports metadata key-value pair inputs with a dedicated UI component
## Subscription line item search
A new search endpoint lets you query subscription line items across subscriptions with full filtering and pagination support.
* **Search endpoint**: `POST /v1/subscriptions/lineitems/search` accepts a `SubscriptionLineItemFilter` body with status, price, and date filters
* **Pagination**: Results are paginated and sortable — no need to fetch the entire subscription to inspect individual line items
* **Dashboard UI**: The subscription edit charges section now uses this endpoint for filtering and browsing line items directly
## Usage analytics export
Usage analytics data can now be exported to CSV from both the API and the dashboard — covering per-customer, per-meter breakdowns for any billing period.
* **Export API**: New endpoint returns a CSV of usage analytics including external customer IDs, meter breakdowns, and metadata fields
* **Metadata columns**: Credit usage reports now include additional metadata fields configured on the export, with nil-safe handling for missing external IDs
* **Dashboard UI**: Usage analytics pages now expose a download button for CSV exports
## Webhook retry job configuration
The Temporal-based stale webhook retry job is now fully configurable via server config — enabling per-deployment tuning without code changes.
* **Kill switch**: Set `enabled: false` to disable the retry job entirely for a deployment
* **Rate limiting**: `rate_limit` (default: 5/s) caps delivery throughput within each cron run using a token-bucket limiter
* **Tenant exclusions**: `excluded_tenants` skips specific tenant IDs from retry processing
* **Event-type allowlist**: `allowed_event_types` restricts retries to specific webhook event names — empty list retries all types
* **Max attempts**: `max_attempts` (default: 5) replaces the hardcoded failure-count threshold in the retry query
**Other changes**
* Inherited subscription usage now includes child account events in the usage pipeline — parent subscriptions aggregate usage from all attached child customers
* Billing service methods refactored to accept structured DTO inputs with validation, replacing positional argument chains
* Subscription billing schedule decommissioned and replaced with the unified `SubscriptionBilling` Temporal schedule
* Environment name can now be updated from the settings page
* System event domain repository added for structured event querying in the webhook pipeline
* Analytics merge now correctly aggregates `PriceResponses` from child accounts when computing parent subscription usage
* Metadata filter nil-check corrected in in-memory store to use map lookup instead of index-0 access
* Customer ID filtering optimized in `GetUsage` and `matchesBaseFilters` for the in-memory event store
* Subscription future date restriction removed — subscriptions can now be created with a `start_date` in the future without a validation error
* New `POST /v1/subscriptions/lineitems/search` endpoint for filtering and paginating subscription line items
* `cancel_at` parameter added to `CancelSubscriptionRequest` for scheduled future cancellation
* Customer list API now accepts `metadata` key-value filters via `MetadataFilter` in the request body
* Usage analytics export endpoint added with support for external customer ID resolution and partial-count logging
## Subscription trial periods
Subscriptions can now be created with a trial period — pass `trial_period_days` on creation and Flexprice automatically sets the trial window, then transitions to paid billing when the trial ends.
* **`trial_period_days` on create**: Pass a non-negative integer to set the trial length; if omitted, the value is inherited from the plan's recurring fixed prices (must be uniform across prices)
* **`SUBSCRIPTION_TRIAL_START`**: When a **`trialing`** subscription is finalized on create (non-draft), Flexprice emits a **`$0`** invoice with **`SUBSCRIPTION_TRIAL_START`** — preview line items for the trial window; real charges still begin with **`SUBSCRIPTION_TRIAL_END`**
* **Temporal trial end workflow**: A Temporal workflow handles trial expiry — updates subscription status, computes the first invoice, and activates billing at the correct period boundary
* **Inherited subscription cascading**: Trial end processing propagates to child subscriptions when the parent trial expires
* **Validation**: `trial_period_days` and explicit `trial_start`/`trial_end` are mutually exclusive — the API returns a clear error if both are provided
## Temporal-native scheduling
Cron jobs are now managed as Temporal schedules — replacing the legacy cron HTTP endpoints with durable, observable, and pauseable Temporal workflows.
* **Temporal schedules**: All recurring billing jobs are now registered as Temporal schedules at worker startup
* **Legacy endpoints removed**: The deprecated cron trigger HTTP endpoints have been removed from the API surface
* **Subscription billing schedule**: The `SubscriptionBillingPeriods` legacy schedule is replaced by the unified `SubscriptionBilling` Temporal schedule
## Add-on lifecycle controls
Add-on subscriptions now support fine-grained lifecycle configuration — set a custom billing cadence, proration behavior, and effective end date when attaching an add-on to a subscription.
* **Billing cadence**: Choose the billing frequency for the add-on independently of the base subscription cadence
* **Proration behavior**: Select whether mid-cycle add-on changes are prorated or charged at full price
* **Effective end date**: Set an explicit termination date for the add-on without cancelling the base subscription
* **Dashboard UI**: The add-on dialog now exposes these controls under an advanced options section
**Other changes**
* Future subscription start dates are now allowed — the restriction requiring `start_date` to be in the past or present has been removed
* Invoice pagination added to the customer invoice tab — large invoice lists no longer load all at once
* Add-on subscription query cache is invalidated on add or cancel, keeping the subscription detail page in sync
* Event names are now trimmed of leading/trailing whitespace at ingestion and query time for consistent matching
* Subscription pause and resume endpoints removed — use cancellation or trial periods instead
* Billing anchor adjusted for anniversary-cycle subscriptions to prevent unintended prorated charges on the first invoice
* Date handling corrected for add-on line items when the subscription entity type resolves effective dates
* Inherited subscription invoicing customer logic corrected — child subscriptions now invoice to the correct parent customer
* `trial_period_days` parameter added to `POST /v1/subscriptions` — sets the trial window at subscription creation
* `proration_behavior` field added to subscription line item create and modify requests
* Pause and resume subscription endpoints removed from the API
## Billing anchor
Anniversary-cycle subscriptions now support a custom billing anchor date — override the default subscription start date to align billing renewals to a specific day of the month.
* **`billing_anchor` parameter**: Pass a timestamp on subscription creation when `billing_cycle` is `anniversary` to pin the billing renewal date to that day
* **Validation**: The API enforces that `billing_anchor` is only accepted for anniversary billing cycles — passing it with other cycle types returns a validation error
* **Next billing date**: The first billing period end is computed from the anchor date, so proration on the initial period reflects the actual anchor offset
## Backdated credit grant catch-up
Credit grants now retroactively apply to subscriptions created after the grant's start date — a catch-up loop issues the missed credit grant applications for all elapsed billing periods.
* **Catch-up loop**: When a subscription is created and a credit grant already has elapsed periods, the system issues one `CreditGrantApplication` per missed period in sequence
* **Inherited subscriptions**: The catch-up logic mirrors across inherited child subscriptions, keeping credit entitlements consistent across the parent-child billing hierarchy
* **EE parity**: The enterprise edition credit grant service runs the same catch-up path, ensuring consistent behavior across deployment tiers
## Invoice downloads
Invoices can now be downloaded as CSV or PDF directly from the customer portal and the customer invoice tab in the dashboard.
* **Format picker**: A dialog lets users choose between PDF and CSV before initiating the download
* **Customer portal**: The invoice download button in the customer-facing portal now surfaces both formats with distinct icons
* **Sorting**: Customer invoices can be sorted by billing period start date, making it easier to locate invoices for a specific period
## Webhook manual retry
Failed webhook deliveries can now be retried manually via a new retry endpoint — useful for re-sending events after a destination outage without waiting for the automatic retry job.
* **Retry endpoint**: `POST /v1/webhooks/{id}/retry` re-queues a failed webhook event for immediate redelivery
* **Svix and native support**: The retry logic works across both the Svix-managed and native webhook delivery backends
**Other changes**
* OTel trace attributes now include a `region` field for region-aware observability in multi-region deployments
* Environment list API now returns only `PUBLISHED` environments by default
* Stale branch and stale PR GitHub Actions added to automate repository hygiene — branches older than 2 months and PRs idle for 15 days are flagged automatically
* Wallet top-up conversion rate display improved — conversion rates are now shown clearly in the wallet UI
* Credit grant tab on the plan detail page now filters to `PUBLISHED` grants only
* Stripe invoice `due_date` is now always set when using `send_invoice` collection mode — previously missing in some flows
* Webhook publisher nil-guard added for `systemEventRepo` to prevent panics on deployments without the system event store wired
* Coupon percentage type now correctly excludes currency from the payload — fixing validation errors on currency-agnostic percentage coupons
* Invoice nil pointer dereference in `IsFinalizationDue` resolved
* `POST /v1/webhooks/{id}/retry` endpoint added for manual webhook redelivery
* `billing_anchor` parameter added to `POST /v1/subscriptions` for anniversary billing cycle anchor date override
## Environment cloning
Duplicate an entire environment — all published features and plans are cloned into a new target environment via a Temporal workflow, preserving entitlement relationships and configuration.
* **Temporal workflow**: `EnvironmentCloneWorkflow` runs two sequential activities — features are cloned first (so plan entitlements resolve correctly), then plans are cloned with ID remapping
* **Cross-env ID mapping**: Source entity IDs are mapped to their cloned counterparts in the target environment, keeping references intact
* **Dashboard support**: Environment cloning is accessible from the environment management UI — select a source and target environment to kick off the clone
## One-time charges
Prices can now be configured as one-time charges using `billing_period: ONETIME` — a cleaner model that separates cadence from charge type and integrates directly into the subscription billing pipeline.
* **`BILLING_PERIOD_ONETIME`**: One-time charges are now identified by `billing_period` rather than `billing_cadence`, with the cadence defaulting to `RECURRING` — a data migration normalizes existing records
* **Auto-inclusion**: Subscriptions automatically include `ONETIME` prices during billing cycle evaluation — no explicit line item attachment required
* **Period ordering**: The period comparison utilities now handle `ONETIME` correctly for sorting and validation alongside standard billing periods
* **Validation**: The minimum billing period count check no longer applies to one-time prices
## AI-assisted pricing setup - Prompt to Plan
Prompt to Plan introduces a new way to set up billing in Flexprice. Instead of manually configuring plans, prices, meters, and entitlements, you can describe your pricing model in plain English and generate a working configuration.
* **Chat-based setup**: Describe subscription, usage-based, or hybrid pricing models conversationally and get a complete configuration back
* **Full model support**: The workflow covers metered features with overages and limits, credit-based pricing with grants and expiration, add-ons, and entitlements
* **Preview before publishing**: Review the generated setup before pushing it live — nothing is saved until you confirm
* **Templates**: Pre-built templates based on real-world pricing models are available as starting points and can be customized and deployed directly
## Zoho Books integration
Connect Flexprice to Zoho Books for automated invoice synchronization — invoices finalized in Flexprice are pushed to Zoho Books via a Temporal workflow with OAuth-backed authentication.
* **OAuth connection**: Authenticate with Zoho Books via the integrations page — the OAuth flow stores connection metadata and tokens for the tenant
* **Invoice sync workflow**: `ZohoBooksInvoiceSyncWorkflow` runs on invoice finalization and pushes the invoice payload to Zoho Books
* **Connection management**: The integrations UI shows connection status and supports re-authentication
## Mid-cycle subscription modification
Subscriptions can now be modified mid-cycle — change line item quantities with a full proration preview before committing.
* **Proration preview**: Call the modification preview endpoint to see the prorated credit and debit line items before applying the change
* **Quantity change dialog**: The dashboard surfaces a quantity modification dialog on subscription line items with a summary of the expected invoice impact
* **Proration moved outside transaction**: The proration calculation is now performed outside the database transaction to avoid long-held locks during wallet lookups
**Other changes**
* Meter usage pipeline added — a dedicated pipeline processes feature usage events and maps them to meter usage records for downstream billing
* Feature lookup keys can now be auto-generated from the feature name — reducing manual input when creating features
* Customer invoice summary now includes invoices billed to parent customers, giving a complete view of charges for child accounts
* Invoice table in the customer view now shows a `Billing Entity` column and a `Subscription Customer` column for parent-child billing scenarios
* Usage benchmark service added with ClickHouse-backed storage for tracking event processing latency
* Subscription `BillingCadence` now defaults to `RECURRING` in `CreateSubscriptionRequest` validation — prevents misconfigured subscriptions when cadence is omitted
* Meter usage list lookup now cached in-process per event name — reduces ClickHouse round-trips for high-volume ingestion
* Unique hash generation in meter usage fixed to correctly deduplicate cache entries
* New `POST /v1/ai/pricing/parse` endpoint powers the Prompt to Plan chat workflow — accepts pricing descriptions and returns structured plan, price, meter, and entitlement configurations
* New `POST /v1/subscriptions/{id}/modify/preview` endpoint returns a prorated invoice preview before applying a subscription modification
* Environment clone endpoint added to trigger the `EnvironmentCloneWorkflow` for a source/target environment pair
## Inherited subscriptions
Subscriptions now support an inheritance model — a parent subscription can propagate charges, cancellations, and billing configuration to child subscriptions attached to the same customer.
* **Subscription types**: Subscriptions are now typed as `STANDALONE`, `PARENT`, or `INHERITED` — controlling how billing is routed and how cancellations cascade
* **Inherited subscription creation**: Create subscriptions that inherit from a parent, with automatic customer resolution and unified code paths for create and modify operations
* **Cascade cancellation**: Cancelling a parent subscription automatically propagates cancellation to all inherited child subscriptions
* **Validation guards**: The API prevents creating standalone or parent subscriptions on customers that already have an inherited subscription, avoiding conflicting billing hierarchies
* **Dashboard support**: The subscription edit page now shows an inherited subscriptions section for managing child subscriptions from the parent view
## SDK v2.1
The Flexprice Go, Python, and TypeScript SDKs have been updated to v2.1, with major developer experience improvements across error handling, idempotency, and pagination.
* **Error utilities**: New `errorutils` package provides typed helpers — `IsNotFound`, `IsValidation`, `IsConflict` — for clean error handling without string matching
* **Per-request idempotency**: `WithIdempotencyKey` option lets you attach an idempotency key to any mutating request without modifying global config
* **Auto-pagination**: List endpoints now support automatic pagination — iterate over all results without managing page tokens manually
* **Retry configuration**: Read-only `POST` operations (like usage queries and previews) now retry automatically; retry settings are configurable globally or per-request
* **SDK examples**: Comprehensive end-to-end examples added for Go, TypeScript, and Python covering the full subscription and billing lifecycle
## Invoice internal preview
Preview invoice charges before finalization with a new internal preview endpoint — useful for validating billing logic before committing to an invoice.
* **Preview endpoint**: New `POST /v1/invoices/internal/preview` computes a draft invoice view without persisting any state — returns line items, totals, and applied discounts
* **PDF force regeneration**: The invoice PDF URL endpoint now accepts a `force_generate` parameter to regenerate PDFs on demand, bypassing the cached version
## Usage charge arrear constraint
Usage-based charges are now constrained to arrear billing — the API enforces that usage charges cannot be configured for advance billing.
* **Arrear-only enforcement**: Prices with usage charge models are validated at creation and update time to ensure they are always billed in arrear, preventing misconfigured advance billing setups
## Entitlement usage reset period
Plan entitlements can now be configured with a usage reset period directly from the plan builder.
* **Reset period UX**: The plan creation and edit flow now includes a reset period selector for entitlements — configure how frequently usage limits reset (daily, weekly, monthly, etc.) without leaving the dashboard
**Other changes**
* Customer list now defaults to filtering by `PUBLISHED` status — inactive customers are hidden by default with the ability to clear the filter
* Invoice finalization delay reduced from 5 days to 2 hours — integrations and async workflows have a shorter window before invoices are sealed
* Temporal invoice workflows now start with a configurable delay, giving downstream systems time to react before processing begins
* System events enhanced with entity type and entity ID fields, and a new `event_name` column for structured audit log queries
* OTel log level filtering now respected in the pipeline — debug-level noise no longer propagates to exporters configured for higher levels
* Temporal worker concurrency defaults reduced to prevent out-of-memory kills under high load
* API error messages extracted and displayed more accurately throughout the dashboard — network and validation errors now surface actionable text instead of raw response objects
* Revenue dashboard date range filter now includes UTC-inclusive period boundaries — data at period edges was previously excluded
* ClickHouse aggregator queries no longer use the `FINAL` keyword — removes read inconsistencies on tables with frequent merges
* Invoice computation now correctly identifies inherited subscriptions and skips re-computation when appropriate
* Credit purchased invoice payment status flow corrected — status transitions now follow the expected finalization sequence
* Subscription invoice draft idempotency key now includes `billing_reason` — prevents duplicate drafts across different billing triggers within the same period
* Duplicate draft event publishing removed from `CreateInvoice` — downstream consumers no longer receive redundant `invoice.drafted` events
* Invoice PDF generation now handles nil line items without panicking
* New `POST /v1/invoices/internal/preview` endpoint for computing a preview invoice without persisting state
* Subscription type field (`STANDALONE`, `PARENT`, `INHERITED`) added to the subscriptions API with filtering support
* New endpoint for modifying subscription inheritance — add or remove inherited subscriptions from a parent
* `force_generate` parameter added to the invoice PDF URL endpoint for on-demand PDF regeneration
* Go SDK v2.1.0 and v2.1.1 released with error utilities, idempotency key support, auto-pagination, and configurable retry strategy
* TypeScript SDK updated to use `CustomerFromLookup` type for customer resolution responses
## Revenue dashboard
Visualize revenue across your customer base with a new analytics dashboard — filterable by time period with per-customer breakdowns.
* **Time-series graphs**: View total revenue and usage trends over configurable date ranges (this month, this quarter, this year, and prior-period comparisons)
* **Per-customer breakdown**: Each customer row shows total revenue, usage-based revenue, and fixed revenue with drill-down links to customer details
* **Flexible filtering**: Filter by custom date ranges or preset periods — results are sorted by total revenue descending for quick identification of top accounts
## Invoice compute workflow
Trigger invoice computation on demand via API — synchronously for immediate results or asynchronously via Temporal for large invoices.
* **Sync and async modes**: Call `POST /v1/invoices/:id/compute` with `?sync=true` for blocking execution, or default async mode that returns a workflow ID for polling
* **Temporal-backed processing**: Async compute runs as a durable Temporal workflow with automatic retries, 10-minute timeout, and exponential backoff
* **Deferred credits and taxes**: Subscription invoice computation now applies only coupons during draft — credit deductions and tax calculations are deferred to finalization, preventing premature wallet debits
## Scheduled subscription cancellation
Cancel subscriptions on a specific future date instead of only immediately or at period end.
* **Scheduled date cancellation**: New `scheduled_date` cancellation type accepts a `cancel_at` timestamp for precise future cancellation scheduling
* **Guard validations**: The scheduled date must be in the future — the API rejects past dates with a clear validation error
* **Dashboard support**: The subscription cancellation dialog in the dashboard now includes the scheduled cancellation option with a date picker
## Integration events system
Integrations now react to system events in real time — invoice finalization and customer creation automatically trigger vendor sync workflows.
* **Event-driven dispatch**: A dedicated Kafka consumer group listens to `system_events` and routes `invoice.finalized` and `customer.created` events to the appropriate provider sync workflows
* **Integration mapping link**: New `POST /v1/integrations/link` endpoint manually links Flexprice entities to external provider records with provider-specific side effects (e.g., Razorpay customer metadata sync)
* **Configurable rate limits**: Integration event processing supports per-tenant consumer group isolation and configurable rate limits via `integration_events` settings
**Other changes**
* OpenTelemetry observability expanded with a generic exporter, configurable protocol, health-check filtering, and structured OTel log fields
* PostgreSQL → ClickHouse sync added for prices, subscriptions, and subscription line items for analytics workloads
* Wallet balance calculation now excludes future-period draft invoices from pending balance to avoid overstating committed spend
* Default invoice finalization delay increased to 10 minutes — gives integrations and async workflows time to complete before sealing
* Skipped invoices are now hidden from search results and restricted to subscription invoice types only
* Razorpay payment links now enforce a minimum expiration buffer to prevent premature link expiry
* Commitment bucket fill logic capped with `now()` for period end to prevent future-dated empty bucket entries
* Temporal workflow configuration now supports max concurrent workflow limits
* SDK READMEs updated with webhook handling documentation for all supported languages
* Fixed credit deduction timing — credits and taxes on subscription invoices are now deferred to finalization instead of being applied during draft computation
* Fixed silent zero-usage bug — `GetDistinctEventNames` failures now return errors instead of silently zeroing meter usage
* Fixed calendar billing period end calculation for quarterly and half-yearly subscriptions
* Fixed invoice compute API to correctly handle recomputation of skipped and draft invoices
* Removed `final` keyword from ClickHouse event queries to avoid read inconsistencies
* Deprecated parent customer hierarchy removed from the frontend customer views
* New `POST /v1/invoices/:id/compute` endpoint with sync and async modes for on-demand invoice computation
* New `POST /v1/integrations/link` endpoint to manually map Flexprice entities to external provider records
* New `POST /v1/dashboard/revenue-dashboard` endpoint for per-customer revenue analytics with time-series data
* Subscription cancellation API now accepts `scheduled_date` cancellation type with a `cancel_at` field
## Paddle payment integration
Accept payments through Paddle with full checkout, customer sync, and webhook support — no custom code required.
* **Checkout overlay**: Customers complete payments via Paddle's hosted checkout, embedded directly in your billing flow with JWT-signed checkout URLs
* **Bidirectional customer sync**: Customers are automatically synced between Flexprice and Paddle, with deduplication and address management
* **Webhook processing**: Transaction completions, customer creation, and address updates from Paddle are processed automatically with built-in idempotency
* **Enhanced error handling**: Multi-layer idempotency checks on invoice sync prevent duplicate Paddle transactions on retries
## Multi-cadence billing
A single subscription can now contain line items with different billing periods — monthly, quarterly, and annual charges all on one subscription.
* **Mixed billing periods**: Assign different billing cadences (monthly, quarterly, annual) to individual line items within the same subscription
* **Smart invoice inclusion**: Line items are automatically included on the correct invoices based on their cadence and invoice timing (advance or arrear)
* **Flexible cancellation strategies**: Choose between immediate, minimum-period-end, or maximum-period-end cancellation to honor varying commitment lengths
## Bulk event ingestion
Ingest raw events in bulk for backfills, testing, and high-throughput pipelines with a new dedicated API endpoint.
* **Bulk raw event API**: New `POST /v1/events/raw/bulk` endpoint accepts up to 1,000 raw events per request for async processing via Kafka
* **Ingestion filter**: Allowlist specific external customer IDs for live billing while preserving all raw events — ideal for controlled pilot rollouts
* **Sequential usage fetch**: New `GetUsageBulkSync` method provides reliable, sequential meter usage lookups with per-meter timeout protection for billing accuracy
## Draft subscriptions
Create subscriptions in a draft state before activating them, allowing configuration and invoice preview before going live.
* **Draft lifecycle**: Create subscriptions with `DRAFT` status that skip invoice generation and payment processing until explicitly activated
* **Invoice preview**: Preview estimated charges for draft subscriptions before activation
* **Dashboard support**: Draft subscriptions now appear in customer subscription lists and support activation with configurable start dates
## Invoice overpaid tracking
Track and display overpaid amounts when customers pay more than the invoice total.
* **Overpaid amount display**: Invoices now show an `overpaid_amount` field when the payment exceeds the amount due
* **Status visibility**: A new "Overpaid" payment status is displayed in invoice tables and detail views
**Other changes**
* Webhook delivery now uses Kafka for reliable, distributed event processing — in-memory PubSub support has been removed
* Sentry logging enhanced with log-level filtering, unique per-request trace IDs, and health endpoint noise reduction
* Multi-addon entitlement fetching improved to support multiple instances of the same add-on per subscription
* Onboarding experience refreshed with welcome banners, embedded video tutorials, and quick-start guide cards
* MCP scope annotations added to Swagger documentation for better AI-agent interoperability
* Webhook event types now documented in the OpenAPI spec
* Go Docker image bumped to golang:1.24-alpine
* Fixed inactive line item handling for previous billing period calculations
* Invoice idempotency key generation scoped to minute-level precision to prevent duplicate invoices within the same billing cycle
* Add-on line item termination now correctly handled during subscription cancellation
* Webhook secrets are now obscured in API responses for improved security
* Reverted overly strict settings validation that blocked certain configuration reads
* New `POST /v1/events/raw/bulk` endpoint for bulk raw event ingestion
* Subscriptions API now supports listing and filtering by `DRAFT` status
* Event ingestion filter setting added for customer-level allowlisting
* GitHub release step added for Go SDK CI pipeline
## Organization members & dashboard access
Invite teammates to your organization and manage dashboard access from the Flexprice UI.
* Invite and manage team members within your organization directly from the dashboard.
## Feature grouping
Organize features into logical groups for cleaner plan configuration and customer-facing entitlement display.
* **Logical feature groups**: Group related features together for better organization and clarity
**Other changes**
* General UI optimizations for improved performance and consistency
* Enhanced navigation and visual polish across the dashboard
## Settings overhaul
Settings have been restructured for faster navigation and a clearer separation of tenant, billing, and integration configurations.
* **Revamped settings**: Improved organization and discoverability of configuration options
## Custom dashboard theming
Personalize the Flexprice dashboard with your brand colors and visual preferences.
* **Theme customization**: Apply custom colors and branding to match your organization’s identity
**Other changes**
* Sentry spans now include user ID for more actionable error attribution and debugging
## Plan cloning
Duplicate an existing plan with all its prices and configuration as a starting point for a new plan.
* **Clone plans**: Quickly create new plans based on existing configurations
## Multiple billing periods
A single plan can now support multiple billing cadences (monthly, annual, etc.) simultaneously.
* **Flexible billing cadences**: Offer customers different billing period options within the same plan
## Price grouping
Added support for grouping in prices for better organization and display.
* **Group prices**: Organize related prices into logical groups
## Wallet alerts
Real-time wallet balance monitoring with configurable alert thresholds.
* **Real-time balance event processing**: Wallet alert triggers now process all balance events with improved cache handling for accuracy
* **Ongoing balance notifications**: Low-balance alerts are sent continuously as the wallet balance stays below threshold, not just on first breach
* **Alert cache expiration**: Alert cache entries now expire after a configurable max-alive window to prevent stale suppression of alerts
## API cleanup
Simplified the onboarding surface by removing deprecated APIs.
* **Deprecated tenant APIs removed**: Removed deprecated tenant creation APIs to streamline onboarding
**Other changes**
* Plan-level price sync lock implemented with a configurable TTL (default 2 hours) to prevent conflicting concurrent updates
## Parent subscriptions
Group related subscriptions under a parent for consolidated billing and hierarchy management.
* **Subscription hierarchy**: Organize subscriptions with parent-child relationships
## Commitment duration
Define minimum commitment periods on subscriptions to enforce contract terms.
* **Backend support**: Define commitment periods programmatically via API
* **Dashboard configuration**: Configure and view commitment durations directly from the subscription management dashboard
## Multiple instance add-ons
Attach the same add-on multiple times to a single subscription with independent quantities and pricing.
* **Flexible add-on instances**: Support for multiple instances of the same add-on per subscription
## Custom analytics
Build and view custom usage and revenue breakdowns tailored to your business metrics.
* **Custom dashboards**: Create analytics views specific to your business needs
* **Overridden price tracking**: Analytics now reflect overridden prices accurately
**Other changes**
* Temporal workflow history and filters for better observability into background job execution
* VAPI pricing units added to support voice API consumption-based billing
* Edit subscription UI improvements
* Charges search API now supports filtering for more precise query results
## Credit grant editing
Modify the amount, expiry, or metadata of existing credit grants without needing to void and recreate them.
* **Edit existing grants**: Update credit grant details directly without voiding
## Prepaid and postpaid wallets
Choose between prepaid balance top-ups or postpaid usage-based wallet modes per customer.
* **Wallet modes**: Configure wallets as prepaid or postpaid based on customer needs
## Backdated pricing
Create and update prices with past effective dates to accurately reflect historical billing arrangements.
* **Backdated price creation**: Create prices with a past effective date
* **Retroactive price updates**: Update existing prices retroactively to correct or adjust past pricing configurations
## Sentry and observability improvements
We've reduced noise and improved traceability for background workflows.
* **Repository-level span reduction**: Disabled repository-level Sentry span creation to reduce quota usage
* **Temporal integration**: Integrated Sentry into Temporal workflows via a dedicated worker interceptor, improving traceability for background tasks
## Wallet & credit accuracy
Enhanced wallet logic to correctly compute and expose credits available breakdown when retrieving real-time balance.
* **Transaction status verifiction**: Wallet eligibility logic now verifies transaction status is completed before applying credits
* **Workflow optimizations**: Overall wallet workflow optimizations for performance and correctness
## Subscription API enhancements
Added a new API to fetch a subscription by ID with a minimal response and optional expand parameters.
## Pricing & revenue analytics
Pricing validation and analytics capabilities have been extended.
* **Maximum active prices**: Added validation for the maximum number of active prices to prevent configuration errors
* **Currency-segregated revenue trends**: Revenue analytics extended to support currency-segregated revenue trends, recent subscriptions, and invoice payment status reporting
## Add-ons & usage pricing
Add-on line item commitment support added to better model commitment/overage patterns.
* **Usage-based prices within add-ons**: Support for usage-based prices within add-ons was introduced, enabling richer usage billing models
* **Add-on line item commitment**: Support for add-on line item commitment was added
**Other changes**
* Logging cleanup: enhanced content and removed unnecessary entity logs
* Internal workflow changes to improve consistency and reduce noise in observability
* Subscription start date precision normalized to milliseconds for consistency
* The new subscription retrieval API with minimal response and expand parameters provides a leaner query option for clients
## Bulk import and export in the dashboard
You can now import features and prices and run managed S3 exports from the UI. Billing and credit usage reports are available for download.
* **Import features and prices**: Added UI support for importing features and prices, matching backend bulk-import capabilities.
* **S3 exports**: Managed exports and downloadable reports for billing and credit usage
## Dashboard analytics improvements
Revenue trends and customer usage are clearer and more accurate in the dashboard.
* **Currency-segregated revenue**: Revenue trends and invoice payment status now reflect the correct metrics in the UI
* **Customer usage**: Improved visibility into usage at the customer level
## Region and routing stability
The frontend region selector now correctly handles US-region routing, including [us.flexprice.io](http://us.flexprice.io).
**Other changes**
* Improved filtering, sorting, and usability across pricing, feature, and subscription lists
* Enhanced subscription views with clearer overrides and metadata
* Add-on configuration UI improved, supporting commitments and usage-based context
* Query builder supports multi-select "Select All"
* UI polish across revenue charts and subscription tables
* Fixed onboarding issues for organization name and tenant persistence
* Improved loading and error states in analytics views
* Fixed revenue tooltip inconsistencies from partial invoice data
* Improved frontend integration with updated analytics and usage APIs
* Customer usage views backed by existing usage endpoints
## Wallet and credit balance correctness
Wallet balances are now more accurate and transparent. Credits available reflect what customers can actually use, and wallet operations are safe under concurrency.
* **Wallet balances**: Clear `credits available breakdown`, improving transparency and accuracy
* **Top-ups**: Purchased credit top-ups now correctly contribute to usable credit balances
* **Eligibility**: Credit eligibility now strictly applies only to completed transactions
* **Advisory locking**: Wallet operations are protected with advisory locking to reduce race conditions under concurrency
## Add-on pricing flexibility
Add-ons now support usage-based pricing and commit-and-overage billing.
* **Usage-based pricing**: Add usage-based prices to add-ons
* **Add-on line-item commitments**: Introduced to support commit-and-overage billing models
## Caching and performance groundwork
Introduced a Redis cache layer with environment-specific key prefixes to support multi-environment deployments safely.
**Other changes**
* Added validation to prevent exceeding the maximum number of active prices
* Extended feature filters to support `not-contains` queries
* Improved wallet balance performance using the updated balance API path
* Logging noise reduced by removing unnecessary debug and print statements
* Subscription start dates normalized to millisecond precision
* Reduced unnecessary observability overhead by removing redundant spans from data layers
* Added a lightweight subscription fetch-by-ID API with optional expand parameters
* Removed support for fetching inactive prices via lookup keys
* Analytics APIs now expose recent subscriptions and invoice payment status
## Import, export, and workflow foundations
Backend support for bulk import and S3 exports is in place. Invoice processing and workflow reliability are improved.
* **Bulk import**: Added backend support for bulk importing `prices` and `features`
* **S3 exports**: Added managed `S3 export support` for reports such as credit usage
* **Invoice and usage**: Improved invoice processing alignment with feature usage tracking
* **Workflows**: Strengthened workflow reliability with improved Temporal identity handling and timeout tuning
## Subscription and credit grant controls
You can cancel future subscription grants and set end dates on credit grants.
* **Cancel future grants**: Added support for cancelling future subscription grants
* **Credit grant end-date**: Added end-date support for credit grants
**Other changes**
* Reintroduced price lookup keys in a controlled and validated manner
* Improved tenant and environment isolation for exports
* General workflow stability and timeout tuning
* Fixed wallet balance discrepancies affecting “credits available”
* Improved reliability of scheduled background workflows
* Import and export endpoints for prices and features made available
For release history before these updates, see our [GitHub releases](https://github.com/flexprice/flexprice/releases).
# Checkout Sessions API
Source: https://docs.flexprice.io/docs/checkout/checkout-sessions
Create a time-bounded payment session that activates a subscription on successful payment.
A Checkout Session represents a single payment attempt. Call `POST /v1/checkout/sessions`, get back a payment URL, redirect your customer, and Flexprice activates the subscription once payment is confirmed.
## Session lifecycle
| Status | Meaning |
| ----------- | ------------------------------------------------------------ |
| `initiated` | Session created; payment provider call in progress |
| `pending` | Payment URL returned; awaiting customer payment |
| `completed` | Payment confirmed; subscription activated, invoice finalized |
| `failed` | Payment failed or provider cancelled the link |
| `expired` | Session timed out without payment |
## Create a Checkout Session
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/checkout/sessions \
-H "Authorization: Bearer " \
-H "X-Environment-ID: " \
-H "Content-Type: application/json" \
-d '{
"customer_external_id": "cust_abc123",
"action": "create_subscription",
"payment_provider": "razorpay",
"configuration": {
"create_subscription_params": {
"plan_id": "plan_xyz",
"currency": "INR",
"billing_period": "monthly"
}
},
"success_url": "https://app.example.com/checkout/success",
"failure_url": "https://app.example.com/checkout/failure",
"cancel_url": "https://app.example.com/checkout/cancel",
"idempotency_key": "order_unique_ref_001"
}'
```
**Response (201):**
```json theme={null}
{
"id": "chk_01HXXX",
"customer_id": "cust_01HABC",
"action": "create_subscription",
"checkout_status": "pending",
"payment_provider": "razorpay",
"expires_at": "2024-01-01T12:15:00Z",
"payment_action": {
"type": "payment_link",
"url": "https://rzp.io/l/XXXXXXX"
}
}
```
Redirect the customer to `payment_action.url`. Each session has its own link. Do not reuse URLs across sessions.
### Request fields
| Field | Required | Description |
| --------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------- |
| `customer_external_id` | Yes | Your identifier for the customer. Must already exist in Flexprice. |
| `action` | Yes | Billing action to perform. `create_subscription` is supported. |
| `payment_provider` | Yes | Payment gateway to use. See [Choose your provider](/docs/checkout/overview#choose-your-payment-provider). |
| `configuration.create_subscription_params.plan_id` | Yes | The plan to subscribe the customer to. |
| `configuration.create_subscription_params.currency` | Yes | Billing currency (e.g., `INR`, `USD`). |
| `configuration.create_subscription_params.billing_period` | No | `monthly`, `quarterly`, `annual`, etc. Defaults to the plan's period. |
| `configuration.create_subscription_params.start_date` | No | Subscription start date. Defaults to now. |
| `success_url` | No | Where to redirect after successful payment. |
| `failure_url` | No | Where to redirect after a failed payment. |
| `cancel_url` | No | Where to redirect if the customer cancels. |
| `idempotency_key` | No | Prevents duplicate sessions on retry. A duplicate key for an active session returns 409. |
## Get a Checkout Session
```bash theme={null}
curl https://api.flexprice.io/v1/checkout/sessions/ \
-H "Authorization: Bearer " \
-H "X-Environment-ID: "
```
Use this to confirm `checkout_status` on your redirect pages before showing the customer a result.
## Delete a Checkout Session
```bash theme={null}
curl -X DELETE https://api.flexprice.io/v1/checkout/sessions/ \
-H "Authorization: Bearer " \
-H "X-Environment-ID: "
```
Sessions in a terminal state (`completed`, `failed`, `expired`) cannot be deleted.
## Webhook events
Subscribe to these in **Settings > Webhooks** to drive fulfillment from your backend.
| Event | Fired when |
| ---------------------------- | --------------------------------------------------------- |
| `checkout.session.initiated` | Session created and payment URL returned |
| `checkout.session.completed` | Payment confirmed; subscription active, invoice finalized |
| `checkout.session.failed` | Payment failed or provider cancelled |
| `checkout.session.expired` | Session timed out without payment |
Safe to receive the same event twice. If your payment provider delivers `payment_link.paid` more than once, Flexprice ignores the duplicate. Your webhook handler should also be idempotent for the same reason.
## Requirements
**Customer must exist first.** The `customer_external_id` must refer to a customer already in Flexprice. Create them at sign-up if they don't exist yet.
**Plan must have a non-zero charge.** Checkout requires a payable invoice amount to generate a payment link. Plans with only free or zero-amount charges will cause session creation to fail.
**Idempotency on retry.** If your create call times out, retry with the same `idempotency_key`. You'll get back the existing session instead of a duplicate.
Configure the webhook events Razorpay needs to send Flexprice.
Full end-to-end integration with code examples.
**API References**
* [Create a Checkout Session](/api-reference/checkout/create-checkout-session)
* [Get a Checkout Session](/api-reference/checkout/get-checkout-session)
* [Delete a Checkout Session](/api-reference/checkout/delete-checkout-session)
# Implementation Guide
Source: https://docs.flexprice.io/docs/checkout/implementation-guide
Step-by-step guide to integrating Flexprice Checkout: backend setup, customer redirect, webhook handling, and go-live checklist.
This guide covers the complete integration: backend session creation, frontend redirect, webhook handling, and going live. By the end, customers will be able to select a plan, pay, and get an active subscription without any manual step on your side.
## Prerequisites
* [ ] Payment provider connected to Flexprice: see [Razorpay Setup](/docs/checkout/razorpay-checkout)
* [ ] Required webhook events enabled on your provider dashboard
* [ ] Flexprice webhook endpoint registered to receive `checkout.session.*` events
* [ ] Customer record exists in Flexprice before the checkout starts
* [ ] Plan has at least one non-zero charge
## Architecture
```
Your frontend Your backend Flexprice Razorpay
───────────── ──────────── ───────── ────────
User clicks POST /checkout/ Draft sub + Payment
"Subscribe" ───────► sessions ──────► invoice ──► link created
Returns URL ◄─────────────────────────────────────
Redirect ◄───────────
User pays ──────────────────────────────────────────────────► Checkout
◄── payment_link.paid ──────
Activates subscription
Finalizes invoice
◄── checkout.session.completed ──────────
Fulfill order ◄──────
```
## Step 1: Create a customer
A customer must exist in Flexprice before a session can be created. Create one at sign-up:
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/customers \
-H "Authorization: Bearer " \
-H "X-Environment-ID: " \
-H "Content-Type: application/json" \
-d '{
"external_id": "user_12345",
"name": "Jane Smith",
"email": "jane@example.com",
"currency": "INR"
}'
```
Use the `external_id` as your reference in subsequent calls.
## Step 2: Create a Checkout Session
Call this from your backend only: never expose your API key to the frontend.
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/checkout/sessions \
-H "Authorization: Bearer " \
-H "X-Environment-ID: " \
-H "Content-Type: application/json" \
-d '{
"customer_external_id": "user_12345",
"action": "create_subscription",
"payment_provider": "razorpay",
"configuration": {
"create_subscription_params": {
"plan_id": "plan_pro_monthly",
"currency": "INR",
"billing_period": "monthly"
}
},
"success_url": "https://app.example.com/welcome?session_id={CHECKOUT_SESSION_ID}",
"failure_url": "https://app.example.com/pricing?error=payment_failed",
"cancel_url": "https://app.example.com/pricing",
"idempotency_key": "order_"
}'
```
Two things worth noting here:
* `{CHECKOUT_SESSION_ID}` in `success_url` is a placeholder that Razorpay replaces on redirect. Use it to identify which session the customer just completed.
* `idempotency_key` tied to your internal order ID means a retry on timeout gives you back the same session, not a duplicate.
Store the returned `id` (`chk_...`) in your database alongside the order.
## Step 3: Redirect the customer
Return the `payment_action.url` from your backend to your frontend and redirect immediately:
```javascript theme={null}
window.location.href = session.payment_action.url;
```
Do a full-page redirect, not an iframe. The payment link expires after 15 minutes, so don't add unnecessary steps before the redirect.
## Step 4: Handle the webhook
Do not rely on the redirect URL to confirm payment: it can be bypassed. The `checkout.session.completed` webhook is the authoritative signal.
Register your endpoint in **Settings → Webhooks**, then handle the events:
```javascript theme={null}
app.post('/webhooks/flexprice', express.raw({ type: 'application/json' }), async (req, res) => {
const { event_type, data } = JSON.parse(req.body);
res.status(200).send('ok'); // respond fast, process async
if (event_type === 'checkout.session.completed') {
// subscription is active, invoice is finalized
await grantAccess(data.customer_id);
await sendWelcomeEmail(data.customer_id);
}
if (event_type === 'checkout.session.failed') {
await notifyPaymentFailed(data.id);
}
if (event_type === 'checkout.session.expired') {
await clearPendingState(data.id);
}
});
```
Make this handler idempotent: Flexprice delivers events at least once. Check whether you've already fulfilled the order before acting.
## Step 5: Handle success and failure pages
Use the redirect URLs for UX only, not for fulfillment logic.
**Success page**: poll the session to confirm before showing a result:
```bash theme={null}
curl https://api.flexprice.io/v1/checkout/sessions/ \
-H "Authorization: Bearer " \
-H "X-Environment-ID: "
```
If `checkout_status` is `completed`, show the success state. If it's still `pending`, the webhook hasn't arrived yet: show a "processing" state and poll again in a few seconds.
**Failure or cancel page:** The customer can start over. Create a new session. The previous session and everything it created are cleaned up automatically.
## Step 6: Test the integration
Use your provider's test credentials before switching to live keys. For Razorpay test cards and UPI IDs, see [Razorpay Checkout Setup](/docs/checkout/razorpay-checkout#step-4-test-the-flow).
After a test payment, verify:
* [ ] Session `checkout_status` is `completed`
* [ ] Subscription is `active`
* [ ] Invoice is `finalized`
* [ ] Your backend received `checkout.session.completed`
* [ ] Fulfillment ran (access granted, emails sent, DB updated)
## Error reference
| Scenario | Response | Fix |
| ---------------------------------------------- | ---------------------------------- | ----------------------------------------------------------- |
| Customer doesn't exist | 404 | Create the customer before starting checkout |
| Plan produces zero-amount invoice | 400 | Use a plan with at least one non-zero charge |
| Duplicate `idempotency_key` for active session | 409 | Fetch the existing session; use a new key for a new attempt |
| Customer doesn't pay in time | Session expires; drafts cleaned up | Let the user start a new checkout |
| `payment_link.paid` not enabled | Subscription stays inactive | Enable the event in your payment provider dashboard |
## Go-live checklist
* [ ] Switch provider connection from test keys to live keys
* [ ] Confirm all required webhook events are enabled on the production URL (not test)
* [ ] `success_url`, `failure_url`, `cancel_url` point to production domains
* [ ] Webhook endpoint responds within 5 seconds and uses HTTPS
* [ ] Fulfillment handler is idempotent
Test credentials and webhook event configuration.
Full field reference for the create session call.
# Checkout
Source: https://docs.flexprice.io/docs/checkout/overview
Accept payment and activate subscriptions in a hosted checkout flow, without building any payment UI.
Flexprice Checkout is a hosted payment integration that handles the full subscription activation sequence for you. Connect a payment provider, create a session from your backend, and redirect your customer. Flexprice takes care of the payment page, the subscription, the invoice, and the cleanup if something goes wrong.
It is the fastest way to go from a pricing page to an active, paying subscriber.
## When to use Checkout
Use Checkout when your customers initiate and pay for their own subscription: a self-serve pricing page, an upgrade flow, or a sign-up gate.
If you create subscriptions on behalf of customers from your own backend (after a sales call, from an admin dashboard, or via contract), use the [Subscriptions API](/docs/subscriptions/customers-create-subscription) instead.
## How it works
Your backend creates a Checkout Session with the customer, plan, and payment provider. Flexprice generates a hosted payment page URL and returns it to you. You redirect the customer there. Once payment is complete, Flexprice activates the subscription and finalizes the invoice.
If the customer cancels, the session times out, or the payment fails, Flexprice cleans up all draft records automatically.
## Choose your payment provider
Each provider has its own setup page with connection instructions, webhook configuration, and test credentials.
Accept payments via Razorpay payment links.
## Start building
Create, retrieve, and manage sessions.
Backend, frontend redirect, webhook handling, and go-live checklist.
Subscribe to `checkout.session.*` events to trigger fulfillment in your system.
Create subscriptions directly without a payment step.
# Razorpay Checkout Setup
Source: https://docs.flexprice.io/docs/checkout/razorpay-checkout
Configure Razorpay to work with Flexprice Checkout Sessions, including the exact webhook events required.
Before creating your first Checkout Session with Razorpay, you need a connected Razorpay account and the correct webhook events enabled in your Razorpay dashboard.
## Step 1: Connect Razorpay
Follow the [Razorpay Connection Setup guide](/integrations/razorpay/connection-setup) to add your API keys and webhook secret. Come back here once the connection is active.
## Step 2: Enable the required webhook events
Go to your Razorpay Dashboard: **Settings → Webhooks → \[your Flexprice endpoint]** and enable all five events:
| Event | Required for |
| ------------------------ | ---------------------------------------------------- |
| `payment.captured` | Payment reconciliation |
| `payment.failed` | Payment failure handling |
| `payment_link.paid` | Activating the subscription when payment completes |
| `payment_link.cancelled` | Cleaning up when a customer cancels the payment link |
| `payment_link.expired` | Cleaning up when the payment link expires |
If `payment_link.paid` is not enabled, Flexprice will never receive payment confirmation and the subscription will stay inactive. Verify all five events are enabled before testing.
### Webhook URL
Your Flexprice webhook URL follows this format:
```
https://api.cloud.flexprice.io/v1/webhooks/razorpay//
```
Find your `TENANT_ID` and `ENVIRONMENT_ID` in **Settings → General**.
## Step 3: Verify the webhook secret
Flexprice validates the signature on every inbound Razorpay webhook. If the secret doesn't match, events are rejected silently.
Go to **Settings → Connections → Razorpay** in Flexprice and confirm the webhook secret matches what's shown in your Razorpay dashboard for that endpoint.
## Step 4: Test the flow
Use Razorpay test mode before going live.
**Test cards:**
| Network | Card number | CVV | Expiry |
| ---------- | --------------------- | ------------ | --------------- |
| Visa | `4386 2894 0766 0153` | Any 3 digits | Any future date |
| Mastercard | `2305 3242 5784 8228` | Any 3 digits | Any future date |
**Test UPI:**
| UPI ID | Simulates |
| ------------------ | ------------------ |
| `success@razorpay` | Successful payment |
| `failure@razorpay` | Failed payment |
After a test payment, confirm:
* Session status is `completed`
* Subscription status is `active`
* Invoice status is `finalized`
* Your server received the `checkout.session.completed` event
If the session stays `pending` after payment, check the Razorpay webhook delivery log and confirm all five events are enabled on the correct endpoint URL.
## Troubleshooting
| Symptom | Likely cause | Fix |
| ----------------------------------------- | --------------------------------------------- | --------------------------------------------------- |
| Session stays `pending` after payment | `payment_link.paid` not enabled | Add it in Razorpay Dashboard → Webhooks |
| Session fails immediately on creation | Razorpay connection inactive | Check status in Settings → Connections |
| Webhook secret mismatch error | Secret out of sync | Re-copy the secret from Razorpay into Flexprice |
| Subscription stays inactive after payment | Wrong tenant or environment ID in webhook URL | Verify the URL in Razorpay matches the format above |
Connect your Razorpay account to Flexprice.
Create your first session once Razorpay is configured.
# Get Started
Source: https://docs.flexprice.io/docs/collectors/get-started
Install and run the Flexprice Bento Collector with Docker or Kubernetes
Get the Flexprice Bento Collector running in minutes.
## Prerequisites
* Flexprice API key
* Event source (Kafka, webhooks, database)
## Quick Start with Docker
**1. Create `config.yaml`:**
```yaml theme={null}
input:
kafka:
addresses: [${KAFKA_BROKERS}]
topics: [usage-events]
consumer_group: flexprice-collector
pipeline:
processors:
- mapping: |
root.event_name = this.event_name
root.external_customer_id = this.customer_id
root.properties = this.properties.map_each(p -> p.value.string())
root.timestamp = this.timestamp.or(now().format_timestamp("2006-01-02T15:04:05Z07:00"))
output:
flexprice:
api_host: https://api.flexprice.io
api_key: ${FLEXPRICE_API_KEY}
scheme: https
batching:
count: 10
period: 2s
```
**2. Run with Docker:**
```bash theme={null}
docker run -d \
--name bento-collector \
-e FLEXPRICE_API_KEY=fp_live_xxxxx \
-e KAFKA_BROKERS=broker:9092 \
-v $(pwd)/config.yaml:/bento.yaml:ro \
ghcr.io/flexprice/bento-collector:latest \
-c /bento.yaml
```
**3. Check logs:**
```bash theme={null}
docker logs -f bento-collector
# Expected:
# INFO Flexprice output connected and ready
# INFO Input type kafka is now active
```
## Kubernetes Deployment
**1. Create Secret:**
```bash theme={null}
kubectl create secret generic bento-collector \
--from-literal=FLEXPRICE_API_KEY=fp_live_xxxxx \
--from-literal=KAFKA_BROKERS=broker:9092
```
**2. Deploy:**
```yaml theme={null}
apiVersion: apps/v1
kind: Deployment
metadata:
name: bento-collector
spec:
replicas: 2
selector:
matchLabels:
app: bento-collector
template:
metadata:
labels:
app: bento-collector
spec:
containers:
- name: bento
image: ghcr.io/flexprice/bento-collector:latest
args: ["-c", "/config/config.yaml"]
envFrom:
- secretRef:
name: bento-collector
resources:
requests:
memory: "256Mi"
cpu: "250m"
```
## Configuration
### Batching
Tune based on event volume:
```yaml theme={null}
output:
flexprice:
batching:
count: 10 # Events per batch
period: 2s # Max wait time
max_in_flight: 10 # Concurrent requests
```
**Guidelines:**
* Low volume (\< 100/min): `count: 5, period: 5s`
* Medium volume: `count: 10, period: 2s` (default)
* High volume (> 1000/min): `count: 50, period: 1s`
### Kafka Performance
```yaml theme={null}
input:
kafka:
fetch_buffer_cap: 256 # Fetch more messages for batching
commit_period: 1s
max_processing_period: 100ms
```
## Troubleshooting
**Events not batching?**
* Increase `fetch_buffer_cap` in Kafka input
* Check event volume is sufficient
* Rebuild binary if code changed
**Connection errors?**
* Verify API key is correct
* Check Kafka broker addresses
* Ensure `external_customer_id` exists in Flexprice
**Memory issues?**
* Reduce `fetch_buffer_cap`
* Lower `max_in_flight`
* Set resource limits
## Environment Variables
| Variable | Description | Example |
| -------------------- | ----------------- | -------------------------- |
| `FLEXPRICE_API_KEY` | Flexprice API key | `fp_live_abc123...` |
| `FLEXPRICE_API_HOST` | API hostname | `https://api.flexprice.io` |
| `KAFKA_BROKERS` | Kafka brokers | `broker:9092` |
## Observability
The collector exposes Prometheus metrics at `/metrics`:
```yaml theme={null}
metrics:
prometheus:
enabled: true
prefix: bento
http:
enabled: true
address: 0.0.0.0:4195
```
Access metrics: `http://localhost:4195/metrics`
## Next Steps
* [How It Works](/docs/collectors/how-it-works) - Architecture and advanced config
* [Bento Docs](https://warpstreamlabs.github.io/bento/docs/about) - Full Bento documentation
Need help? Contact **[support@flexprice.io](mailto:support@flexprice.io)**
# How It Works
Source: https://docs.flexprice.io/docs/collectors/how-it-works
Architecture, batching, and performance optimization for the Bento Collector
Deep dive into the Bento Collector's internals and optimization.
## Architecture
The collector pipeline has three components:
```
Input (Kafka) → Processors (Transform) → Output (Flexprice)
```
1. **Input**: Reads data with offset management and backpressure handling
2. **Processors**: Transforms data using Bloblang mapping language
3. **Output**: Batches and sends to Flexprice API with retries
## How Batching Works
Events accumulate until either condition is met:
* **Count threshold**: Batch reaches configured size (e.g., 10 events)
* **Time window**: Time limit reached (e.g., 2 seconds)
**Example timeline:**
```
Time: 0s 0.5s 1s 1.5s 2s
Event: 1─────2──────3─────4──────5
└─▶ Batch sent (5 events, 2s elapsed)
```
The collector automatically routes to the appropriate endpoint:
* **1 event**: `POST /events`
* **multiple events**: `POST /events/bulk`
## Data Transformation
Use Bloblang to transform your data format to Flexprice requirements:
### Type Conversion
All property values must be strings:
```yaml theme={null}
pipeline:
processors:
- mapping: |
root.properties = this.properties.map_each(p -> p.value.string())
```
**Before:** `{"duration": 245, "status": 200}`\
**After:** `{"duration": "245", "status": "200"}`
### Conditional Processing
```yaml theme={null}
pipeline:
processors:
- mapping: |
# Only process successful requests
root = if this.status >= 200 && this.status < 300 {
this
} else {
deleted()
}
```
## Performance Optimization
### Kafka Input
```yaml theme={null}
input:
kafka:
fetch_buffer_cap: 256 # Fetch more messages at once
checkpoint_limit: 1024 # Buffer messages in flight
commit_period: 1s
```
### Output Concurrency
```yaml theme={null}
output:
flexprice:
max_in_flight: 10 # Parallel API requests
batching:
count: 50
period: 5s
```
**Guidelines:**
* Low volume: `max_in_flight: 5`
* Medium volume: `max_in_flight: 10` (default)
* High volume: `max_in_flight: 20-50`
## Error Handling
The collector automatically retries failed requests:
```yaml theme={null}
output:
flexprice:
retry_max_attempts: 3
retry_backoff:
initial_interval: 1s
max_interval: 60s
```
**Retry sequence:**
1. Fail → Wait 1s → Retry
2. Fail → Wait 2s → Retry
3. Fail → Wait 4s → Retry
4. Drop and log error
## Observability
### Metrics
Key Prometheus metrics:
| Metric | Description |
| ---------------------- | -------------------------- |
| `bento_input_received` | Messages received |
| `bento_output_sent` | Messages sent successfully |
| `bento_output_error` | Output errors |
| `bento_batch_created` | Batches created |
### Logging
Configure log level and format:
```yaml theme={null}
logger:
level: INFO # DEBUG, INFO, WARN, ERROR
format: logfmt # or json
add_timestamp: true
```
## Additional Resources
* **Bento Docs**: [warpstreamlabs.github.io/bento](https://warpstreamlabs.github.io/bento/docs/about)
* **Bloblang Guide**: [Mapping language](https://warpstreamlabs.github.io/bento/docs/guides/bloblang/about)
* **Flexprice Events API**: [Events reference](/docs/event-ingestion/overview)
Need help? **[support@flexprice.io](mailto:support@flexprice.io)**
# Flexprice Collector
Source: https://docs.flexprice.io/docs/collectors/overview
Stream usage events from Kafka and other sources into Flexprice with reliable delivery
The Flexprice Collector makes it easy to send usage data into Flexprice. It takes care of reading, transforming, and delivering events so you don’t have to worry about scaling or building custom pipelines.
Just plug in your data source, and the collector handles the rest.
## How It Works
The Flexprice Collector is built on [Bento](https://warpstreamlabs.github.io/bento/docs/about), a robust stream processing tool. Bento's strong delivery guarantees and retry capabilities make it ideal for reliable event ingestion.
The collector connects your data sources (Kafka, webhooks, databases) to Flexprice through a processing pipeline with three components:
* **Inputs** read data from various sources (Kafka, webhooks, databases, files)
* **Processors** validate, transform, and filter data to match Flexprice format
* **Outputs** send data to Flexprice API with bulk batching and retry logic
## Key Features
The Flexprice Collector is optimized for high-performance usage metering:
* **Automatic Batching**: Groups events for bulk ingestion (10 events or 2s by default)
* **Smart Routing**: Uses `/events` for single events, `/events/bulk` for batches
* **Retry Logic**: Exponential backoff ensures no data loss on network failures
* **High Throughput**: Handles millions of events per day with minimal latency
## Ingesting Data into Flexprice
The collector always outputs to the Flexprice API:
```yaml theme={null}
output:
flexprice:
api_host: https://api.flexprice.io
api_key: ${FLEXPRICE_API_KEY}
scheme: https
batching:
count: 10
period: 2s
```
Transform your data to match Flexprice event format:
```yaml theme={null}
pipeline:
processors:
- mapping: |
root.event_name = this.event_name
root.external_customer_id = this.customer_id
root.properties = this.properties.map_each(p -> p.value.string())
root.timestamp = this.timestamp.or(now().format_timestamp("2006-01-02T15:04:05Z07:00"))
```
Configure your input source. Example with Kafka:
```yaml theme={null}
input:
kafka:
addresses: [${KAFKA_BROKERS}]
topics: [usage-events]
consumer_group: flexprice-collector
```
The collector supports 100+ input sources. Learn more in the [Bento documentation](https://warpstreamlabs.github.io/bento/docs/components/inputs/about).
## Installation
The Flexprice Collector is available via:
* **Binaries** from GitHub Releases
* **Container images**
* **Kubernetes** deployment (see Get Started guide)
## Performance & Reliability
The collector provides:
* **Bulk Ingestion**: Batches up to 100 events per API call
* **Automatic Retries**: Failed events retry with exponential backoff
* **At-Least-Once Delivery**: Kafka consumer groups ensure no data loss
* **Observability**: Prometheus metrics and structured logging
Contact [support@flexprice.io](mailto:support@flexprice.io) for performance tuning assistance.
# Model Context Protocol (MCP)
Source: https://docs.flexprice.io/docs/connect/mcp-server
Connect Cursor, Claude, VS Code, and other MCP clients to your Flexprice API
> Connect Cursor, Claude, VS Code, and other MCP-compatible tools to your Flexprice API.
The Flexprice Model Context Protocol (MCP) server provides a set of tools that AI agents can use to interact with the Flexprice API: customers, plans, subscriptions, invoices, payments, events, and more.
## What is the Flexprice MCP server?
* An MCP server that exposes your Flexprice API as **MCP tools** so AI assistants can call your API without reading REST docs.
* Authenticated via **API key** (no OAuth). You provide the key in your MCP client config.
* Works with any MCP client that supports stdio: Cursor, VS Code, Claude Desktop, Claude Code, Gemini, Windsurf, and others.
**How you can connect:** Flexprice currently supports two ways to run the MCP server. Pick what fits your workflow:
* **npx (recommended):** One command, no install. Run `npx -y @flexprice/mcp-server start` with your API key and server URL. Ideal for Cursor, VS Code, Claude Code, Gemini, Windsurf, and most MCP clients.
* **Local or Docker:** Clone the repo and run with Node, or use the official Docker image. Great for development, custom builds, or environments where you prefer not to use npx.
## Quick start
One-click install
One-click install
See instructions
See instructions
See instructions
See all options
## Authentication
The MCP server uses your **Flexprice API key** only (no OAuth).
* **Where to get the key:** From your Flexprice account (Dashboard or API keys settings).
* **Where to set it:** In the MCP config as `--api-key-auth YOUR_API_KEY` (npx) or in the `env` object as `API_KEY_APIKEYAUTH` (local/Docker). Do not commit API keys; rotate if exposed.
**Base URL / API URL (important):** The MCP server’s `--server-url` (or
equivalent) must use one of FlexPrice’s API base URLs. The two allowed URLs
are:
* **US Region:** `https://us.api.flexprice.io/v1`
* **India Region:** `https://api.cloud.flexprice.io/v1`
## Security and credentials
* Do not commit API keys. Keep your key only in local MCP config or environment variables.
* Rotate keys if a key is ever committed or exposed.
* We recommend enabling human confirmation for write or destructive operations in your MCP client.
We advise caution when giving LLMs write access to production data. Always
review actions before execution. Use caution when using the Flexprice MCP with
other servers to avoid prompt injection attacks.
## Installation instructions
### Cursor
Click to install the MCP server configuration for Cursor.
Manual installation:
1. Open **Cursor → Settings → Cursor Settings** and go to the **MCP** tab.
2. Click **+ Add new global MCP server** (or open the MCP configuration file).
3. Add the following to your `~/.cursor/mcp.json` file. Replace `YOUR_API_KEY` with your Flexprice API key. See [Cursor MCP documentation](https://docs.cursor.com/context/model-context-protocol).
```json theme={null}
{
"mcpServers": {
"flexprice": {
"command": "npx",
"args": [
"-y",
"@flexprice/mcp-server",
"start",
"--server-url",
"https://us.api.flexprice.io/v1",
"--api-key-auth",
"YOUR_API_KEY"
]
}
}
}
```
4. Save and restart Cursor.
### VS Code
Click to install the MCP server configuration for VS Code.
Manual installation:
1. Open the Command Palette (**Ctrl+Shift+P** / **Cmd+Shift+P**) and run **MCP: Add Server** or **MCP: Open User Configuration**.
2. Add the following to your MCP config. Replace `YOUR_API_KEY` with your Flexprice API key. See [VS Code MCP documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers).
```json theme={null}
{
"servers": {
"flexprice": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@flexprice/mcp-server",
"start",
"--server-url",
"https://us.api.flexprice.io/v1",
"--api-key-auth",
"YOUR_API_KEY"
]
}
}
}
```
3. Save and restart VS Code.
### Claude Code
See instructions below.
Manual installation:
1. Run the following command to add the Flexprice MCP server. Replace `YOUR_API_KEY` with your Flexprice API key.
```bash theme={null}
claude mcp add Flexprice -- npx -y @flexprice/mcp-server start --server-url https://us.api.flexprice.io/v1 --api-key-auth YOUR_API_KEY
```
2. Enter Claude Code: `claude`
3. List MCP servers with `/mcp` to confirm the server is connected. See [Claude Code MCP documentation](https://docs.anthropic.com/en/docs/claude-code/mcp#configure-mcp-servers).
### Gemini
See instructions below.
Manual installation:
1. Run the following command. Replace `YOUR_API_KEY` with your Flexprice API key.
```bash theme={null}
gemini mcp add Flexprice -- npx -y @flexprice/mcp-server start --server-url https://us.api.flexprice.io/v1 --api-key-auth YOUR_API_KEY
```
2. Enter Gemini: `gemini` and use `/mcp list` to verify. See [Gemini CLI](https://geminicli.com/) for details.
### Windsurf
See instructions below.
Manual installation:
1. Open Windsurf Settings → Cascade → **Manage MCPs** → **View raw config**.
2. Add the Flexprice server to the config (same JSON structure as Cursor). Replace `YOUR_API_KEY` with your Flexprice API key. See [Windsurf MCP documentation](https://docs.windsurf.com/windsurf/cascade/mcp).
### Other clients
See instructions below.
For **Claude Desktop** and other MCP clients, use the same npm package config. Example (Claude Desktop: add to your Claude Desktop config file, then quit and reopen):
```json theme={null}
{
"mcpServers": {
"flexprice": {
"command": "npx",
"args": [
"-y",
"@flexprice/mcp-server",
"start",
"--server-url",
"https://us.api.flexprice.io/v1",
"--api-key-auth",
"YOUR_API_KEY"
]
}
}
}
```
**Config file locations:** Claude Desktop (macOS): `~/Library/Application Support/Claude/claude_desktop_config.json`; Windows: `%APPDATA%\Claude\claude_desktop_config.json`.
**Alternative: run from local repo (Node).** If you cloned the Flexprice repo and run the server with `node`:
```json theme={null}
{
"mcpServers": {
"flexprice": {
"command": "node",
"args": ["/path/to/mcp-server/bin/mcp-server.js", "start"],
"env": {
"API_KEY_APIKEYAUTH": "your_api_key_here",
"BASE_URL": "https://us.api.flexprice.io/v1"
}
}
}
}
```
**Alternative: Docker (stdio).**
```json theme={null}
{
"mcpServers": {
"flexprice": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"API_KEY_APIKEYAUTH",
"-e",
"BASE_URL",
"flexprice-mcp"
],
"env": {
"API_KEY_APIKEYAUTH": "your_api_key_here",
"BASE_URL": "https://us.api.flexprice.io/v1"
}
}
}
}
```
Replace `/path/to/mcp-server` and env values as needed. Save the config and restart your client.
## Example workflows
Once connected, you can ask your AI assistant to:
* "List my customers" or "Get customer by id …"
* "Create a subscription for customer … on plan …"
* "Get plan by id …" or "List plans"
* "Get usage for subscription …"
## Available tools
The server exposes the Flexprice API as MCP tools (one tool per OpenAPI operation). We recommend enabling human confirmation of tools and exercising caution when using the Flexprice MCP with other servers to avoid prompt injection attacks.
| Resource | Example tools |
| ------------- | ---------------------------------------------------------------------------- |
| Customers | `get_customers`, `post_customers`, `get_customers_id` |
| Plans | `get_plans`, `get_plans_id`, `post_plans` |
| Subscriptions | `post_subscriptions`, `get_subscriptions_id`, `post_subscriptions_id_cancel` |
| Invoices | `get_invoices`, `get_invoices_id`, `post_invoices` |
| Payments | `get_payments`, `post_payments` |
| Events | `post_events`, `post_events_bulk` |
| Features | `get_features`, `post_features` |
| Prices | `get_prices`, `post_prices` |
| Wallets | `get_wallets_id`, `post_wallets_id_top_up` |
For the full list, see your MCP client's tool list after connecting or the [API Reference](/api-reference/introduction).
## Troubleshooting
| Symptom | Fix |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Server does not appear | Save the MCP config and restart your client (Cursor, VS Code, Claude Desktop). |
| Invalid URL or 404 | The server uses `BASE_URL` + path. Set base URL to `https://us.api.flexprice.io/v1` (include `/v1`, no trailing slash). For npx: use `--server-url https://us.api.flexprice.io/v1`. For local/Docker: set `BASE_URL` in `.env` or the MCP config `env` object. |
| API or connection errors | Verify your API key and base URL. Test with `curl -H "x-api-key: YOUR_KEY" https://us.api.flexprice.io/v1/customers`. |
| Rate limiting | Reduce request frequency or contact Flexprice support. |
| Port in use (local run) | Change the port in config or stop the process using it (e.g. `lsof -i :3000` then `kill -9 PID`). |
| Docker issues | Ensure env vars are passed into the container. Rebuild with `--no-cache` if the build fails. |
## See also
* [API Reference](/api-reference/introduction): full list of Flexprice API operations (each maps to an MCP tool).
# Project Architecture
Source: https://docs.flexprice.io/docs/contributing-guide/architechture
Understanding the project structure and architecture
## Overview
Flexprice is a comprehensive **usage-based billing and subscription management platform** built in Go. The system handles the complete billing lifecycle from event ingestion through invoice generation and payment processing, designed for B2B SaaS companies requiring flexible pricing models.
## High-Level Architecture
Flexprice implements a **three-tier layered architecture** with clear separation of concerns, bootstrapped using **Uber FX dependency injection framework**:
### Architecture Layers
```mermaid theme={null}
graph TB
subgraph "API Layer"
Router["Gin HTTP Router"]
Handlers["API Handlers (v1)"]
Middleware["Auth & Access Control"]
end
subgraph "Service Layer"
EventService["Event Processing"]
BillingService["Billing Logic"]
SubscriptionService["Subscription Management"]
PaymentService["Payment Processing"]
OtherServices["20+ Domain Services"]
end
subgraph "Repository Layer"
PostgresRepos["PostgreSQL Repositories"]
ClickHouseRepos["ClickHouse Repositories"]
CacheLayer["In-Memory Cache"]
end
subgraph "Infrastructure"
Postgres["PostgreSQL (Transactional)"]
ClickHouse["ClickHouse (Analytics)"]
Kafka["Kafka (Event Streaming)"]
Temporal["Temporal (Workflows)"]
S3["S3 (Document Storage)"]
end
Router --> Middleware
Middleware --> Handlers
Handlers --> EventService
Handlers --> BillingService
Handlers --> SubscriptionService
Handlers --> PaymentService
EventService --> PostgresRepos
EventService --> ClickHouseRepos
BillingService --> PostgresRepos
BillingService --> ClickHouseRepos
PostgresRepos --> Postgres
ClickHouseRepos --> ClickHouse
EventService --> Kafka
BillingService --> Temporal
```
## Technology Stack
### Core Technologies
Flexprice uses a modern Go stack with specialized components:
**Key Dependencies:**
* **Gin** - HTTP routing and middleware
* **Ent** - Type-safe ORM for PostgreSQL
* **Uber FX** - Dependency injection framework
* **ClickHouse** - Columnar database for analytics
* **Kafka (Sarama)** - Event streaming platform
* **Temporal** - Workflow orchestration engine
* **Stripe** - Payment processing integration
## Dual-Database Strategy
Flexprice employs a **specialized database strategy** for different workloads:
### PostgreSQL (Transactional Data)
* **Purpose**: ACID transactions, relational data
* **Data**: Subscriptions, invoices, customers, plans, prices, wallets
* **Access Pattern**: Read-write, complex joins, referential integrity
* **ORM**: Ent for type-safe queries
### ClickHouse (Analytical Data)
* **Purpose**: High-volume inserts, aggregation queries
* **Data**: Events (raw), events\_lazy, processed\_events, system\_events
* **Access Pattern**: Append-only writes, time-series queries
* **Tables**: Optimized for usage metering and analytics
## Event Processing Pipeline
The core of Flexprice's usage-based billing is an **asynchronous event processing pipeline** powered by Kafka:
### Pipeline Stages
```mermaid theme={null}
graph LR
A[Application] --> B[events]
A --> C[events_lazy]
B --> D[Real-time Processing]
C --> E[Deferred Processing]
D --> F[events_post_processing]
F --> G[Post Actions]
D --> H[system_events]
H --> I[Webhooks]
```
## Deployment Modes
Flexprice supports **multiple deployment configurations** to separate concerns and scale independently:
### Available Modes
| Mode | Components | Purpose |
| --------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------- |
| **`local`** | API Server + Router (with event consumption handlers) + Temporal Worker | Full development environment with all services enabled |
| **`api`** | API Server + Router (event consumption disabled) | Handle HTTP requests with routing but no event processing |
| **`consumer`** | Router only (with event consumption handlers) | Dedicated event processing without HTTP API |
| **`temporal_worker`** | Temporal workers only | Background workflows and scheduled tasks execution |
### Mode Implementation Details
* **`local`**: Starts API server, registers all router handlers with event consumption enabled (`true`), starts router, and starts Temporal worker
* **`api`**: Starts API server, registers router handlers with event consumption disabled (`false`), starts router only
* **`consumer`**: Requires Kafka consumer, registers all router handlers with event consumption enabled (`true`), starts router without API server
* **`temporal_worker`**: Starts only Temporal worker service, no API server or router
## Service Layer Architecture
The service layer implements business logic through 30+ domain services organized by domain:
### Core Services
* **Billing Service**: Handles invoice generation, usage calculations, and billing cycles
* **Event Service**: Processes incoming usage events and manages event lifecycle
* **Subscription Service**: Manages customer subscriptions, upgrades, and downgrades
* **Customer Service**: Customer management and tenant operations
* **Payment Service**: Payment processing and gateway integrations
* **Plan Service**: Pricing plan management and feature configuration
* **Wallet Service**: Prepaid credits and wallet balance management
### Service Factory Pattern
All services are instantiated through factory functions that receive a centralized `ServiceParams` struct containing shared dependencies:
```go theme={null}
type ServiceParams struct {
// Individual Repository Interfaces
AuthRepo auth.Repository
UserRepo user.Repository
EventRepo events.Repository
ProcessedEventRepo events.ProcessedEventRepository
// ... 40+ more domain specific repositories
// Publishers
EventPublisher publisher.EventPublisher
WebhookPublisher webhookPublisher.WebhookPublisher
// Services
Logger *logger.Logger
Config *config.Configuration
DB postgres.IClient
PDFGenerator pdf.Generator
S3 s3.Service
// http client
Client httpclient.Client
// Proration
ProrationCalculator proration.Calculator
// Integration Factory
IntegrationFactory *integration.Factory
}
```
## API Layer & Routing
The API layer uses **Gin framework** with a comprehensive middleware stack for security, logging, and request processing:
### Middleware Stack
1. **Recovery Middleware**: Panic recovery and graceful error handling
2. **CORS Middleware**: Cross-origin resource sharing configuration
3. **Rate Limiting**: Request throttling per API key/tenant
4. **Authentication**: JWT and API key validation
5. **Tenant Isolation**: Multi-tenant context enforcement
6. **Request Logging**: Structured logging for all requests
7. **Metrics Collection**: Prometheus metrics for observability
### Authentication & Authorization
Flexprice supports multiple authentication methods:
* **API Keys**: Tenant-scoped keys for programmatic access
* **JWT Tokens**: User session tokens with role-based permissions
* **Webhook Signatures**: HMAC verification for incoming webhooks
### API Endpoints Structure
```
/v1
├── /auth # Authentication endpoints
├── /customers # Customer management
├── /plans # Pricing plans and features
├── /subscriptions # Subscription lifecycle
├── /events # Event ingestion
├── /invoices # Invoice generation and retrieval
├── /usage # Usage queries and aggregations
├── /payments # Payment processing
└── /webhooks # Webhook management
```
## Multi-Tenancy Architecture
Flexprice implements **row-level multi-tenancy** with environment isolation enforced at multiple architectural layers:
### Tenant Isolation Strategy
* **Database Level**: All tables include `tenant_id` and `environment` columns
* **Middleware Level**: Request context includes tenant/environment validation
* **Service Level**: All business logic operations are tenant-scoped
* **Repository Level**: Automatic filtering on all database queries
### Middleware Enforcement
Tenant and environment isolation is enforced through the `AuthenticateMiddleware`, which handles both authentication and tenant context extraction:
```go theme={null}
func AuthenticateMiddleware(cfg *config.Configuration, secretService service.SecretService, logger *logger.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
// Check for API key in X-API-Key header
// Check for JWT token in Authorization header
c.Next()
}
}
```
### Authentication Methods
API keys are configured with tenant and user association:
* **Tenant API Keys**: Scoped to specific tenant and environment
* **User Tokens**: Associated with user permissions within tenant
* **System Keys**: Cross-tenant access for administrative operations
## Repository Layer
The repository layer provides data access abstraction with automatic tenant/environment filtering:
### Repository Interface
```go theme={null}
type Repository interface {
// All methods automatically filter by tenant/environment from context
GetCustomer(ctx context.Context, customerID string) (*Customer, error)
CreateInvoice(ctx context.Context, invoice *Invoice) error
GetUsageEvents(ctx context.Context, filters EventFilters) ([]Event, error)
}
```
### Automatic Tenant Filtering
All repositories automatically append tenant and environment filters to queries using typed context constants:
```go theme={null}
func (r *PostgresRepository) GetCustomer(ctx context.Context, customerID string) (*Customer, error) {
tenantID := ctx.Value(types.CtxTenantID).(string)
environment := ctx.Value(types.CtxEnvironmentID).(string)
return r.db.Customer.
Query().
Where(customer.ID(customerID)).
Where(customer.TenantID(tenantID)).
Where(customer.Environment(environment)).
Only(ctx)
}
```
All repositories are created through factory functions that receive shared dependencies like database connections, caching layers, and configuration.
## Message Processing with PubSub Router
Flexprice uses **Watermill** for message routing with built-in retry mechanisms and rate limiting:
### Kafka Integration
```go theme={null}
router := message.NewRouter(message.RouterConfig{}, logger)
// Event processing pipeline
router.AddHandler(
"events_processor",
"events",
subscriber,
"events_post_processing",
publisher,
eventProcessingHandler,
)
```
### Message Processing Features
* **Dead Letter Queues**: Failed messages are routed to error topics
* **Retry Logic**: Exponential backoff for transient failures
* **Rate Limiting**: Configurable processing rates per topic
* **Metrics**: Built-in monitoring for message processing
## Temporal Workflow Orchestration
Background jobs and scheduled tasks use **Temporal** for reliable execution and workflow management:
### Workflow Types
* **Billing Workflows**: Monthly/annual invoice generation
* **Usage Aggregation**: Periodic usage calculations
* **Payment Processing**: Async payment collection workflows
* **Data Exports**: Large dataset exports to S3
* **Webhook Delivery**: Reliable event notifications
### Workflow Registration
```go theme={null}
func RegisterWorkflows(worker worker.Worker) {
// Billing workflows
worker.RegisterWorkflow(billing.GenerateInvoiceWorkflow)
worker.RegisterWorkflow(billing.ProcessPaymentWorkflow)
// Usage workflows
worker.RegisterWorkflow(usage.AggregateUsageWorkflow)
// Export workflows
worker.RegisterWorkflow(export.ExportDataWorkflow)
}
```
### Worker Lifecycle
Workers are deployed separately and can be scaled independently:
```go theme={null}
func StartTemporalWorker(config *config.Config) {
client := temporal.NewClient()
worker := worker.New(client, "billing-queue", worker.Options{})
RegisterWorkflows(worker)
RegisterActivities(worker)
worker.Run(worker.InterruptCh())
}
```
## External Integrations
Flexprice uses a factory pattern for external service integrations, supporting multiple providers through specific integration implementations:
```go theme={null}
// Integration Factory creates specific integration instances
type Factory struct {
config *config.Configuration
logger *logger.Logger
}
// Specific integration getters with proper error handling
func (f *Factory) GetStripeIntegration(ctx context.Context) (*StripeIntegration, error) {
// Initialize and return Stripe integration with configuration
return NewStripeIntegration(f.config, f.logger), nil
}
func (f *Factory) GetHubSpotIntegration(ctx context.Context) (*HubSpotIntegration, error) {
// Initialize and return HubSpot integration with configuration
return NewHubSpotIntegration(f.config, f.logger), nil
}
func (f *Factory) GetRazorpayIntegration(ctx context.Context) (*RazorpayIntegration, error) {
// Initialize and return Razorpay integration with configuration
return NewRazorpayIntegration(f.config, f.logger), nil
}
```
### Webhook Delivery
Flexprice has a flexible webhook delivery system with **optional Svix integration**. By default, it uses an internal webhook system with in-memory or Kafka pub/sub:
```go theme={null}
type WebhookService struct {
config *config.Configuration
publisher publisher.WebhookPublisher
handler handler.Handler
factory payload.PayloadBuilderFactory
client httpclient.Client
logger *logger.Logger
}
func (w *WebhookService) DeliverEvent(event *Event, endpoints []WebhookEndpoint) error {
// Route based on configuration
if w.config.Webhook.SvixConfig.Enabled {
w.logger.Info("Using Svix for webhook delivery")
return w.deliverViaSvix(event)
}
w.logger.Info("Using internal webhook system")
return w.deliverViaInternalSystem(event, endpoints)
}
// Configuration controls webhook delivery method
// svix_config:
// enabled: false # Default - uses internal system
// enabled: true # Optional - uses Svix for delivery
```
## Infrastructure Services
### Monitoring & Observability
**Sentry** for error tracking and performance monitoring:
```go theme={null}
sentry.Init(sentry.ClientOptions{
Dsn: config.SentryDSN,
Environment: config.Environment,
TracesSampleRate: 0.1,
})
// Error tracking in handlers
if err != nil {
sentry.CaptureException(err)
return gin.Error{Err: err, Type: gin.ErrorTypePublic}
}
```
**Pyroscope** for continuous profiling and performance analysis:
```go theme={null}
pyroscope.Start(pyroscope.Config{
ApplicationName: "flexprice-api",
ServerAddress: config.PyroscopeURL,
ProfileTypes: []pyroscope.ProfileType{
pyroscope.ProfileCPU,
pyroscope.ProfileAllocObjects,
pyroscope.ProfileAllocSpace,
},
})
```
### Document Storage
Invoice PDFs are stored in **S3** with presigned URL generation for secure access:
```go theme={null}
func (s *S3Service) StoreInvoice(invoiceID string, pdfData []byte) (string, error) {
key := fmt.Sprintf("invoices/%s/%s.pdf", s.tenantID, invoiceID)
_, err := s.client.PutObject(&s3.PutObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
Body: bytes.NewReader(pdfData),
})
// Return presigned URL for download
return s.GeneratePresignedURL(key, 24*time.Hour)
}
```
### Caching Layer
Flexprice uses **in-memory caching only** for performance optimization. Caching can be enabled/disabled via configuration:
```go theme={null}
// Cache interface in internal/cache/
type Cache interface {
// Get retrieves a value from the cache
// Returns the value and a boolean indicating whether the key was found
Get(ctx context.Context, key string) (interface{}, bool)
// Set adds a value to the cache with the specified expiration
// If expiration is 0, the item never expires (but may be evicted)
Set(ctx context.Context, key string, value interface{}, expiration time.Duration)
// Delete removes a key from the cache
Delete(ctx context.Context, key string)
// DeleteByPrefix removes all keys with the given prefix
DeleteByPrefix(ctx context.Context, prefix string)
// Flush removes all items from the cache
Flush(ctx context.Context)
}
// Usage example
func (s *PlanService) GetPlan(ctx context.Context, planID string) (*Plan, error) {
// Check if caching is enabled
if !s.config.Cache.Enabled {
return s.repo.GetPlan(ctx, planID)
}
// Try cache first
if cached, found := s.cache.Get(ctx, planID); found {
return cached.(*Plan), nil
}
// Cache miss - fetch from database
plan, err := s.repo.GetPlan(ctx, planID)
if err == nil {
s.cache.Set(ctx, planID, plan, time.Hour)
}
return plan, err
}
// Configuration
// cache:
// enabled: true/false # Simple boolean to enable/disable caching
```
## Configuration Management
Configuration is loaded from YAML files with environment variable overrides for deployment flexibility:
```yaml theme={null}
# config.yaml
server:
port: 8080
host: "0.0.0.0"
database:
postgres:
host: ${DB_HOST:localhost}
port: ${DB_PORT:5432}
database: ${DB_NAME:flexprice}
clickhouse:
host: ${CLICKHOUSE_HOST:localhost}
port: ${CLICKHOUSE_PORT:9000}
kafka:
brokers: ${KAFKA_BROKERS:localhost:29092}
topics:
events: "events"
events_lazy: "events_lazy"
processed_events: "events_post_processing"
system_events: "system_events"
temporal:
host: ${TEMPORAL_HOST:localhost:7233}
namespace: ${TEMPORAL_NAMESPACE:default}
```
### Configuration Structure
```go theme={null}
type Configuration struct {
Deployment DeploymentConfig
Server ServerConfig
Auth AuthConfig
Kafka KafkaConfig
ClickHouse ClickHouseConfig
Logging LoggingConfig
Postgres PostgresConfig
Sentry SentryConfig
Pyroscope PyroscopeConfig
Event EventConfig
DynamoDB DynamoDBConfig
Temporal TemporalConfig
Webhook Webhook
Secrets SecretsConfig
Billing BillingConfig
S3 S3Config
Cache CacheConfig
EventProcessing EventProcessingConfig
EventProcessingLazy EventProcessingLazyConfig
EventPostProcessing EventPostProcessingConfig
FeatureUsageTracking FeatureUsageTrackingConfig
FeatureUsageTrackingLazy FeatureUsageTrackingLazyConfig
EnvAccess EnvAccessConfig
FeatureFlag FeatureFlagConfig
Email EmailConfig
}
```
## Application Lifecycle
The application lifecycle is managed by **Uber FX** with hooks for startup and shutdown:
```go theme={null}
func main() {
fx.New(
// Configuration
fx.Provide(config.Load),
fx.Provide(logger.New),
// Infrastructure
fx.Provide(database.NewPostgres),
fx.Provide(database.NewClickHouse),
fx.Provide(kafka.NewProducer),
fx.Provide(temporal.NewClient),
// Services
fx.Provide(service.NewBillingService),
fx.Provide(service.NewEventService),
// HTTP Server
fx.Provide(server.New),
fx.Invoke(server.RegisterRoutes),
// Lifecycle hooks
fx.Invoke(func(lc fx.Lifecycle, server *server.Server) {
lc.Append(fx.Hook{
OnStart: server.Start,
OnStop: server.Stop,
})
}),
).Run()
}
```
## Notes
**Key Architectural Decisions:**
1. **Dual-Database Strategy**: PostgreSQL for transactional consistency, ClickHouse for high-volume analytics
2. **Event-Driven Architecture**: Kafka enables asynchronous processing, scalability, and fault tolerance
3. **Microservices-Ready**: Deployment modes allow splitting components into separate services
4. **Strong Multi-Tenancy**: Enforced at middleware, service, and repository layers
5. **Workflow Orchestration**: Temporal provides reliable background job execution
6. **Open Integration**: Factory pattern allows multiple payment gateway implementations
The architecture is designed for **high scalability**, **reliability**, and **flexibility** to support diverse pricing models while maintaining clean separation of concerns across all layers.
# Backend Development Setup
Source: https://docs.flexprice.io/docs/contributing-guide/backend-setup
Complete guide to setting up the Flexprice backend development environment
## Prerequisites
* [Golang](https://go.dev/)
* [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/)
* Supported platforms:
* Linux-based environment
* macOS (Darwin)
* WSL under Windows
## Quick Setup with Docker Compose
The easiest way to get started is using our Docker Compose setup:
```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 command will:
1. Start all required infrastructure (PostgreSQL, Kafka, ClickHouse, Temporal)
2. Build the Flexprice application image
3. Run database migrations and initialize Kafka
4. Start all Flexprice services (API, Consumer, Worker)
## Accessing Services
Once setup is complete, you can access:
* Flexprice API: [http://localhost:8080](http://localhost:8080)
* Temporal UI: [http://localhost:8088](http://localhost:8088)
* Kafka UI: [http://localhost:8084](http://localhost:8084) (with profile 'dev')
* ClickHouse UI: [http://localhost:8123](http://localhost:8123)
## Useful Commands
```bash theme={null}
# Restart only the Flexprice services
make restart-flexprice
# Stop all services
make down
# Clean everything and start fresh
make clean-start
# Build the Flexprice image separately
make build-image
# Start only the Flexprice services
make start-flexprice
```
## Running Without Docker
If you prefer to run the application directly:
```bash theme={null}
# Start the required infrastructure
docker compose up -d postgres kafka clickhouse temporal temporal-ui
# Run the application locally
go run cmd/server/main.go
```
## Project Structure
Flexprice follows a **clean architecture** pattern with clear separation of concerns:
```
internal/
├── domain/
│ ├── events/
│ │ ├── model.go # Core event domain model
│ │ ├── repository.go # Repository interface
│ └── meter/
│ ├── model.go # Core meter domain model
│ ├── repository.go # Repository interface
├── repository/
│ ├── clickhouse/
│ │ └── event.go
│ ├── postgres/
│ │ └── meter.go
│ └── factory.go # Factory for creating repositories
├── service/
│ ├── event.go # Event service implementation
│ └── meter.go # Meter service implementation
├── api/
│ ├── v1/
│ │ ├── events.go # Event API implementation
│ │ └── meter.go # Meter API implementation
│ ├── dto/
│ │ ├── event.go
│ │ └── meter.go
│ └── router.go # API router implementation
└── cmd/server/
└── main.go # Server application entry point
└── docs/
└── ... # Documentation files
├── swagger
│ ├── swagger.yaml # Generated Swagger API specifications
```
### Layer Responsibilities
#### Domain Layer
* Contains core business logic and domain models
* Defines interfaces for repositories
* No dependencies on external packages or other layers
#### Repository Layer
* Implements data access interfaces defined in domain
* Handles database operations
#### Service Layer
* Orchestrates business operations
* Implements business logic
* Uses repository interfaces for data access
* Handles cross-cutting concerns
#### API Layer
* Handles HTTP requests/responses
* Converts between DTOs and domain models
* No business logic, only request validation and response formatting
### Key Design Principles
1. **Dependency Rule**: Dependencies only point inward. Domain layer has no outward dependencies.
2. **Interface Segregation**: Repository interfaces are defined in domain layer but implemented in repository layer.
3. **Dependency Injection**: Using fx for clean dependency management.
4. **Separation of Concerns**: Each layer has a specific responsibility.
### Example Flow
For an event ingestion:
1. **API Layer** (`/api/v1/events.go`) receives HTTP request
2. Converts DTO to domain model
3. Calls service layer
4. **Service layer** (`/service/event_service.go`) handles business logic
5. **Repository layer** persists data
6. Response flows back through the layers
### Adding New Features
1. Define domain models and interfaces in domain layer
2. Implement repository interfaces if needed
3. Add service layer logic
4. Create API handlers and DTOs
5. Register routes and dependencies
## Testing
We use `testutil` package to create test data and setup test environment.
### Testing Guidelines
#### Unit Tests
* Each package should have corresponding `*_test.go` files
* Use table-driven tests when testing multiple scenarios
* Mock external dependencies using interfaces
* Aim for >80% test coverage for critical paths
#### Integration Tests
* Place integration tests in a separate `integration` package
* Use test containers for database tests
* Integration tests should use the same config structure as production
#### Running Tests
Use the following make commands:
```bash theme={null}
make test # Run all tests
make test-verbose # Run all tests with verbose output
make test-coverage # Run all tests and generate coverage report
```
## Development Credentials
### PostgreSQL
* Host: localhost
* Port: 5432
* Database: flexprice
* Username: flexprice
* Password: flexprice123
### ClickHouse
* Host: localhost
* Port: 9000
* Database: flexprice
* Username: flexprice
* Password: flexprice123
### Kafka
* Bootstrap Server: localhost:29092
* UI: [http://localhost:8084](http://localhost:8084) (with profile 'dev')
## API Documentation
The API documentation is available in OpenAPI 3.0 format at `docs/swagger/swagger-3-0.json`.
### Setting up Postman Collection
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:
* Name: Local
* Variable: `baseUrl`
* Initial Value: `http://localhost:8080/v1`
* Current Value: `http://localhost:8080/v1`
* Variable: `apiKey`
* Initial Value: `0cc505d7b917e0b1f25ccbea029dd43f4002edfea46b7f941f281911246768fe`
* Current Value: `0cc505d7b917e0b1f25ccbea029dd43f4002edfea46b7f941f281911246768fe`
## Troubleshooting
### Docker Issues
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 [service_name]
```
### Database Connection Issues
1. Check database logs:
```bash theme={null}
docker compose logs postgres
docker compose logs clickhouse
```
2. Verify the database is running:
```bash theme={null}
docker compose ps postgres
docker compose ps clickhouse
```
### Kafka Issues
1. Verify Kafka is running:
```bash theme={null}
docker compose logs kafka
```
2. Check topic list:
```bash theme={null}
docker compose exec kafka kafka-topics --bootstrap-server kafka:9092 --list
```
3. View Kafka UI at [http://localhost:8084](http://localhost:8084)
## Additional Resources
* [Contribution Guidelines](https://github.com/flexprice/flexprice/blob/main/CONTRIBUTING.md)
* [API Documentation](https://docs.flexprice.io)
* [Code of Conduct](https://github.com/flexprice/flexprice/blob/main/CODE_OF_CONDUCT.md)
* [Flexprice Website](https://flexprice.io)
# Code of Conduct
Source: https://docs.flexprice.io/docs/contributing-guide/code-of-contuct
Community guidelines and standards
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [support@flexprice.io](mailto:support@flexprice.io) . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html)
For answers to common questions about this code of conduct, see [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq)
# Frontend Development Setup
Source: https://docs.flexprice.io/docs/contributing-guide/frontend-setup
Complete guide to setting up the Flexprice frontend development environment
This guide will help you set up the Flexprice frontend development environment and get you ready to contribute to the dashboard and user interface components.
## Getting Started
### Prerequisites
Before getting started, ensure you have:
* **Node.js 16+** and npm/yarn
* **Git** for version control
* **VS Code** (recommended) or any modern editor
* **Docker** (optional, for containerized development)
### One-Click Setup Script
```bash theme={null}
# Clone the flexprice frontend repository
git clone https://github.com/flexprice/flexprice-front
cd flexprice-front
# Run the automated setup script
./setup
```
### Alternative: Install Latest Release
```bash theme={null}
# Download and install the latest release
curl -s https://api.github.com/repos/flexprice/flexprice-front/releases/latest | grep "browser_download_url.*tar.gz" | cut -d '"' -f 4 | wget -qi -
tar -xzf flexprice-front-*.tar.gz
cd flexprice-front-*
./setup
```
The setup script will automatically:
✅ Set up environment variables\
✅ Install all dependencies\
✅ Build Docker image (if Docker is available)\
✅ Start the development server\
✅ Open your browser to [http://localhost:3000](http://localhost:3000)
## Manual Development Setup
### Clone & Install
```bash theme={null}
git clone https://github.com/flexprice/flexprice-front
cd flexprice-front
npm install
```
### Environment Setup
```bash theme={null}
# Copy environment template
cp .env.example .env
# Configure these variables in .env.local:
VITE_SUPABASE_URL=your-supabase-url
VITE_SUPABASE_ANON_KEY=your-supabase-anon-key
VITE_API_URL=http://localhost:8080/v1 # or
VITE_ENVIRONMENT=development
```
### Start Development
```bash theme={null}
npm run dev
```
Visit [http://localhost:3000](http://localhost:3000) to see your app running!
## Project Structure
```
src/
├── components/ # UI Components
│ ├── atoms/ # Basic UI elements
│ │ ├── Button/
│ │ ├── Input/
│ │ └── Card/
│ ├── molecules/ # Composite components
│ │ ├── Forms/
│ │ ├── Charts/
│ │ └── Tables/
│ └── organisms/ # Complex UI sections
│ ├── Dashboard/
│ ├── Billing/
│ └── Analytics/
├── pages/ # Route components
├── hooks/ # Custom React hooks
├── store/ # State management
├── utils/ # Helper functions
├── models/ # TypeScript types
└── core/ # Core business logic
```
## Self-Hosting Guide
### Docker Deployment
```bash theme={null}
# Build the Docker image
docker build -t flexprice-frontend .
# Run the container
docker run -p 80:80 \
-e VITE_API_URL=your-api-url \
-e VITE_AUTH_DOMAIN=your-auth-domain \
flexprice-frontend
```
### Manual Deployment
```bash theme={null}
# Build the application
npm run build
# Serve the static files
# Using nginx
cp nginx.conf /etc/nginx/conf.d/flexprice.conf
nginx -s reload
# Or using serve
npx serve -s dist
```
## Available Scripts
```bash theme={null}
# Development
npm run dev # Start development server
npm run build # Build for production
npm run preview # Preview production build
# Code Quality
npm run lint # Run ESLint
npm run lint:fix # Fix ESLint errors
npm run format # Format with Prettier
```
## Common Development Tasks
### Adding New Features
1. **Create a feature branch:**
```bash theme={null}
git checkout -b feat/new-feature
```
2. **Create component structure:**
```bash theme={null}
mkdir -p src/components/organisms/NewFeature
touch src/components/organisms/NewFeature/index.tsx
touch src/components/organisms/NewFeature/NewFeature.test.tsx
```
3. **Add route (if needed):**
```typescript theme={null}
// src/core/routes/Routes.tsx
import NewFeature from '@/components/organisms/NewFeature'
// Add to routes array
{
path: '/new-feature',
element:
}
```
### Styling Components
We use Tailwind CSS with custom configurations:
```typescript theme={null}
// Example component with Tailwind
const Button = ({ children }) => (
);
```
## Troubleshooting
### Common Issues
#### Build Failures
```bash theme={null}
# Clear dependencies and cache
rm -rf node_modules
rm -rf .vite
npm install
```
#### Stale Development Server
```bash theme={null}
# Reset development server
rm -rf node_modules
rm -rf .vite
npm install
npm run dev
```
# Contributing Guide
Source: https://docs.flexprice.io/docs/contributing-guide/introduction
Learn how to contribute to this open source project
Thank you for your interest in contributing to this project! We welcome contributions from developers of all skill levels. This guide will help you get started with contributing to our codebase.
## Getting Started
### Prerequisites
Before you start contributing, make sure you have:
* A GitHub account
* Git installed on your local machine
* Basic knowledge of the technologies used in this project
* Familiarity with the command line
### Flexprice Repositories
* **Backend**: [https://github.com/flexprice/flexprice](https://github.com/flexprice/flexprice) - Core Flexprice backend services
* **Frontend**: [https://github.com/flexprice/flexprice-front](https://github.com/flexprice/flexprice-front) - Dashboard and user interface
* **Documentation**: [https://github.com/flexprice/flexprice-docs](https://github.com/flexprice/flexprice-docs) - Documentation site
Each repository has its own setup instructions in the respective README files.
## Types of Contributions
We welcome various types of contributions:
* **Bug fixes** - Help us identify and fix issues
* **Feature development** - Implement new features
* **Documentation** - Improve or add documentation
* **Testing** - Add or improve test coverage
* **Code refactoring** - Improve code quality and performance
* **UI/UX improvements** - Enhance user experience
## Before You Start
1. **Check existing issues** to see if your idea or bug report already exists
2. **Create an issue** if you're planning a significant change
3. **Discuss your approach** with maintainers before starting work
4. **Read the code of conduct** to understand our community standards
## Getting Help
If you need help or have questions:
* Check the existing documentation at [docs.flexprice.io](https://docs.flexprice.io)
* Look through existing issues and discussions in the relevant repository
* Create a new issue with the "question" label
* **Join our community**: [Flexprice Community Slack](https://flexpricecommunity.slack.com) - Connect with other contributors and maintainers
## Community Links
* **Slack Community**: [https://flexpricecommunity.slack.com](https://flexpricecommunity.slack.com)
* **GitHub Organization**: [https://github.com/flexprice](https://github.com/flexprice)
* **Website**: [https://flexprice.io](https://flexprice.io)
* **Documentation**: [https://docs.flexprice.io](https://docs.flexprice.io)
We’re here to support you every step of the way.
# Archive a customer
Source: https://docs.flexprice.io/docs/customers/archive
As businesses evolve, some customers may become inactive due to churn, contract expiration, or strategic decisions. Instead of permanently deleting customer data, archiving allows you to preserve historical records while preventing further transactions or modifications. This ensures compliance with data retention policies while keeping your active customer list clean and manageable.
**Steps to archive a customer**
* Click on **Customers** to view the list of registered customers.
* Click on the three-dot menu next to the customer you want to archive
* Select Archive from the dropdown options
* A confirmation dialog will appear asking if you want to archive the customer.
* Click **Archive** to proceed, or **Cancel** if you change your mind.
Please note:
* **No New Transactions**: The customer can no longer be billed or invoiced.
* **Existing Subscriptions End After the Current Cycle**: Any ongoing subscriptions will continue until the end of the current billing period and will not renew.
* **Data Retention**: Customer details, invoices, and payment history remain accessible for reporting purposes.
# Creating a customer
Source: https://docs.flexprice.io/docs/customers/create
To create a new customer in Flexprice, follow these steps:
Navigate to the customers dashboard by clicking on Customers from the left navigation panel.
* Click on "**+ Add”**. This opens the Add Customer form
* **Enter Customer Details**
* **Name**: The name of the customer.
* **External Customer ID**: A unique identifier (useful for mapping to external systems).
* **Email (Optional)**: Customer’s contact email for invoices and notifications.
* Click on **Add Billing Details (Optional) and enter:**
* **Country**: The country for tax and compliance purposes.
* **Address Line 1 & 2**: The billing address.
* **State & City**: The customer’s location.
* Click on **Save Customer** to complete the process.
Once created, the customer appears in the list with an **Active** status. You are not required to fill in all details initially; additional information can be added later as needed.
# Customer Portal
Source: https://docs.flexprice.io/docs/customers/customer-portal
Generate secure sessions for customers to access their billing information
The Customer Portal is a self-service interface that gives your customers secure access to their billing information, account details, and usage analytics. Instead of contacting support for every billing question, customers can independently view their subscriptions, invoices, wallet balances, and usage data.
## What is the Customer Portal?
The Customer Portal empowers your customers to manage their account information independently. Through a secure, branded interface, customers can:
* **View account overview** - See wallet balances, active subscriptions, and usage at a glance
* **Track credits and transactions** - Monitor prepaid wallet balances and transaction history
* **Access invoices** - View, search, and download invoices with payment status
* **Analyze usage** - Understand feature-level usage patterns and associated costs
This self-service approach reduces support burden while giving customers transparency and control over their billing information.
**Secure Access**
The portal uses secure session tokens that expire after 1 hour, ensuring customer data remains protected while providing convenient access.
## Portal Overview
The Customer Portal is organized into four main sections, each accessible via tabs at the top of the interface:
1. **Overview** - Get a quick snapshot of your account: wallet balance, active subscriptions, and usage trends
2. **Credits** - View detailed prepaid wallet information and complete transaction history
3. **Invoices** - Browse, search, and download all invoices with payment status
4. **Usage** - Analyze feature-level usage patterns and associated costs over time
Each section provides read-only access to your billing and account information, giving you full visibility into your account status without needing to contact support.
## Generating a Portal Session
To give a customer access to their portal, you need to generate a secure session token. This token authenticates the customer and provides temporary access to their portal.
**When to generate a session:**
* When a customer requests access to their billing information
* After customer onboarding to provide self-service access
* When redirecting customers from your application to the portal
* When a previous session token has expired
**Endpoint**: `GET /v1/customers/portal/{external_id}`
**Authentication**: Requires your Flexprice API key
**Headers**:
```
x-api-key:
```
**Path Parameters**:
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ---------------------- |
| `external_id` | string | Yes | Customer's external ID |
### Example Request
```bash cURL theme={null}
curl --request GET \
--url https://api.cloud.flexprice.io/v1/customers/portal/cust_123 \
--header 'x-api-key: '
```
```javascript JavaScript theme={null}
const response = await fetch(
"https://api.cloud.flexprice.io/v1/customers/portal/cust_123",
{
method: "GET",
headers: {
"x-api-key": process.env.FLEXPRICE_API_KEY,
},
}
);
const session = await response.json();
console.log("Token:", session.token);
console.log("Expires at:", session.expires_at);
```
```python Python theme={null}
import requests
response = requests.get(
'https://api.cloud.flexprice.io/v1/customers/portal/cust_123',
headers={
'x-api-key': os.environ['FLEXPRICE_API_KEY']
}
)
session = response.json()
print(f"Token: {session['token']}")
print(f"Expires at: {session['expires_at']}")
```
### Response
```json theme={null}
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"url": "https://portal.yourapp.com?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_at": "2025-01-06T17:27:17Z"
}
```
**Response Fields**:
| Field | Type | Description |
| ------------ | ------ | -------------------------------------------------------- |
| `token` | string | JWT token for authenticating customer portal requests |
| `url` | string | Pre-built URL with the token (optional, for convenience) |
| `expires_at` | string | ISO 8601 timestamp when the token expires |
**Session Tokens Expire**
Portal session tokens typically expire after 1 hour. You'll need to create a new session when the token expires.
**Redirecting customers to the portal:**
Once you receive the session response, redirect your customer to the `url` field in the response. This pre-built URL includes the token and will automatically authenticate the customer when they access it.
```javascript theme={null}
// Example: Redirecting customer to portal
const session = await response.json();
window.location.href = session.url; // Redirects customer to portal
```
## Accessing the Portal from the UI Dashboard
You can also access the customer portal directly from the Flexprice dashboard without making API calls. There are two ways to open a customer's portal from the UI:
### From the Customers Page
1. Navigate to the **Customers** page in your dashboard
2. Find the customer in the table
3. Click the **three-dot menu (⋮)** at the end of the customer's row
4. Select **"Open Customer Portal"** from the dropdown menu
This will automatically generate a portal session and open the customer's portal in a new tab or window.
### From the Customer Details Page
1. Navigate to the **Customers** page
2. Click on a customer to open their details page
3. Go to the **"Information"** tab (or any tab in the customer details view)
4. In the Customer Details section, you'll find options to:
* **Copy the portal link** - Click the share icon to copy the portal URL to your clipboard
* **Open the portal directly** - The portal link can be shared with the customer or opened directly
**Benefits of UI access:**
* **Quick access** - No need to write code or make API calls
* **One-click opening** - Instantly generate and open portal sessions
* **Easy sharing** - Copy portal links to share with customers via email or other channels
* **Automatic session management** - The UI handles token generation and expiration automatically
**Sharing Portal Links**
When you copy the portal link from the customer details page, you can share it directly with your customer via email, chat, or any other communication channel. The link includes an embedded session token that expires after 1 hour for security.
## Using the Portal Token
Once you have the session token, you can redirect your customer to the portal URL provided in the response. The portal handles authentication automatically using the token embedded in the URL.
### Redirecting Customers
The simplest approach is to use the `url` field from the session response, which includes the token as a query parameter:
```javascript theme={null}
// Example: Redirecting customer to portal
const response = await fetch(
`https://api.cloud.flexprice.io/v1/customers/portal/${customerExternalId}`,
{
method: "GET",
headers: {
"x-api-key": process.env.FLEXPRICE_API_KEY,
},
}
);
const session = await response.json();
// Redirect customer to portal
window.location.href = session.url;
```
### Embedding the Portal
You can also embed the portal in an iframe within your application:
```html theme={null}
```
### Handling Token Expiration
Since portal tokens expire after 1 hour, you should:
1. **Generate tokens on-demand** - Create a new session when the customer requests portal access
2. **Handle expiration gracefully** - If a customer's session expires, generate a new token and redirect them again
3. **Monitor token lifetime** - Use the `expires_at` field to track when tokens will expire
```javascript theme={null}
// Example: Checking token expiration
const session = await response.json();
const expiresAt = new Date(session.expires_at);
const now = new Date();
if (expiresAt > now) {
// Token is still valid, redirect to portal
window.location.href = session.url;
} else {
// Token expired, generate a new one
// ... generate new session
}
```
### Direct API Access
If you're building a custom portal interface, you can use the session token directly in API requests. Include the token in the `X-Session-Token` header:
```
X-Session-Token:
```
The customer portal provides read-only access to:
* Customer profile information
* Active and past subscriptions
* Invoices and payment history
* Wallet balances and transactions
* Usage analytics and cost data
## Overview Tab
The Overview tab provides a comprehensive snapshot of your account at a glance. This is the default view when customers first access the portal.
### Prepaid Wallet
The Prepaid Wallet section displays your current credit balance in two formats:
* **Credits** - The raw credit amount (e.g., "434.2 credits")
* **Dollar Value** - The monetary equivalent of your credits (e.g., "\$43.42 value")
This dual display helps you understand both your credit balance and its real-world value. Credits are typically used for metered billing, where usage is deducted from your prepaid balance.
### Subscriptions
The Subscriptions section shows your active subscription plan:
* **Plan Name** - The name of your current subscription plan (e.g., "Plan Base")
* **Status** - Current subscription status, typically "Active" for active subscriptions
* **Subscription Period** - The current billing period with start and end dates (e.g., "Jan 6, 2026 - Feb 6, 2026")
* **Next Billing** - The date when your next billing cycle begins
This information helps you track your subscription status and know when your next billing will occur.
### Usage Graph
The Usage section includes an interactive graph showing your usage trends over time:
* **Time Range Selector** - Choose between 1 day (1d), 7 days (7d), or 30 days (30d) views
* **Usage Visualization** - A line graph showing usage patterns over the selected time period
* **Y-Axis** - Represents usage quantity (e.g., 0 to 6,000 units)
* **X-Axis** - Represents time (dates)
The graph helps you identify usage patterns, spikes, and trends. Use the time range selector to zoom in on recent activity or get a broader view of your usage over the past month.
## Credits Tab
The Credits tab provides detailed information about your prepaid wallet, including balance details and complete transaction history.
### Prepaid Wallet Details
At the top of the Credits tab, you'll see your Prepaid Wallet summary:
* **Balance** - Your current credit balance displayed prominently
* **Status** - Wallet status indicator (typically "Active" for active wallets)
* **Dollar Value** - The monetary equivalent of your credits
This gives you a quick reference for your available balance before diving into transaction details.
### Transaction History
The Transaction History table provides a complete record of all credit-related transactions:
| Column | Description |
| ---------------- | ------------------------------------------------------------------- |
| **Transactions** | Type of transaction (e.g., "Invoice Payment", "Free Credits Added") |
| **Payment Date** | When the transaction occurred |
| **Expiry Date** | When credits expire (if applicable) |
| **Priority** | Transaction priority level |
| **Amount** | Transaction amount in both dollars and credits |
**Transaction Types:**
* **Invoice Payment** - Credits deducted when paying invoices (shown as negative amounts, e.g., "-20 \$ / -200 credits")
* **Free Credits Added** - Credits added to your account (shown as positive amounts, e.g., "+100 \$ / +1000 credits")
The dual currency display (dollars and credits) helps you understand both the monetary impact and credit balance changes. Negative amounts indicate deductions, while positive amounts indicate additions to your wallet.
## Invoices Tab
The Invoices tab allows you to view, search, and download all your invoices with their payment status.
### Invoice Table
The invoice table displays all your invoices with the following information:
| Column | Description |
| ------------- | --------------------------------------------------------------- |
| **DATE** | The date the invoice was issued |
| **INVOICE #** | Unique invoice identification number (e.g., "INV-202601-00003") |
| **STATUS** | Payment status of the invoice |
| **AMOUNT** | Total invoice amount |
| **DOWNLOAD** | Icon to download the invoice PDF |
### Invoice Status
Invoices display their payment status with color-coded indicators:
* **Paid** (Green) - Invoice has been successfully paid
* **Pending** (Orange) - Invoice is awaiting payment
This visual status helps you quickly identify which invoices need attention and which are complete.
### Search Functionality
Use the search bar at the top to quickly find specific invoices. You can search by:
* Invoice number
* Date range
* Amount
* Status
Simply type your search term in the "Search invoices..." field to filter the invoice list.
### Downloading Invoices
Click the download icon in the **DOWNLOAD** column to save a PDF copy of any invoice. This is useful for:
* Record keeping
* Accounting purposes
* Dispute resolution
* Tax documentation
## Usage Tab
The Usage tab provides detailed analytics about your feature-level usage and associated costs. When features are assigned to [groups](/docs/product-catalogue/groups), `Usage Breakdown` show data by group (e.g. GPU Usage, Add-ons).
### Usage Graph
The interactive usage graph visualizes your usage patterns over time:
* **Time Range Selector** - Choose between 1 day (1d), 7 days (7d), or 30 days (30d) views
* **Feature-Level Tracking** - Each feature is represented by a different color on the graph
* **Interactive Tooltips** - Hover over data points to see detailed information:
* Exact date and time
* Feature name
* Usage quantity at that point
The graph helps you:
* Identify usage spikes and patterns
* Compare usage across different time periods
* Understand when peak usage occurs
* Track usage trends over time
### Usage Breakdown Table
Below the graph, the Usage Breakdown table provides feature-level usage statistics:
| Column | Description |
| --------------- | ------------------------------------------------------------------ |
| **Feature** | The name of the feature being tracked (e.g., "Feature B") |
| **Total Usage** | Total usage quantity with unit (e.g., "5,911 calls") |
| **Events** | Number of usage events recorded |
| **Total Cost** | Total cost associated with this feature's usage (e.g., "\$141.86") |
This breakdown helps you:
* Understand which features consume the most resources
* Calculate cost per feature
* Identify optimization opportunities
* Track usage efficiency
**Interpreting the Data:**
* **Total Usage** shows the cumulative usage quantity for the selected time period
* **Events** indicates how many individual usage events contributed to the total
* **Total Cost** represents the billing amount for this feature's usage, helping you understand the financial impact
## Understanding Your Data
This section helps you interpret the information displayed throughout the portal.
### Wallet Balances
Your prepaid wallet balance represents credits available for billing. When you use services, credits are deducted from this balance. The dollar value shows the monetary equivalent, helping you understand the real-world value of your credits.
**Key points:**
* Credits are deducted when invoices are paid or services are used
* Free credits and top-ups increase your balance
* Monitor your balance to ensure sufficient credits for continued service
### Subscription Status
Your subscription status indicates the current state of your plan:
* **Active** - Your subscription is active and billing will continue as scheduled
* The subscription period shows your current billing cycle
* Next billing date indicates when your next charge will occur
### Usage Graphs
Usage graphs help you visualize consumption patterns:
* **Spikes** indicate periods of high usage
* **Trends** show whether usage is increasing, decreasing, or stable
* **Time range selection** lets you zoom in on recent activity or view longer-term patterns
* **Feature colors** help distinguish between different features on the same graph
### Cost Tracking
Understanding costs helps you manage your budget:
* **Total Cost** in the Usage Breakdown shows billing for each feature
* **Invoice amounts** show what you've been charged
* **Transaction history** shows credit deductions and additions
* Compare usage to costs to understand your cost per unit of usage
## Portal Features
The Customer Portal provides self-service access to your billing information, but it has specific capabilities and limitations.
### What You Can Do
* **View account information** - See your customer profile, subscriptions, and account details
* **Monitor wallet balances** - Track prepaid credits and transaction history
* **Access invoices** - View, search, and download all invoices
* **Analyze usage** - Review feature-level usage patterns and costs
* **Track subscriptions** - Monitor active subscriptions and billing cycles
### Limitations
The portal provides **read-only access**. You cannot:
* Modify subscription plans
* Update payment methods
* Change billing information
* Cancel subscriptions
* Make payments
These actions require contacting your account administrator or support team.
### When to Contact Support
Contact support when you need to:
* Change your subscription plan
* Update payment methods or billing information
* Cancel or pause your subscription
* Resolve billing disputes
* Request refunds or credits
* Get help interpreting your usage data
* Report technical issues with the portal
## Related Resources
Learn about customer management →
Understand subscription management →
Learn about invoice management →
Understand prepaid wallets →
View full API documentation →
# Overview
Source: https://docs.flexprice.io/docs/customers/overview
A **Customer** in Flexprice represents a business or individual that subscribes to your plans and is billed for usage. Each customer is uniquely identified and can be assigned to specific pricing plans and entitlements.
**Why Manage Customers in Flexprice?**
* **Track subscriptions**: Associate customers with specific plans and features.
* **Automate billing**: Generate invoices based on usage.
* **Manage entitlements**: Control feature access at an individual level.
* **Analyze usage**: Monitor consumption for better insights.
Customers can be created manually in the dashboard or through API calls for automation.
To configure **Customer Hierarchy** on a subscription (for example consolidated billing across subsidiaries, or a delegated payer who pays on behalf of another customer), see [Customer Hierarchy](/docs/subscriptions/customer-hierarchy). That guide also explains how **usage, analytics, wallet balance, and invoices** behave in **consolidated** (inherited) setups versus **delegated payer**, including why child customers may show no usage in usage-summary APIs and how to use analytics with `include_children` for parent-level rollups.
# Alerts and Notifications
Source: https://docs.flexprice.io/docs/customers/threshold-notifications
Configure proactive threshold-based alerts for wallet balances and usage — at the customer level or individual feature level — with real-time webhook delivery on every state change.
Flexprice provides four types of threshold alerts so you can stay ahead of credit exhaustion, budget overruns, and usage limits. Alerts can monitor wallet balances (how much prepaid credit remains) or usage (how much has been consumed during a billing cycle) — and both can be scoped either to the overall customer or to a specific feature.
All four alert types share the same three-level model (`info` → `warning` → `critical`) and deliver webhook notifications on every state transition, giving you a single consistent pattern to build on.
## Alert Types at a Glance
| Alert Type | What is Monitored | Scope | Webhook Event |
| ----------------------------------------------------------------- | ---------------------------------------------- | -------- | ------------------------------ |
| [Low Wallet Balance](#1-low-wallet-balance-alert) | Customer prepaid wallet balance | Customer | `wallet.alert` |
| [Low Feature Wallet Balance](#2-low-feature-wallet-balance-alert) | Feature-specific prepaid balance | Feature | `feature.wallet_balance.alert` |
| [Total Usage](#3-total-usage-alert) | Cumulative spend in a billing cycle | Customer | `customer.usage.alert` |
| [Feature-Level Usage](#4-feature-level-usage-alert) | Usage of a specific feature in a billing cycle | Feature | `feature.usage.alert` |
***
## Alert Levels and States
Every alert configuration supports up to three independent threshold levels:
| Level | API State | Purpose |
| ------------ | ---------- | ------------------------------------------------------------- |
| **Info** | `info` | Early milestone — balance or usage is approaching a limit |
| **Warning** | `warning` | Action needed — balance or usage is significantly low or high |
| **Critical** | `in_alarm` | Immediate action required — service continuity may be at risk |
| **OK** | `ok` | Healthy — no thresholds breached |
### Conditions
Each threshold is paired with a **condition** that controls which direction triggers the alert:
| Condition | Triggers when | Typical use case |
| --------- | ----------------- | ------------------------------------------- |
| `below` | value ≤ threshold | Wallet depletion, prepaid credit exhaustion |
| `above` | value ≥ threshold | Budget cap, billing cycle spend limits |
### Threshold Ordering Rules
**For `below` conditions** (monitoring depletion):
```
Critical < Warning < Info
```
**For `above` conditions** (monitoring accumulation):
```
Critical > Warning > Info
```
A Warning threshold requires a Critical threshold. You cannot configure Warning without Critical. Info can be set independently.
### State Transitions
Alerts transition automatically as values change. Webhooks fire on **every state change** — including recovery back to `ok`.
```
ok ──► info ──► warning ──► in_alarm
◄──────────────────────────────────
(recovery)
```
State transitions can skip levels. For example, a sudden large debit may move a wallet directly from `ok` to `in_alarm`, bypassing `info` and `warning`.
***
## 1. Low Wallet Balance Alert
Monitor the **prepaid wallet balance** of a customer. This alert fires when the overall credit balance crosses a configured threshold — ideal for preventing service interruptions caused by depleted prepaid credits.
### When to Use
* Customer has a prepaid wallet shared across all their features
* You want to notify customers (or trigger auto top-up) before credits run out
* You need to monitor overall credit health across a billing cycle
### Configuration
Configure thresholds when creating or updating a wallet via the API:
```json theme={null}
{
"alert_enabled": true,
"alert_threshold_name": "info",
"alert_thresholds": [
{ "name": "info", "threshold": "200.00", "condition": "below" },
{ "name": "warning", "threshold": "100.00", "condition": "below" },
{ "name": "critical", "threshold": "0.00", "condition": "below" }
]
}
```
**Example scenario:** A customer starts with \$1,000 in prepaid credits.
| Balance | Alert state | Recommended action |
| ------- | ----------- | --------------------------------------------- |
| ≤ \$200 | `info` | Notify customer to review credit balance |
| ≤ \$100 | `warning` | Prompt top-up or trigger auto top-up |
| ≤ \$0 | `in_alarm` | Block new usage or notify on-call immediately |
### Webhook Payload
**Event type:** `wallet.alert`
```json theme={null}
{
"event_type": "wallet.alert",
"alert_type": "wallet_balance",
"alert_status": "warning",
"wallet": {
"id": "wallet_01K8DC3ZAH6R55GTPFTWSR2EHF",
"customer_id": "cust_01K8DBYJ0ST5GD57EB6EATEACE",
"name": "Prepaid Wallet",
"wallet_type": "PRE_PAID",
"balance": "85.00",
"credit_balance": "85.00",
"currency": "usd",
"alert_enabled": true,
"alert_state": "warning",
"alert_thresholds": [
{ "name": "info", "threshold": "200.00", "condition": "below" },
{ "name": "warning", "threshold": "100.00", "condition": "below" },
{ "name": "critical", "threshold": "0.00", "condition": "below" }
],
"wallet_status": "active",
"created_at": "2025-10-25T09:45:12.273046Z",
"updated_at": "2025-10-25T11:14:15.925198Z"
}
}
```
### Webhook Fields
| Field | Description |
| ------------------------- | ------------------------------------------------------------ |
| `alert_status` | Current alert state: `ok`, `info`, `warning`, or `in_alarm` |
| `alert_type` | `wallet_balance` |
| `wallet.balance` | Current total wallet balance |
| `wallet.credit_balance` | Current credit balance |
| `wallet.alert_state` | Same as `alert_status` — reflects state on the wallet object |
| `wallet.alert_thresholds` | Full threshold configuration at time of event |
### Use Cases
* **Prepaid SaaS** — alert at $200, $100, \$0 so customers can top up before service is affected
* **API credit products** — send in-app notification at `info`, email at `warning`, pause at `in_alarm`
* **Combined with auto top-up** — use `warning` as the trigger for automated wallet recharge
***
## 2. Low Feature Wallet Balance Alert
Monitor the **balance of a feature-specific wallet**. Each feature can have its own prepaid allocation; this alert fires when that feature's balance crosses a threshold — independently of the customer's overall wallet.
### When to Use
* Different features have separate credit allocations
* You want independent spend monitoring per service (e.g. "Compute Credits" separate from "Storage Credits")
* You need to notify on a per-feature basis rather than at the aggregate customer level
### Configuration
Configure thresholds on the feature's alert settings:
```json theme={null}
{
"alert_enabled": true,
"critical": { "threshold": "0.00", "condition": "below" },
"warning": { "threshold": "10.00", "condition": "below" },
"info": { "threshold": "20.00", "condition": "below" }
}
```
**Example scenario:** A feature wallet for "Compute Credits" is loaded with \$500.
| Feature Balance | Alert state | Recommended action |
| --------------- | ----------- | -------------------------------- |
| ≤ \$20 | `info` | Log and monitor — getting low |
| ≤ \$10 | `warning` | Notify team, consider topping up |
| ≤ \$0 | `in_alarm` | Throttle or block feature access |
### Webhook Payload
**Event type:** `feature.wallet_balance.alert`
```json theme={null}
{
"alert_status": "in_alarm",
"alert_type": "feature_wallet_balance",
"event_type": "feature.wallet_balance.alert",
"feature": {
"id": "feat_01K8DBMBN87CY36DEFY62PEXBT",
"name": "Compute Credits",
"type": "metered",
"alert_settings": {
"alert_enabled": true,
"critical": { "condition": "below", "threshold": "0.00" },
"warning": { "condition": "below", "threshold": "10.00" },
"info": { "condition": "below", "threshold": "20.00" }
},
"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": "Compute Credits Wallet",
"wallet_type": "PRE_PAID",
"balance": "-2.50",
"credit_balance": "-2.50",
"currency": "usd",
"alert_enabled": true,
"alert_state": "in_alarm",
"wallet_status": "active",
"customer_id": "cust_01K8DBYJ0ST5GD57EB6EATEACE",
"created_at": "2025-10-25T09:45:12.273046Z",
"updated_at": "2025-10-25T09:58:22.259833Z"
}
}
```
### Webhook Fields
| Field | Description |
| ------------------------ | ----------------------------------------------------------- |
| `alert_status` | Current alert state: `ok`, `info`, `warning`, or `in_alarm` |
| `alert_type` | `feature_wallet_balance` |
| `feature.id` | Unique identifier of the feature |
| `feature.name` | Name of the feature being monitored |
| `feature.alert_settings` | Full alert configuration including all thresholds |
| `wallet.balance` | Current feature wallet balance (may be negative) |
| `wallet.alert_state` | Current alert state reflected on the wallet object |
### Use Cases
* **Per-feature credit allocations** — independently monitor separate feature budgets without cross-contamination
* **Storage vs compute** — alert on compute credits independently from storage credits
* **Tenant isolation** — set different depletion thresholds per feature based on criticality
***
## 3. Total Usage Alert
Monitor **cumulative usage spend** for a customer across their active billing cycle. Unlike wallet balance alerts (which track remaining credits), usage alerts track how much has been consumed — making them ideal for enforcing soft spend caps or budget limits.
### When to Use
* Customer has a monthly or annual spend budget
* You want to warn customers before they exceed a contracted usage limit
* You need to enforce soft or hard caps on total billing cycle spend
### Configuration
Configure thresholds on the customer's usage alert settings:
```json theme={null}
{
"alert_enabled": true,
"alert_thresholds": [
{ "name": "info", "threshold": "1000.00", "condition": "above" },
{ "name": "warning", "threshold": "4000.00", "condition": "above" },
{ "name": "critical", "threshold": "5000.00", "condition": "above" }
]
}
```
**Example scenario:** A customer has a \$5,000 monthly spend budget.
| Cumulative Spend | Alert state | Recommended action |
| ---------------- | ----------- | -------------------------------------------- |
| ≥ \$1,000 | `info` | Log spend milestone — 20% of budget reached |
| ≥ \$4,000 | `warning` | Notify account team — 80% of budget consumed |
| ≥ \$5,000 | `in_alarm` | Enforce cap or escalate to customer |
### Webhook Payload
**Event type:** `customer.usage.alert`
```json theme={null}
{
"event_type": "customer.usage.alert",
"alert_type": "customer_usage",
"alert_status": "warning",
"customer": {
"id": "cust_01K8DBYJ0ST5GD57EB6EATEACE",
"external_id": "acme-corp",
"name": "Acme Corp"
},
"subscription": {
"id": "sub_01K8DBMBN87CY36DEFY62PEXBT",
"plan_id": "plan_01K8DBMBN5NRFNYBMXN1G3WDWY",
"billing_period_start": "2025-11-01T00:00:00Z",
"billing_period_end": "2025-11-30T23:59:59Z"
},
"usage": {
"current_usage_amount": "4250.00",
"currency": "usd",
"billing_period_start": "2025-11-01T00:00:00Z",
"billing_period_end": "2025-11-30T23:59:59Z"
},
"alert_settings": {
"alert_enabled": true,
"alert_state": "warning",
"alert_thresholds": [
{ "name": "info", "threshold": "1000.00", "condition": "above" },
{ "name": "warning", "threshold": "4000.00", "condition": "above" },
{ "name": "critical", "threshold": "5000.00", "condition": "above" }
]
}
}
```
### Webhook Fields
| Field | Description |
| ----------------------------------- | ----------------------------------------------------------- |
| `alert_status` | Current alert state: `ok`, `info`, `warning`, or `in_alarm` |
| `alert_type` | `customer_usage` |
| `customer.id` | Unique identifier of the customer |
| `customer.external_id` | Your system's customer identifier |
| `subscription.billing_period_start` | Start of the billing cycle being monitored |
| `subscription.billing_period_end` | End of the billing cycle being monitored |
| `usage.current_usage_amount` | Cumulative spend amount in this billing cycle |
| `usage.currency` | Currency of the usage amount |
| `alert_settings.alert_state` | Current alert state |
### Use Cases
* **Spend caps for enterprise customers** — alert at 50%, 80%, and 100% of a contracted spend limit
* **Proactive customer communication** — send automated emails at `info`, outreach at `warning`, and sales escalation at `in_alarm`
* **Soft vs hard limits** — use `warning` to notify and `in_alarm` to enforce a hard stop via the API
***
## 4. Feature-Level Usage Alert
Monitor **cumulative usage of a specific feature** within a billing cycle. This gives you granular visibility into per-feature consumption — independent of the customer's overall usage — so you can track individual API endpoints, compute resources, or storage usage separately.
### When to Use
* Different features have separate usage budgets or rate limits
* You want feature-specific alerts without noise from unrelated usage
* You need to alert on physical units (e.g. API calls, GB, minutes) rather than monetary amounts
### Configuration
Configure thresholds on the feature's usage alert settings:
```json theme={null}
{
"alert_enabled": true,
"alert_thresholds": [
{ "name": "info", "threshold": "250000", "condition": "above" },
{ "name": "warning", "threshold": "750000", "condition": "above" },
{ "name": "critical", "threshold": "1000000","condition": "above" }
]
}
```
**Example scenario:** A customer has a contract for up to 1,000,000 API calls per month.
| API Calls Used | Alert state | Recommended action |
| -------------- | ----------- | ------------------------------------------ |
| ≥ 250,000 | `info` | 25% consumed — normal tracking |
| ≥ 750,000 | `warning` | 75% consumed — notify customer |
| ≥ 1,000,000 | `in_alarm` | Limit reached — enforce rate cap or upsell |
### Webhook Payload
**Event type:** `feature.usage.alert`
```json theme={null}
{
"event_type": "feature.usage.alert",
"alert_type": "feature_usage",
"alert_status": "warning",
"feature": {
"id": "feat_01K8DBMBN87CY36DEFY62PEXBT",
"name": "API Calls",
"type": "metered",
"meter_id": "meter_01K8DBMBN5NRFNYBMXN1G3WDWY",
"alert_settings": {
"alert_enabled": true,
"alert_state": "warning",
"alert_thresholds": [
{ "name": "info", "threshold": "250000", "condition": "above" },
{ "name": "warning", "threshold": "750000", "condition": "above" },
{ "name": "critical", "threshold": "1000000", "condition": "above" }
]
},
"status": "published",
"created_at": "2025-10-25T09:36:40.616655Z",
"updated_at": "2025-10-25T11:14:15.925198Z"
},
"customer": {
"id": "cust_01K8DBYJ0ST5GD57EB6EATEACE",
"external_id": "acme-corp"
},
"subscription": {
"id": "sub_01K8DBMBN87CY36DEFY62PEXBT",
"billing_period_start": "2025-11-01T00:00:00Z",
"billing_period_end": "2025-11-30T23:59:59Z"
},
"usage": {
"current_usage": "762450",
"unit": "calls",
"billing_period_start": "2025-11-01T00:00:00Z",
"billing_period_end": "2025-11-30T23:59:59Z"
}
}
```
### Webhook Fields
| Field | Description |
| ------------------------------------ | ----------------------------------------------------------- |
| `alert_status` | Current alert state: `ok`, `info`, `warning`, or `in_alarm` |
| `alert_type` | `feature_usage` |
| `feature.id` | Unique identifier of the feature |
| `feature.name` | Name of the feature being monitored |
| `feature.meter_id` | The meter tracking this feature's usage |
| `feature.alert_settings.alert_state` | Current alert state |
| `customer.external_id` | Your system's customer identifier |
| `usage.current_usage` | Cumulative feature usage in this billing cycle |
| `usage.unit` | Unit of measurement (e.g. `calls`, `gb`, `minutes`) |
### Use Cases
* **API rate limits** — alert at 25%, 75%, and 100% of a contracted call volume; enforce hard cap at `in_alarm`
* **Compute or storage quotas** — independent thresholds per resource type without aggregate noise
* **Tiered upsell** — route `warning` webhooks to your CRM to trigger upsell outreach before the customer hits their limit
***
## Webhook Delivery
All four alert types are delivered as webhooks. Flexprice supports two delivery modes:
| Mode | Best for | Retry behavior |
| -------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------- |
| **Native** | Lightweight or internal integrations | Up to 3 retries, exponential backoff (1s → 10s, 2-minute window) |
| **Svix** (Flexprice Cloud) | Production, customer-facing integrations | At-least-once delivery, dead-letter queues, HMAC-SHA256 signatures, full delivery history |
Webhooks fire on **every state transition** — both when crossing a threshold and when recovering to `ok`. You will not receive repeated webhooks if the alert state does not change.
See [Webhooks](/docs/webhook/webhooks) for endpoint setup, signature verification, and retry configuration.
***
## Choosing the Right Alert Type
| If you want to… | Use |
| -------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Know when a customer's overall prepaid credits are running low | [Low Wallet Balance Alert](#1-low-wallet-balance-alert) |
| Know when credits for a specific feature are depleted | [Low Feature Wallet Balance Alert](#2-low-feature-wallet-balance-alert) |
| Enforce a spend cap across an entire billing cycle | [Total Usage Alert](#3-total-usage-alert) |
| Track usage quota for a specific feature independently | [Feature-Level Usage Alert](#4-feature-level-usage-alert) |
| Both prepaid balance and spend monitoring | Combine wallet + usage alerts — they are independent and complementary |
***
## Best Practices
**Set thresholds that allow time to react** — for `below` conditions, set `warning` at 2–5× the `critical` value. For `above` conditions, set `warning` at 80% and `critical` at 100% of the limit.
**Handle recovery webhooks** — when a value recovers (e.g. after a top-up or billing cycle reset), Flexprice sends `alert_status: ok`. Use this to automatically re-enable services or dismiss in-app banners.
**Make webhook handlers idempotent** — under retry scenarios, the same state transition may be delivered more than once. Use the combination of the entity ID (wallet or feature) + `alert_status` as an idempotency key.
**Combine wallet and usage alerts** — wallet alerts track remaining prepaid credit; usage alerts track what has been spent. Using both gives you complete visibility from both directions.
**Use feature-level alerts for independent budgets** — if a customer's features have separate allocations, feature-scoped alerts avoid false alarms from unrelated activity.
**Test in staging first** — configure alert thresholds in a staging environment with test events before enabling in production to verify your webhook handler responds correctly at each level.
***
## Related
* [Low Balance Alert (Wallet)](/docs/wallet/low-balance-alert) — detailed configuration guide for customer wallet alerts
* [Feature Wallet Balance Alert](/docs/product-catalogue/features/wallet-balance-alert) — detailed configuration guide for feature-level wallet alerts
* [Auto Top-Up](/docs/wallet/auto-top-up) — automatically recharge wallets when balance drops below a threshold
* [Webhooks](/docs/webhook/webhooks) — endpoint setup, payload signatures, and retry behavior
# Customer Timezones
Source: https://docs.flexprice.io/docs/customers/timezone-aware-billing
Bill customers in their local timezone. Set a timezone on a customer and Flexprice automatically aligns billing periods, usage windows, entitlement resets, and credit expirations to their local calendar.
When you serve customers across geographies, billing in UTC creates a visible problem: a customer in Bangalore on a monthly plan ends their billing period at 5:30 AM local time, not midnight. Their invoice covers dates that do not match their calendar, and usage that feels like "this month" to them lands on "next month's" invoice. Customer timezones fix this by aligning every billing boundary to the customer's local time.
***
## How It Works
Set a `timezone` on the customer record. Flexprice then aligns the following to the customer's local calendar:
* **Billing period start and end:** each period (daily, weekly, monthly, quarterly, half-yearly, annual) begins and ends at local midnight.
* **Usage windows:** usage is aggregated per local day or month, not UTC.
* **Entitlement resets:** daily and monthly usage limits reset at local midnight.
* **Credit grants:** prepaid credits expiring at period-end use the customer's local boundary.
* **Coupon end dates:** coupons tied to billing periods expire at the local period boundary.
Customers without a timezone set default to `UTC`.
***
## Setting Up
### Via the API
Set the `timezone` field when creating a customer, or update it at any time:
```bash theme={null}
# Create a customer with a timezone
curl -X POST https://api.flexprice.io/v1/customers \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"external_id": "cust_001",
"name": "Acme India",
"email": "billing@acme.in",
"timezone": "Asia/Kolkata"
}'
```
```bash theme={null}
# Update an existing customer's timezone
curl -X PATCH https://api.flexprice.io/v1/customers/{id} \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"timezone": "America/Los_Angeles"
}'
```
Updating a customer's timezone does **not** affect active subscriptions. Only new subscriptions created after the update will use the new timezone.
Use standard IANA timezone names. Common examples:
| Region | IANA Name |
| ----------------- | --------------------- |
| India (IST) | `Asia/Kolkata` |
| US Eastern | `America/New_York` |
| US Pacific | `America/Los_Angeles` |
| Germany | `Europe/Berlin` |
| UK | `Europe/London` |
| Singapore | `Asia/Singapore` |
| Australia Eastern | `Australia/Sydney` |
| Nepal | `Asia/Kathmandu` |
Invalid timezone names are rejected with HTTP 400.
### Via the Dashboard
When creating or editing a customer, select the timezone from the **Timezone** dropdown. It applies to all future subscriptions for that customer.
If no timezone is selected, the customer defaults to `UTC`.
***
## What Stays the Same
Changing a customer's timezone does not affect how you send events or how historical data is stored. Your event pipeline requires no changes.
Existing customers without a timezone and their historical subscriptions are unaffected.
***
## Example
A customer in India (IST, UTC+5:30) on a monthly calendar plan:
| | Without timezone | With timezone (IST) |
| --------------------- | ------------------------------ | -------------------------------- |
| Daily usage resets at | 5:30 AM IST | 12:00 AM IST ✓ |
| Entitlement resets at | 5:30 AM IST | 12:00 AM IST ✓ |
| Invoice period | March 15 – April 1 (UTC dates) | March 15 – April 1 (IST dates) ✓ |
***
## Limitations
**Changing timezone on an active subscription**
Updating a customer's timezone only applies to new subscriptions. To rebill an existing subscription under a new timezone, cancel it and create a new one.
**Non-standard offsets**
Half-hour and 45-minute offsets such as India (UTC+5:30), Nepal (UTC+5:45), and Newfoundland (UTC-3:30) are fully supported.
***
## Related Articles
Create customers via the dashboard or API.
Create a subscription and set the start date for a customer.
Credit grants that expire at billing period boundaries respect the customer's timezone.
Coupons tied to billing periods use the customer's local timezone for expiry.
***
**API References**
* [Create a Customer](/api-reference/customers/create-customer)
* [Update a Customer](/api-reference/customers/update-customer)
* [Create a Subscription](/api-reference/subscriptions/create-subscription)
# Automated Data Export to Amazon S3
Source: https://docs.flexprice.io/docs/data-exports/amazon-s3/export
Automate data exports to Amazon S3 with flexible scheduling and configuration
## Overview
The Amazon S3 Export feature enables automated, scheduled exports of your Flexprice data directly to Amazon S3 buckets. This robust, enterprise-grade solution allows you to configure scheduled tasks to export data at regular intervals, trigger manual exports on-demand, and track all export operations with comprehensive execution history.
**Key Benefits:**
* **Automated Backups**: Schedule regular exports to maintain up-to-date data backups
* **Flexible Scheduling**: Export data hourly or daily based on your business needs
* **Secure Storage**: Data encrypted at rest with multiple encryption options
* **Compression Support**: Reduce storage costs with GZIP compression
* **Execution Tracking**: Monitor all export runs with detailed execution history
* **On-Demand Exports**: Trigger manual exports for immediate data exports
## Supported Data Types
Flexprice currently supports exporting the following data types to S3:
### Invoices
Finalized invoices without line items, taxes, and payment details.
**Example Export (First Row):**
```csv theme={null}
id,tenant_id,environment_id,customer_id,subscription_id,invoice_number,invoice_type,invoice_status,payment_status,currency,amount_due,amount_paid,amount_remaining,subtotal,total,total_discount,total_tax,adjustment_amount,refunded_amount,billing_sequence,description,due_date,paid_at,voided_at,finalized_at,billing_period,period_start,period_end,invoice_pdf_url,billing_reason,metadata,idempotency_key,version,status,created_by,updated_by,created_at,updated_at
inv_01K76TCFSQX6JY5C43WT2G1EE8,tenant_01K0F7AQCZR5THSG9MHEZE8VKR,env_01K0F7AQEQFTV06P178C1KTR6X,cust_01K4CGWJ4493R9V2Y5PZW7CNRZ,subs_01K4M3DVXZNFRN50P9T5EKB9HV,INV-202510-00079,SUBSCRIPTION,FINALIZED,PENDING,usd,10,0,10,10,10,0,0,0,0,2,Invoice for subscription subs_01K4M3DVXZNFRN50P9T5EKB9HV,2025-10-09T05:47:27Z,,,2025-10-10T10:24:09Z,MONTHLY,2025-09-08T05:47:27Z,2025-10-08T05:47:27Z,https://pay.stripe.com/invoice/...,SUBSCRIPTION_CYCLE,"{""stripe_hosted_invoice_url"":""...""}",subscription_invoice-ff29843f8cde4859,3,published,d95842bc-7d6c-4a4e-8dc9-eb9d0780d9f3,d95842bc-7d6c-4a4e-8dc9-eb9d0780d9f3,2025-10-10T10:24:08Z,2025-10-10T10:24:12Z
```
### Events
Feature usage events and billing-related activity data.
**Example Export (First Row):**
```csv theme={null}
id,tenant_id,environment_id,external_customer_id,customer_id,subscription_id,sub_line_item_id,price_id,meter_id,feature_id,event_name,source,timestamp,ingested_at,period_id,qty_total,properties,unique_hash
event_01K8D3HHYJVAC90K3X8R0YZA5N,tenant_01K0F7AQCZR5THSG9MHEZE8VKR,env_01K0F7AQEQFTV06P178C1KTR6X,cus_TIciSJDL8QS8OU,cust_01K8D2PERMJ6TX8ATP9C8WCP1Z,subs_01K8D3BJ78M7F0FCPFBBXJWJPG,subs_line_01K8D2Q72ESN6FPP72RA2SWWQR,price_01K42Q7VQEDETDGSA80A8WRJ2Y,meter_01K42Q6R916Q4KB9MC6S46PWQ2,feat_01K42Q6R941C3QS6Q59CGGSPFB,api_call,onboarding,2025-10-25T07:15:20Z,1970-01-01T00:00:00Z,1761376322000,20,"{""api_calls"":20}",baae5efc04d556dc97407d259169b46390946ec4330c974304e70b037aebadcb
```
### Credit Top-Ups
Completed credit wallet transactions for customers.
**Example Export (First Row):**
```csv theme={null}
topup_id,external_id,name,wallet_id,amount,credit_balance_before,credit_balance_after,reference_id,transaction_reason,created_at
wt_01K9X5HJKM9PQR2ST3UV4WXY6Z,cust_ext_123abc,Acme Corporation,wallet_01K9X5HJKM9PQR2ST3UV4WXY6Z,500.00,1000.00,1500.00,topup_ref_abc123,Manual credit top-up for Q1 usage,2025-10-15T14:30:00Z
```
### Credit Usage
Customer-level aggregated credit balances across all wallets. This export provides a snapshot of each customer's current and real-time credit balances, aggregated from all their wallets.
**Example Export (First Row):**
```csv theme={null}
name,customer_external_id,customer_id,current_balance,real_time_balance,number_of_wallets
Acme Corporation,cust_ext_123abc,cust_01K4CGWJ4493R9V2Y5PZW7CNRZ,146.00,146.00,1
```
**Data Included:**
* **name**: Customer name
* **customer\_external\_id**: Customer external ID
* **customer\_id**: Internal customer ID
* **current\_balance**: Aggregated current credit balance across all wallets (decimal string)
* **real\_time\_balance**: Aggregated real-time credit balance including pending charges (decimal string)
* **number\_of\_wallets**: Total number of wallets for the customer
**Balance Aggregation**: Balances are aggregated at the customer level across all wallets. Customers with no wallets are included with zero balances.
## Setting Up S3 Export
### Prerequisites
Before configuring S3 exports, ensure you have:
1. An AWS account with S3 access
2. An S3 bucket created in your desired region
3. AWS credentials (Access Key ID and Secret Access Key)
4. Appropriate IAM permissions for S3 operations
### Step 1: Create an S3 Connection
An S3 connection stores your AWS credentials and serves as the authentication mechanism for all export operations.
Flexprice offers two connection types:
#### Option 1: Flexprice-Managed S3 Connection
Use Flexprice's managed S3 infrastructure for hassle-free exports:
**Benefits:**
* ✅ No AWS account required
* ✅ Instant setup with zero configuration
* ✅ Managed security and access control
* ✅ Download files directly from Flexprice dashboard
**When to Use:**
* Quick setup without AWS infrastructure
* Don't want to manage S3 buckets and IAM policies
* Need immediate data exports with minimal configuration
**Flexprice-Managed Storage**: Your data will be stored securely in Flexprice-managed S3 buckets. Files are encrypted at rest and accessible via secure download links.
#### Option 2: Your Own S3 Connection
Connect to your own AWS S3 bucket for complete control:
**Required Information:**
1. **Connection Name**: A descriptive name for your S3 connection (e.g., "Production S3 Export")
2. **AWS Access Key ID**: Your AWS access key ID
* Format: Starts with `AKIA` for permanent credentials
* Format: Starts with `ASIA` for temporary/session credentials
* Required for authentication
3. **AWS Secret Access Key**: Your AWS secret access key
* This credential is encrypted and stored securely
* Never exposed in API responses
4. **AWS Session Token** (Optional): Required only for temporary credentials
* Use when working with assumed roles or temporary credentials
* Leave empty for permanent IAM user credentials
**Security Best Practice**: Create a dedicated IAM user with minimal
permissions for S3 exports. Grant only the necessary S3 permissions
(`s3:PutObject`, `s3:GetObject`, `s3:ListBucket`).
#### AWS IAM Policy Example
Create an IAM policy with the following permissions:
```json theme={null}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "FlexPriceS3ExportAccess",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::your-bucket-name/*",
"arn:aws:s3:::your-bucket-name"
]
},
{
"Sid": "HeadObject",
"Effect": "Allow",
"Action": "s3:HeadObject",
"Resource": "*"
}
]
}
```
Replace `your-bucket-name` with your actual S3 bucket name. Ensure the IAM
user or role has this policy attached before creating the connection.
### Step 2: Configure a Scheduled Task
Once your S3 connection is established, create a scheduled task to automate exports.
#### Configuration Options
**1. Connection**
* Select the S3 connection created in Step 1
* Each scheduled task can use different connections if needed
**2. Entity Type** (Required)
Choose what data to export:
| Entity Type | Description | Data Included |
| ------------------ | -------------------------- | -------------------------------------------------------------------------------- |
| **invoice** | Finalized invoices | All invoice data, without line items, taxes, payments |
| **events** | Feature usage events | Event data, usage metrics, customer activity |
| **credit\_topups** | Credit wallet transactions | Completed credit top-ups with wallet balance details |
| **credit\_usage** | Customer credit balances | Aggregated current and real-time credit balances across all wallets per customer |
**3. Schedule Interval** (Required)
Select how frequently exports should run:
| Interval | Frequency | Cron Schedule | Best For |
| ---------- | ------------------------------ | ------------- | -------------------------------------------- |
| **hourly** | Every hour at :15 minutes past | `15 * * * *` | Real-time data needs, high-frequency updates |
| **daily** | Every day at 00:15 AM | `15 0 * * *` | Standard backups, daily reporting |
**Why the 15-minute buffer?**
Scheduled exports include a 15-minute buffer after the interval boundary to ensure all data has been ingested and processed. For example, an hourly export scheduled at 10:15 AM will export data from 9:00 AM to 10:00 AM.
**4. S3 Configuration** (Required)
Configure S3-specific settings for your exports:
##### Bucket (Required)
* The name of your S3 bucket where files will be stored
* Example: `flexprice-exports-prod`
* Must exist before creating the scheduled task
##### Region (Required)
* AWS region where your bucket is located
* Example: `us-west-2`, `eu-central-1`, `ap-south-1`
* Must match your bucket's region
##### Key Prefix (Optional)
* Directory path prefix for organizing files in your bucket
* Example: `flexprice-exports/invoices/`
* Example: `production/events/2024/`
* Files will be stored as: `{key_prefix}/{filename}.csv`
Use key prefixes to organize exports by environment, data type, or date. This
makes it easier to manage and query exported data.
##### Endpoint URL (Optional)
Custom endpoint URL for S3-compatible storage services:
* **Use Case**: Connect to S3-compatible storage providers other than AWS S3
* **Examples**:
* MinIO: `https://minio.example.com`
* DigitalOcean Spaces: `https://nyc3.digitaloceanspaces.com`
* Backblaze B2: `https://s3.us-west-002.backblazeb2.com`
* **Default**: Leave empty to use AWS S3 (default)
When using a custom endpoint, ensure your credentials and bucket configuration
are compatible with that provider's S3 API implementation.
##### Compression (Optional)
Control file compression to reduce storage costs:
| Option | Description | File Extension | Compression Ratio |
| -------- | ------------------------ | -------------- | ----------------------- |
| **none** | No compression (default) | `.csv` | N/A (original size) |
| **gzip** | GZIP compression | `.csv.gz` | \~70-80% size reduction |
**When to use compression:**
* Large datasets with many records
* Storage cost optimization
* Bandwidth-limited environments
**When to skip compression:**
* Small datasets (\< 1MB)
* Files need to be immediately readable
* Downstream systems don't support GZIP
##### Encryption (Optional)
Secure your data at rest with server-side encryption:
| Option | Description | Key Management | Best For |
| ---------------- | --------------------------------------- | ------------------------ | ----------------------- |
| **AES256** | S3-managed AES-256 encryption (default) | AWS manages keys | Standard security needs |
| **aws:kms** | AWS KMS encryption | You control keys via KMS | Compliance requirements |
| **aws:kms:dsse** | KMS with dual-layer encryption | Enhanced KMS security | Maximum security needs |
All encryption options are server-side encryption (SSE). Data is encrypted at
rest in S3. AES256 is the default and recommended for most use cases.
**5. Enable/Disable Toggle**
* Enable the scheduled task to start automatic exports
* Disable to pause exports without deleting the configuration
* Can be toggled at any time
### Step 3: Verify Configuration
After creating a scheduled task:
1. **Verify Connection**: Ensure your S3 connection is validated and active
2. **Check Schedule**: Confirm the interval matches your requirements
3. **Review S3 Settings**: Double-check bucket name, region, and paths
4. **Test with Manual Export**: Trigger a manual export to verify the setup
### Viewing Export Details
Once you've created an export task, you can view its configuration and status by clicking on the task. The export details page shows:
* **Basic Information**: Status, entity type, interval, and connection
* **S3 Configuration**: Bucket, region, key prefix, compression, and encryption settings
* **Timestamps**: Creation date and last update time
## Manual Export
Manual Export allows you to trigger an immediate export without waiting for the next scheduled execution. This is useful for:
* Testing your export configuration
* Backfilling historical data
* Responding to urgent data requests
* Exporting specific time ranges
### How to Trigger a Manual Export
Navigate to the export details page and click the "Manual Export" button to trigger a manual export.
#### Option 1: Automatic Time Range (Default)
Trigger a manual export without specifying time ranges. The system automatically calculates the export window based on the task's interval:
**Example - Hourly Task:**
* Current time: 10:30 AM
* Export window: 10:00 AM to 11:00 AM (current hour)
**Example - Daily Task:**
* Current date: October 16, 2025
* Export window: October 16, 00:00 to October 17, 00:00 (current day)
**API Request:**
```bash theme={null}
POST /api/v1/tasks/scheduled/{task_id}/run
```
**Response:**
```json theme={null}
{
"workflow_id": "task_export_01HK8DBMBN87CY36DEFY62PEXBT",
"message": "Manual export triggered successfully",
"start_time": "2025-10-16T10:00:00Z",
"end_time": "2025-10-16T11:00:00Z",
"mode": "manual"
}
```
#### Option 2: Custom Time Range
Specify exact start and end times for the export:
**API Request:**
```bash theme={null}
POST /api/v1/tasks/scheduled/{task_id}/run
Content-Type: application/json
{
"start_time": "2025-10-01T00:00:00Z",
"end_time": "2025-10-15T23:59:59Z"
}
```
**Response:**
```json theme={null}
{
"workflow_id": "task_export_01HK8DBMBN87CY36DEFY62PEXBT",
"message": "Manual export triggered successfully",
"start_time": "2025-10-01T00:00:00Z",
"end_time": "2025-10-15T23:59:59Z",
"mode": "custom"
}
```
Custom time ranges must be valid timestamps in RFC3339 format (ISO 8601). The
end time must be after the start time.
### Manual Export Behavior
* **Independent Execution**: Manual exports don't affect the regular schedule
* **Parallel Execution**: Can run alongside scheduled exports
* **Tracked Separately**: Appears in the Runs tab with "manual" or "custom" mode
* **Same Configuration**: Uses the same S3 settings as the scheduled task
* **Immediate Processing**: Starts execution within seconds
## Runs Tab - Execution History
The Runs tab displays a comprehensive history of all export executions for a scheduled task, including both automatic scheduled runs and manual exports.
Each run record shows the execution status, time range, file details, and any errors that occurred. Use this to monitor export health and access exported files.
### Downloading Exported Files
You can download exported files directly from the Runs tab for any connection type:
Go to your scheduled task and click the **Runs** tab to view execution history
Find the completed export run you want to download from the list
Click the **Download** button next to the run record to download the CSV file.
**Benefits:**
* ✅ Works with both Flexprice-managed and your own S3 connections
* ✅ No S3 console access required
* ✅ Download files directly
### Accessing Exported Files
Each completed run provides a direct S3 file URL:
**File Naming Convention:**
```
{entity_type}-{start_time}-{end_time}.csv
```
**Examples:**
```
invoice-251016100000-251016110000.csv
events-251016000000-251017000000.csv.gz
credit_usage-251223110000-251223120000.csv
```
**Time Format in Filename:**
* `YYMMDDHHMMSS` (Year, Month, Day, Hour, Minute, Second)
* Example: `251016100000` = October 16, 2025, 10:00:00
### Troubleshooting Failed Runs
When a run fails, the error summary provides details about what went wrong:
**Common Failure Reasons:**
| Error | Cause | Solution |
| --------------------- | --------------------------------------- | -------------------------------------------------- |
| **Connection Failed** | Invalid AWS credentials | Verify and update S3 connection credentials |
| **Bucket Not Found** | S3 bucket doesn't exist or wrong region | Check bucket name and region in task configuration |
| **Permission Denied** | Insufficient IAM permissions | Review and update IAM policy |
| **No Data Found** | No records in the specified time range | Verify the time range or check data availability |
| **Timeout** | Export took longer than 15 minutes | Contact support or split into smaller time ranges |
## Best Practices
**Use Descriptive Connection Names**
Name your S3 connections based on environment and purpose (e.g., "Production Invoice Exports", "Staging Events Backup").
**Organize with Key Prefixes**
Use key prefixes to organize exports by environment, data type, and date:
* `production/invoices/2025/`
* `staging/events/`
**Enable Compression for Large Datasets**
Use GZIP compression for exports with thousands of records to reduce storage costs by 70-80%.
**Choose the Right Interval**
* **Hourly**: Real-time analytics, high-frequency reporting
* **Daily**: Standard backups, daily batch processing
**Test with Manual Export**
Always test a new scheduled task with a manual export before relying on automatic execution.
**Monitor Execution History**
Regularly review the Runs tab to ensure exports are completing successfully and adjust configuration if needed.
**Use IAM Best Practices**
* Create dedicated IAM users for exports
* Grant minimal required permissions
* Rotate credentials regularly
* Enable CloudTrail for audit logging
**Plan for S3 Lifecycle Policies**
Configure S3 lifecycle policies to automatically archive or delete old exports:
* Transition to S3 Glacier after 90 days
* Delete files older than 1 year
## Troubleshooting
### Scheduled Task Not Running
**Possible causes:**
* Task is disabled
* S3 connection is invalid
* Temporal workflow service is down
**Solution:**
1. Verify the task is enabled in the dashboard
2. Check the S3 connection status
3. Test with a manual export to isolate the issue
4. Review Temporal workflow logs
### Files Not Appearing in S3
**Possible causes:**
* Incorrect bucket name or region
* Missing IAM permissions
* Key prefix typo
**Solution:**
1. Verify bucket name matches exactly (case-sensitive)
2. Confirm region matches the bucket's region
3. Review IAM policy and ensure `s3:PutObject` permission
4. Check key prefix for typos or trailing slashes
### Export Timeout Errors
**Possible causes:**
* Very large time range (months of data)
* Thousands of records in a single export
* S3 upload speed issues
**Solution:**
1. Reduce the time range for custom exports
2. Use hourly intervals instead of daily for high-volume data
3. Enable GZIP compression to reduce upload time
4. Contact support if issue persists
### Invalid Credentials Error
**Possible causes:**
* AWS credentials expired or rotated
* Incorrect Access Key ID or Secret Key
* Session token expired (for temporary credentials)
**Solution:**
1. Verify credentials in AWS IAM console
2. Update the S3 connection with new credentials
3. For temporary credentials, refresh the session token
4. Ensure the IAM user/role still exists
## API Reference
### Create Scheduled Task
```bash theme={null}
POST /api/v1/tasks/scheduled
Content-Type: application/json
Authorization: Bearer {token}
{
"connection_id": "conn_01HK8DBMBN87CY36DEFY62PEXBT",
"entity_type": "invoice", // Options: "invoice", "events", "credit_topups", "credit_usage"
"interval": "daily",
"enabled": true,
"job_config": {
"bucket": "flexprice-exports",
"region": "us-west-2",
"key_prefix": "invoices/",
"endpoint_url": "", // Optional: Custom S3-compatible endpoint
"compression": "gzip",
"encryption": "AES256"
}
}
```
### Update Scheduled Task
```bash theme={null}
PATCH /api/v1/tasks/scheduled/{task_id}
Content-Type: application/json
Authorization: Bearer {token}
{
"enabled": false
}
```
### Trigger Manual Export
```bash theme={null}
POST /api/v1/tasks/scheduled/{task_id}/run
Content-Type: application/json
Authorization: Bearer {token}
{
"start_time": "2025-10-01T00:00:00Z",
"end_time": "2025-10-15T23:59:59Z"
}
```
### List Scheduled Tasks
```bash theme={null}
GET /api/v1/tasks/scheduled?entity_type=invoice&status=published
Authorization: Bearer {token}
```
### Get Task Execution History
```bash theme={null}
GET /api/v1/tasks?scheduled_task_id={task_id}&task_type=EXPORT
Authorization: Bearer {token}
```
## Summary
S3 Export provides a comprehensive solution for automated data exports to Amazon S3. Key takeaways:
* **Four Data Types**: Export invoices, events, credit top-ups, and credit usage data
* **Flexible Scheduling**: Hourly or daily automated exports
* **Secure Storage**: Multiple encryption options for data at rest
* **Manual Export Capability**: Trigger manual exports anytime
* **Execution Tracking**: Comprehensive run history and monitoring
* **Enterprise-Ready**: Compression, encryption, and scalable architecture
* **Easy Integration**: Standard CSV format for downstream processing
By implementing S3 Export, you can automate data backups, feed data warehouses, meet compliance requirements, and integrate Flexprice data with your broader data infrastructure.
# Event Ingestion API Reference
Source: https://docs.flexprice.io/docs/event-ingestion/api-reference
Complete API reference for sending events to Flexprice
This document provides the complete API reference for sending events to Flexprice. It includes all endpoints, request/response formats, error codes, and examples.
## Base URL
```
https://api.cloud.flexprice.io/v1
```
## Authentication
All requests require authentication using an API key in the request header:
```
x-api-key:
```
**Security Note**: Keep your API key secure and never expose it in client-side code or public repositories.
## Endpoints
### Single Event
Send a single event to Flexprice.
**Endpoint**: `POST /events`
**Headers**:
```
Content-Type: application/json
x-api-key:
```
**Request Body**:
```json theme={null}
{
"event_name": "string",
"external_customer_id": "string",
"properties": {
"string": "any"
},
"event_id": "string",
"timestamp": "string",
"source": "string"
}
```
**Response**:
* **Status**: `202 Accepted`
* **Body**:
```json theme={null}
{
"event_id": "string",
"message": "Event accepted for processing"
}
```
### Bulk Events
Send multiple events in a single request for better performance.
**Endpoint**: `POST /events/bulk`
**Headers**:
```
Content-Type: application/json
x-api-key:
```
**Request Body**:
```json theme={null}
{
"events": [
{
"event_name": "string",
"external_customer_id": "string",
"properties": {
"string": "any"
},
"event_id": "string",
"timestamp": "string",
"source": "string"
}
]
}
```
**Response**:
* **Status**: `202 Accepted`
* **Body**:
```json theme={null}
{
"event_ids": ["string"],
"message": "Events accepted for processing"
}
```
## Request Fields
### Required Fields
| Field | Type | Description | Example |
| ---------------------- | ------ | -------------------------------------------- | --------------- |
| `event_name` | string | Must exactly match your feature's Event Name | `"model.usage"` |
| `external_customer_id` | string | Your identifier for the customer | `"cust_123"` |
### Conditional Fields
| Field | Type | When Required | Description | Example |
| -------------------- | ------------- | ---------------------------------- | ------------------------------------ | ------------------- |
| `properties` | object | For Sum, Max, Latest, Unique Count | Contains values to aggregate | `{"credits": 2}` |
| `properties.` | number/string | When aggregation field is set | Exact field name from feature config | `2` or `"user_123"` |
### Optional Fields
| Field | Type | Description | Example |
| ----------- | ------ | ------------------------------------ | ---------------------------- |
| `event_id` | string | Your unique identifier for the event | `"evt_abc123"` |
| `timestamp` | string | ISO 8601 UTC timestamp | `"2025-08-22T07:05:49.441Z"` |
| `source` | string | Origin of the event | `"api"`, `"worker"` |
## Data Types
### Property Values
* **Numbers**: Use for Sum, Max aggregations
```json theme={null}
"properties": { "credits": 2 }
```
* **Strings**: Use for Unique Count, Latest aggregations
```json theme={null}
"properties": { "user_id": "user_123" }
```
### Timestamps
* **Format**: ISO 8601 UTC
* **Example**: `"2025-08-22T07:05:49.441Z"`
* **Default**: Server time if omitted
## Examples
### Basic Event (Count Aggregation)
```bash theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/events \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"event_name": "api.calls",
"external_customer_id": "cust_123"
}'
```
### Event with Properties (Sum Aggregation)
```bash theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/events \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": {
"credits": 2
}
}'
```
### Complete Event
```bash theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/events \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": {
"credits": 2,
"model": "gpt-4",
"region": "us-east-1"
},
"event_id": "evt_abc123",
"timestamp": "2025-08-22T07:05:49.441Z",
"source": "api"
}'
```
### Bulk Events
```bash theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/events/bulk \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"events": [
{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": { "credits": 2 }
},
{
"event_name": "model.usage",
"external_customer_id": "cust_456",
"properties": { "credits": 5 }
}
]
}'
```
## Language Examples
### JavaScript (Node.js)
```javascript theme={null}
const fetch = require("node-fetch");
async function sendEvent() {
const response = await fetch("https://api.cloud.flexprice.io/v1/events", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.FLEXPRICE_API_KEY,
},
body: JSON.stringify({
event_name: "model.usage",
external_customer_id: "cust_123",
properties: { credits: 2 },
source: "api",
}),
});
if (response.status === 202) {
const result = await response.json();
console.log("Event sent:", result);
} else {
const error = await response.text();
console.error("Error:", error);
}
}
```
### Python
```python theme={null}
import os
import requests
import json
def send_event():
url = "https://api.cloud.flexprice.io/v1/events"
headers = {
"Content-Type": "application/json",
"x-api-key": os.environ["FLEXPRICE_API_KEY"]
}
data = {
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": {"credits": 2},
"source": "api"
}
response = requests.post(url, headers=headers, json=data)
if response.status_code == 202:
result = response.json()
print("Event sent:", result)
else:
print("Error:", response.text)
```
### Go
```go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
type Event struct {
EventName string `json:"event_name"`
ExternalCustomerID string `json:"external_customer_id"`
Properties map[string]interface{} `json:"properties,omitempty"`
Source string `json:"source,omitempty"`
}
func sendEvent() error {
event := Event{
EventName: "model.usage",
ExternalCustomerID: "cust_123",
Properties: map[string]interface{}{
"credits": 2,
},
Source: "api",
}
jsonData, err := json.Marshal(event)
if err != nil {
return err
}
req, err := http.NewRequest("POST", "https://api.cloud.flexprice.io/v1/events", bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", os.Getenv("FLEXPRICE_API_KEY"))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 202 {
fmt.Println("Event sent successfully")
} else {
fmt.Printf("Error: HTTP %d\n", resp.StatusCode)
}
return nil
}
```
### PHP
```php theme={null}
'model.usage',
'external_customer_id' => 'cust_123',
'properties' => [
'credits' => 2
],
'source' => 'api'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'x-api-key: ' . $apiKey
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 202) {
echo "Event sent successfully\n";
} else {
echo "Error: HTTP $httpCode\n";
echo "Response: $response\n";
}
}
```
## Error Responses
### HTTP Status Codes
| Status | Description |
| ----------------------- | ------------------------------------------ |
| `202 Accepted` | Event accepted for processing |
| `400 Bad Request` | Invalid payload or missing required fields |
| `401 Unauthorized` | Invalid or missing API key |
| `429 Too Many Requests` | Rate limit exceeded |
### Error Response Format
```json theme={null}
{
"error": "Error description",
"details": "Additional error details"
}
```
### Common Error Messages
#### Missing Required Field
```json theme={null}
{
"error": "Missing required field: event_name"
}
```
#### Invalid Event Name
```json theme={null}
{
"error": "Event name 'unknown.event' not found"
}
```
#### Invalid Customer
```json theme={null}
{
"error": "Customer with external_customer_id 'invalid_id' not found"
}
```
#### Invalid JSON
```json theme={null}
{
"error": "Invalid JSON format"
}
```
#### Rate Limit Exceeded
```json theme={null}
{
"error": "Rate limit exceeded. Try again later."
}
```
## Rate Limits
* **Single events**: 1000 requests per minute
* **Bulk events**: 100 requests per minute (up to 1000 events per request)
* **Response**: 429 status code when exceeded
### Rate Limit Headers
When approaching rate limits, responses include headers:
```
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 50
X-RateLimit-Reset: 1640995200
```
## Best Practices
### 1. Error Handling
Implement proper error handling with retries:
```javascript theme={null}
async function sendEventWithRetry(eventData, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch("https://api.cloud.flexprice.io/v1/events", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.FLEXPRICE_API_KEY,
},
body: JSON.stringify(eventData),
});
if (response.status === 202) {
return await response.json();
}
if (response.status === 429) {
// Rate limited - wait and retry
const retryAfter =
response.headers.get("Retry-After") || attempt * 1000;
await new Promise((resolve) => setTimeout(resolve, retryAfter));
continue;
}
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
// Wait before retry
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
}
```
### 2. Bulk Events for High Volume
Use bulk events when sending many events:
```javascript theme={null}
const events = [
{ event_name: "api.calls", external_customer_id: "cust_1" },
{ event_name: "api.calls", external_customer_id: "cust_2" },
// ... more events
];
const response = await fetch("https://api.cloud.flexprice.io/v1/events/bulk", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.FLEXPRICE_API_KEY,
},
body: JSON.stringify({ events }),
});
```
### 3. Idempotency
Use event\_id for idempotency:
```json theme={null}
{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": { "credits": 2 },
"event_id": "unique_event_id_123"
}
```
### 4. Timestamps
Include timestamps for historical events:
```json theme={null}
{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": { "credits": 2 },
"timestamp": "2025-08-22T07:05:49.441Z"
}
```
## Validation
### Event Name Validation
* Must match a feature's Event Name exactly
* Case-sensitive
* Cannot be changed after feature creation
### Customer Validation
* external\_customer\_id must exist in Flexprice
* Customer must be active
### Property Validation
* Aggregation field must exist for Sum, Max, Latest, Unique Count
* Data types must match aggregation function requirements
## Testing
### Test Endpoint
For testing purposes, you can use the same endpoints with test data:
```bash theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/events \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"event_name": "test.feature",
"external_customer_id": "test_customer",
"properties": {
"value": 1
}
}'
```
### Validation Checklist
Before sending production events:
* Feature exists and is active
* Event Name matches exactly
* Customer exists in Flexprice
* Required properties are included
* Data types are correct
* API key has proper permissions
## Support
For API-related issues:
1. Check error messages for specific details
2. Verify your request format matches the examples
3. Test with a simple event first
4. Contact support with specific error details and examples
# Connecting to Billing
Source: https://docs.flexprice.io/docs/event-ingestion/connecting-to-billing
Learn how to connect events to billing and see usage in invoices
Events alone don't generate revenue - they need to be connected to pricing plans and customer subscriptions to create billable charges. This guide shows you how to transform your ingested events into actual invoices.
## How Events Flow to Billing
The complete flow from events to invoices:
1. **Events** → Usage data transmitted to Flexprice
2. **Features** → Define what you're tracking and how to aggregate
3. **Plans** → Set pricing for features
4. **Subscriptions** → Connect customers to plans
5. **Invoices** → Automatic billing based on usage
## Prerequisites
* A metered feature created and configured
* Events being transmitted and validated
* A customer created in Flexprice
## Step 1: Create a Pricing Plan
### Navigate to Plans
1. Log into your Flexprice Admin Dashboard
2. In the left sidebar, click on **Product Catalog**
3. Click on **Plans**
4. Click **"Create Plan"**
### Configure the Plan
1. **Plan Name**: Give your plan a descriptive name (e.g., "Basic Plan", "Pro Plan")
2. **Description**: Optional description of what the plan includes
3. **Billing Cycle**: Choose how often customers are billed (monthly, annually, etc.)
### Add Your Feature
1. Click **"Add Feature"** or **"Add Charge"**
2. Select your metered feature from the list
3. Configure the pricing:
**For Usage-Based Pricing:**
* **Price Model**: Choose "Usage Based"
* **Price**: Set the price per unit (e.g., \$1 per credit)
* **Free Tier**: Optional free usage before billing starts
**Example Configuration:**
* Feature: Model Usage
* Price: \$1.00 per credit
* Free Tier: 100 credits per month
### Advanced Pricing Options
**Tiered Pricing:**
* First 1000 credits: \$1.00 each
* Next 1000 credits: \$0.80 each
* Additional credits: \$0.60 each
**Package Pricing:**
* 1000 credits for $800 (effective $0.80 per credit)
**Volume Discounts:**
* Set percentage discounts based on usage volume
### Save the Plan
Click **"Save Plan"** to create your pricing plan.
## Step 2: Assign Plan to Customer
### Create Subscription
1. Go to **Customers** in the left sidebar
2. Find and click on your customer
3. Click **"Create Subscription"**
4. Select the plan you just created
5. Set the subscription start date
6. Click **"Create Subscription"**
### Verify Subscription
After creating the subscription, you should see:
* Subscription status: Active
* Plan name and billing cycle
* Next billing date
## Step 3: Monitor Usage and Billing
### Check Upcoming Invoices
1. Go to the customer's subscription page
2. Click on **"Upcoming Invoices"** tab
3. You'll view a preview of the next invoice with:
* Usage quantities from your events
* Calculated amounts based on your pricing
* Billing period information
**Example Invoice Preview:**
```
Subscription Description Interval Quantity Amount
Test Feature Usage Based Aug 22 - Sep 22 2 $2
```
### Understanding the Billing
* **Quantity**: Total usage aggregated from your events
* **Amount**: Calculated based on your pricing plan
* **Interval**: Current billing period
* **Description**: Shows "Usage Based" for metered features
## Real-World Example
Let's walk through a complete example:
### 1. Feature Configuration
* **Feature Name**: Model Usage
* **Event Name**: `model.usage`
* **Aggregation**: Sum of `credits`
* **Usage Reset**: Periodic (monthly)
### 2. Events Transmitted
```json theme={null}
[
{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": { "credits": 5 }
},
{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": { "credits": 3 }
}
]
```
### 3. Plan Configuration
* **Plan Name**: Basic Plan
* **Feature**: Model Usage
* **Pricing**: \$1.00 per credit
* **Free Tier**: 10 credits per month
### 4. Billing Calculation
* **Total Usage**: 5 + 3 = 8 credits
* **Free Credits**: 10 (no charge for first 10)
* **Billable Credits**: 0 (8 \< 10, so no charge)
* **Amount**: \$0.00
### 5. If More Usage Occurs
If the customer uses 15 credits total:
* **Total Usage**: 15 credits
* **Free Credits**: 10
* **Billable Credits**: 5
* **Amount**: \$5.00
## Advanced Billing Scenarios
### Multiple Features
You can add multiple metered features to a single plan:
**Plan: Pro Plan**
* Model Usage: \$1.00 per credit
* API Calls: \$0.01 per call
* Storage: \$0.10 per GB
**Customer Usage:**
* 50 credits consumed
* 1000 API calls
* 5 GB storage
**Invoice:**
```
Model Usage Usage Based Aug 22 - Sep 22 50 $50
API Calls Usage Based Aug 22 - Sep 22 1000 $10
Storage Usage Based Aug 22 - Sep 22 5 $0.50
Total: $60.50
```
### Tiered Pricing
**Plan: Enterprise Plan**
* Model Usage:
* First 1000 credits: \$0.80 each
* Next 1000 credits: \$0.60 each
* Additional: \$0.40 each
**Customer Usage: 2500 credits**
* First 1000: \$800
* Next 1000: \$600
* Additional 500: \$200
* **Total: \$1600**
## Monitoring and Analytics
### Usage Trends
1. Go to **Usage Tracking** → **Query**
2. Select your feature and customer
3. View usage over time to identify patterns
### Billing Analytics
1. Go to **Invoices** section
2. View historical invoices and revenue
3. Analyze customer usage patterns
## Common Billing Issues
### 1. No Charges Appearing
**Possible Causes:**
* Feature not added to plan
* Customer not subscribed to plan
* Usage below free tier
* Wrong billing period
**Solutions:**
* Verify feature is in the plan
* Check subscription status
* Review free tier settings
* Confirm billing cycle dates
### 2. Wrong Amounts
**Possible Causes:**
* Incorrect pricing configuration
* Wrong aggregation values
* Billing period mismatch
**Solutions:**
* Review plan pricing settings
* Check event aggregation
* Verify billing cycle alignment
### 3. Unexpected Charges
**Possible Causes:**
* Events transmitted to wrong customer
* Incorrect usage reset setting
* Duplicate events
**Solutions:**
* Verify external\_customer\_id
* Check Usage Reset configuration
* Review event history for duplicates
## Best Practices
### 1. Test Billing Setup
Before going live:
1. Create a test customer
2. Transmit a few test events
3. Verify charges appear correctly
4. Check invoice calculations
### 2. Monitor Usage Patterns
Regularly check:
* Usage trends and spikes
* Customer behavior patterns
* Revenue impact of pricing changes
### 3. Set Up Alerts
Consider alerts for:
* Unusual usage spikes
* Failed billing attempts
* Revenue thresholds
### 4. Document Pricing
Keep clear documentation of:
* Feature pricing structure
* Free tier limits
* Billing cycle details
* Customer-specific overrides
## Pricing Strategies
### Usage-Based Pricing
**Pros:**
* Aligns cost with value
* Scales automatically
* Fair for customers
**Cons:**
* Revenue can be unpredictable
* Complex to forecast
### Tiered Pricing
**Pros:**
* Encourages higher usage
* Predictable revenue at scale
* Rewards loyal customers
**Cons:**
* More complex to configure
* Can be confusing for customers
### Hybrid Models
Combine multiple pricing approaches:
* Base subscription fee
* Usage-based charges
* Volume discounts
## Next Steps
After setting up billing:
1. **Monitor Performance** - Track usage and revenue
2. **Optimize Pricing** - Adjust based on customer behavior
3. **Scale Operations** - Add more features and customers
4. **Automate Processes** - Set up webhooks and integrations
## Troubleshooting
### "No charges in upcoming invoice"
* Check that events are being transmitted
* Verify feature is in the plan
* Confirm customer subscription is active
* Review free tier settings
### "Wrong billing amounts"
* Verify pricing configuration
* Check event aggregation values
* Review billing period dates
* Confirm usage reset settings
### "Unexpected charges"
* Review recent events for the customer
* Check for duplicate events
* Verify external\_customer\_id mapping
* Review plan configuration changes
## Getting Help
If you're experiencing billing issues:
1. Check the **[Event Debugger](/docs/event-ingestion/event-debugger)** for event processing status
2. Review your plan configuration carefully
3. Verify customer subscription status
4. Contact support with specific examples and error messages
# Creating a Metered Feature
Source: https://docs.flexprice.io/docs/event-ingestion/creating-a-metered-feature
Step-by-step guide to create a metered feature for event ingestion
## Prerequisites
* Access to the Flexprice Admin Dashboard
* A clear understanding of what you want to meter (e.g., API calls, storage usage, credits consumed)
## Step-by-Step Guide
### 1. Navigate to Features
1. Log into your Flexprice Admin Dashboard
2. In the left sidebar, click on **Product Catalog**
3. Click on **Features**
4. Click the **"Add Feature"** button
### 2. Basic Feature Information
Fill in the basic details:
* **Name**: Give your feature a clear, descriptive name (e.g., "Model Usage", "API Calls", "Storage Usage")
* **Type**: Select **"Metered"** from the dropdown
### 3. Configure Event Name
**⚠️ CRITICAL: This is the most important setting**
The **Event Name** is the unique identifier that connects your events to this feature:
* **Must be unique** across all features in your account
* **Cannot be changed** after creation
* **Must match exactly** what you send in your events
* Use lowercase with dots (e.g., `model.usage`, `api.calls`, `storage.gb`)
**Example**: If you set Event Name to `model.usage`, every event you transmit must have `"event_name": "model.usage"`.
### 4. Choose Aggregation Function
Select how you want to measure usage (see [Aggregation Overview](/docs/product-catalogue/features/aggregation/overview) for detailed explanations):
* **Count** — Counts the number of events (no property required)
* **Sum** — Adds up values from a specific property
* **Average** — Calculates the mean value of a specific property
* **Count Unique** — Counts unique values of a property
* **Latest** — Takes the most recent value sent
* **Sum with Multiplier** — Sums values and applies a rate multiplier
* **Max** — Takes the highest value seen in the billing period (supports bucketing)
* **Weighted Sum** — Time-weighted sum for capacity-based billing
**Example**: If you want to track total credits consumed, choose **Sum**.
### 5. Set Aggregation Field (if required)
**⚠️ CRITICAL: This must match your event properties exactly**
If you selected Sum, Max, Latest, or Unique Count, you must specify which property to aggregate:
* **Field Name**: The exact property key from your events
* **Must match case**: `credits` ≠ `Credits` ≠ `CREDITS`
* **Must exist**: Every event must include this property
**Example**: If you set Aggregation Field to `credits`, every event must include:
```json theme={null}
{
"properties": {
"credits": 2
}
}
```
### 6. Configure Usage Reset
Choose how usage accumulates over time:
* **Periodic** — Usage resets at the start of each billing cycle
* Good for: API calls, monthly limits, recurring usage
* Example: Customer gets 1000 API calls per month
* **Cumulative** — Usage keeps growing across billing cycles
* Good for: Storage usage, lifetime metrics
* Example: Total storage used since account creation
### 7. Set Unit Name (Optional)
Define how the quantity appears on invoices:
* **Singular**: `credit`, `API call`, `GB`
* **Plural**: `credits`, `API calls`, `GBs`
* Flexprice will automatically choose the correct form
**Example**: `credit / credits` will show as "1 credit" or "5 credits" on invoices.
### 8. Add Event Filters (Optional)
If you only want to count certain events, add filters:
* **Key**: Property name to filter on
* **Values**: Allowed values for that property
**Example**: Only count events where `region = "EU"` or `model = "gpt-4"`.
### 9. Save the Feature
Click **"Save Feature"** to create your metered feature.
## After Creation
Once saved, you'll see:
* **Feature ID**: A unique identifier for this feature
* **Event Name**: The exact string to use in your events
* **Aggregation Details**: How the feature processes events
**Important**: Note down the **Event Name** and **Aggregation Field** - you'll need these when transmitting events.
## Common Configuration Examples
### API Calls (Count)
* **Event Name**: `api.calls`
* **Aggregation Function**: Count
* **Usage Reset**: Periodic
* **Unit Name**: `API call / API calls`
### Credits Consumed (Sum)
* **Event Name**: `model.usage`
* **Aggregation Function**: Sum
* **Aggregation Field**: `credits`
* **Usage Reset**: Periodic
* **Unit Name**: `credit / credits`
### Storage Usage (Sum)
* **Event Name**: `storage.usage`
* **Aggregation Function**: Sum
* **Aggregation Field**: `gb`
* **Usage Reset**: Cumulative
* **Unit Name**: `GB`
### Peak Concurrent Users (Max)
* **Event Name**: `concurrent.users`
* **Aggregation Function**: Max
* **Aggregation Field**: `count`
* **Usage Reset**: Periodic
* **Unit Name**: `user / users`
## ⚠️ Critical Warnings
### Event Name
* **Cannot be changed** after creation
* Must be **unique** across all features
* **Case sensitive** — `api.calls` ≠ `API.calls`
### Aggregation Field
* **Must match exactly** the property name in your events
* **Case sensitive** — `credits` ≠ `Credits`
* **Required** for Sum, Max, Latest, and Unique Count functions
### Testing
* Always test your feature configuration before transmitting production events
* Use the **[Event Debugger](/docs/event-ingestion/event-debugger)** to validate your setup
## Related Documentation
* **[Features Overview](/docs/product-catalogue/features/overview)** — Learn about different feature types
* **[Aggregation Functions](/docs/product-catalogue/features/aggregation/overview)** — Understand how data is processed
## Next Steps
Now that you have a metered feature, you're ready to:
1. **[Send Events](/docs/event-ingestion/sending-events)** - Learn how to transmit usage data
2. **[Validate Events](/docs/event-ingestion/validating-events)** - Ensure events are being processed correctly
3. **[Connect to Billing](/docs/event-ingestion/connecting-to-billing)** - Set up pricing and subscriptions
## Troubleshooting
### "Events not showing up"
* Check that Event Name matches exactly
* Verify Aggregation Field exists in your event properties
* Ensure the customer exists with the correct `external_customer_id`
### "Wrong aggregation values"
* Verify Aggregation Field name matches your event properties
* Check that property values are the correct data type (numbers for Sum/Max)
* Confirm Usage Reset setting matches your expectations
### "Cannot change Event Name"
* Event Name is immutable after creation
* Create a new feature with the correct name
* Update your application to transmit events with the new name
For more detailed troubleshooting, see our **[Troubleshooting Guide](/docs/event-ingestion/troubleshooting)**.
# Event Debugger
Source: https://docs.flexprice.io/docs/event-ingestion/event-debugger
Monitor and debug event processing in real-time
The Event Debugger provides visibility into your event processing pipeline, helping you track events, identify issues, and ensure accurate billing.
## Overview
When you send events to Flexprice, they go through multiple processing steps—validation, meter matching, aggregation, and billing. The Event Debugger lets you see this entire journey and quickly identify any issues.
## Accessing the Event Debugger
Navigate to **Usage Tracking** → **Events Debugger** in your Flexprice dashboard.
## Viewing Events
### Event Stream
The Event Debugger displays a real-time stream of events entering your system. Each event shows:
* Event ID
* Event Name
* Customer Lookup Key
* Source
* Timestamp
### Event Details
Click on any event to view complete details:
```json theme={null}
{
"id": "event_01",
"external_customer_id": "cust-new",
"event_name": "api_calls",
"timestamp": "2025-10-15T11:38:47.962Z",
"properties": {},
"source": "onboarding",
"environment_id": "env_10"
}
```
## Sorting Events
Click the Sort button to order events by:
* Name
* Email
* Created At
* Updated At
The sort order can be ascending or descending.
## Filtering Events
Use the Filter button to narrow down events. You can combine multiple filters:
### Filter by Event ID
Search for a specific event by its ID:
```
event_id contains "123"
```
### Filter by Event Name
Filter by event type:
```
event_name contains "api_calls"
```
### Filter by Customer
Find events for a specific customer:
```
external_customer_id contains "cust_new"
```
### Filter by Time Range
Find events after a specific time period using Start Time:
**Start Time (After):**
```
start_time after "October 1st, 2025"
```
Find events before a specific time period using End Time:
**End Time (Before):**
```
end_time before "October 10th, 2025"
```
### Filter by Source
Filter events by their source:
```
source contains "onboarding"
```
You can apply multiple filters simultaneously and click "Add filter" to combine conditions.
## Common Use Cases
### Verify Events Are Being Received
After setting up event ingestion, check the Event Debugger to confirm events are arriving:
1. Navigate to the Events Debugger
2. Look for your recent events in the list
3. Click on an event to view its full details
### Debug Missing Events
If expected events aren't showing up:
1. Check that `event_name` matches your metered feature exactly
2. Verify `external_customer_id` is correct
3. Confirm the timestamp format is valid ISO 8601
4. Check the source parameter matches your expected value
### Find Events for a Specific Customer
To review all events for a customer:
1. Click the Filter button
2. Add a filter: External Customer ID contains "your\_customer\_id"
3. Click Apply to see filtered results
### Review Events in a Time Period
To see events within a specific date range:
1. Click the Filter button
2. Set Start Time (After) to your beginning date
3. Set End Time (Before) to your ending date
4. Apply the filters
## Best Practices
**Test Event Integration Early**
Send test events and verify they appear in the debugger before going to production.
**Monitor Regularly**
Check the debugger periodically to ensure events are being received correctly.
**Verify After Changes**
After updating metered features or plans, use the debugger to confirm events still process correctly.
**Use Filters Effectively**
Combine multiple filters to quickly find specific events you need to review.
## Related Documentation
* [Creating a Metered Feature](/docs/event-ingestion/creating-a-metered-feature)
* [Sending Events](/docs/event-ingestion/sending-events)
* [Validating Events](/docs/event-ingestion/validating-events)
* [Troubleshooting](/docs/event-ingestion/troubleshooting)
## Need Help?
If events aren't appearing as expected:
1. Review the [Sending Events](/docs/event-ingestion/sending-events) guide to verify your integration
2. Check the [Validating Events](/docs/event-ingestion/validating-events) documentation
3. See the [Troubleshooting](/docs/event-ingestion/troubleshooting) guide for common issues
# Event Monitoring
Source: https://docs.flexprice.io/docs/event-ingestion/monitoring
Monitor the health and performance of your event ingestion and processing pipeline
The Event Monitoring endpoint helps you track event ingestion and processing in real-time, allowing you to identify bottlenecks and ensure events are flowing correctly through your system.
## API Endpoint
```
GET https://api.cloud.flexprice.io/v1/events/monitoring
```
### Authentication
Include your API key in the request header:
```
x-api-key:
```
### Request Parameters
This endpoint accepts no query parameters. It automatically monitors the last 24 hours of data for your tenant and environment.
### Response
```json theme={null}
{
"total_count": 15432,
"consumption_lag": 245,
"post_processing_lag": 78
}
```
**Response Fields:**
* `total_count` - Total distinct events stored in ClickHouse in the last 24 hours
* `consumption_lag` - Kafka consumer lag for event processing (number of unprocessed messages)
* `post_processing_lag` - Kafka consumer lag for feature usage tracking (number of unprocessed messages)
## Related Documentation
* [Sending Events](/docs/event-ingestion/sending-events)
* [Event Debugger](/docs/event-ingestion/event-debugger)
* [Troubleshooting](/docs/event-ingestion/troubleshooting)
# Overview
Source: https://docs.flexprice.io/docs/event-ingestion/overview
Learn how to send usage events to Flexprice for metered billing
Event ingestion is the core mechanism that powers Flexprice's metered billing system. It enables you to transmit usage data from your application to Flexprice, which then automatically calculates charges and generates invoices based on your pricing plans.
## What is Event Ingestion?
When you have a **Metered Feature** in Flexprice (such as API calls, storage usage, or credits consumed), you need to inform Flexprice when and how much your customers are utilizing that feature. This is accomplished by transmitting **events** - JSON payloads that describe usage activity.
## How It Works
1. **[Create a Metered Feature](/docs/event-ingestion/creating-a-metered-feature)** - Define what you want to track (e.g., "model.usage", "api.calls")
2. **[Send Events](/docs/event-ingestion/sending-events)** - Your application transmits usage data to Flexprice via API
3. **[Validate Events](/docs/event-ingestion/validating-events)** - Verify that events are being processed correctly
4. **[Connect to Billing](/docs/event-ingestion/connecting-to-billing)** - Link the feature to a pricing plan and customer subscription
5. **Automatic Invoicing** - Flexprice calculates charges and generates invoices
## Key Concepts
### Event
A JSON payload containing usage information:
* **Who used the feature** (`external_customer_id`)
* **What feature was used** (`event_name`)
* **How much was used** (`properties`)
* **When it happened** (`timestamp`)
### Aggregation
How Flexprice combines multiple events into a single quantity for billing (see [Aggregation Overview](/docs/product-catalogue/features/aggregation/overview) for detailed explanations):
* **Count** - Number of events
* **Sum** - Total of a numeric property
* **Average** - Mean value of a numeric property
* **Count Unique** - Number of distinct values
* **Latest** - Most recent value
* **Sum with Multiplier** - Sum with rate conversion
* **Max** - Highest value seen (supports bucketing)
* **Weighted Sum** - Time-weighted sum for capacity billing
### Usage Reset
How usage accumulates over time:
* **Periodic** - Resets each billing cycle (e.g., monthly API calls)
* **Cumulative** - Keeps growing (e.g., total storage used)
## Quick Start
Follow these steps to get started with event ingestion:
1. **[Create Your First Metered Feature](/docs/event-ingestion/creating-a-metered-feature)** - Set up what you want to track
2. **[Send Your First Event](/docs/event-ingestion/sending-events)** - Learn how to send usage data
3. **[Validate and Debug](/docs/event-ingestion/validating-events)** - Ensure events are working correctly
4. **[Connect to Billing](/docs/event-ingestion/connecting-to-billing)** - Turn usage into invoices
## Common Use Cases
* **API Usage** - Track API calls, requests, or tokens consumed
* **Storage** - Monitor file storage, database usage, or bandwidth
* **AI/ML** - Count model inferences, training hours, or data processed
* **User Activity** - Track logins, feature usage, or time spent
* **Compute Resources** - Monitor CPU time, memory usage, or GPU hours
## What You'll Learn
This guide will walk you through the complete event ingestion workflow, from setting up your first metered feature to viewing charges on customer invoices. By the end, you'll understand:
* How to configure metered features correctly
* The exact format for sending events
* How to verify that events are being processed
* How usage flows into billing and invoicing
* Common pitfalls and how to avoid them
## Related Documentation
* **[Features Overview](/docs/product-catalogue/features/aggregation/overview)** - Learn about different feature types
* **[Plans](/docs/product-catalogue/plans/overview)** - Understand pricing plan configuration
* **[Subscriptions](/docs/subscriptions/customers-create-subscription)** - Manage customer subscriptions
* **[Invoices](/docs/invoices/overview)** - View and manage billing
## Next Steps
Ready to get started? Begin with **[Creating a Metered Feature](/docs/event-ingestion/creating-a-metered-feature)**.
If you encounter any issues, check our **[Troubleshooting Guide](/docs/event-ingestion/troubleshooting)** for common solutions.
# Sending Events
Source: https://docs.flexprice.io/docs/event-ingestion/sending-events
Learn how to send usage events to Flexprice via API
Once you have created a metered feature, you can start transmitting events to Flexprice. Events are JSON payloads that describe usage activity and are transmitted via HTTP POST requests.
## Prerequisites
* A metered feature created in Flexprice
* Your API key from the Admin Dashboard
* The Event Name from your feature configuration
## API Endpoints
### Single Event
```
POST https://api.cloud.flexprice.io/v1/events
```
### Bulk Events
```
POST https://api.cloud.flexprice.io/v1/events/bulk
```
## Authentication
Include your API key in the request header:
```
x-api-key:
```
**Important**: Keep your API key secure and never expose it in client-side code.
## Event Payload Structure
### Required Fields
| Field | Type | Description | Example |
| ---------------------- | ------ | -------------------------------------------- | --------------- |
| `event_name` | string | Must exactly match your feature's Event Name | `"model.usage"` |
| `external_customer_id` | string | Your identifier for the customer | `"cust_123"` |
### Conditional Fields
| Field | Type | When Required | Description |
| -------------------- | ------------- | ---------------------------------- | ---------------------------------------------------- |
| `properties` | object | For Sum, Max, Latest, Unique Count | Contains the values to aggregate |
| `properties.` | number/string | When aggregation field is set | The exact field name from your feature configuration |
### Optional Fields
| Field | Type | Description | Example |
| ----------- | ------ | ------------------------------------ | ---------------------------- |
| `event_id` | string | Your unique identifier for the event | `"evt_123"` |
| `timestamp` | string | ISO 8601 UTC timestamp | `"2025-08-22T07:05:49.441Z"` |
| `source` | string | Origin of the event | `"api"`, `"worker"` |
## Single Event Examples
### Basic Event (Count Aggregation)
If your feature uses **Count** aggregation, you only need the required fields:
```json theme={null}
{
"event_name": "api.calls",
"external_customer_id": "cust_123"
}
```
### Event with Properties (Sum Aggregation)
If your feature uses **Sum** aggregation with field `credits`:
```json theme={null}
{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": {
"credits": 2
}
}
```
### Complete Event Example
```json theme={null}
{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": {
"credits": 2,
"model": "gpt-4",
"region": "us-east-1"
},
"event_id": "evt_abc123",
"timestamp": "2025-08-22T07:05:49.441Z",
"source": "api"
}
```
## cURL Examples
### Single Event
```bash theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/events \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"event_name": "model.usage",
"external_customer_id": "cust-test-customer",
"properties": {
"credits": 2
}
}'
```
### Response
```json theme={null}
{
"event_id": "event_01K389J4M1F1NZG6XP0AMD6J52",
"message": "Event accepted for processing"
}
```
## Bulk Events
Transmit multiple events in a single request for better performance:
```bash theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/events/bulk \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"events": [
{
"event_name": "model.usage",
"external_customer_id": "cust-test-customer",
"properties": { "credits": 2 }
},
{
"event_name": "model.usage",
"external_customer_id": "cust-another-customer",
"properties": { "credits": 5 }
}
]
}'
```
## Language Examples
### JavaScript (Node.js)
```javascript theme={null}
const fetch = require("node-fetch");
async function sendEvent() {
const response = await fetch("https://api.cloud.flexprice.io/v1/events", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.FLEXPRICE_API_KEY,
},
body: JSON.stringify({
event_name: "model.usage",
external_customer_id: "cust-test-customer",
properties: { credits: 2 },
source: "api",
}),
});
const result = await response.json();
console.log("Event transmitted:", result);
}
```
### Python
```python theme={null}
import os
import requests
import json
def send_event():
url = "https://api.cloud.flexprice.io/v1/events"
headers = {
"Content-Type": "application/json",
"x-api-key": os.environ["FLEXPRICE_API_KEY"]
}
data = {
"event_name": "model.usage",
"external_customer_id": "cust-test-customer",
"properties": {"credits": 2},
"source": "api"
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
print("Event transmitted:", result)
```
### Go
```go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
type Event struct {
EventName string `json:"event_name"`
ExternalCustomerID string `json:"external_customer_id"`
Properties map[string]interface{} `json:"properties,omitempty"`
Source string `json:"source,omitempty"`
}
func sendEvent() error {
event := Event{
EventName: "model.usage",
ExternalCustomerID: "cust-test-customer",
Properties: map[string]interface{}{
"credits": 2,
},
Source: "api",
}
jsonData, err := json.Marshal(event)
if err != nil {
return err
}
req, err := http.NewRequest("POST", "https://api.cloud.flexprice.io/v1/events", bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", os.Getenv("FLEXPRICE_API_KEY"))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
fmt.Printf("Event transmitted with status: %d\n", resp.StatusCode)
return nil
}
```
### PHP
```php theme={null}
'model.usage',
'external_customer_id' => 'cust-test-customer',
'properties' => [
'credits' => 2
],
'source' => 'api'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'x-api-key: ' . $apiKey
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "Event transmitted with status: $httpCode\n";
echo "Response: $response\n";
}
```
## Data Types and Best Practices
### Property Values
* **Numbers**: Use for Sum, Max aggregations
```json theme={null}
"properties": { "credits": 2 }
```
* **Strings**: Use for Unique Count, Latest aggregations
```json theme={null}
"properties": { "user_id": "user_123" }
```
### Timestamps
* Use ISO 8601 format in UTC
* Example: `"2025-08-22T07:05:49.441Z"`
* If omitted, server time is used
### Event IDs
* Optional but recommended for traceability
* Should be unique within your system
* Helps with debugging and idempotency
## Common Patterns
### API Usage Tracking
```json theme={null}
{
"event_name": "api.calls",
"external_customer_id": "cust_123",
"properties": {
"endpoint": "/v1/chat",
"method": "POST",
"response_time": 150
},
"source": "api"
}
```
### Storage Usage
```json theme={null}
{
"event_name": "storage.usage",
"external_customer_id": "cust_123",
"properties": {
"gb": 1.5,
"bucket": "user-uploads"
},
"source": "storage-service"
}
```
### User Activity
```json theme={null}
{
"event_name": "user.login",
"external_customer_id": "cust_123",
"properties": {
"user_id": "user_456",
"platform": "web"
},
"source": "auth-service"
}
```
## Error Handling
### HTTP Status Codes
* **202 Accepted**: Event accepted for processing
* **400 Bad Request**: Invalid payload or missing required fields
* **401 Unauthorized**: Invalid or missing API key
* **429 Too Many Requests**: Rate limit exceeded
### Common Errors
**Missing required field**
```json theme={null}
{
"error": "Missing required field: event_name"
}
```
**Invalid event\_name**
```json theme={null}
{
"error": "Event name 'unknown.event' not found"
}
```
**Invalid customer**
```json theme={null}
{
"error": "Customer with external_customer_id 'invalid_id' not found"
}
```
## Rate Limits
* **Single events**: 1000 requests per minute
* **Bulk events**: 100 requests per minute (up to 1000 events per request)
* **Response**: 429 status code when exceeded
## Best Practices
### 1. Validate Your Configuration
Before transmitting production events:
* Verify Event Name matches your feature exactly
* Confirm Aggregation Field exists in your events
* Test with a small number of events first
### 2. Handle Failures Gracefully
```javascript theme={null}
async function sendEventWithRetry(eventData, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch("https://api.cloud.flexprice.io/v1/events", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.FLEXPRICE_API_KEY,
},
body: JSON.stringify(eventData),
});
if (response.status === 202) {
return await response.json();
}
if (response.status === 429) {
// Rate limited — wait and retry
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
continue;
}
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
// Wait before retry
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
}
```
### 3. Use Bulk Events for High Volume
When transmitting many events, use the bulk endpoint:
```javascript theme={null}
const events = [
{ event_name: "api.calls", external_customer_id: "cust_1" },
{ event_name: "api.calls", external_customer_id: "cust_2" },
// ... more events
];
const response = await fetch("https://api.cloud.flexprice.io/v1/events/bulk", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.FLEXPRICE_API_KEY,
},
body: JSON.stringify({ events }),
});
```
### 4. Include Relevant Metadata
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"
}
```
## Related Documentation
* **[API Reference](/docs/event-ingestion/api-reference)** — Complete API documentation
* **[Validating Events](/docs/event-ingestion/validating-events)** — Check that events are being processed correctly
* **[Troubleshooting](/docs/event-ingestion/troubleshooting)** — Fix common issues
## Next Steps
After sending events, you should:
1. **[Validate Events](/docs/event-ingestion/validating-events)** - Verify that events are being processed correctly
2. **[Connect to Billing](/docs/event-ingestion/connecting-to-billing)** - Set up pricing and subscriptions
## Troubleshooting
### "Event not found" error
* Check that Event Name matches your feature exactly
* Verify the feature exists and is active
### "Customer not found" error
* Ensure the customer exists in Flexprice
* Check that `external_customer_id` matches exactly
### "Missing required field" error
* Verify all required fields are present
* Check field names match exactly (case-sensitive)
### Events not appearing in dashboard
* Check the **[Event Debugger](/docs/event-ingestion/event-debugger)** for processing status
* Verify Event Name and Aggregation Field configuration
* Ensure customer has an active subscription with the feature
# Troubleshooting Event Ingestion
Source: https://docs.flexprice.io/docs/event-ingestion/troubleshooting
Comprehensive guide to fixing common event ingestion issues in Flexprice
This guide helps you diagnose and fix common issues with event ingestion in Flexprice. Follow the systematic approach below to identify and resolve problems.
## Quick Diagnostic Checklist
Before diving into specific issues, run through this checklist:
* Feature exists and is active
* Event Name matches exactly (case-sensitive)
* External Customer ID exists in Flexprice
* API key is valid and has proper permissions
* Events are being transmitted to the correct endpoint
* Required fields are present in event payload
* Aggregation field exists in event properties (if required)
* Customer has an active subscription with the feature
## Common Issues and Solutions
### 1. Events Not Appearing in Dashboard
**Symptoms**: Events transmitted successfully (202 response) but don't show up in Events dashboard.
**Possible Causes and Solutions:**
#### A. Invalid Event Name
**Cause**: Event Name in your event doesn't match the feature configuration.
**Check**:
1. Go to **Product Catalog** → **Features**
2. Find your feature and note the exact Event Name
3. Compare with what you're transmitting in events
**Fix**:
```json theme={null}
// ❌ Wrong
{
"event_name": "Model.Usage",
"external_customer_id": "cust_123"
}
// ✅ Correct
{
"event_name": "model.usage",
"external_customer_id": "cust_123"
}
```
#### B. Customer Doesn't Exist
**Cause**: The external\_customer\_id doesn't exist in Flexprice.
**Check**:
1. Go to **Customers** section
2. Search for your external\_customer\_id
3. Verify the customer exists and is active
**Fix**:
* Create the customer first, or
* Use the correct external\_customer\_id
#### C. Feature Not Active
**Cause**: The feature exists but is not active.
**Check**:
1. Go to **Product Catalog** → **Features**
2. Find your feature
3. Verify status is "Active"
**Fix**:
* Activate the feature if it's archived
### 2. Wrong Aggregation Values
**Symptoms**: Events appear but billing shows incorrect quantities.
**Possible Causes and Solutions:**
#### A. Aggregation Field Name Mismatch
**Cause**: Property name in events doesn't match the Aggregation Field.
**Check**:
1. Go to your feature configuration
2. Note the exact Aggregation Field name
3. Compare with your event properties
**Fix**:
```json theme={null}
// Feature config: Aggregation Field = "credits"
// ❌ Wrong
{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": {
"tokens": 2 // Wrong field name
}
}
// ✅ Correct
{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": {
"credits": 2 // Correct field name
}
}
```
#### B. Wrong Data Type
**Cause**: Using strings instead of numbers for numeric aggregation.
**Check**:
* Numeric aggregations (SUM, AVERAGE, SUM WITH MULTIPLIER, MAX, WEIGHTED SUM) require numbers
* COUNT UNIQUE can use strings or numbers
* LATEST can use strings or numbers
* COUNT doesn't use properties
**Fix**:
```json theme={null}
// ❌ Wrong (for Sum aggregation)
{
"properties": {
"credits": "2" // String instead of number
}
}
// ✅ Correct
{
"properties": {
"credits": 2 // Number
}
}
```
#### C. Missing Properties
**Cause**: Events don't include the required aggregation field.
**Check**:
* Every event must include the aggregation field
* Check for typos in property names
**Fix**:
```json theme={null}
// ❌ Wrong (missing properties for Sum aggregation)
{
"event_name": "model.usage",
"external_customer_id": "cust_123"
// Missing properties object
}
// ✅ Correct
{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": {
"credits": 2
}
}
```
### 3. No Charges in Billing
**Symptoms**: Events are processed correctly but no charges appear in invoices.
**Possible Causes and Solutions:**
#### A. Feature Not in Plan
**Cause**: The feature exists but isn't added to any pricing plan.
**Check**:
1. Go to **Product Catalog** → **Plans**
2. Find the plan your customer is subscribed to
3. Verify the feature is included
**Fix**:
* Add the feature to the plan with appropriate pricing
#### B. Customer Not Subscribed
**Cause**: Customer exists but doesn't have an active subscription.
**Check**:
1. Go to **Customers** → Find your customer
2. Check subscription status
3. Verify the subscription includes the feature
**Fix**:
* Create a subscription for the customer
* Ensure the subscription includes the feature
#### C. Usage Below Free Tier
**Cause**: Usage is within the free tier limit.
**Check**:
1. Review plan configuration
2. Check free tier settings
3. Verify usage quantities
**Fix**:
* Adjust free tier limits if needed
* Transmit more usage to exceed free tier
#### D. Wrong Billing Period
**Cause**: Events are in a different billing period than expected.
**Check**:
1. Review subscription billing cycle
2. Check event timestamps
3. Verify billing period dates
**Fix**:
* Wait for the correct billing period
* Check upcoming invoices for the right period
### 4. API Errors
**Symptoms**: Getting error responses when transmitting events.
#### A. 400 Bad Request
**Common Causes**:
* Missing required fields
* Invalid JSON format
* Invalid field values
**Debug**:
```bash theme={null}
# Check your payload
curl -v --request POST \
--url https://api.cloud.flexprice.io/v1/events \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": {
"credits": 2
}
}'
```
**Common Fixes**:
```json theme={null}
// ❌ Missing required field
{
"external_customer_id": "cust_123"
// Missing event_name
}
// ✅ Complete payload
{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": {
"credits": 2
}
}
```
#### B. 401 Unauthorized
**Cause**: Invalid or missing API key.
**Fix**:
1. Verify your API key is correct
2. Check API key permissions
3. Ensure the key is active
#### C. 429 Too Many Requests
**Cause**: Rate limit exceeded.
**Fix**:
* Implement exponential backoff
* Use bulk events for high volume
* Reduce request frequency
### 5. Unexpected Usage Resets
**Symptoms**: Usage resets when it shouldn't.
#### A. Wrong Usage Reset Setting
**Check**:
1. Go to your feature configuration
2. Review Usage Reset setting:
* **Periodic**: Resets each billing cycle
* **Cumulative**: Keeps growing
**Fix**:
* Change Usage Reset setting if needed
* Note: This may require creating a new feature
#### B. Subscription Changes
**Cause**: Subscription was modified or recreated.
**Check**:
1. Review subscription history
2. Check for plan changes
3. Verify billing cycle alignment
### 6. Duplicate Events
**Symptoms**: Same event counted multiple times.
**Causes**:
* Retrying failed requests
* Multiple event sources
* Application bugs
**Solutions**:
* Use event\_id for idempotency
* Implement proper retry logic
* Check for duplicate event sources
## Systematic Debugging Approach
### Step 1: Verify Event Ingestion
1. Send a test event
2. Check API response (should be 202 Accepted)
3. Look for the event in Events dashboard
4. Verify the payload is correct
### Step 2: Check Feature Configuration
1. Confirm Event Name matches exactly
2. Verify Aggregation Function is correct
3. Check Aggregation Field name
4. Review Usage Reset setting
### Step 3: Validate Customer Setup
1. Ensure customer exists
2. Check external\_customer\_id matches
3. Verify subscription is active
4. Confirm plan includes the feature
### Step 4: Review Billing Configuration
1. Check plan pricing settings
2. Verify free tier configuration
3. Review billing cycle dates
4. Check upcoming invoices
## Debugging Tools
### Event Debugger
Use the Event Debugger for real-time insights:
1. Go to **Usage Tracking** → **Event Debugger**
2. Monitor events as they're processed
3. Look for errors or warnings
### Events Dashboard
Check the Events dashboard for:
1. Event processing status
2. Complete event payloads
3. Timestamp information
4. Customer mapping
### Query Tool
Use the Query tool to:
1. View aggregated usage
2. Check billing period alignment
3. Verify aggregation calculations
## Testing Your Fix
After implementing a fix:
1. **Send Test Events**
```bash theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/events \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"event_name": "model.usage",
"external_customer_id": "cust-test-customer",
"properties": {
"credits": 1
}
}'
```
2. **Verify Event Processing**
* Check Events dashboard
* Confirm payload is correct
* Verify aggregation values
3. **Check Billing Impact**
* Review upcoming invoices
* Confirm charges appear correctly
* Verify amounts are accurate
## Prevention Best Practices
### 1. Test Before Production
* Always test with a small number of events
* Use test customers and features
* Verify the complete flow end-to-end
### 2. Monitor Regularly
* Check Events dashboard daily
* Review usage patterns weekly
* Monitor billing accuracy monthly
### 3. Use Consistent Naming
* Standardize Event Names across your system
* Use consistent external\_customer\_id format
* Follow naming conventions for properties
### 4. Implement Proper Error Handling
* Handle API errors gracefully
* Implement retry logic with backoff
* Log failed events for debugging
### 5. Document Your Setup
* Keep records of feature configurations
* Document Event Names and field mappings
* Maintain customer ID mappings
## Getting Additional Help
If you're still experiencing issues:
1. **Gather Information**:
* Event payload examples
* Feature configuration screenshots
* Error messages and timestamps
* Customer and subscription details
2. **Check Documentation**:
* Review relevant guides
* Check API reference
* Look for similar issues
3. **Contact Support**:
* Provide specific error details
* Include relevant screenshots
* Share event examples (with sensitive data removed)
## Common Configuration Examples
### API Usage Tracking
```json theme={null}
// Feature: API Calls (Count)
{
"event_name": "api.calls",
"external_customer_id": "cust_123"
}
// Feature: API Usage (Sum of tokens)
{
"event_name": "api.usage",
"external_customer_id": "cust_123",
"properties": {
"tokens": 150
}
}
```
### Storage Usage
```json theme={null}
// Feature: Storage (Sum of GB)
{
"event_name": "storage.usage",
"external_customer_id": "cust_123",
"properties": {
"gb": 1.5
}
}
```
### User Activity
```json theme={null}
// Feature: Active Users (Unique Count)
{
"event_name": "active.users",
"external_customer_id": "cust_123",
"properties": {
"user_id": "user_456"
}
}
```
Remember: The key to successful event ingestion is attention to detail. Small differences in naming, data types, or configuration can cause significant issues. Always test thoroughly and monitor continuously.
# Validating Events
Source: https://docs.flexprice.io/docs/event-ingestion/validating-events
Learn how to verify that events are being processed correctly in Flexprice
After transmitting events to Flexprice, it's crucial to verify that they're being processed correctly. This guide shows you how to check event ingestion, debug issues, and ensure your billing calculations are accurate.
## Prerequisites
* Events have been transmitted to Flexprice
* Access to the Flexprice Admin Dashboard
* Knowledge of your feature configuration
## Checking Event Ingestion
### 1. Navigate to Events Dashboard
1. Log into your Flexprice Admin Dashboard
2. In the left sidebar, click on **Developers**
3. Click on **[Event Debugger](https://admin.flexprice.io/usage-tracking/events?page=1)**
You'll see a list of all ingested events with key information:
* **Event ID**: Unique identifier for each event
* **Event Name**: The feature being tracked
* **External Customer ID**: Customer associated with the event
* **Source**: Origin of the event (if provided)
* **Timestamp**: When the event occurred
### 2. View Event Details
Click on any event in the list to view the complete payload that was transmitted:
This displays the exact JSON payload that Flexprice received, including:
* All properties transmitted with the event
* Timestamp information
* Customer details
* Any additional metadata
**Important**: Verify that the `properties` object contains the correct aggregation field with the expected value.
## Using the Event Debugger
The Event Debugger provides real-time insights into event processing:
1. Go to **Usage Tracking** → **Event Debugger**
2. You can see:
* Events being processed in real-time
* Any errors or warnings
* Processing status and timing
This is especially useful for debugging issues with event ingestion.
## Validating Aggregation
### Check Feature Configuration
1. Go to **Product Catalog** → **Features**
2. Find your metered feature
3. Verify the configuration:
* **Event Name** matches what you're transmitting
* **Aggregation Function** is set correctly
* **Aggregation Field** exists in your event properties
### Verify Event Properties
In the Events dashboard, check that:
1. **Event Name** matches your feature exactly
2. **Properties** contain the aggregation field
3. **Property values** are the correct data type
**Example**: If your feature aggregates `credits`, ensure every event has:
```json theme={null}
{
"properties": {
"credits": 2
}
}
```
## Common Validation Issues
### 1. Events Not Appearing
**Possible Causes:**
* Invalid Event Name
* Missing required fields
* Customer doesn't exist
* API key issues
**Solutions:**
* Check Event Name spelling and case
* Verify all required fields are present
* Ensure customer exists in Flexprice
* Validate API key permissions
### 2. Wrong Aggregation Values
**Possible Causes:**
* Aggregation field name mismatch
* Incorrect data types
* Missing properties
**Solutions:**
* Verify field name matches exactly (case-sensitive)
* Use numbers for Sum/Max, strings for Unique Count
* Ensure every event includes the required property
### 3. Events Appearing but No Billing
**Possible Causes:**
* Feature not added to a plan
* Customer not subscribed to the plan
* Usage reset configuration
**Solutions:**
* Add feature to a pricing plan
* Subscribe customer to the plan
* Check Usage Reset setting (Periodic vs Cumulative)
## Testing Your Setup
### Step-by-Step Validation
1. **Transmit a Test Event**
```bash theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/events \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--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
# 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://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://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://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://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://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://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://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://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://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://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://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://api.flexprice.io/v1/coupons \
-H "x-api-key: "
# Get a coupon
curl https://api.flexprice.io/v1/coupons/ \
-H "x-api-key: "
# Search coupons by status
curl -X POST https://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://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://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.
# 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
# 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://api.flexprice.io/v1/prices/{price_id}" \
-H "Authorization: Bearer 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://api.flexprice.io/v1/prices/{price_id}" \
-H "Authorization: Bearer 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://api.flexprice.io/v1/plans/{plan_id}/sync/subscriptions" \
-H "Authorization: Bearer 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. Manage post-creation changes via the subscription modification API.
A **tax association** is the link between a tax rate and an entity. 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).
## 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 |
| `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://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",
"priority": 0
}'
```
**Response**
```json theme={null}
{
"id": "txa_01abc123",
"tax_rate_id": "txr_01abc456",
"entity_type": "tenant",
"entity_id": "",
"auto_apply": true,
"currency": "USD",
"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://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",
"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://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"
}
]
}'
```
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"` |
| `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://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://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://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://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://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://api.flexprice.io/v1/taxes/associations?entity_type=customer&entity_id=" \
-H "x-api-key: "
```
## Update an association
You can update `priority`, `auto_apply`, and `metadata` without deleting and recreating.
```bash theme={null}
curl -X PUT https://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.
## Common patterns
**Exempt a specific customer from tax**
Create no customer-level associations and ensure no tenant associations set to auto-apply are inherited. Customers with no resolvable active association generate invoices with zero tax.
**Give one subscription different tax treatment**
Use `tax_rate_overrides` at subscription creation. The customer's associations remain unchanged for their other subscriptions.
**Stop a tax mid-lifecycle**
Use the modification API with `action: "remove"`. Pass the `tax_association_id` from listing the subscription's associations.
# 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. You create it once with a unique `code`, then reference that `code` whenever you link it to a customer, subscription, or tenant.
## Create a tax rate
```bash theme={null}
curl -X POST https://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"
}
```
## Request fields
| Field | Type | Required | Description |
| ------------------ | -------------- | ------------------------------- | --------------------------------------------------------------------- |
| `name` | string | Yes | Display name shown in invoices and the dashboard |
| `code` | string | Yes | Unique identifier used in associations. **Immutable after creation.** |
| `tax_rate_type` | string | Yes | `percentage` or `fixed` |
| `percentage_value` | decimal string | When `tax_rate_type=percentage` | Rate as a percent, e.g. `"8.25"` for 8.25% |
| `fixed_value` | decimal string | When `tax_rate_type=fixed` | Flat amount added per invoice, e.g. `"5.00"` |
| `description` | string | No | Notes for your own reference |
| `metadata` | object | No | Key-value pairs for your own tracking |
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.
## Create a fixed-amount tax rate
Use `tax_rate_type: fixed` when you need to add a flat fee to every invoice regardless of the subtotal, such as a regulatory levy.
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/taxes/rates \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{
"name": "Regulatory Fee",
"code": "FEE_REG_US",
"tax_rate_type": "fixed",
"fixed_value": "5.00"
}'
```
## List tax rates
```bash theme={null}
curl https://api.flexprice.io/v1/taxes/rates \
-H "x-api-key: "
```
## Get a tax rate
```bash theme={null}
curl https://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://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://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
Once you have a tax rate, [apply it to a customer or subscription](/docs/product-catalogue/taxes/apply).
# Taxes Overview
Source: https://docs.flexprice.io/docs/product-catalogue/taxes/overview
How flexprice handles tax rates, associations, and automatic inheritance across customers and subscriptions.
flexprice uses a two-object model for taxes: a **tax rate** (the reusable definition) and a **tax association** (the link between that rate and an entity). You define rates once and attach them to your tenant, customers, or individual subscriptions. Invoice generation picks up the right rates automatically and applies them to the post-discount subtotal.
## Two objects, one workflow
**Tax Rate**
The rate definition: its `tax_rate_type` (`percentage` or `fixed`), its value, and a unique `code` you reference everywhere else.
**Tax Association**
The link between a tax rate and an entity (`tenant`, `customer`, `subscription`, or `invoice`). The association controls whether the rate fires automatically on new invoices and at what priority when multiple rates apply.
## Inheritance chain
flexprice resolves which tax rates apply to an invoice by walking this chain, from least specific to most specific:
```
Tenant-level associations
(inherited when a customer has no associations of its own)
Customer-level associations
(inherited when a subscription has no tax_rate_overrides)
Subscription-level associations <-- highest specificity, always wins
```
Setting `tax_rate_overrides` on a subscription replaces, not adds to, the customer's associations for that subscription's invoices. Omit `tax_rate_overrides` entirely to inherit from the customer.
An invoice gets **no tax** if none of the three levels has an active association set to apply automatically. Verify your inheritance chain before going live.
## Where tax fits in invoice calculation
Tax is always computed on the post-discount, post-credit balance:
| Step | Value |
| ------------------------------ | ------------------------------------------------------- |
| Subtotal | Sum of all line items |
| Line-item coupon discounts | Applied per line item |
| Invoice-level coupon discounts | Applied to running subtotal |
| Wallet / prepaid credits | Deducted |
| **Taxable amount** | `MAX(subtotal - all_discounts, 0)` |
| **Tax** | Sum of all matching tax rates applied to taxable amount |
| **Invoice total** | `subtotal - discounts - wallet_credits + tax` |
Tax is never charged on a negative base. If coupons eliminate the subtotal, tax is zero.
## Tax rate types
| `tax_rate_type` | Use case | Value field |
| --------------- | ---------------------------------------- | -------------------------------------------- |
| `percentage` | Sales tax, VAT, GST | `percentage_value` (e.g. `"8.25"` for 8.25%) |
| `fixed` | Flat regulatory fees, per-invoice levies | `fixed_value` |
## Multiple tax rates
You can attach multiple tax rate associations to the same entity. Each rate is applied independently to the same taxable base (they do not compound). Use `priority` (lower number fires first) to control application order when ordering matters for reporting.
## Quick start
1. [Create a tax rate](/docs/product-catalogue/taxes/configure) with a unique `code` and a `percentage` or `fixed` value.
2. [Create a tax association](/docs/product-catalogue/taxes/apply) to link that rate to your tenant, a customer, or a subscription.
3. Generate an invoice. flexprice resolves applicable rates via the inheritance chain and applies them to the taxable amount automatically.
# Manage API Keys
Source: https://docs.flexprice.io/docs/rbac/api
Generate and manage API keys with specific permissions
API keys allow your services to authenticate with Flexprice. When you create an API key for a service account, it inherits that service account's permissions.
## How API Keys Work
When you create an API key:
1. The key copies the service account's roles
2. The key's role are set and cannot be changed
3. Your service uses this key to make API calls
4. Flexprice checks the key's permissions for each request
**Permissions Are Set at Creation**
Once an API key is created, its permissions don't change.
To get new permissions, create a new API key with required.
## Creating an API Key
### Via Dashboard
Navigate to **Developers > API Keys** and click **Create** to open the API key creation dialog.
In the dialog:
1. **Name**: Give your API key a descriptive name
2. **Account Type**: Select between User Account or Service Account
3. **Mapped to Identity**: If Service Account is selected, choose which service account to use
4. **Expiration**: Set when the key should expire (or select Never)
5. Click **Create**
The API key will be shown once - make sure to copy and save it securely.
### Via API - For Your Own Account
Create an API key for yourself:
```bash cURL theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/secrets/api/keys \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"expires_at": "",
"name": "",
"type": "private_key"
}'
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.cloud.flexprice.io/v1/secrets/api/keys', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': ''
},
body: JSON.stringify({
name: 'My Production Key',
type: 'private_key'
})
});
```
```python Python theme={null}
import requests
response = requests.post(
'https://api.cloud.flexprice.io/v1/secrets/api/keys',
headers={
'Content-Type': 'application/json',
'x-api-key': ''
},
json={
'name': 'My Production Key',
'type': 'private_key'
}
)
```
### For a Service Account
Create an API key for a service account:
```bash theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/secrets/api/keys \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"expires_at": "",
"name": "",
"service_account_id": "",
"type": "private_key"
}'
```
### Response
```json theme={null}
{
"id": "sec_xyz789",
"name": "Event Service Production Key",
"api_key": "sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"roles": ["event_ingestor"],
"created_at": "2025-11-14T10:00:00Z"
}
```
**Save Your API Key Now**
The `api_key` value is only shown once, when you create it. Copy and store it securely immediately - you cannot retrieve it later.
## Storing API Keys Securely
**✅ Good Practices:**
* Store in environment variables
* Use secret management services (AWS Secrets Manager, HashiCorp Vault)
* Use Kubernetes Secrets for containerized apps
* Store in your CI/CD platform's secret storage
**❌ Bad Practices:**
* Committing to Git repositories
* Hardcoding in your application code
* Sharing via email or messaging apps
* Storing in plain text files
## Request Parameters
A descriptive name to help you identify this key later.
Examples: `"Production Event Service"`, `"Staging Analytics"`, `"Development Integration"`
The ID of the service account to create the key for. If not provided, creates a key for your own account.
## Understanding Roles and Permissions
### Example 1: Event Ingestor Role
**Service Account has**: `event_ingestor` role
**API Key inherits**: `event_ingestor` role
**What happens**:
* ✅ Can send events
* ❌ Cannot read events (403 Forbidden)
### Example 2: Multiple Roles
**Service Account has**: Both `event_ingestor` and `event_reader` roles
**API Key inherits**: Both roles
**What happens**:
* ✅ Can send events (from `event_ingestor`)
* ✅ Can read events (from `event_reader`)
## Best Practices
**Use Descriptive Names**
Name your keys clearly: `"production-event-service"`, `"staging-analytics"`, `"dev-integration"`
**Rotate Keys Regularly**
Create new API keys and delete old ones every 90 days.
**One Key Per Service**
Create separate API keys for each service or integration, even if they use the same service account.
## Troubleshooting
### API Key Not Working
**Symptom**: Getting "Unauthorized" errors when using the API key.
**Possible Causes**:
* API key format is incorrect
* API key has been deleted
* Using the wrong key for the environment (test vs. production)
**Solution**:
1. Check if the key still exists
2. Make sure you're using the correct environment
### Getting 403 Forbidden Errors
**Symptom**: API calls return "Forbidden" errors.
**Possible Causes**:
* The service account doesn't have the required role
* The API key was created before roles were assigned
* The role doesn't include the permission you need
**Solution**:
1. Check what roles the API key has
2. Verify those roles include the permission you need
3. If roles were recently added, create a new API key with the service account associated with required roles
### Lost API Key
**Symptom**: You didn't save the API key when you created it.
**Solution**: API keys cannot be retrieved after creation. You need to:
1. Create a new API key for the same service account
2. Update your service with the new key
3. Delete the old key
## Related Resources
Learn how to create service accounts →
See all available roles and permissions →
# Overview
Source: https://docs.flexprice.io/docs/rbac/overview
Control access to your Flexprice resources with role-based permissions
Flexprice's Role-Based Access Control (RBAC) system allows you to create service accounts with specific permissions for automated services and integrations. Instead of sharing your main account credentials, create dedicated service accounts with only the access they need.
## What is RBAC?
RBAC lets you create service accounts for your automated systems - like event ingestion services, analytics dashboards, or CI/CD pipelines - and assign each one specific roles that control what it can access.
**Key Benefits:**
* **Enhanced Security**: Limit what each service can access
* **Controlled Access**: Assign specific permissions based on what each service needs to do
* **Easy Management**: Create and revoke service account access without affecting your main account
* **Clear Separation**: Different API keys for different purposes
## How It Works
When you create a service account, you assign it one or more roles. Each role grants specific permissions to access certain resources.
**Example Flow:**
1. Create a service account and assign it the `event_ingestor` role
2. Generate an API key for that service account
3. The API key inherits the `event_ingestor` permissions
4. Your service can now ingest events but cannot access customer data or billing information
## User Types
### Regular Users (User Accounts)
Your standard user accounts for people. These accounts:
* Can log in to the dashboard
* Have full access to all Flexprice resources
* API keys from user accounts have full access
### Service Accounts
Automated accounts for your services and integrations. These accounts:
* Cannot log in to the dashboard
* Must have at least one role assigned
* Access Flexprice only through API keys
* API keys from service accounts have restricted access based on assigned roles
## Available Roles
### super\_admin
For services that need unrestricted access to every Flexprice resource.
**Can do:**
* Perform any action on any resource (read, write, delete)
**Cannot do:**
* Nothing — this role grants `*` permissions on `*` resources
**Use for:** Internal automation, migrations, or admin tooling that needs the same access as a user account. Treat the API key like a root credential.
Only assign `super_admin` when no narrower role works. Rotate keys frequently and store them in a secret manager.
### event\_ingestor
For services that send events to Flexprice.
**Can do:**
* Send events
**Cannot do:**
* Read events
* Access any other resources
**Use for:** Event ingestion services, usage tracking tools
### event\_reader
For services that read event data.
**Can do:**
* Read events
**Cannot do:**
* Send events
* Modify any data
**Use for:** Analytics dashboards, reporting tools
## Real-World Use Cases
### Use Case 1: Event Ingestion Service
**Scenario**: You have a microservice that sends usage events to Flexprice.
**Solution**: Create a service account with the `event_ingestor` role and generate an API key.
**What happens**:
* ✅ Your service can send events successfully
* ❌ If it tries to read events, it gets 403 Forbidden
* ❌ It cannot access any other resources
### Use Case 2: Analytics Dashboard
**Scenario**: Your analytics dashboard needs to read events for reporting.
**Solution**: Create a service account with the `event_reader` role and generate an API key.
**What happens**:
* ✅ Your dashboard can read event data
* ❌ If it tries to send events, it gets 403 Forbidden
* ❌ It cannot modify any data
## Validation Rules
**Service Accounts Require Roles**
Every service account must have at least one role assigned. You cannot create a service account without specifying roles.
### Common Validation Errors
**Error: No Roles Assigned**
```json theme={null}
{
"error": "Service accounts must have at least one role assigned"
}
```
**Error: Invalid Role**
```json theme={null}
{
"error": "Invalid role: custom_role"
}
```
## Error Responses
### 403 Forbidden
When a service account tries to access a resource it doesn't have permission for:
```json theme={null}
{
"error": "Forbidden",
"message": "Insufficient permissions to write event"
}
```
**What this means**: The API key doesn't have the required permission. You need to:
1. Check which role the service account has
2. Verify that role includes the permission you need
3. If needed, assign a different role to the service account
4. Generate a new API key (permissions are set when the key is created)
## Getting Started
### Step 1: Create a Service Account
Choose the appropriate role for your use case and create a service account:
```bash theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/users \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"roles": [
""
],
"type": "user"
}'
```
### Step 2: Generate an API Key
Create an API key for your service account:
```bash theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/secrets/api/keys \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"expires_at": "",
"name": "",
"service_account_id": "",
"type": "private_key"
}'
```
The API key is only shown once. Copy and store it securely immediately.
### Step 3: Use the API Key
Use the API key in your service:
```bash theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/events \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"customer_id": "customer456",
"event_id": "event123",
"event_name": "api_request",
"external_customer_id": "customer456",
"properties": {
"\"response_status\"": "200}",
"{\"request_size\"": "100"
},
"source": "api",
"timestamp": "2024-03-20T15:04:05Z"
}'
```
## Best Practices
**Use Specific Roles**
Assign only the roles your service needs. Don't use broader permissions than necessary.
**Rotate API Keys Regularly**
Generate new API keys periodically (recommended: every 90 days) and delete old ones.
**Store Keys Securely**
Use environment variables or secret management systems. Never commit API keys to your code repository.
## Troubleshooting
### Getting 403 Errors
**Symptom**: Your service is getting "Forbidden" errors when making API calls.
**Possible Causes**:
* The service account doesn't have the role that grants the required permission
* The API key was created before you assigned roles to the service account
* You're trying to access a resource that the assigned role doesn't cover
**Solution**:
1. Check what roles the service account has
2. Verify those roles include the permission you need
3. If you recently added roles, create a new API key
4. Test with a simple request to verify permissions
### Cannot Create Service Account
**Symptom**: Getting an error when trying to create a service account.
**Possible Causes**:
* Didn't specify any roles
* Specified an invalid role name
**Solution**: Make sure you're providing at least one valid role in the `roles` array.
## Next Steps
Learn how to create service accounts →
Learn how to generate and manage API keys →
# Creating a Service Account
Source: https://docs.flexprice.io/docs/rbac/service-accounts/create
Create a service account with specific permissions for your automated services
A service account is a special type of account for automated services, integrations, and scripts. Unlike your regular user account, service accounts are assigned specific roles that control what resources they can access in Flexprice.
## Why Use Service Accounts?
Service accounts let you:
* **Isolate access**: Each service gets its own account with only the permissions it needs
* **Improve security**: If an API key is compromised, the damage is limited to that service's permissions
* **Track usage**: See which service is making which API calls
* **Manage easily**: Revoke access for one service without affecting others
## Creating a Service Account
### Choose the Right Role
Before creating a service account, decide which role fits your use case:
Flexprice currently supports following custom roles:
| Role | Use When |
| ---------------- | --------------------------------------------------------------------------------- |
| `super_admin` | Your service needs full, unrestricted access (treat the key as a root credential) |
| `event_ingestor` | Your service needs to send events to Flexprice |
| `event_reader` | Your service needs to read event data |
### Via Dashboard
Navigate to **Developers > Service Accounts** and click **Create** to open the service account creation dialog.
In the dialog:
1. Select the roles you want to assign (Event Ingestor, Event Reader, or both)
2. Click **Create Service Account**
The service account is created with the selected roles and you can now generate API keys for it.
### Via API
```bash cURL theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/users \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"type": "service_account",
"name": "Event Ingestion Worker",
"roles": ["event_ingestor"]
}'
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.cloud.flexprice.io/v1/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': ''
},
body: JSON.stringify({
type: 'service_account',
name: 'Event Ingestion Worker',
roles: ['event_ingestor']
})
});
```
```python Python theme={null}
import requests
response = requests.post(
'https://api.cloud.flexprice.io/v1/users',
headers={
'Content-Type': 'application/json',
'x-api-key': ''
},
json={
'type': 'service_account',
'name': 'Event Ingestion Worker',
'roles': ['event_ingestor']
}
)
```
### Response
```json theme={null}
{
"id": "usr_abc123",
"type": "service_account",
"roles": ["event_ingestor"],
"created_at": "2025-11-14T10:00:00Z"
}
```
Save the `id` from the response. You'll need it when creating API keys.
## Request Fields
Must be set to `"service_account"`.
List of roles to assign. Must include at least one role.
Available roles:
* `super_admin` - Full access to every resource
* `event_ingestor` - Can send events
* `event_reader` - Can read events
Display name shown in the dashboard and audit logs. Helps you identify the
service account later when listing or updating it.
## Validation Rules
**At Least One Role Required**
You must assign at least one role when creating a service account. Empty roles will return an error.
### Common Errors
**No Roles Provided**
```json theme={null}
{
"error": "Service accounts must have at least one role assigned"
}
```
**Invalid Role Name**
```json theme={null}
{
"error": "Invalid role: custom_role"
}
```
## Real-World Examples
### Example 1: Event Ingestion Service
**Scenario**: You have a service that sends user events to Flexprice.
**Create service account with**:
* Role: `event_ingestor`
**What happens**:
* ✅ Service can send events
* ❌ Service cannot read events (403 Forbidden)
* ❌ Service cannot access other resources (403 Forbidden)
### Example 2: Analytics Dashboard
**Scenario**: You have a dashboard that displays event data.
**Create service account with**:
* Role: `event_reader`
**What happens**:
* ✅ Dashboard can read events
* ❌ Dashboard cannot send events (403 Forbidden)
* ❌ Dashboard cannot modify anything (403 Forbidden)
## Assigning Multiple Roles
You can assign both roles to give combined permissions:
**Service account with both roles**:
* Roles: `event_ingestor` and `event_reader`
**What happens**:
* ✅ Can send events (from `event_ingestor`)
* ✅ Can read events (from `event_reader`)
## Updating a Service Account
Rename a service account at any time. Only the display `name` can be changed; roles and type are immutable.
```bash theme={null}
curl --request PUT \
--url https://api.cloud.flexprice.io/v1/users/{id} \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"name": "Event Ingestion Worker (prod)"
}'
```
To rotate which roles a service account has, create a new service account with the new role set and migrate the API key over.
## Deleting a Service Account
Archive a service account when it is no longer in use. Existing API keys for that account stop working immediately.
```bash theme={null}
curl --request DELETE \
--url https://api.cloud.flexprice.io/v1/users/{id} \
--header 'x-api-key: '
```
The endpoint returns `204 No Content` on success. The account is soft-deleted, so its history remains in audit logs.
## Best Practices
**One Service Account Per Service**
Create separate service accounts for each automated service or integration.
**Assign Minimal Permissions**
Only assign the roles your service actually needs.
## Troubleshooting
### Service Getting 403 Errors
**Symptom**: Your service is getting "Forbidden" errors when making API calls.
**Possible Causes**:
1. The service account doesn't have the required role
2. The API key was created before you assigned roles
3. The role doesn't include permission for that endpoint
**Solution**:
1. Check what roles the service account has: `GET /v1/users/usr_abc123`
2. Verify those roles include the permission you need
3. If you recently added roles, create a new API key
### Cannot Create Service Account
**Symptom**: Getting an error when creating the service account.
**Possible Causes**:
1. Forgot to include `roles` in the request
2. Used an invalid role name
3. Specified `roles` as an empty array
**Solution**: Make sure your request includes at least one valid role:
```json theme={null}
{
"type": "service_account",
"roles": ["event_ingestor"]
}
```
## Next Steps
Create API keys for your service account →
# How to evolve pricing
Source: https://docs.flexprice.io/docs/scenarios/how-to-evolve-pricing
Use scenarios to experiment with packaging, rates, and pricing models before rolling them out
Pricing should evolve in lockstep with your business. Market conditions shift, competitors adjust, and customer feedback reveals gaps in your current packaging. Scenarios let you model alternatives across all of Flexprice's pricing capabilities and make the best decision, with no end-customer consequences.
## Experimenting with packaging
Plans (a pre-bundled set of prices) simplify how customers buy your product. But they can quickly become outdated as customer needs, willingness to pay, and market conditions shift. If your historical best-sellers no longer drive value, it's time to refresh your packaging.
Create a [scenario](/docs/scenarios/overview) and start from your current plan prices so the hypothesis is grounded in what customers pay today.
Add, remove, or modify usage-based or fixed fee prices to define your hypothesis. For a more significant packaging refresh or full plan redesign, start from scratch instead.
## Updating price rates
To simulate rate changes for existing prices, work from a copy of your current pricing so each hypothesis is comparable against the baseline.
Copy your current pricing into a hypothesis to preserve your baseline for comparison.
Edit the individual rate or change the pricing model on the hypothesis.
Duplicate the hypothesis to model additional rate changes side by side.
## Simulating adjustments
Whether you're planning the rollout of new pricing or understanding the impact of negotiated rates for enterprise customers, simulate the impact of [adjustments](/docs/product-catalogue/plans/price-overrides) (including discounts and spend maximums) before rolling them out.
## Transitioning to a credits-first model
Run an impact analysis of a credits-first pricing model before making the switch. Create a hypothesis with a prepaid credit allocation to understand the revenue contribution of an upfront fixed commitment plus overage spend, relative to your current accrued pricing.
A credits-first model changes how and when revenue is recognized. Review how [prepaid and promotional credits](/docs/wallet/prepaid-and-promotional) work in Flexprice before modeling the transition.
# How to launch new products
Source: https://docs.flexprice.io/docs/scenarios/how-to-launch-new-products
Use scenarios to de-risk first-time monetization and turn pricing into a growth lever
Monetizing new experiences is all about value capture and alignment. Scenarios help you navigate first-time monetization or evolve your pricing as your product matures, without real-world billing risk.
## Early-stage products: transitioning from legacy pricing
The shift from legacy subscription pricing to usage-first monetization requires careful risk management. If you've launched a new AI feature or agent, folding this functionality into the price of an existing seat-based subscription won't scale. As adoption grows, so do your costs.
Introduce usage-first pricing via a scenario hypothesis. This lets you monitor product adoption and costs over time while building a business case for when and how to price a new product. Common starting points include:
* **Standalone pricing**: charge for the new product directly to de-risk the rest of your offerings
* **Hybrid pricing**: reduce fixed fees alongside usage-based pricing to lower the barrier to entry while maintaining repeatable revenue
## Growth-stage products: finding your value metric
For products entering the growth and scale stage, monetization is about aligning pricing with the value your product delivers. For AI builders especially, this means evaluating and transitioning across different metric types:
* **Cost-centric**: pricing is directly tied to unit economics. In early stages, the cost of using your product is passed through to customers.
* **Usage-first**: pricing is tied to overall adoption and usage. Typically commands a higher rate than cost-centric pricing.
* **Value-based / outcome-based**: pricing is tied to outcomes and product value. When your customers win, you win. This commands the highest price point and encourages further product use.
In Flexprice Scenarios, model each metric type as a separate hypothesis. Comparing results side by side surfaces the differences, implications, and tradeoffs of each pricing approach, without any real-world consequences.
## Mature products: tuning rates to the market
Even for established products, changing market conditions and competitive pressure require constant pricing refinement.
Copy your existing pricing into a hypothesis to preserve a baseline for comparison.
Edit the individual rate or change the pricing model on the hypothesis.
Duplicate the hypothesis to model additional changes side by side.
These tools let you quickly test new pricing hypotheses and experiment with your model, all grounded in real historical data.
# Overview
Source: https://docs.flexprice.io/docs/scenarios/overview
Visualize the impact of pricing changes using your real historical data before making them live
Scenarios let you model price changes in Flexprice before you make them, with none of the billing risk that comes with real-world experimentation. Define the price change you're considering, set your parameters, and Flexprice outputs forecasted revenue, customer impact, and churn guidance using your real historical data, so you can make an informed decision before going live.
Because Flexprice sits at the intersection of your product usage and revenue data, Scenarios are useful for:
* **Backtesting** to de-risk price changes before rolling them out
* **Forecasting** for new product pricing
* **Churn prediction** to identify which customers are most at risk of churning if prices change
## Before you start
To use Scenarios, make sure you're up and running with Flexprice. Specifically, you'll need to:
* [**Ingest events**](/docs/event-ingestion/overview): gives Flexprice visibility into your product usage to power the scenario
* [**Define a meter**](/docs/event-ingestion/creating-a-metered-feature): models the specific usage metric the price change applies to
* [**Have a plan with subscriptions**](/docs/subscriptions/customers-create-subscription): defines the customer cohort your scenario will run over
## Creating a scenario
Name your scenario for easy reference, set a historical time frame, and select a plan to run the scenario over. This determines the historical data used in your analysis.
Aim for a window that provides sufficient, representative data. At least 30 to 90 days is recommended for meaningful results. The longer the window, the more visibility you have into customer behavior and seasonal patterns.
A hypothesis represents a hypothetical price change you're considering. Flexprice supports two pricing types:
* **Usage-based prices**: best for modeling prices that scale with variable events: tokens consumed, API calls made, minutes of audio processed.
* **Fixed fee prices**: best for capturing upfront repeatable revenue, such as a platform fee or minimum spend.
A scenario can include one or multiple hypotheses to compare against your baseline. Common setups include:
* **Optimize the price point**: create a hypothesis, duplicate it, and assign different rates to compare
* **Find your value metric**: create one hypothesis per metric candidate to compare them side by side
* **Compare pricing strategies**: model standalone usage-based pricing vs. hybrid usage + fixed fee
* **Compare pricing models**: evaluate unit pricing vs. tiered pricing on the same data
We recommend up to 5 hypotheses per scenario. Beyond that, the results become harder to compare at a glance.
Flexprice runs your scenario against real historical data. You'll receive a notification when results are ready to review.
## Acting on insights
Scenario results are designed to help you decide whether and how to implement your price change. Key revenue and customer risk impacts are surfaced for consideration, replacing the manual spreadsheet alternative.
### Top-line insights
The summary cards highlight the upside potential and downside risk associated with your price change across the customers in your scenario. Flexprice highlights:
* The hypothesis with the **largest absolute revenue increase**
* The hypothesis with the **lowest relative % change** to what customers currently pay
When evaluating a price change, don't optimize purely for revenue uplift. Weigh the risk of price increases on your existing customers, especially your largest ones.
### Revenue over time
Monitoring how incremental revenue changes over time across hypotheses helps you:
* Diagnose unexpected revenue variability
* Identify seasonality impacts
* Surface underlying product usage patterns worth considering
Aim for a smooth, consistent revenue curve. Inconsistent spikes may suggest the price change is not aligned with your optimal monetization strategy.
### Impact concentration
Understand where the impact of your price change is concentrated. A $5 increase affects a customer paying $1 very differently from one paying \$100.
Aim for outcomes concentrated in the lower-right of the impact chart: the least relative % change to baseline revenue, with the greatest absolute revenue upside.
### Customer impact table
Proactively identify which customers will be affected by your change. Get ahead of notifying your customer-facing teams before the change goes live. From this table, you can:
* Review [historical subscriptions and invoices](/docs/subscriptions/view) to understand customer LTV
* Create a [price override](/docs/product-catalogue/plans/price-overrides) to accommodate exceptions
* Migrate affected customers to a new plan version while grandfathering marquee accounts on existing pricing
## Explore use cases
# Settings
Source: https://docs.flexprice.io/docs/settings/settings
Configure subscription auto-cancellation, invoice numbering, and wallet balance alerts at the tenant and environment level.
Settings in Flexprice control behavior for respective environment against tenant, providing complete isolation between different environments within and across tenants. All settings are scoped to per (tenant, environment) pair, validated on write, cached for one hour, and audited.
**Benefits:**
* **Tenant and environment isolation** — Each tenant and environment has its own settings; no cross-contamination
* **Flexible updates** — Partial updates supported; only send the fields you want to change
* **Automatic caching** — Settings are cached for one hour to reduce latency
* **Audit trail** — All changes tracked with user and timestamp
* **Validated on write** — Strict type checking and value constraints prevent invalid configuration
## Available Settings
Flexprice supports three setting keys:
* **Subscription Configuration** (`subscription_config`) — Controls subscription auto-cancellation for unpaid invoices
* **Invoice Configuration** (`invoice_config`) — Controls invoice number generation and due date calculation
* **Wallet Balance Alert Configuration** (`wallet_balance_alert_config`) — Wallet balance alert thresholds configurations per tenant per environment
***
## Subscription Configuration
**Key:** `subscription_config`
Controls subscription auto-cancellation for unpaid invoices. When enabled, subscriptions are automatically cancelled after grace period if invoices remain unpaid.
### Schema
```json theme={null}
{
"grace_period_days": number,
"auto_cancellation_enabled": boolean
}
```
* **`grace_period_days`** — Required for new settings; optional on update. Integer ≥ 1, no decimals. Number of days after invoice due date before auto-cancellation.
* **`auto_cancellation_enabled`** — Optional; defaults to `false`. Boolean. Enable or disable auto-cancellation for this tenant and environment.
### Default Values
When creating a new setting via API, defaults are:
```json theme={null}
{
"grace_period_days": 3,
"auto_cancellation_enabled": false
}
```
### What Happens When Auto-Cancellation Is Enabled
When you enable auto-cancellation and set a grace period:
* **Subscriptions with overdue unpaid invoices** — If an invoice remains unpaid past its due date plus the grace period, the subscription is cancelled immediately (not at the end of the billing period).
* **You get notified** — A `subscription.canceled` webhook is sent so you can sync your systems.
* **Clean shutdown** — Future credit grants for that subscription are cancelled, and a final invoice is generated for any partial period used.
* **Audit trail** — Cancellations are recorded with reason and metadata so you can see why and when a subscription was cancelled.
### Validation
* **New settings:** `grace_period_days` required, integer ≥ 1, no decimals. `auto_cancellation_enabled` optional, boolean, default `false`.
* **Updates:** Partial updates allowed; only provided fields validated; omitted fields unchanged.
***
## Invoice Configuration
**Key:** `invoice_config`
Controls invoice number generation (prefix, format, sequence, timezone) and due date calculation.
### Schema
```json theme={null}
{
"prefix": string,
"format": string,
"start_sequence": number,
"timezone": string,
"separator": string,
"suffix_length": number,
"due_date_days": number
}
```
* **`prefix`** — Required for new settings; optional on update. Non-empty string, not only whitespace. Invoice number prefix (e.g. `"INV"`).
* **`format`** — Required for new settings; optional on update. One of `YYYYMM`, `YYYYMMDD`, `YYMMDD`, `YY`, `YYYY`. Date format in invoice number.
* **`start_sequence`** — Required for new settings; optional on update. Integer ≥ 0, no decimals. Starting sequence number.
* **`timezone`** — Required for new settings; optional on update. Valid IANA timezone (e.g. `America/New_York`, `UTC`) or common abbreviation (e.g. `EST`, `GMT`). Timezone for date formatting.
* **`separator`** — Required for new settings; optional on update. String (empty allowed). Character(s) between prefix, date, and sequence (e.g. `"-"`).
* **`suffix_length`** — Required for new settings; optional on update. Integer 1–10. Number of digits for sequence (padded with zeros).
* **`due_date_days`** — Optional; defaults to `1`. Integer ≥ 0, no decimals. Number of days after invoice creation to set due date.
### Default Values
When creating a new setting via API, defaults are:
```json theme={null}
{
"prefix": "INV",
"format": "YYYYMM",
"start_sequence": 1,
"timezone": "UTC",
"separator": "-",
"suffix_length": 5,
"due_date_days": 1
}
```
### What You Get
Invoice numbers are generated in the format you configure: prefix, optional separator, date (based on your format and timezone), then a zero-padded sequence. Each tenant and environment has its own sequence, so numbers stay unique and predictable. The sequence resets according to the date format (e.g. monthly for `YYYYMM`, daily for `YYYYMMDD`).
**Example with separator:**
Configuration: `prefix: "INV"`, `format: "YYYYMM"`, `separator: "-"`, `suffix_length: 5`.
Generated: `INV-202501-00001`, `INV-202501-00002`.
**Example without separator:**
Configuration: `prefix: "INV"`, `format: "YYYYMM"`, `separator: ""`, `suffix_length: 5`.
Generated: `INV20250100001`, `INV20250100002`.
### Supported Timezones
* **IANA names (recommended):** `America/New_York`, `Europe/London`, `Asia/Tokyo`, `UTC`.
* **Common abbreviations:** `EST`, `CST`, `MST`, `PST`, `GMT`, `CET`, `EET`, `IST`, `JST`, `KST`, `AEST`, `AWST`, and more.
### Validation
* **New settings:** All fields except `due_date_days` required; see constraints above.
* **Updates:** Partial updates allowed; same rules for provided fields; omitted fields unchanged.
***
## Wallet Balance Alert Configuration
**Key:** `wallet_balance_alert_config`
Sets the **default** balance alert configuration for the tenant and environment. It applies only to wallets that do not have their own alert settings. Per-wallet alert configuration is done via [Wallet Sentinel Alerts](/docs/wallet/low-balance-alert) (or `alert_settings` on the wallet in the API).
### What This Setting Controls
* **When enabled** — Wallets in this tenant and environment that have no per-wallet alert settings use this config as their default. Balance alerts run for those wallets using the thresholds you set here.
* **When disabled** — Wallets that would otherwise use this default do not receive balance alerts until they have their own alert settings or you turn this setting back on.
* **Per-wallet overrides** — Any wallet with its own `alert_settings` always uses those; this setting only applies to wallets without per-wallet config.
Resolution order for a given wallet: Flexprice uses the wallet’s own alert settings if present; otherwise it uses this tenant-and-environment default. If the default is disabled, wallets without per-wallet settings do not receive balance alerts.
### Schema
```json theme={null}
{
"alert_enabled": boolean,
"critical": { "threshold": string, "condition": "below" | "above" },
"warning": { "threshold": string, "condition": "below" | "above" },
"info": { "threshold": string, "condition": "below" | "above" }
}
```
* **`alert_enabled`** — Required. Boolean. Turn alerts on or off at tenant and environment level.
* **`critical`**, **`warning`**, **`info`** — Optional. Each has `threshold` (string, e.g. `"0"`, `"-5"`, `"10"`) and `condition` (`"below"` or `"above"`).
### Default Value
When no custom value is stored (or after DELETE), the default is:
```json theme={null}
{
"critical": { "threshold": "0", "condition": "below" },
"alert_enabled": false
}
```
### Validation
* If `alert_enabled` is `true`, at least one of `critical`, `warning`, or `info` must be present.
* Threshold ordering: for `"below"`, critical \< warning \< info; for `"above"`, critical > warning > info (as applicable).
### Alert behavior (same as per-wallet alerts)
Once a wallet is using alert config (either its own or this default), behavior matches [Wallet Sentinel Alerts](/docs/wallet/low-balance-alert): balance is monitored against the thresholds, **Info** / **Warning** / **Critical** are raised when thresholds are crossed, state is updated on the wallet, and webhooks are sent when alert state changes.
## API Endpoints
### Get Setting
```
GET /v1/settings/:key
```
**Parameters**:
* `key` (path): Required. One of: `subscription_config`, `invoice_config`
**Response**:
```json theme={null}
{
"value": {
// Setting specific fields based on key type
},
"tenant_id": "string",
"environment_id": "string",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
```
**Error Responses**:
* `404 Not Found`: Setting not found for tenant × environment
* `400 Bad Request`: Invalid setting key
* `403 Forbidden`: Missing tenant or environment context
### Create/Update Setting
```
PUT /v1/settings/:key
```
**Parameters**:
* `key` (path): Required. One of: `subscription_config`, `invoice_config`
**Request**:
```json theme={null}
{
"value": {
// Setting specific fields based on key type
}
}
```
**Response**:
```json theme={null}
{
"value": {
// Complete setting after operation
},
"tenant_id": "string",
"environment_id": "string",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
```
**Error Responses**:
* `400 Bad Request`: Invalid setting key or validation failed
* `403 Forbidden`: Missing tenant or environment context
**Behavior**:
* Single endpoint handles both creation and updates
* If setting doesn't exist for tenant × environment:
* Creates new setting
* All required fields must be provided
* Default values applied for optional fields
* If setting exists for tenant × environment:
* Updates existing setting
* Supports partial updates
* Only provided fields are validated
* Omitted fields retain their existing values
* No defaults are applied
### Delete Setting
```
DELETE /v1/settings/:key
```
**Parameters**:
* `key` (path): Required. One of: `subscription_config`, `invoice_config`
**Response**:
```json theme={null}
{
"message": "Setting deleted successfully"
}
```
**Error Responses**:
* `404 Not Found`: Setting not found for tenant × environment
* `400 Bad Request`: Invalid setting key
* `403 Forbidden`: Missing tenant or environment context
***
## Use Cases
### Subscription Auto-Cancellation
* **SaaS with trial plans** — Auto-cancel free trials after grace period if no payment method is added
* **B2B billing** — Cancel subscriptions with overdue invoices after a grace period (e.g. 30 days)
* **Compliance** — Automatically stop service for customers who haven't paid within the grace period
### Invoice Numbering
* **Multi-region operations** — Use timezone to generate invoice numbers in the customer's local time
* **Custom branding** — Set prefix and separator to match your invoice format (e.g. `ACME-202501-00001`)
* **Sequence isolation** — Each tenant and environment has its own sequence; no conflicts
### Wallet Balance Alerts (Tenant and Environment Default)
* **Enable alerts for all wallets** — Set tenant and environment default so new wallets inherit alert config
* **Disable alerts globally** — Turn off async consumer for that environment by setting `alert_enabled: false`
* **Fallback for wallets without config** — Wallets with no `alert_settings` use this default
***
## Best Practices
💡 **Send required fields on create** — When creating a new setting, include all required fields; defaults apply only on API create.
💡 **Use partial updates** — Send only the fields you want to change; omitted fields keep their existing values.
💡 **Prefer IANA timezones** — For `invoice_config`, use IANA names (e.g. `America/New_York`) instead of abbreviations for clarity and daylight saving time support.
💡 **Set a default for wallet balance alerts** — Use `wallet_balance_alert_config` to define default thresholds for all wallets in this tenant and environment that don’t have their own alert settings. Enable it so those wallets get balance alerts; disable or DELETE to turn alerts off for them.
💡 **Restrict who can change settings** — Limit setting changes to admin users; use tenant-scoped API keys and review audit logs.
💡 **Match grace period to your dunning process** — Set `grace_period_days` to align with how long you allow overdue invoices before cancelling (e.g. after how many reminder emails or days).
💡 **Monitor auto-cancellation outcomes** — Use `subscription.canceled` webhooks to see which subscriptions were cancelled and adjust grace period or dunning if needed.
💡 **Use invoice prefix and format for traceability** — A clear prefix and consistent date format in invoice numbers make it easier to match invoices to periods and environments.
💡 **Combine with per-wallet alerts** — Set `wallet_balance_alert_config` as the default, then override on specific wallets via [Wallet Sentinel Alerts](/docs/wallet/low-balance-alert) where you need different thresholds.
***
## Related Documentation
* [Wallet Sentinel Alerts](/docs/wallet/low-balance-alert) — Configure per-wallet balance alerts
* [Subscriptions](/docs/subscriptions/customers-create-subscription) — Manage customer subscriptions
* [Invoices](/docs/invoices/overview) — Invoice lifecycle and payment tracking
# Consolidated Billing
Source: https://docs.flexprice.io/docs/subscriptions/billing-workflows/consolidated-billing
One parent subscription covers multiple child customers. Usage from all children rolls up into a single invoice sent to the parent.
## Overview
Consolidated billing lets an enterprise or holding company purchase one plan that covers multiple subsidiary teams or entities. Each child customer generates usage independently, but all usage aggregates under the parent subscription, producing a single invoice for the parent.
`subscription_type` results:
* Parent subscription: **`parent`**
* Each child: **`inherited`** (auto-created skeleton, no line items)
## How it works
```mermaid theme={null}
flowchart TB
subgraph create [Creation]
A[Create subscription for parent customer]
B[Pass child external IDs in inheritance block]
A --> B
end
subgraph skeletons [Auto-created by Flexprice]
C[inherited subscription for child 1]
D[inherited subscription for child 2]
end
subgraph usage [Usage ingestion]
E[Events for child 1 customer]
F[Events for child 2 customer]
G[Rolled up to parent subscription]
E --> G
F --> G
end
subgraph invoice [Billing]
H[Single invoice to parent customer]
I[One line item per feature, usage aggregated]
H --> I
end
B --> C
B --> D
G --> H
```
| Property | Where it lives |
| -------------------------- | -------------------------------------------------------- |
| Plan and line items | Parent subscription |
| Usage tracking (billing) | Parent subscription (rolled up from all children) |
| Usage tracking (analytics) | Per child customer (use `include_children` to aggregate) |
| Invoice | Parent customer only |
| Wallet | Parent customer |
| Entitlements | Parent subscription |
| Child subscriptions | Skeleton only: no line items, no independent invoice |
## When to use consolidated billing
* **Enterprise with subsidiaries**: Global HQ purchases an enterprise plan. Regional divisions (APAC, EMEA, Americas) each generate usage independently. HQ receives one consolidated invoice.
* **Holding company**: A holding company owns multiple brands. Each brand operates as a customer. Finance at HQ handles all invoices.
* **Internal chargeback**: A company tracks usage per department for internal reporting, but the central IT team is invoiced.
## Prerequisites
* Parent customer created in Flexprice
* Each child customer created in Flexprice (with a known `external_id`)
* A plan created in the product catalogue
* Child customers must **not** already have an active `inherited` subscription under another parent
On create, you cannot combine `inheritance.external_customer_ids_to_inherit_subscription` with `invoicing_customer_external_id` or `subscriptions_ids_for_grouped_invoicing` in the same request.
## Configure
### Step 1: Create customers
Create the parent customer and each child customer through the Dashboard or API. Customer records have no hierarchy fields; they stay flat.
### Step 2: Create the parent subscription with child inheritance
1. Navigate to **Subscriptions** and click **Create Subscription**
2. Select the **parent customer** and choose a plan
3. In the **Customer Hierarchy** section, enter the external IDs of the child customers who will inherit this subscription
4. Click **Create Subscription**
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/subscriptions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_customer_id": "ext-global-hq",
"plan_id": "plan_enterprise_monthly",
"currency": "usd",
"billing_period": "month",
"billing_period_count": 1,
"start_date": "2026-06-01T00:00:00Z",
"inheritance": {
"external_customer_ids_to_inherit_subscription": [
"ext-apac-team",
"ext-emea-team"
]
}
}'
```
Response (abbreviated):
```json theme={null}
{
"id": "sub_01parent",
"subscription_type": "parent",
"customer_id": "cus_hq",
"status": "active"
}
```
Flexprice automatically creates an `inherited` skeleton subscription for each child external ID. Verify by calling `GET /subscriptions` filtered by the child customer.
### Step 3: Verify
Open the parent subscription's detail page. Child skeleton subscriptions appear under it.
## Post-creation changes
You can add additional child customers to an existing parent subscription using the modify API.
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/subscriptions/{parent_subscription_id}/modify/execute \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "inheritance",
"inheritance_params": {
"external_customer_ids_to_inherit_subscription": ["ext-latam-team"]
}
}'
```
Replace `{parent_subscription_id}` with the `id` of the parent subscription.
You can preview the modify operation without committing changes using `POST /subscriptions/{id}/modify/preview` with the same request body.
## Analytics: per-child and aggregated
Inherited children do **not** appear in billing usage summaries. All billable usage is attributed to the parent subscription. For analytics (non-billing), use `POST /events/analytics`.
**Aggregate all children under the parent:**
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/events/analytics \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_customer_id": "ext-global-hq",
"start_time": "2026-06-01T00:00:00Z",
"end_time": "2026-06-30T23:59:59Z",
"include_children": true
}'
```
**Per-child usage breakdown:**
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/events/analytics \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_customer_id": "ext-apac-team",
"start_time": "2026-06-01T00:00:00Z",
"end_time": "2026-06-30T23:59:59Z"
}'
```
`include_children: true` aggregates all inherited children's usage into the parent's response. Query each child's `external_customer_id` individually to see per-child contribution.
[**Get customer usage summary**](/api-reference/customers/get-customer-usage-summary) for a child that only has an **inherited** subscription returns **no billable usage** on that child. This is expected. Confirm totals on the **parent** customer or use analytics as above.
## Validations and constraints
**Child already has an inherited subscription.** A customer that already has an `inherited` subscription under another parent cannot be added as a child here. Error: `"customer already has an inherited subscription"`. Resolve: cancel the existing inherited subscription or use a different child customer.
**Inherited subscriptions cannot be cancelled directly.** Calling cancel on an `inherited` subscription returns an error: `"inherited subscription cannot be cancelled directly"`. Cancel the **parent** subscription instead. All inherited children are cancelled automatically at the same time.
**Cascade behavior.** When the parent subscription is paused, resumed, or cancelled, all inherited children are updated to match automatically.
**Wallet balance.** The parent customer's wallet is used for all settlement. Inherited children have no independent wallet balance for this subscription.
**Mutual exclusion.** The `inheritance.external_customer_ids_to_inherit_subscription` field cannot be combined with `invoicing_customer_external_id` or `subscriptions_ids_for_grouped_invoicing` in the same request.
## Frequently asked questions
No. Currency is set on the parent subscription and applies to all invoices. Inherited children use the parent's currency.
Plan changes apply to the parent subscription only. Inherited skeleton subscriptions keep routing usage events to the updated parent.
Billable usage for inherited subscriptions is attributed to the **parent** subscription. A child with only an inherited subscription has no independent billing usage ledger. This is expected. Use `POST /events/analytics` for per-child breakdowns.
## Related workflows
When children need their own subscriptions but a third party pays
When separate subscriptions should merge into one invoice
# Delegated Invoicing
Source: https://docs.flexprice.io/docs/subscriptions/billing-workflows/delegated-invoicing
The subscription belongs to one customer, but the invoice is sent to a different customer: the delegated payer. Entitlements, usage, and line items stay on the subscription owner.
## Overview
Delegated invoicing lets you separate **who uses** a subscription from **who pays** for it. The subscription owner (child) keeps full ownership of their plan, line items, usage tracking, and entitlements. Every invoice is raised against a designated billing customer: the delegated payer.
`subscription_type` result: **`standalone`** (with `invoicing_customer_id` set on the subscription record)
Delegated invoicing does not create a new subscription type. The subscription owner's subscription is still `standalone`. What changes is the **invoicing customer ID** stored on the subscription, which redirects invoice generation and wallet drawdown to the delegated payer.
## How it works
```mermaid theme={null}
flowchart LR
A[Subscription owner usage] --> B[Subscription standalone]
B --> C[Invoice generated]
C --> D[Sent to delegated payer]
D --> E[Payer wallet drawn down]
```
| Property | Where it lives |
| ------------------- | ------------------------------------------ |
| Plan and line items | Subscription owner's subscription |
| Usage tracking | Subscription owner |
| Entitlements | Subscription owner's subscription |
| Invoice | **Delegated payer** |
| Wallet | **Delegated payer** |
| Tax | Based on delegated payer's billing details |
## When to use delegated invoicing
* **Reseller model**: A reseller signs up 10 end customers. Each customer has their own starter plan and usage. The reseller receives and pays all invoices centrally.
* **Central billing department**: A company's Finance team handles all vendor payments. Engineering, Marketing, and Sales each track their own usage, but invoices go to Finance.
* **Managed service provider**: An MSP provisions software for their clients. Each client has their own subscription, but the MSP is billed for all of them.
## Prerequisites
* Subscription owner (child) customer created in Flexprice
* Delegated payer customer created in Flexprice with a known `external_id`
* Delegated payer customer must have `status: published` (active) at the time the subscription is created
* A plan created in the product catalogue
On create, `invoicing_customer_external_id` cannot be combined with `external_customer_ids_to_inherit_subscription` or `subscriptions_ids_for_grouped_invoicing`.
## Configure
1. Navigate to **Subscriptions** and click **Create Subscription**
2. Select the **subscription owner** (end customer) as the customer
3. Choose a plan
4. In the **Invoice To** field, enter the delegated payer's external ID
5. Click **Create Subscription**
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/subscriptions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_customer_id": "ext-end-customer",
"plan_id": "plan_starter_monthly",
"currency": "usd",
"billing_period": "month",
"billing_period_count": 1,
"start_date": "2026-06-01T00:00:00Z",
"inheritance": {
"invoicing_customer_external_id": "ext-reseller"
}
}'
```
Response (abbreviated):
```json theme={null}
{
"id": "sub_01child",
"subscription_type": "standalone",
"customer_id": "cus_end_customer",
"invoicing_customer_id": "cus_reseller",
"status": "active"
}
```
## Post-creation changes
The delegated payer is **immutable** after subscription creation. To change the invoicing customer, cancel the existing subscription and create a new one with the correct `invoicing_customer_external_id`.
## Analytics
Analytics work the same as standalone. Query the subscription owner's `external_customer_id` directly. No special flags are needed. Usage events are ingested under the subscription owner, not the delegated payer.
## Validations and constraints
**Invoicing customer must be active.** The delegated payer must have `status: published` at creation time. Error: `"invoicing customer is not active"`. Resolve: activate the delegated payer customer before creating the subscription.
**Invoicing customer is immutable.** The `invoicing_customer_external_id` cannot be changed after the subscription is created. To reassign, cancel and recreate the subscription.
**Subscription type.** The subscription owner's subscription will show `subscription_type: standalone`. This is correct. Delegated invoicing is not a separate type; it is a standalone subscription with an invoicing redirect.
## Related workflows
When one parent contract should cover multiple customers with a single plan
When separate subscriptions should merge into one invoice
# Grouped Invoicing
Source: https://docs.flexprice.io/docs/subscriptions/billing-workflows/grouped-invoicing
Merge invoices from multiple separate subscriptions into a single consolidated invoice. Each subscription keeps its own line items, entitlements, and usage tracking.
## Overview
Grouped invoicing lets a customer with multiple separate subscriptions receive a single invoice that combines all line items from every subscription in the group. Each subscription retains its own plan, line items, usage tracking, and entitlements. Only the invoice consolidation changes.
`subscription_type` results:
* Parent (invoice anchor): **`parent`**
* Each grouped child subscription: **`grouped_invoicing`**
A standalone subscription acting as the parent is **automatically promoted** to `parent` type when the first child is added.
## How it works
```mermaid theme={null}
flowchart TB
subgraph subs [Three separate subscriptions]
A[sub_analytics grouped_invoicing]
B[sub_storage grouped_invoicing]
C[sub_parent parent]
end
subgraph invoicing [Invoice generation]
D[All line items merged]
E[Single invoice to parent customer]
D --> E
end
A --> D
B --> D
C --> D
```
| Property | Where it lives |
| ------------------- | --------------------------------------------------------------------- |
| Plan and line items | Each child's own subscription |
| Usage tracking | Each child's own subscription |
| Entitlements | Each child's own subscription |
| Invoice | **Single consolidated invoice** to the parent subscription's customer |
| Wallet | Parent subscription's customer |
## When to use grouped invoicing
* **Multi-product company**: A company uses analytics, storage, and compute as three separate Flexprice subscriptions. Finance wants a single monthly invoice instead of three.
* **Department consolidation**: Different teams each have their own subscription. A central account manager wants one invoice per billing cycle.
* **Simplify payment**: A customer manages multiple subscriptions independently but wants to reduce invoice processing overhead.
## Prerequisites
Before adding a subscription to a grouped invoicing group, all of the following must be true:
| Requirement | What to check |
| ---------------------------- | ------------------------------------------------------------------------------------- |
| Child subscription type | Must be `standalone` |
| Child subscription status | Must be `active` or `trialing` |
| Child has no existing parent | `parent_subscription_id` must be null |
| Parent subscription type | Must be `parent` or `standalone` (auto-promoted on first child add) |
| Parent subscription status | Must be `active` or `trialing` |
| Billing period | Child and parent must have the **same** `billing_period` (for example both `month`) |
| Billing period count | Child and parent must have the **same** `billing_period_count` (for example both `1`) |
| Billing anchor | Child and parent must share the **same** day-of-month and time-of-day billing anchor |
| Currency | Child and parent must use the **same** currency |
| Start date | Child `start_date` must be **on or after** parent `start_date` |
Flexprice validates all of these requirements before adding a subscription to a group. A single mismatch returns a validation error with a specific hint. Check the error's `hint` field for which constraint failed.
## Configure
### Option A: Create a parent subscription and group existing subscriptions at creation time
Use this when you already have standalone subscriptions that you want to group under a new parent.
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/subscriptions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_customer_id": "ext-acme-corp",
"plan_id": "plan_base_monthly",
"currency": "usd",
"billing_period": "month",
"billing_period_count": 1,
"start_date": "2026-06-01T00:00:00Z",
"inheritance": {
"subscriptions_ids_for_grouped_invoicing": [
"sub_analytics",
"sub_storage"
]
}
}'
```
This creates a new `parent` subscription and converts `sub_analytics` and `sub_storage` from `standalone` to `grouped_invoicing` type.
Response (abbreviated):
```json theme={null}
{
"id": "sub_01parent",
"subscription_type": "parent",
"customer_id": "cus_acme",
"status": "active"
}
```
At creation, `subscriptions_ids_for_grouped_invoicing` cannot be combined with `external_customer_ids_to_inherit_subscription`, `invoicing_customer_external_id`, or `parent_subscription_id` in the same request.
### Option B: Add a subscription to an existing group
Use this when the parent already exists and you want to add more subscriptions to the group.
Preview before executing to confirm all constraints will pass:
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/subscriptions/{parent_subscription_id}/modify/preview \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "grouped_invoicing",
"grouped_invoicing_params": {
"action": "add",
"parent_subscription_id": "{parent_subscription_id}",
"child_subscription_ids": ["sub_compute"]
}
}'
```
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/subscriptions/{parent_subscription_id}/modify/execute \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "grouped_invoicing",
"grouped_invoicing_params": {
"action": "add",
"parent_subscription_id": "{parent_subscription_id}",
"child_subscription_ids": ["sub_compute"]
}
}'
```
Replace `{parent_subscription_id}` with the parent subscription's `id`. The same ID goes in both the URL and `grouped_invoicing_params.parent_subscription_id`.
On success, `sub_compute` is converted from `standalone` to `grouped_invoicing` and linked to the parent.
### Option C: Remove a subscription from the group
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/subscriptions/{parent_subscription_id}/modify/execute \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "grouped_invoicing",
"grouped_invoicing_params": {
"action": "remove",
"child_subscription_ids": ["sub_compute"]
}
}'
```
On success, `sub_compute` reverts from `grouped_invoicing` back to `standalone` and its `parent_subscription_id` is cleared.
## Post-creation changes
Adding or removing group membership is done with the modify API (`Option B` and `Option C` above). There is no separate PATCH on the subscription resource for grouped invoicing membership beyond these flows.
## Timing behavior
**Adding a subscription to a group** takes effect at the **next billing period boundary**. For the current period, any advance charges on the child have already been invoiced independently. Only usage accrued after the period rolls over appears on the consolidated parent invoice.
**Removing a subscription from a group** applies to the **entire current billing period**. The child's invoice for the full current period (all usage and charges) is raised directly against the child customer's subscription, regardless of when during the period the removal occurred.
## Analytics
Each subscription in the group has its own usage tracking. Query each subscription owner's `external_customer_id` individually. There is no `include_children` flag for grouped invoicing; that flag is for consolidated billing and inherited subscriptions only.
## Validations and constraints
**Billing period, billing period count, billing anchor, and currency must all match exactly.** The billing anchor comparison uses the day-of-month plus time-of-day (hour:minute:second). Year and month are ignored, so a child added later in the year is accepted when its intra-cycle cadence aligns.
**Cannot combine with consolidated billing at creation.** `subscriptions_ids_for_grouped_invoicing` cannot be used in the same request as `external_customer_ids_to_inherit_subscription`, `invoicing_customer_external_id`, or `parent_subscription_id`.
**Standalone parent auto-promotion.** If the parent subscription is currently `standalone`, it is automatically promoted to `parent` type when the first child is added via the modify API. No manual type change is needed.
## Frequently asked questions
Yes. Use `POST /subscriptions/{id}/modify/preview` with `type: "grouped_invoicing"` and `action: "add"`. The preview validates constraints and returns what would change without making writes.
The cancelled child generates its own final invoice for the current period. The remaining children and the parent continue their normal billing cycle.
No. A subscription has one type. Grouped invoicing and consolidated billing use different mechanisms and cannot be mixed on the same parent subscription.
## Related workflows
When one parent plan should cover multiple customers
When a third party should receive the invoice for a standalone subscription
# Subscription Billing Workflows
Source: https://docs.flexprice.io/docs/subscriptions/billing-workflows/overview
Choose the billing workflow that matches your business model. Each workflow controls who owns the subscription, who receives the invoice, and how usage is tracked.
## Which workflow do I need?
Every Flexprice subscription has a `subscription_type` that controls billing ownership. Use the table below to pick the right workflow before you start.
| Workflow | Your situation | Who gets invoiced | `subscription_type` |
| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------ | ---------------------------------------------------- |
| [Standalone](/docs/subscriptions/billing-workflows/standalone) | Each customer is independent: their own plan, invoice, and wallet | Subscription owner | `standalone` |
| [Consolidated Billing](/docs/subscriptions/billing-workflows/consolidated-billing) | One parent contract covers multiple child entities; one invoice to HQ | Parent customer | `parent` (HQ) + `inherited` (children, auto-created) |
| [Delegated Invoicing](/docs/subscriptions/billing-workflows/delegated-invoicing) | Each customer has their own subscription, but a third party pays all the bills | Delegated payer | `standalone` with invoicing redirect |
| [Grouped Invoicing](/docs/subscriptions/billing-workflows/grouped-invoicing) | Multiple separate subscriptions for the same company; Finance wants a single invoice | Parent subscription's customer | `grouped_invoicing` (children) + `parent` |
If none of these match, start with **Standalone**. It is the default and works for the majority of direct B2B and B2C billing scenarios.
## Key concept: hierarchy lives on subscriptions, not customers
Flexprice keeps customer records flat. There are no parent or child fields on a customer. All billing relationships are configured on the **subscription**. This means:
* You can mix billing workflows per customer without restructuring your customer data
* Switching a customer from standalone to grouped invoicing is a subscription operation, not a customer edit
* Two customers can be related for billing purposes while remaining independent entities in your system
## Workflows at a glance
Default. One customer, one subscription, one invoice. No configuration needed.
Enterprise HQ buys one plan. Subsidiaries generate usage. One consolidated invoice.
Reseller model. Each end customer has their own subscription. One party pays all invoices.
Multi-product company. Three separate subscriptions. Finance wants one monthly invoice.
## Mutual exclusion
The four `inheritance` fields on Create Subscription are mutually exclusive. You can only specify one billing relationship per subscription:
| Field | Cannot be combined with |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `external_customer_ids_to_inherit_subscription` | `invoicing_customer_external_id`, `subscriptions_ids_for_grouped_invoicing`, `parent_subscription_id` |
| `invoicing_customer_external_id` | `external_customer_ids_to_inherit_subscription`, `subscriptions_ids_for_grouped_invoicing` |
| `subscriptions_ids_for_grouped_invoicing` | `external_customer_ids_to_inherit_subscription`, `invoicing_customer_external_id`, `parent_subscription_id` |
| `parent_subscription_id` | `external_customer_ids_to_inherit_subscription`, `subscriptions_ids_for_grouped_invoicing` |
# Standalone Billing
Source: https://docs.flexprice.io/docs/subscriptions/billing-workflows/standalone
The default subscription workflow. Each customer has their own independent subscription, invoice, and wallet. No hierarchy configuration required.
## Overview
Standalone is the default `subscription_type` in Flexprice. Every subscription is standalone unless you explicitly configure one of the other billing workflows. Use it whenever customers are fully independent: each has their own plan, usage ledger, invoice, and wallet.
`subscription_type` result: **`standalone`**
## How it works
```mermaid theme={null}
flowchart LR
A[Customer] --> B[Subscription]
B --> C[Usage events]
B --> D[Invoice to customer]
B --> E[Wallet drawdown]
```
Each subscription is self-contained:
| Property | Where it lives |
| ------------------- | --------------------------------- |
| Plan and line items | Subscription |
| Usage tracking | Customer |
| Invoice | Customer (the subscription owner) |
| Wallet | Customer |
| Entitlements | Subscription |
## When to use standalone
* **Direct SaaS**: Your customers sign up directly. Each company or user is an independent billing entity with no relationship to other customers.
* **Multi-product**: One customer uses several of your products, each as a separate subscription. Each subscription invoices independently.
* **Default**: You are not sure which workflow to use yet. Start here and migrate later.
## Prerequisites
* A customer created in Flexprice (`POST /customers` or Dashboard)
* A plan created in the product catalogue
## Configure
1. Navigate to **Subscriptions** and click **Create Subscription**
2. Select the customer
3. Choose a plan
4. Set start date, billing period, and any overrides
5. Click **Create Subscription**
No additional configuration is needed. A subscription created without any `inheritance` fields is automatically standalone.
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/subscriptions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_customer_id": "cust-acme",
"plan_id": "plan_enterprise_monthly",
"currency": "usd",
"billing_period": "month",
"billing_period_count": 1,
"start_date": "2026-06-01T00:00:00Z"
}'
```
Response (abbreviated):
```json theme={null}
{
"id": "sub_01abc",
"subscription_type": "standalone",
"customer_id": "cus_acme",
"status": "active"
}
```
## Post-creation changes
Standalone subscriptions support the standard subscription lifecycle and modification APIs (plan changes, pauses, overrides, and so on). There is no hierarchy or invoicing redirect to update. If you later need consolidated billing, delegated invoicing, or grouped invoicing, use the workflows in those guides and create or modify subscriptions accordingly.
## Analytics
Usage analytics behave like any normal subscription. Call [**Get usage analytics**](/api-reference/events/get-usage-analytics) (`POST /events/analytics`) with the subscriber's `external_customer_id`. You do not need `include_children` (that flag applies to consolidated billing rollups only).
## Validations and constraints
A customer that already has an **inherited** subscription (from a consolidated billing parent) cannot create a standalone subscription. If you need a standalone subscription for such a customer, remove them from the parent hierarchy first.
## Related workflows
When one parent contract should cover multiple customers
When a third party should receive the invoice
When separate subscriptions should merge into one invoice
# Commitment
Source: https://docs.flexprice.io/docs/subscriptions/commitment/overview
Commitment-based pricing lets a customer guarantee a minimum spend or quantity per billing period while billing actual usage above that floor at a configurable overage rate
Commitment-based pricing is a billing model that lets customers commit to a minimum spend or quantity for each billing period. It gives the business predictable revenue and lets the customer pay for actual usage beyond the commitment at a defined overage rate.
A commitment is configured on a subscription line item and, by default, applies across the entire billing period. To vary the commitment by time of day — for example, higher rates during business hours and lower rates overnight — use [time-of-day buckets](/docs/subscriptions/commitment/time-of-day-buckets).
## How Commitment Works
When a commitment is configured on a subscription line item:
1. **Commitment amount** — The minimum the customer commits to per billing period (e.g. \$1,000/month). This guarantees the revenue floor.
2. **Overage factor** — A multiplier applied to usage cost above the commitment (e.g. 1.5x). When usage exceeds the commitment, the excess is billed at the overage rate.
3. **Usage calculation**:
* If total usage cost ≤ commitment: all usage is charged at the base rate.
* If total usage cost > commitment: the commitment portion is charged at the base rate, and the excess is charged at `base_rate × overage_factor`.
## Steps to Add a Commitment
1. Click **Add Subscription**.
2. Scroll to the **Commitment & Overage** section.
3. Set the commitment amount and overage factor.
## Understanding True-Up
True-up ensures the customer pays the full commitment even when actual usage falls short. When true-up is enabled and usage at period end is below the commitment, the shortfall is billed as a separate true-up line item.
### How True-Up Works
1. **During the billing period** — Flexprice tracks usage and computes the running usage cost.
2. **At invoice generation**:
* Usage cost \< commitment AND true-up enabled → a true-up line item is added for the shortfall.
* Usage cost ≥ commitment → standard commitment-and-overage logic applies; no true-up is needed.
### Steps to Add True-Up
1. Click **Add Subscription**.
2. Scroll to the **Commitment & Overage** section.
3. Set the commitment amount and overage factor.
4. Enable true-up using the toggle.
## Time-of-Day Commitment Buckets
A line-item commitment applies for all 24 hours of the day. If you need different commitments and prices for different parts of the day — for example, reserved capacity 09:00–17:00 UTC and standard pricing outside — configure [time-of-day buckets](/docs/subscriptions/commitment/time-of-day-buckets) on the line item.
# Time-of-Day Commitment Buckets
Source: https://docs.flexprice.io/docs/subscriptions/commitment/time-of-day-buckets
Time-of-day buckets override a line item's commitment, price, and overage for specific hour ranges within a UTC day
A line-item [commitment](/docs/subscriptions/commitment/overview) applies for the full 24 hours of every day in the billing period. Time-of-day buckets let you override that commitment for specific hour ranges — each bucket carries its own price, commitment value, overage factor, and true-up flag. Usage that falls inside a bucket is billed under the bucket's terms; usage that falls outside every bucket continues to be billed under the line item's all-day commitment.
**When to use buckets:**
* **Peak / off-peak pricing** — Higher commitment and rate during business hours, lower outside.
* **Reserved windows** — Reserved capacity at a guaranteed rate inside a fixed window, standard pricing elsewhere.
* **Tiered SLAs** — Different minimum spends for production hours versus maintenance windows.
The diagram shows four pricing layers in a single day, each mapped to a configuration field on the line item:
| Layer | Time | Configured on | Behavior |
| ----------------------------------- | ---------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| **Reserved 10 units, Price A** | 00:00–23:59 | Line item (`commitment_value: 10`, `commitment_true_up_enabled: true`) | The line-item commitment reserves 10 units across the full day. True-up bills the full 10 units even when usage is lower. |
| **Reserved 10 more units, Price C** | 07:00–17:00 | Bucket (`commitment_value: 10`, `price` = Price C) | The bucket reserves an additional 10 units at peak time. Inside the bucket window the customer has 20 reserved units total. |
| **Overage 25 units, Price B** | 00:00–07:00 (outside bucket) | Line item base price | Usage above the line-item commitment outside any bucket is billed at the line-item base price. |
| **Overage 10 units, Price D** | 07:00–17:00 (inside bucket) | Bucket base price + `overage_factor` | Usage above the bucket commitment inside the bucket is billed at the bucket's price multiplied by its overage factor. |
## How Buckets Override the Line Item
A subscription line item carries a base price and an optional base commitment that apply across the entire billing period. A bucket attaches to that line item and overrides three things — but only for usage whose timestamp falls inside the bucket's `[start, end)` range:
| Field | Inside a bucket | Outside every bucket |
| ---------------- | ------------------------ | -------------------------------------- |
| Price | Bucket price | Line-item base price |
| Commitment value | Bucket commitment | Line-item commitment |
| Overage factor | Bucket overage factor | Line-item overage factor |
| True-up | Bucket `true_up_enabled` | Line-item `commitment_true_up_enabled` |
You can attach as many buckets as you need to one line item. Buckets are stored in the order they are provided and matched at billing time by checking each window's start timestamp against bucket ranges.
All bucket times are interpreted in **UTC**. Convert your local peak hours to UTC before configuring buckets.
### Example scenario
A line item meters API calls with:
* Base price: \$1.00 per call
* Base commitment: 10 reserved units per window, true-up enabled (Price A)
The customer adds a peak bucket from 07:00 to 17:00 UTC reserving 10 more units at Price C, and configures overage prices for both regions of the day (Price B outside the bucket, Price D inside). The resulting billing surface looks like this within a single day:
| UTC time | Price applied | Commitment in effect | Charged for |
| ----------- | ------------------------------------------------------------------------- | -------------------- | -------------------------------------------------------- |
| 00:00–07:00 | Price A (reserved) + Price B (overage) | 10 units, true-up on | Reserved 10 units + 25 overage units at Price B |
| 07:00–17:00 | Price A (reserved) + Price C (bucket reserved) + Price D (bucket overage) | 10 + 10 = 20 units | Reserved 10 + bucket-reserved 10 + 10 overage at Price D |
| 17:00–23:59 | Price A (reserved) + Price B (overage) | 10 units, true-up on | Reserved 10 units, overage at Price B if exceeded |
The line item's reserved 10 units (Price A) and true-up apply across the full day. The bucket layers an additional 10 reserved units at Price C only during 07:00–17:00, with its own overage at Price D for usage exceeding the bucket commitment.
## Constraints
### 1. The meter must be bucketed
A bucket can only be attached to a line item whose meter has an aggregation window — a "bucketed" meter that applies commitment per window rather than once per billing cycle. The line item must have `commitment_windowed: true`.
Attaching `commitment_time_buckets` to a line item with `commitment_windowed: false` returns a validation error: `commitment_time_buckets requires commitment_windowed=true`.
### 2. Bucket duration must be at least one meter window
If the meter aggregates per hour, a bucket cannot be shorter than 1 hour. A bucket that spans less than one full meter window cannot accumulate a complete usage measurement.
### 3. Bucket duration must be an exact multiple of the meter window
`end - start` must be divisible by the meter's window size. If the meter window is `HOUR`, the bucket `12:00 → 13:30` is rejected: the 90-minute duration leaves 30 minutes of a second meter window unaccounted for.
| Meter window | Bucket `[start, end)` | Duration | Valid? |
| -------------- | --------------------- | -------- | ---------------------------------- |
| HOUR (60 min) | 09:00 → 10:00 | 60 min | Yes (1× window) |
| HOUR (60 min) | 09:00 → 12:00 | 180 min | Yes (3× window) |
| HOUR (60 min) | 09:00 → 10:30 | 90 min | No — not a multiple |
| HOUR (60 min) | 09:30 → 10:30 | 60 min | No — start not aligned to the hour |
| 15-min | 09:00 → 09:45 | 45 min | Yes (3× window) |
| DAY (1440 min) | 00:00 → 24:00 | 1440 min | Yes (1× window) |
The bucket `start` must also align to the meter window grid — for an hourly meter, the start minute-of-day must be divisible by 60.
### 4. Buckets cannot overlap
Two buckets on the same line item cannot share any minute of the day. Adjacent buckets are allowed because ranges are half-open `[start, end)`:
* `[09:00, 12:00)` and `[12:00, 17:00)` → **valid** (adjacent, no shared minute).
* `[09:00, 12:00)` and `[11:00, 14:00)` → **invalid** (overlap on 11:00–12:00).
If a use case genuinely needs different rates for the same period (for example, a tiered surcharge that stacks on top of a base bucket), model that as **slab pricing within a single bucket** and register it as one window — not as overlapping buckets.
### 5. Outside a bucket, the line-item commitment continues to apply
Buckets are an override, not a replacement. Any usage window whose start timestamp falls outside every configured bucket is billed at the line item's base price and base commitment, with the line item's overage factor and true-up flag. If the line item has no base commitment, out-of-bucket usage is billed at the base rate only.
### Midnight-wrapping ranges
A bucket can wrap midnight. `{ "start": { "hour": 22, "minute": 0 }, "end": { "hour": 6, "minute": 0 } }` covers 22:00–23:59 and 00:00–05:59 in UTC. The `24:00` end value is allowed only with `minute: 0` and represents end-of-day.
## Bucket Fields
| Field | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `start` | `{ "hour": 0-24, "minute": 0-59 }`. The first minute the bucket applies. |
| `end` | `{ "hour": 0-24, "minute": 0-59 }`. The first minute the bucket no longer applies. `24:00` is the only allowed end-of-day value. |
| `price` | Inline subscription-scoped price. Required when creating a new bucket; omit on updates when keeping the existing price. |
| `id` | Server-assigned bucket ID (`cmt_bkt_…`). Provide it on updates to keep the existing bucket and its price. |
| `commitment_type` | `amount` or `quantity`. Matches the line-item commitment type. |
| `commitment_value` | Minimum spend (when `commitment_type` is `amount`) or minimum quantity (when `commitment_type` is `quantity`) for usage windows inside the bucket. Must be > 0. |
| `overage_factor` | Multiplier applied to usage above the bucket commitment. Must be ≥ 1.0. |
| `true_up_enabled` | When `true`, the shortfall against `commitment_value` is billed at the last window of the commitment period. |
## Example: Peak and Off-Peak Commitment
The line item meters API calls with an hourly window. Peak hours 09:00–17:00 UTC carry a higher reserved commitment and rate; off-peak hours carry a lower commitment, a discounted price, and true-up.
```json theme={null}
{
"commitment_type": "amount",
"commitment_windowed": true,
"commitment_duration": "DAY",
"commitment_time_buckets": [
{
"start": { "hour": 9, "minute": 0 },
"end": { "hour": 17, "minute": 0 },
"commitment_type": "amount",
"commitment_value": "500.00",
"overage_factor": "1.5",
"true_up_enabled": false,
"price": {
"type": "USAGE",
"billing_model": "FLAT_FEE",
"billing_period": "DAY",
"billing_period_count": 1,
"invoice_cadence": "ARREAR",
"amount": "0.10"
}
},
{
"start": { "hour": 17, "minute": 0 },
"end": { "hour": 9, "minute": 0 },
"commitment_type": "amount",
"commitment_value": "100.00",
"overage_factor": "1.2",
"true_up_enabled": true,
"price": {
"type": "USAGE",
"billing_model": "FLAT_FEE",
"billing_period": "DAY",
"billing_period_count": 1,
"invoice_cadence": "ARREAR",
"amount": "0.04"
}
}
]
}
```
In this configuration the second bucket wraps midnight: `[17:00, 09:00)` covers 17:00–23:59 and 00:00–08:59 UTC. Together the two buckets tile the full day, so every usage window matches exactly one bucket and the line-item base commitment is never consulted.
### Billing walk-through
With an hourly meter and the configuration above, consider three windows in a single day:
| Window start (UTC) | Calls | Bucket matched | Bucket price | Bucket commitment | Charge |
| ------------------ | ----- | -------------- | ------------ | ----------------- | ---------------------------------------------- |
| 09:00 | 6,000 | Peak | \$0.10 | \$500 | $500 + ($600 − $500) × 1.5 = $650 |
| 14:00 | 5,000 | Peak | \$0.10 | \$500 | \$500 (usage exactly meets commitment) |
| 23:00 | 1,000 | Night | \$0.04 | \$100 | $100 via true-up ($40 actual + \$60 shortfall) |
The billing engine picks the bucket whose `[start, end)` range contains the window start timestamp, then runs the standard commitment/overage/true-up calculation against that bucket's values.
## Updating Buckets
On `PATCH` of a subscription line item, the `commitment_time_buckets` array replaces the existing array:
* **Keep an existing bucket** — include its `id` and omit `price`. Commitment fields are read from the request, so you can change `commitment_value`, `overage_factor`, or `true_up_enabled` while keeping the original price.
* **Add a new bucket** — omit `id` and include an inline `price`. The server creates a subscription-scoped price and assigns a bucket ID.
* **Remove all buckets** — send `"commitment_time_buckets": []`. Omitting the field entirely keeps the existing buckets unchanged.
A single bucket entry cannot include both `id` and `price`. Provide `id` to reuse an existing bucket's price, or provide `price` to create a new bucket.
## Validation Errors
| Condition | Error |
| ---------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Buckets exist on a non-windowed line item | `commitment_time_buckets requires commitment_windowed=true` |
| Buckets exist but the meter has no aggregation window | `buckets require a windowed meter` |
| Meter window is larger than a day | `meter window must be <= 1 day when using buckets` |
| `end - start` is not a multiple of the meter window | `bucket duration must be a multiple of the meter window` |
| `start` is not aligned to the meter window grid | `bucket start alignment error: start must be on the meter window grid` |
| Two buckets share any minute of the day | `buckets overlap` |
| `start` equals `end` | `bucket start must differ from end` |
| `commitment_value` ≤ 0 | `commitment_value must be > 0` |
| `overage_factor` is missing or \< 1.0 | `overage_factor must be at least 1.0` |
| Buckets combined with a cumulative subscription-level commitment | `per-bucket commitment cannot be combined with cumulative subscription commitment` |
# Customer Hierarchy
Source: https://docs.flexprice.io/docs/subscriptions/customer-hierarchy
Configure billing relationships at the subscription level so you can separate who uses your product from who pays for it.
This page covers consolidated billing and delegated invoicing. Flexprice now has a dedicated **Billing Workflows** section with full coverage of all four workflows, including grouped invoicing.
* [Overview: Which workflow do I need?](/docs/subscriptions/billing-workflows/overview)
* [Standalone](/docs/subscriptions/billing-workflows/standalone)
* [Consolidated Billing](/docs/subscriptions/billing-workflows/consolidated-billing)
* [Delegated Invoicing](/docs/subscriptions/billing-workflows/delegated-invoicing)
* [Grouped Invoicing](/docs/subscriptions/billing-workflows/grouped-invoicing)
**Customer = identity, Subscription = contract, Hierarchy = subscription
configuration**
## Overview
**Customer Hierarchy** is an enterprise feature for configuring billing relationships on each subscription. Flexprice keeps **customers** flat (no parent fields on customer records). All hierarchy behavior lives on the **subscription**, so you can mix models per contract without restructuring customer data.
With Customer Hierarchy you can:
* Send invoices to a different customer than the one using the service (**delegated payer**)
* Cover many customers with one parent subscription and roll up usage into a single invoice (**consolidated subscription**)
* Change how a subscription bills without editing customer records
## Two ways to use Customer Hierarchy
Customer Hierarchy supports two independent patterns. Pick one based on who owns the plan and line items:
| | Consolidated subscription | Delegated payer |
| ---------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| **Plan & line items** | Owned by the parent subscription | Owned by the child's subscription |
| **Billing, usage summary, wallet** | **Parent** customer (rolled up from all inherited children) | **Child** subscription; invoices settle against the **delegated payer's** wallet |
| **Usage events & identity** | Still associated with **child** customers for ingestion and analytics | Child customer (their own subscription) |
| **Invoice goes to** | Parent customer | A specified billing customer (bill-to) |
| **Best for** | Enterprise: one plan, many subsidiaries | Reseller: each customer has their own plan, a central payer |
| **API field** | `inheritance.external_customer_ids_to_inherit_subscription` | `inheritance.invoicing_customer_external_id` |
These two modes are mutually exclusive. You cannot combine
`external_customer_ids_to_inherit_subscription` and
`invoicing_customer_external_id` in the same subscription.
## How it works
Every subscription has a `subscription_type` that describes its role in a hierarchy:
| Type | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `standalone` | A regular subscription with no hierarchy relationship (default) |
| `parent` | Owns the plan and line items; usage from child subscriptions rolls up here |
| `inherited` | A skeleton subscription auto-created for each child customer; it carries no line items, and events are matched via the parent subscription |
Customers remain flat entities. There are no parent fields on a customer record; hierarchy is configured only on subscriptions.
**Key design principles (consolidated mode)** Hierarchy lives at the
**subscription** layer, not on customer records. That gives you **clear
billing ownership**, **centralized wallet management**, and **scalable
multi-tenant usage tracking** on a single parent contract. The tradeoff:
**child-level isolation is logical** (you can still break out analytics per
child), **not financial**. Children with only inherited subscriptions are not
separate billing entities for usage summary, wallet balance, or invoices.
## Visual flow (consolidated subscription)
The diagrams and screenshots below apply to **consolidated subscription** mode: a `parent` subscription plus `inherited` skeleton subscriptions for each child.
### End-to-end flow
```mermaid theme={null}
flowchart TB
subgraph createParent [Parent_subscription_creation]
A[Create_subscription_for_parent_customer]
B[Configure_child_external_IDs_to_inherit]
A --> B
end
subgraph linkChildren [Child_linkage]
C[Inherited_skeleton_subscriptions_created]
end
subgraph usagePath [Usage_ingestion]
D[Events_ingested_for_child_customers]
E[Rolled_up_to_parent_contract]
D --> E
end
subgraph invoices [Invoice_generation]
F[Single_invoice_to_parent]
G[One_line_item_per_feature_aggregated]
F --> G
end
createParent --> linkChildren
linkChildren --> usagePath
usagePath --> invoices
```
### Dashboard: add customers to inherit
Use the **Add customers to inherit** flow when linking child customers to the parent subscription (wording and entry point may vary slightly by dashboard version).
### Dashboard: inherited subscription on a child
On a **child** customer, the **Inherited subscriptions** view shows the parent they inherit from, the plan, and key dates.
The **parent subscription** creation screen (with inheritance configured) is shown in [Step 2: Create the parent subscription](#step-2-create-the-parent-subscription) below.
## Usage tracking behavior (inherited subscriptions)
For **consolidated subscription** setups only (not delegated payer):
* **Usage for billing** is tracked at the **parent** subscription. Inherited children do **not** keep a separate billing usage ledger.
* [**Get customer usage summary**](/api-reference/customers/get-customer-usage-summary) (`getCustomerUsageSummary`, `GET /customers/usage`) for a **child** customer that only has an **inherited** subscription returns **no billable usage** on that child. Usage is attributed to the **parent** contract.
* [**Get wallet balance**](/api-reference/wallets/get-wallet-balance) (`getWalletBalance`, `GET /wallets/{id}/balance/real-time`) does **not** represent an independent prepaid balance for inherited-only children. Query the **parent** customer's wallet for consolidated billing state.
A child customer showing **zero** usage in usage-summary APIs while events are
ingested for that customer is **expected** for inherited subscriptions.
Confirm totals on the **parent** customer or use **analytics** (next section)
for per-child breakdowns.
## Analytics behavior
Analytics are more flexible than billing aggregates:
* **Per-child contribution:** call [**Get usage analytics**](/api-reference/events/get-usage-analytics) (`getUsageAnalytics`, `POST /events/analytics`) with the **child** `external_customer_id` (and any filters you already use, such as time range or `group_by`).
* **Aggregated view at the parent:** use the same endpoint scoped to the **parent** and include child usage in the aggregation:
```json theme={null}
{
"include_children": true
}
```
Add this alongside the other fields your request already sends (for example `external_customer_id`, `start_time`, `end_time`, and `group_by`).
## Wallet and balance semantics (consolidated mode)
In **consolidated subscription** mode:
* The **wallet** used for settlement is the **parent** customer's wallet (the invoicing customer for the parent subscription).
* **Balance** reflects credits and drawdowns **aggregated across all children** covered by that parent subscription.
* Wallet-related APIs therefore reflect **global parent-level state**, not a separate financial balance per inherited child.
## Invoice behavior (consolidated mode)
In **consolidated subscription** mode:
* Invoices are generated **only** for the **parent** customer.
* The invoice structure is **one line item per feature** (or priced line), with **usage from all children aggregated** into that line item.
* There are **no child-level invoices** for inherited skeleton subscriptions.
## Consolidated subscription
Use this when **one plan** covers multiple customers and you want **one invoice** to the parent (usage roll-up on a single contract).
Flexprice implements this with a **parent** subscription plus **inherited** skeleton subscriptions for each child. Inherited subscriptions have no line items of their own; usage from child customers is matched and aggregated through the parent.
**Example:** Global Corp HQ purchases an enterprise plan. APAC Team and EMEA Team each generate usage, but HQ receives one invoice.
### Step 1: Create customers
Create the parent customer and each child customer through the Dashboard or API. Customer records have no hierarchy fields; they stay flat.
### Step 2: Create the parent subscription
1. Navigate to **Subscriptions** and click **Create Subscription**
2. Select the **parent customer** and choose a plan
3. In the **Customer Hierarchy** section, add the external IDs of the child customers who will inherit this subscription
4. Click **Create Subscription**
```http theme={null}
POST /subscriptions
{
"customer_id": "cus_hq_internal_id",
"external_customer_id": "ext-global-hq",
"plan_id": "plan_enterprise",
"currency": "usd",
"billing_period": "month",
"billing_period_count": 1,
"inheritance": {
"external_customer_ids_to_inherit_subscription": [
"ext-apac-team",
"ext-emea-team"
]
}
}
```
### What Flexprice creates
When you submit the request above, Flexprice:
1. Creates the **parent subscription** with `subscription_type: parent` for the HQ customer
2. Auto-creates an **inherited skeleton subscription** (`subscription_type: inherited`) for each child external ID
3. Keeps inherited subscriptions free of plan line items; usage events from child customers are matched and aggregated via the parent subscription
### Step 3: Verify
Open the parent subscription's detail page. Child skeleton subscriptions appear under it.
## Delegated payer
Use this when each end customer has their **own** subscription (plan, line items, and usage on the child), but a **different customer** should be the **bill-to** party: invoices, tax, and wallet drawdown follow that **delegated payer** (for example a parent company or reseller).
**Example:** A reseller manages 10 end customers, each on their own starter plan. The reseller receives and pays all related invoices centrally.
### Step 1: Create customers
Create the subscription owner (child) and the billing customer separately. Neither customer record has hierarchy fields.
### Step 2: Create the subscription with a delegated payer
1. Navigate to **Subscriptions** and click **Create Subscription**
2. Select the **child customer** as the subscription owner
3. Choose a plan
4. In the **Invoice To** field, select or enter the delegated payer's external ID (the customer who will receive the invoice)
5. Click **Create Subscription**
```http theme={null}
POST /subscriptions
{
"customer_id": "cus_child_internal_id",
"external_customer_id": "ext-end-customer",
"plan_id": "plan_starter",
"currency": "usd",
"billing_period": "month",
"billing_period_count": 1,
"inheritance": {
"invoicing_customer_external_id": "ext-reseller"
}
}
```
### What Flexprice creates
* Subscription is created with `subscription_type: standalone` on the child customer
* Plan, line items, entitlements, and usage tracking all remain on the child
* Invoices are issued to the delegated payer (`ext-reseller`)
* The delegated payer's wallet and payment methods are used for settlement
The invoicing customer is set at subscription creation and cannot be changed
afterwards. If you need to change it, cancel the subscription and create a new
one.
## Invoice and payment behavior
| | Consolidated subscription | Delegated payer |
| ------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------- |
| **Usage for billing** | **Parent** subscription (rolled up; child usage-summary APIs show no isolated billable usage) | Child customer (their own subscription) |
| **Invoice generated for** | Parent customer only (one line item per feature, usage aggregated across children) | Delegated payer (billing customer) |
| **Wallet deducted from** | Parent customer | Delegated payer |
| **Tax based on** | Parent customer's billing details | Delegated payer's billing details |
| **Currency** | Set at the parent subscription | Set at the child subscription |
## Use cases
**Enterprise with subsidiaries** *(Consolidated subscription)*
Global HQ purchases a plan. Regional divisions (APAC, EMEA, Americas) each generate usage independently, but HQ receives **one consolidated invoice** to the parent, typically **one line item per feature** with usage **aggregated across divisions** (use analytics with `include_children` or per-child queries to see each division's contribution).
**Reseller / partner model** *(Delegated payer)*
A reseller signs up their clients as separate customers in Flexprice. Each client has their own subscription and usage tracking, but the reseller receives and pays all invoices.
**Department-level billing** *(Consolidated subscription or delegated payer)*
A company's central IT or Finance team handles vendor payments. Engineering, Marketing, and Sales each track their own usage, and invoices roll up to the central team (or the central team is set as the delegated payer, depending on your model).
**Multi-brand holding company** *(Consolidated subscription)*
A holding company owns multiple brands. Each brand operates independently as a customer with its own subscriptions. The holding company's finance department receives all invoices.
## Frequently asked questions
In **consolidated subscription** mode, currency is set on the parent subscription and applies to all invoices generated for it. Child inherited subscriptions use the parent's currency.
In **delegated payer** mode, each subscription sets its own currency independently, since each child owns a full subscription.
Tax uses the **invoicing customer's** billing details: the parent customer in
consolidated subscription mode, or the delegated payer in delegated payer
mode. The subscription owner's billing address is not used for tax in either
pattern.
The wallet is deducted from the invoicing customer's balance, not the
subscription owner's. In consolidated subscription mode, the parent customer's
wallet is drawn down (balance reflects aggregated usage across inherited
children). In delegated payer mode, the delegated payer's wallet is used.
In **consolidated subscription** mode, billable usage is attributed to the
**parent** subscription. A child with only an **inherited** subscription does
not have a separate billing usage ledger, so usage-summary APIs for that child
can correctly return **no usage**. Use the **parent** customer for billing
summaries, or use [**Get usage
analytics**](/api-reference/events/get-usage-analytics) for per-child
breakdowns (and `include_children` when querying the parent for a rolled-up
analytics view).
Plan changes apply to the parent subscription only. Inherited skeleton
subscriptions are not directly affected; they keep routing usage events to the
updated parent. (This applies to **consolidated subscription** setups with a
`parent` subscription.)
A final invoice is generated for the invoicing customer. Inherited skeleton
subscriptions linked to the parent are cancelled at the same time. (This
applies to **consolidated subscription** mode.)
No. The invoicing customer is set at creation time and is immutable. To change it, cancel the existing subscription and create a new one with the correct `invoicing_customer_external_id`.
# Create subscription for a customer
Source: https://docs.flexprice.io/docs/subscriptions/customers-create-subscription
Once a customer is created, you can create a subscription for them by following these steps:
* Navigate to the **Customers Dashboard**.
* Click on the customer you want to create a subscription for. This will open the customer's details page.
* On the customer details page, click the **Add Subscription** button.
* Select a pricing plan from the dropdown menu. Only plans that have been created will be listed.
* Based on the selected plan, subscription preview along with breakdown of charges will appear on the right side of the screen. This includes all recurring charges and usage-based charges that you've created in the plan for the given billing period.
* Specify the **Subscription Start Date** thats marks as the beginning of the billing cycle.
* Define the **Subscription End Date** if required. By default, this is set to "Forever."
* **Optional: Configure Override Line Items** - If you need to customize pricing for this specific subscription, you can add override line items to modify amounts, quantities, billing models, or tier structures. See [Subscription Override Line Items](/docs/subscriptions/override-line-items) for detailed information.
* Once all details are entered, click **Add Subscription** to save.
* The subscription will now appear under the customer's details with its status and billing information.
### **Managing Multiple Subscriptions for a Customer**
* A single customer can have multiple active subscriptions.
* Each subscription operates independently based on its assigned billing period, charges, and start date.
* For example, a customer can have both a `Basic Plan` and an `Addon: AI Agent Sessions` subscription, each billed separately.
### **Customizing Subscription Pricing**
* **Override Line Items**: Customize pricing for specific subscriptions by overriding plan prices
* **Special Pricing**: Offer enterprise discounts, bulk pricing, or promotional rates
* **Flexible Billing**: Modify billing models, tier structures, and quantities per subscription
For comprehensive information on customizing subscription pricing, see [Subscription Override Line Items](/docs/subscriptions/override-line-items).
# Subscription Override Line Items
Source: https://docs.flexprice.io/docs/subscriptions/override-line-items
Learn how to customize pricing for specific subscriptions by overriding plan prices with custom amounts, quantities, billing models, and tier structures.
Subscription override line items allow you to customize pricing for specific
subscriptions by overriding the default plan prices. This feature is useful
when you need to offer special pricing, bulk discounts, or custom billing
arrangements for individual customers.
## Overview
Override line items enable you to modify various aspects of a subscription's pricing:
* **Quantity**: Override the default quantity for a specific price
* **Amount**: Set custom pricing amounts for specific line items
* **Billing Model**: Change the billing model for a specific price
* **Tier Mode**: Modify how tiered pricing is calculated
* **Tiers**: Define custom pricing tiers for the subscription
* **Transform Quantity**: Apply quantity transformations for package pricing
## When to Use Override Line Items
Use override line items in these scenarios:
* **Custom Pricing**: Offer special rates for enterprise customers
* **Bulk Discounts**: Provide volume-based pricing adjustments
* **Promotional Offers**: Apply temporary pricing changes
* **Contract Negotiations**: Honor specific pricing agreements
* **Regional Pricing**: Adjust prices based on customer location
## Structure
Each override line item requires:
```json theme={null}
{
"price_id": "string (required)",
"quantity": "decimal (optional)",
"amount": "decimal (optional)",
"billing_model": "string (optional)",
"tier_mode": "string (optional)",
"tiers": "array (optional)",
"transform_quantity": "object (optional)"
}
```
### Required Fields
* **`price_id`**: References the plan price to override (must exist in the selected plan)
### Optional Fields
* **`quantity`**: Custom quantity for this line item
* **`amount`**: New price amount that overrides the original price
* **`billing_model`**: Override the billing model for this price
* **`tier_mode`**: Change how tiered pricing is calculated
* **`tiers`**: Define custom pricing tiers
* **`transform_quantity`**: Apply quantity transformations
## Billing Model Overrides
### Flat Fee Billing Model
When overriding to `FLAT_FEE`, you must provide either an `amount` or `quantity`:
```json theme={null}
{
"price_id": "price_123",
"billing_model": "FLAT_FEE",
"amount": "29.99"
}
```
**Use Cases:**
* Fixed monthly fees
* One-time charges
* Service-based pricing
### Tiered Billing Model
When overriding to `TIERED`, you must provide either `tier_mode` or `tiers`:
```json theme={null}
{
"price_id": "price_123",
"billing_model": "TIERED",
"tier_mode": "VOLUME",
"tiers": [
{
"up_to": 1000,
"unit_amount": "0.10",
"flat_amount": "5.00"
},
{
"up_to": null,
"unit_amount": "0.08"
}
]
}
```
**Tier Modes:**
* **`VOLUME`**: All units priced based on the final tier reached
* **`SLAB`**: Tiers apply progressively as quantity increases
**Tier Structure:**
* **`up_to`**: Quantity threshold (null for the last tier)
* **`unit_amount`**: Price per unit in this tier
* **`flat_amount`**: Additional flat fee for this tier (optional)
### Package Billing Model
When overriding to `PACKAGE`, you must provide `transform_quantity`:
```json theme={null}
{
"price_id": "price_123",
"billing_model": "PACKAGE",
"transform_quantity": {
"divide_by": 10,
"round": "up"
}
}
```
**Transform Quantity Fields:**
* **`divide_by`**: Number of units per package (must be > 0)
* **`round`**: Rounding behavior (`"up"` or `"down"`)
**Use Cases:**
* Bulk pricing (e.g., 10 units for \$5.00)
* Resource bundling
* Simplified billing for large quantities
## Validation Rules
### General Requirements
* At least one override field must be provided
* All amounts must be non-negative
* All quantities must be non-negative
* Price ID must reference a valid price in the selected plan
### Billing Model Specific Rules
#### Tiered Billing
* Must provide either `tier_mode` or `tiers`
* If `tiers` provided, each tier must have a valid `unit_amount`
* `unit_amount` must be a valid decimal number
* `flat_amount` (if provided) must be a valid decimal number
#### Package Billing
* `transform_quantity.divide_by` must be greater than 0
* `transform_quantity.round` must be either `"up"` or `"down"`
* Defaults to `"up"` if not specified
#### Flat Fee Billing
* Must provide either `amount` or `quantity`
## Examples
### Basic Quantity Override
```json theme={null}
{
"price_id": "price_123",
"quantity": "5.0"
}
```
### Custom Tiered Pricing
```json theme={null}
{
"price_id": "price_456",
"billing_model": "TIERED",
"tier_mode": "VOLUME",
"tiers": [
{
"up_to": 100,
"unit_amount": "1.00"
},
{
"up_to": 500,
"unit_amount": "0.75"
},
{
"up_to": null,
"unit_amount": "0.50"
}
]
}
```
### Package Pricing with Custom Rounding
```json theme={null}
{
"price_id": "price_789",
"billing_model": "PACKAGE",
"transform_quantity": {
"divide_by": 25,
"round": "down"
}
}
```
### Multiple Overrides
```json theme={null}
{
"override_line_items": [
{
"price_id": "price_123",
"amount": "19.99"
},
{
"price_id": "price_456",
"quantity": "10.0",
"billing_model": "TIERED",
"tier_mode": "SLAB",
"tiers": [
{
"up_to": 5,
"unit_amount": "2.00"
},
{
"up_to": null,
"unit_amount": "1.50"
}
]
}
]
}
```
## Best Practices
### 1. Plan Your Overrides
* Identify which prices need customization
* Understand the impact on billing calculations
* Consider how overrides affect reporting
### 2. Validate Business Logic
* Ensure override amounts make business sense
* Verify tier structures are logical
* Test edge cases with different quantities
### 3. Document Changes
* Keep records of why overrides were applied
* Note any special conditions or time limits
* Update customer contracts if necessary
### 4. Monitor Usage
* Track how overrides affect revenue
* Identify patterns in override usage
* Optimize pricing based on override data
## Common Use Cases
### Enterprise Discounts
```json theme={null}
{
"price_id": "enterprise_plan",
"amount": "199.99",
"quantity": "1.0"
}
```
### Volume Pricing
```json theme={null}
{
"price_id": "api_calls",
"billing_model": "TIERED",
"tier_mode": "VOLUME",
"tiers": [
{
"up_to": 10000,
"unit_amount": "0.001"
},
{
"up_to": null,
"unit_amount": "0.0005"
}
]
}
```
### Promotional Offers
```json theme={null}
{
"price_id": "premium_feature",
"amount": "0.00",
"quantity": "1.0"
}
```
## Troubleshooting
### Common Issues
1. **Validation Errors**
* Ensure at least one override field is provided
* Check that amounts and quantities are non-negative
* Verify price ID exists in the selected plan
2. **Billing Model Conflicts**
* Tiered billing requires tier configuration
* Package billing needs transform quantity
* Flat fee requires amount or quantity
3. **Tier Configuration**
* Validate tier boundaries are logical
* Ensure last tier has `up_to: null`
* Check unit amounts are valid decimals
### Error Messages
* **"price not found in plan"**: Verify the price ID exists in the selected plan
* **"at least one override field must be provided"**: Add at least one override field
* **"invalid tier unit amount format"**: Ensure tier amounts are valid decimal numbers
* **"transform\_quantity.divide\_by must be greater than 0"**: Set a positive package size
## Related Topics
* [Creating Subscriptions](/docs/subscriptions/customers-create-subscription)
* [Pricing Plans](/docs/product-catalogue/plans/pricing)
* [Billing Models](/docs/product-catalogue/plans/billing-models/flat-fee)
* [Viewing Subscriptions](/docs/subscriptions/view)
***
*For technical implementation details, refer to the [API Reference](/api-reference/subscriptions/create-subscription).*
# Plan Price Overrides
Source: https://docs.flexprice.io/docs/subscriptions/plan-price-overrides
Give a specific customer custom rates without changing the plan for everyone else. Apply overrides at subscription creation time.
Plan price overrides let you customize pricing for a specific customer when creating a subscription. The plan itself stays unchanged — only this subscription uses the overridden rates.
## When to Use
* Enterprise customer with a negotiated rate
* Promotional or introductory pricing for a specific account
* Volume discounts agreed outside of your standard plan tiers
* Early adopter or partner pricing
If you need to change pricing for **all** subscribers, update the plan price instead and run a price sync. See [Subscription Line Item Overrides](/docs/subscriptions/subscription-line-item-overrides).
## How to Apply Overrides
Pass an `override_line_items` array in the create subscription request. Each entry references a plan price by `price_id` and specifies what to change.
```json theme={null}
POST /subscriptions
{
"customer_id": "cust_xxx",
"plan_id": "plan_xxx",
"currency": "usd",
"billing_cadence": "RECURRING",
"billing_period": "MONTHLY",
"override_line_items": [
{
"price_id": "price_base_fee",
"amount": "299.00"
},
{
"price_id": "price_api_calls",
"billing_model": "TIERED",
"tier_mode": "VOLUME",
"tiers": [
{ "up_to": 100000, "unit_amount": "0.0005" },
{ "up_to": null, "unit_amount": "0.0002" }
]
}
]
}
```
You can override multiple prices in one request.
## What You Can Override
| Field | Type | Notes |
| -------------------- | ------- | -------------------------------------------------------- |
| `price_id` | string | **Required.** The plan price to override |
| `amount` | decimal | For flat fee prices (FIAT pricing) |
| `quantity` | decimal | Fixed prices only — cannot be set for usage-based prices |
| `billing_model` | string | `FLAT_FEE`, `TIERED`, or `PACKAGE` |
| `tier_mode` | string | `VOLUME` or `SLAB` — for TIERED billing model |
| `tiers` | array | Custom tier breakpoints for TIERED/FIAT pricing |
| `transform_quantity` | object | For PACKAGE billing: `divide_by` and `round` |
| `price_unit_amount` | decimal | For custom price unit prices |
| `price_unit_tiers` | array | Custom tiers for custom price unit + TIERED |
## What You Cannot Override
These fields are always inherited from the original plan price and cannot be changed:
| Field | Why |
| ----------------------------------------- | ------------------------------------- |
| `currency` | Locked to the subscription's currency |
| `billing_period` / `billing_period_count` | Billing cycle alignment is fixed |
| `billing_cadence` | Cannot switch recurring ↔ one-time |
| `invoice_cadence` | ARREAR/ADVANCE is fixed per price |
| `trial_period` | Trial config is plan-level |
| `meter_id` | The usage metric cannot be swapped |
| `price_unit_type` | Cannot switch FIAT ↔ CUSTOM |
| `display_name` | Preserved from original |
## Validation Rules
* At least one override field must be provided per entry
* `amount` and `quantity` must be non-negative
* `quantity` is not allowed on usage-based prices (usage is metered, not quantity-based)
* Cannot mix FIAT fields (`amount`, `tiers`) with custom price unit fields (`price_unit_amount`, `price_unit_tiers`) in the same entry
* `transform_quantity.divide_by` must be greater than 0
* `price_id` must reference a price that exists in the selected plan
## How It Works Internally
When an override is processed, Flexprice:
1. Creates a new **subscription-scoped price** with `entity_type = SUBSCRIPTION` and the overridden values
2. Sets `parent_price_id` on the new price to the original plan price's ID — preserving the lineage
3. The subscription's line item points to this subscription-scoped price
The plan price is untouched. Other subscribers on the same plan are unaffected.
```
Plan Price (shared)
└── Subscription-scoped Price (this customer only)
└── Line Item → price_id = subscription-scoped price
```
## Billing Behavior After Override
* All invoicing for this subscription uses the subscription-scoped price
* If the plan price is later updated, **this subscription's override is not affected**
* If a [price sync](/docs/subscriptions/subscription-line-item-overrides) is run, **the overridden line item is not replaced** — sync only fills in missing line items
## Examples
### Flat Fee Discount
```json theme={null}
{
"price_id": "price_monthly_base",
"amount": "199.00"
}
```
### Custom Usage Tiers
```json theme={null}
{
"price_id": "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" }
]
}
```
### Package Pricing Override
```json theme={null}
{
"price_id": "price_sms",
"billing_model": "PACKAGE",
"transform_quantity": {
"divide_by": 500,
"round": "up"
}
}
```
### Quantity Override (Fixed Price)
```json theme={null}
{
"price_id": "price_seats",
"quantity": "50.0"
}
```
## Updating an Override After Creation
To change an override on a live subscription, update the line item directly using the subscription line item update API. See [Subscription Line Item Overrides](/docs/subscriptions/subscription-line-item-overrides).
## Related Topics
* [Creating Subscriptions](/docs/subscriptions/customers-create-subscription)
* [Subscription Line Item Overrides](/docs/subscriptions/subscription-line-item-overrides)
* [Plan Pricing](/docs/product-catalogue/plans/pricing)
* [Billing Models](/docs/product-catalogue/plans/billing-models/flat-fee)
# Progressive Billing
Source: https://docs.flexprice.io/docs/subscriptions/progressive-billing
Progressive billing automatically triggers mid-period invoices when a subscription's current-period usage crosses a defined threshold
## Overview
Progressive billing lets you set a usage amount threshold on a subscription. When the customer's billed usage for the current period reaches that amount, FlexPrice automatically creates and finalizes a mid-period invoice without waiting for the period to end. This is useful for high-volume customers where waiting until period end creates cash flow or fraud risk.
## How It Works
1. You set `auto_invoice_threshold` on a subscription (e.g. \$500).
2. FlexPrice evaluates usage every **5 minutes** against the current window (`current_period_start` → now).
3. When usage ≥ threshold, a mid-period invoice is created and finalized immediately.
4. `current_period_start` advances to the moment the invoice was issued; `current_period_end` remains unchanged. Usage resets for the new sub-window and the threshold applies fresh.
5. At period end, a normal invoice is issued for remaining usage.
Only **arrear usage charges** count toward the threshold. Fixed or advance charges are excluded. The invoice amount may slightly exceed the threshold since evaluation happens on a 5-minute interval, not in real time.
## Configuring Progressive Billing
1. Navigate to the **Subscriptions** section and open or create a subscription.
2. Locate the **Auto invoice threshold** section (progressive billing).
3. Enter your threshold amount.
4. Save the subscription.
Pass `auto_invoice_threshold` when creating a subscription:
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/subscriptions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "cust_01xyz",
"plan_id": "plan_01abc",
"currency": "USD",
"billing_cadence": "RECURRING",
"billing_period": "MONTHLY",
"billing_period_count": 1,
"start_date": "2025-05-01T00:00:00Z",
"auto_invoice_threshold": "500.00"
}'
```
Response (abbreviated):
```json theme={null}
{
"id": "sub_01def456",
"status": "active",
"auto_invoice_threshold": "500.00",
"current_period_start": "2025-05-01T00:00:00Z",
"current_period_end": "2025-06-01T00:00:00Z"
}
```
`auto_invoice_threshold` is set at the subscription level, not the plan level. Each subscription can have its own threshold.
## Invoicing Behavior
| Scenario | Behavior |
| ----------------------------------- | ------------------------------------------------------ |
| Usage reaches threshold | Mid-period invoice created and finalized immediately |
| Usage below threshold at period end | Normal period-end invoice covers all remaining usage |
| Threshold not set (`null`) | Progressive billing disabled; standard billing applies |
## API Reference
### Subscription create fields
| Field | Type | Required | Description |
| ------------------------ | ---------------- | -------- | -------------------------------------------------------------------------------------------------- |
| `auto_invoice_threshold` | string (decimal) | No | Usage amount that triggers a mid-period invoice. Must be > 0. `null` disables progressive billing. |
### Subscription response fields
| Field | Type | Description |
| ------------------------ | -------------------------- | ------------------------------------------------------------------ |
| `auto_invoice_threshold` | string (decimal), nullable | The configured threshold for this subscription. `null` if not set. |
## Webhooks & Events
No new webhook event type is introduced for progressive billing. Standard invoice events fire when a threshold invoice is created and finalized. Use the `billing_reason` field on the invoice object to identify threshold-triggered invoices:
| Event | When it fires |
| ------------------- | ---------------------------------------------------------------------- |
| `invoice.created` | When usage crosses the threshold and a mid-period invoice is generated |
| `invoice.finalized` | When the threshold invoice is finalized and ready for payment |
The `billing_reason` on threshold invoices is `AUTO_INVOICE_THRESHOLD`.
## Best Practices
**Set thresholds proportional to expected spend.** A threshold at 50 to 80% of expected monthly spend avoids too-frequent invoices while still providing early billing signals.
**Ensure your payment method handles mid-period invoices.** Invoices created via progressive billing are finalized immediately. Make sure your payment collection flow can process charges outside the normal billing cycle.
**Monitor via invoice webhooks.** No new webhook event type is introduced for progressive billing. Listen to standard invoice creation and finalization events; the invoice billing reason is `AUTO_INVOICE_THRESHOLD`, which you can use to distinguish these from regular period-end invoices.
**Do not rely on real-time triggering.** There is up to a 5-minute delay between crossing the threshold and invoice creation. Do not use progressive billing for hard real-time spend caps.
# Record payment for subscription
Source: https://docs.flexprice.io/docs/subscriptions/record-payment
Complete guide to Flexprice payment system including payment methods, behaviors, and wallet payment logic
## Overview
Flexprice's payment system provides flexible subscription billing with intelligent payment processing. The system combines **payment behaviors** (how to handle payment failures) with **collection methods** (when to charge customers) to create a robust billing experience.
### Key Concepts
* **Collection Method**: Determines when payments are collected (`charge_automatically` or `send_invoice`)
* **Payment Behavior**: Defines how the system responds to payment failures
* **Payment Methods**: Card payments and wallet/credits with intelligent splitting
* **Flow Types**: Different contexts like subscription creation, renewals, manual payments
* **Partial Payments**: Smart fallback to wallet payments when card payments fail
## Why Payment Behaviors and Collection Methods Matter
Flexprice separates **when** to collect payment from **how** to handle failures. This design provides flexibility for different business models:
### Business Model Alignment
* **SaaS Products**: Use `charge_automatically` + `allow_incomplete` for graceful failure handling
* **Enterprise**: Use `send_invoice` + `default_active` for manual billing cycles
* **Freemium**: Use `charge_automatically` + `default_active` for service-first approach
* **Premium Services**: Use `charge_automatically` + `error_if_incomplete` for strict payment requirements
## Valid Payment Configurations
The following table shows all valid combinations of collection methods, payment behaviors, and their outcomes:
| Collection Method | Payment Behavior | Card Payment | Wallet Payment | Payment Success | Payment Failure |
| ---------------------- | --------------------- | --------------- | --------------- | --------------- | ------------------- |
| `charge_automatically` | `allow_incomplete` | ✅ Attempted | ✅ Fallback | ✅ Active | ⚠️ Incomplete |
| `charge_automatically` | `error_if_incomplete` | ✅ Attempted | ❌ No fallback\* | ✅ Active | ❌ Failed/Incomplete |
| `charge_automatically` | `default_active` | ✅ Attempted | ✅ Fallback | ✅ Active | ✅ Active |
| `send_invoice` | `default_active` | ❌ Not attempted | ❌ Not attempted | ✅ Active | ✅ Active |
| `send_invoice` | `default_incomplete` | ❌ Not attempted | ❌ Not attempted | ✅ Active | ⚠️ Incomplete |
**Note**: \*Wallet fallback depends on flow type - see Partial Payment Logic section below.
### Invalid Combinations
❌ `charge_automatically` + `default_incomplete` - Conflicts with automatic charging\
❌ `send_invoice` + `allow_incomplete` - No automatic payment to fail\
❌ `send_invoice` + `error_if_incomplete` - No automatic payment to fail
* When no collection method is provided, the default is `charge_automatically`.
* When no payment behavior is provided, the default is `default_active`.
## Partial Payment Logic
### Overview
When card payments fail, the system can attempt wallet payments as a fallback. This behavior depends on the **payment behavior** and **flow type** combination.
### Decision Matrix
| Flow Type | Payment Behavior | Card Fails | Wallet Attempted | Rationale |
| ----------------------- | --------------------- | ---------- | ---------------- | ----------------------------------- |
| `subscription_creation` | `default_active` | ❌ | ✅ **Yes** | Subscription activates anyway |
| `subscription_creation` | `allow_incomplete` | ❌ | ❌ **No** | Payment affects subscription status |
| `subscription_creation` | `error_if_incomplete` | ❌ | ❌ **No** | Payment required for activation |
| `renewal` | **Any** | ❌ | ✅ **Yes** | Partial payment better than none |
| `manual` | **Any** | ❌ | ✅ **Yes** | Customer-initiated, allow partial |
| `cancel` | **Any** | ❌ | ✅ **Yes** | Proration handling |
### Rules Summary
* **Subscription Creation + Default Active**: Always allow partial wallet payment
* **Other Subscription Creation**: No partial payment (affects activation)
* **All Other Flows**: Always allow partial payment (better than failure)
#### Wallet Categorization
Wallets are categorized based on their `allowed_price_types` configuration:
1. **ALL Wallets**: Can pay any price type (USAGE + FIXED)
2. **USAGE Wallets**: Can only pay usage-based charges
3. **FIXED Wallets**: Can only pay fixed charges
4. **USAGE+FIXED Wallets**: Treated as ALL wallets
#### Wallet Selection Algorithm
```mermaid theme={null}
graph TD
A[Get Customer Wallets] --> B[Filter Active Wallets]
B --> C[Categorize by Price Type Restrictions]
C --> D[USAGE Wallets]
C --> E[FIXED Wallets]
C --> F[ALL Wallets]
D --> G[Sort by Balance Descending]
E --> G
F --> G
G --> H[Return in Priority Order]
```
#### Payment Processing Order
1. **USAGE Wallets First**: Pay usage-based charges
2. **FIXED Wallets Second**: Pay fixed charges
3. **ALL Wallets Last**: Pay remaining amounts
4. **Balance Sorting**: Highest balance wallets used first
### **Endpoint**: `POST /v1/subscriptions`
**Request**:
```json theme={null}
{
"customer_id": "cust_123",
"plan_id": "plan_456",
"collection_method": "charge_automatically",
"payment_behavior": "allow_incomplete",
"gateway_payment_method_id": "pm_789"
}
```
**Parameters**:
* `collection_method`: `"charge_automatically"` | `"send_invoice" ` (default: `"charge_automatically"`)
* `payment_behavior`: `"allow_incomplete"` | `"error_if_incomplete"` | `"default_active"` | `"default_incomplete"` (default: `"default_active"`)
* `gateway_payment_method_id` (optional): Override default payment method
## Payment Flow Architecture
```mermaid theme={null}
graph TD
A[Invoice Created] --> B{Collection Method?}
B -->|charge_automatically| C[Payment Behavior Handler]
B -->|send_invoice| D[Send Invoice to Customer]
C --> E{Payment Behavior?}
E -->|allow_incomplete| F[Attempt Payment + Continue on Failure]
E -->|error_if_incomplete| G[Attempt Payment + Block on Failure]
E -->|default_active| H[Activate + Attempt Payment]
F --> I[Payment Processing]
G --> I
H --> I
I --> J[Card Payment First]
J --> K{Card Success?}
K -->|Yes| L[Process Wallet Payment]
K -->|No| M{Allow Partial Payment?}
M -->|Yes| N[Attempt Wallet Payment]
M -->|No| O[Payment Failed - Stop]
L --> P[Payment Complete]
N --> Q{Any Payment Made?}
Q -->|Yes| R[Partial Payment Success]
Q -->|No| O
P --> S[Update Subscription Status]
R --> S
O --> S
```
### Card-First Strategy
The system prioritizes card payments with intelligent wallet fallback:
1. **Calculate Split**: Determine what wallets can pay based on price type restrictions
2. **Card First**: Attempt card payment for the full eligible amount
3. **Evaluate Result**: Check if card payment succeeded or failed
4. **Partial Decision**: Use behavior + flow type to decide on wallet fallback
5. **Wallet Payment**: Process available wallet payments if allowed
6. **Final Status**: Determine overall payment success and subscription status
## Payment Flow Types
### 1. Subscription Creation (`InvoiceFlowSubscriptionCreation`)
**New subscription activation with payment collection**
* **Behavior**: Applies full payment behavior logic
* **Status Updates**: Subscription status depends on payment result and behavior
* **Partial Payments**: Only allowed for `default_active` behavior
### 2. Renewal (`InvoiceFlowRenewal`)
**Periodic billing for existing subscriptions**
* **Behavior**: Non-blocking payment processing
* **Status Updates**: Subscription continues regardless of payment result
* **Partial Payments**: Always allowed (partial payment better than none)
### 3. Manual Payment (`InvoiceFlowManual`)
**Customer-initiated payment attempts**
* **Behavior**: Uses card-first logic with wallet fallback
* **Status Updates**: Activates subscription only on full payment success
* **Partial Payments**: Always allowed
### 4. Cancellation (`InvoiceFlowCancel`)
**Proration handling during subscription cancellation**
* **Behavior**: Processes final charges/credits
* **Status Updates**: No subscription status changes
* **Partial Payments**: Always allowed
## Payment Methods
### 1. Card Payments (`CARD`)
**Primary payment method for automatic billing**
* **Processing**: Immediate via Stripe integration
* **Requirements**: Customer must exist in Stripe with valid payment method
* **Gateway Support**: Can specify `gateway_payment_method_id` for subscription-specific cards
* **Fallback**: Wallet payment attempted if card fails (based on partial payment rules)
### 2. Credits/Wallet Payments (`CREDITS`)
**Prepaid balance payments with price type restrictions**
* **Processing**: Instant, no external dependencies
* **Restrictions**: Wallets can be limited to USAGE, FIXED, or ALL price types
* **Multi-Wallet**: Supports multiple wallets per customer with intelligent selection
* **Priority**: Promotional wallets used before paid wallets
### 3. Offline Payments (`OFFLINE`)
**Manually recorded payments for enterprise customers**
* **Processing**: Manual reconciliation required
* **Use Cases**: Wire transfers, checks, cash payments
* **Status**: Immediate success upon manual confirmation
### 4. Payment Link (`PAYMENT_LINK`)
**Customer-initiated payments through hosted checkout**
* **Processing**: Redirects to external payment page
* **Features**: Supports 3D Secure, multiple payment methods
* **Integration**: Webhook-based status updates
## API Reference
### Create Subscription with Payment Configuration
**Endpoint**: `POST /v1/subscriptions`
**Request**:
```json theme={null}
{
"customer_id": "cust_123",
"plan_id": "plan_456",
"collection_method": "send_invoice",
"payment_behavior": "default_active",
"gateway_payment_method_id": "pm_789"
}
```
**Parameters**:
* `collection_method`: `"charge_automatically"` | `"send_invoice"`
* `payment_behavior`: `"allow_incomplete"` | `"error_if_incomplete"` | `"default_active"` | `"default_incomplete"`
* `gateway_payment_method_id` (optional): Override default payment method
## Payment Method Strategy
* **Primary**: Configure card payments for automatic billing
* **Fallback**: Set up wallet payments for partial payment scenarios
* **Enterprise**: Enable offline payments for wire transfers
* **Customer Choice**: Offer payment links for flexible payment options
## Troubleshooting
### Common Issues
1. **Payment Declined**: Verify customer's payment method and billing details
2. **Wallet Restrictions**: Check wallet `allowed_price_types` match invoice charges
3. **Subscription Not Activating**: Verify payment behavior allows activation on failure
4. **Partial Payments**: Check flow type and behavior combination in decision matrix
For complete API documentation, see:
* [Create Subscription API](/api-reference/subscriptions/create-subscription)
* [Attempt Payment API](/api-reference/invoices/attempt-invoice-payment)
# Reservation / Commitment Discount
Source: https://docs.flexprice.io/docs/subscriptions/reservation-discount
Reservation discount (also called **commitment discount**) lets you guarantee a minimum spend or usage level on any metered charge, charge a premium for usage above that level, and optionally enforce the minimum even when actual usage falls short all configurable per charge, per subscription.
### Who is it for?
* **Cloud infrastructure** — GPU/CPU reservations, storage commitments
* **AI / ML platforms** — inference-hour commitments, training-job minimums
* **SaaS & API providers** — API-call or data-transfer commitments
***
## Key Terminology
| | |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Commitment** | The guaranteed minimum a customer agrees to for a billing period. Can be expressed as a currency amount (e.g. \$1,000/month) or a unit quantity (e.g. 500 GPU-hours/month). |
| **Overage** | Usage above the commitment. Charged at **unit price × overage factor** — the factor can add a premium (1.5×), keep the same rate (1.0×), or even discount excess (0.8×). |
| **Overage Factor** | A multiplier applied to the excess cost beyond the commitment. 1.5× = 50% premium, 1.0× = no premium, 0.8× = 20% discount on excess. |
| **True-Up** | When enabled, the customer always pays at least the full commitment amount — even if actual usage is lower. The shortfall is added as a true-up fee. When disabled, the customer pays only for what they used. |
| **Commitment Type: Amount** | Customer commits to a minimum **amount** per period. |
| **Commitment Type: Quantity** | Customer commits to a minimum **number of units** per period. The commitment cost is derived as quantity × unit price. |
***
## How Billing Works
For each billing period, Flexprice evaluates one of two outcomes:
```mermaid theme={null}
flowchart TD
A[Usage Reported] --> B{Usage ≥ Commitment?}
B -- Yes --> C[Charge: Commitment + Overage]
B -- No --> D{True-Up Enabled?}
D -- Yes --> E[Charge: Full Commitment Amount]
D -- No --> F[Charge: Actual Usage Only]
```
* **Usage ≥ Commitment →** Total = Commitment + (Usage Cost − Commitment) × Overage Factor. The customer pays the committed amount at standard rate, plus a premium on everything above it.
* **Usage \< Commitment, True-Up enabled →** Customer pays the full commitment amount (shortfall added as true-up fee).
* **Usage \< Commitment, True-Up disabled →** Customer pays only for actual usage.
***
## Example 1 — AI Inference Platform (Per-Period Commitment)
An AI platform sells inference compute at **\$2 / vCPU-hour**. They want customers to commit to a minimum of **500 vCPU-hours/month** and charge a **1.5× premium** on any usage above that.
| Parameter | Value |
| -------------- | -------------------------------------- |
| Metered charge | vCPU-hours |
| Unit price | \$2 / vCPU-hour |
| Commitment | 500 vCPU-hours/month (= \$1,000/month) |
| Overage factor | 1.5× |
| True-up | Enabled |
The vCPU-hours line item is configured with a unit price of \$2 / vCPU-hour in the pricing plan.
Navigate to **Customers** → Select a customer → **Add Subscription**, and select your pricing plan. Once the plan is loaded, click the three-dot menu (`...`) next to the vCPU-hours charge and select **Configure Commitment**.
In the configuration modal, set the commitment type to **Quantity** with a value of **500**, apply an overage factor of **1.5x**, and enable **True Up**. Save the commitment and proceed to create the subscription.
```
Overage qty: 700 − 500 = 200 vCPU-hours
Commitment: 500 vCPU-hours × $2 = $1,000
Overage: 200 vCPU-hours × ($2 × 1.5) = $600
Total Invoice: $1,000 + $600 = $1,600
```
True-up is enabled so the full commitment is enforced.
```
Shortfall qty: 500 − 300 = 200 vCPU-hours
Usage cost: 300 vCPU-hours × $2 = $600
True-up fee: 200 vCPU-hours × $2 = $400
Total Invoice: $600 + $400 = $1,000
```
Same usage but with true-up turned off. The customer pays only for what they consumed.
```
Usage cost: 300 vCPU-hours × $2 = $600
Total Invoice: = $600
```
***
## Example 2 — Cloud Infra Company (Windowed Commitment)
A cloud infrastructure company sells GPU compute at **\$2 / GPU-hour**. Instead of a single monthly commitment, they want to commit customers on an **hourly basis** — a minimum of **10 GPU-hours every hour**. Each hour settles independently and the totals are summed at the end of the billing period.
Windowed commitment is available only on **bucketed** features. The window size is inherited from the meter's bucket configuration.
| Parameter | Value |
| -------------- | ----------------------------------- |
| Metered charge | GPU-hours |
| Unit price | \$2 / GPU-hour |
| Commitment | 10 GPU-hours per hour (= \$20/hour) |
| Overage factor | 1.5× |
| True-up | Enabled |
| Window | Hourly |
The GPU-hours line item is configured with a unit price of \$2 / GPU-hour in the pricing plan.
Navigate to **Customers** → Select a customer → **Add Subscription**, and select your pricing plan. Once the plan is loaded, click the three-dot menu (`...`) next to the GPU-hours charge and select **Configure Commitment**.
In the configuration modal, set the commitment type to **Quantity** with a value of **10**. Apply a **1.5x** overage factor, enable both **True Up** and **Windowed Commitment**, and save your changes before creating the subscription.
```
Overage qty: 15 − 10 = 5 GPU-hours
Commitment: 10 GPU-hours × $2 = $20
Overage: 5 GPU-hours × ($2 × 1.5) = $15
Hour 1 charge: $20 + $15 = $35
```
```
Shortfall qty: 10 − 6 = 4 GPU-hours
Usage cost: 6 GPU-hours × $2 = $12
True-up fee: 4 GPU-hours × $2 = $8
Hour 2 charge: $12 + $8 = $20
```
```
Usage cost: 10 GPU-hours × $2 = $20
Hour 3 charge: = $20
```
```
Total: $35 + $20 + $20 = $75
```
***
Reservation discount applies per-charge commitment and overage. For a single minimum spend across the **entire subscription**, see [Commitment](/docs/subscriptions/commitment/overview).
# Seat-Based Pricing
Source: https://docs.flexprice.io/docs/subscriptions/seat-based-pricing
Bill customers per user, seat, or license. Set quantity at subscription creation and adjust it at any time with optional future-dating.
Seat-based pricing lets you charge customers based on the number of seats (users, licenses, or any countable unit) they provision. Set the initial seat count when creating a subscription, then increase or decrease it at any time - immediately or on a future date - using the modification API.
## When to Use
* SaaS products that charge per user (e.g. \$20/user/month)
* License-based billing (editor seats, API keys, agent instances)
* Hybrid plans: a fixed base fee plus per-seat charges
* Enterprise contracts with minimum seat commitments
If charges are driven by events (API calls, compute time, etc.), use [Event Ingestion](/docs/event-ingestion/overview) instead. Seat-based billing is for known, provisioned quantities, not metered consumption.
## How It Works
Seat-based billing has three stages:
1. **Configure a per-seat price** on a plan - a fixed price with `billing_model: FLAT_FEE` and a per-unit `amount`.
2. **Create a subscription** and set the initial seat count via `line_items[].quantity` (or `override_line_items[].quantity` for a customer-specific count).
3. **Adjust seat count** mid-subscription using the modification API with an optional `effective_date`.
The billing engine multiplies `amount x quantity` when generating invoice line items each billing period.
## Step 1: Configure a Per-Seat Price on Your Plan
A per-seat price is a fixed price with `FLAT_FEE` billing model. The `amount` is the cost per seat per billing period.
```json theme={null}
POST /prices
{
"entity_type": "plan",
"entity_id": "plan_01JVKM3P9YQRS5GHTU0MCXA4T",
"type": "FIXED",
"currency": "usd",
"amount": "20.00",
"billing_model": "FLAT_FEE",
"billing_period": "MONTHLY",
"billing_period_count": 1,
"invoice_cadence": "ADVANCE",
"description": "Per seat, billed monthly",
"lookup_key": "seat_monthly"
}
```
| Field | Value | Notes |
| ---------------------- | ----------------------------- | ------------------------------------------------------------------------ |
| `entity_type` | `"plan"` | Attaches this price to a plan |
| `entity_id` | plan ID | The plan this price belongs to |
| `type` | `"FIXED"` | Fixed charge (not metered). Use `"USAGE"` for metered prices |
| `amount` | `"20.00"` | Cost per seat per billing period |
| `billing_model` | `"FLAT_FEE"` | Multiplies amount x quantity |
| `billing_period` | `"MONTHLY"` / `"ANNUAL"` etc. | How often the seat charge recurs |
| `billing_period_count` | `1` | Number of periods per cycle. `1` with `MONTHLY` means charge every month |
| `invoice_cadence` | `"ADVANCE"` | Bill at the start of each period (typical for seats) |
**Package pricing for seat buckets:** If you charge per block of seats (e.g. \$100 per 5 seats), use `billing_model: PACKAGE` and set `transform_quantity.divide_by: 5`. The billing engine divides the raw quantity by 5 before applying the price.
## Step 2: Create a Subscription with an Initial Seat Count
Set the seat count in `line_items` when creating the subscription. Reference the price by its `price_id` and pass the initial quantity.
```json theme={null}
POST /subscriptions
{
"customer_id": "cust_01JVKM2N8XPQR4FGHT9LBWZ3S",
"plan_id": "plan_01JVKM3P9YQRS5GHTU0MCXA4T",
"currency": "usd",
"billing_cadence": "RECURRING",
"billing_period": "MONTHLY",
"billing_cycle": "anniversary",
"line_items": [
{
"price_id": "price_01JVKM4Q0ZRST6HITUV1NDYB5",
"quantity": "25"
}
]
}
```
The response includes the created subscription with `line_items[].id`. Save this ID - you will need it in Step 3 to modify seat count.
```json theme={null}
{
"id": "sub_01JVKM6S2BTUV8JKVWX3PFAD7",
"status": "ACTIVE",
"customer_id": "cust_01JVKM2N8XPQR4FGHT9LBWZ3S",
"plan_id": "plan_01JVKM3P9YQRS5GHTU0MCXA4T",
"line_items": [
{
"id": "li_01JVKM5R1ASTU7IJUVW2OEZC6",
"price_id": "price_01JVKM4Q0ZRST6HITUV1NDYB5",
"quantity": "25",
"price_type": "FIXED",
"start_date": "2026-06-01T00:00:00Z",
"end_date": null
}
]
}
```
This creates a subscription where 25 seats x $20 = **$500/month\*\* is charged at the start of each period.
### Customer-Specific Seat Count (Override)
For enterprise customers with a negotiated seat count, use `override_line_items` instead. The plan stays unchanged - only this subscription uses the overridden quantity.
```json theme={null}
POST /subscriptions
{
"customer_id": "cust_01JVKN3P0ZRST7IJUVW3OEZD7",
"plan_id": "plan_01JVKM3P9YQRS5GHTU0MCXA4T",
"currency": "usd",
"billing_cadence": "RECURRING",
"billing_period": "ANNUAL",
"billing_cycle": "anniversary",
"override_line_items": [
{
"price_id": "price_01JVKM4Q0ZRST6HITUV1NDYB5",
"quantity": "500"
}
]
}
```
`quantity` cannot be set on usage-based (metered) prices. It is only valid on fixed prices with `FLAT_FEE`, `PACKAGE`, or `TIERED` billing models.
## Step 3: Adjust Seat Count on a Live Subscription
Use the subscription modification API to change the seat count after the subscription is active. You can apply the change immediately or schedule it for a future date.
### 3a. Find the Line Item ID
The modification API takes a line item ID (`li_...`), not a price ID. The `line_items[].id` is returned when you create the subscription (see Step 2) or when you fetch it:
```
GET /subscriptions/sub_01JVKM6S2BTUV8JKVWX3PFAD7
```
Match the line item to the right price via `line_items[].price_id`.
### 3b. Preview the Change (Recommended)
Before executing, preview the billing impact:
```json theme={null}
POST /subscriptions/sub_01JVKM6S2BTUV8JKVWX3PFAD7/modify/preview
{
"type": "quantity_change",
"quantity_change_params": {
"line_items": [
{
"id": "li_01JVKM5R1ASTU7IJUVW2OEZC6",
"quantity": "40"
}
]
}
}
```
The UI preview shows the old and new line item periods and confirms a proration invoice will be generated. The API returns the same information in `changed_resources`:
```json theme={null}
{
"subscription": { "id": "sub_01JVKM6S2BTUV8JKVWX3PFAD7", "..." : "..." },
"changed_resources": {
"line_items": [
{
"id": "li_01JVKM5R1ASTU7IJUVW2OEZC6",
"price_id": "price_01JVKM4Q0ZRST6HITUV1NDYB5",
"quantity": "25",
"start_date": "2026-06-01T00:00:00Z",
"end_date": "2026-06-10T14:32:00Z",
"change_action": "ended"
},
{
"id": "li_01JVKN7T3CUVW9KLVWX4QGBE8",
"price_id": "price_01JVKM4Q0ZRST6HITUV1NDYB5",
"quantity": "40",
"start_date": "2026-06-10T14:32:00Z",
"end_date": null,
"change_action": "created"
}
],
"invoices": [
{
"id": "inv_01JVKN8U4DVWX0LMWXY5RHCF9",
"action": "created",
"status": "preview",
"invoice": {
"amount_due": "203.23",
"currency": "usd"
}
}
]
}
}
```
Nothing is billed until you call the execute endpoint. The `status: "preview"` on the invoice confirms this is a dry run.
### 3c. Execute an Immediate Seat Change
Omit `effective_date` to apply the change right now:
```json theme={null}
POST /subscriptions/sub_01JVKM6S2BTUV8JKVWX3PFAD7/modify/execute
{
"type": "quantity_change",
"quantity_change_params": {
"line_items": [
{
"id": "li_01JVKM5R1ASTU7IJUVW2OEZC6",
"quantity": "40"
}
]
}
}
```
The current line item is terminated immediately and a new one starts with `quantity: 40`. The response has the same shape as the preview but with `status: "issued"` on the invoice, confirming the charge was generated:
```json theme={null}
{
"subscription": { "id": "sub_01JVKM6S2BTUV8JKVWX3PFAD7", "..." : "..." },
"changed_resources": {
"line_items": [
{
"id": "li_01JVKM5R1ASTU7IJUVW2OEZC6",
"price_id": "price_01JVKM4Q0ZRST6HITUV1NDYB5",
"quantity": "25",
"start_date": "2026-06-01T00:00:00Z",
"end_date": "2026-06-10T14:32:00Z",
"change_action": "ended"
},
{
"id": "li_01JVKN7T3CUVW9KLVWX4QGBE8",
"price_id": "price_01JVKM4Q0ZRST6HITUV1NDYB5",
"quantity": "40",
"start_date": "2026-06-10T14:32:00Z",
"end_date": null,
"change_action": "created"
}
],
"invoices": [
{
"id": "inv_01JVKN8U4DVWX0LMWXY5RHCF9",
"action": "created",
"status": "issued",
"invoice": {
"amount_due": "203.23",
"currency": "usd"
}
}
]
}
}
```
### 3d. Schedule a Future-Dated Seat Change
Pass an `effective_date` to defer the change:
```json theme={null}
POST /subscriptions/sub_01JVKM6S2BTUV8JKVWX3PFAD7/modify/execute
{
"type": "quantity_change",
"quantity_change_params": {
"line_items": [
{
"id": "li_01JVKM5R1ASTU7IJUVW2OEZC6",
"quantity": "40",
"effective_date": "2026-07-01T00:00:00Z"
}
]
}
}
```
What happens internally:
1. The current line item's `end_date` is set to `2026-07-01T00:00:00Z`
2. A new line item is created with `start_date: 2026-07-01T00:00:00Z` and `quantity: 40`
3. Billing continues at the old quantity until July 1, then switches to 40 seats
Use this when a customer confirms they'll add seats at the start of next month and you don't want a mid-period invoice now.
## Proration on Seat Changes
When you change seat count mid-period, Flexprice prorates based on days remaining in the current billing period.
### Upgrade (Adding Seats): Mid-Month Example
**Before change:** 25 seats x $20 = $500/month, billed on the 1st.
**Change on day 10:** Increase to 40 seats.
**Days remaining:** 21 of 31.
| Line item | Calculation | Amount |
| ------------------------------------- | --------------- | ------------ |
| Credit for unused days at 25 seats | (21/31) x \$500 | -\$338.71 |
| Charge for remaining days at 40 seats | (21/31) x \$800 | +\$541.94 |
| **Net invoice** | | **\$203.23** |
### Downgrade (Removing Seats): Mid-Month Example
**Before change:** 40 seats x $20 = $800/month, billed on the 1st.
**Change on day 10:** Decrease to 25 seats.
**Days remaining:** 21 of 31.
| Line item | Calculation | Amount |
| ------------------------------------- | --------------- | ------------- |
| Credit for unused days at 40 seats | (21/31) x \$800 | -\$541.94 |
| Charge for remaining days at 25 seats | (21/31) x \$500 | +\$338.71 |
| **Net** | | **-\$203.23** |
What gets issued depends on the price's `invoice_cadence`:
* **`ADVANCE`** (billed at period start): the customer was already charged for the full period, so a **refund** of \$203.23 is issued.
* **`ARREAR`** (billed at period end): nothing has been charged yet, so a **one-off invoice** for $338.71 (the remaining days at 25 seats) is generated immediately and the $541.94 credit offsets the end-of-period invoice.
### Skip Proration: Change at Period End
To avoid a mid-period invoice entirely, set `effective_date` to the start of the next billing period. The old seat count bills through the end of the current period and the new count takes effect at renewal.
## Seat-Based Billing in the UI
You can manage seat counts from the Subscription detail page in the Flexprice dashboard without touching the API.
**Viewing seat history:** The subscription timeline shows every quantity change with its effective date and the invoice it triggered.
**Viewing the prorated invoice:** When a mid-period seat change generates an invoice, the invoice detail shows two line items: the credit for the old quantity and the charge for the new quantity.
## Common Patterns
| Situation | Approach |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Set initial seat count at subscription creation | `line_items[].quantity` in `POST /subscriptions` |
| Override seat count for one enterprise customer | `override_line_items[].quantity` in `POST /subscriptions` |
| Increase seats immediately with proration | `POST /subscriptions/{id}/modify/execute` without `effective_date` |
| Decrease seats at the end of the current period | `POST /subscriptions/{id}/modify/execute` with `effective_date` = start of next period |
| Preview billing impact before changing seats | `POST /subscriptions/{id}/modify/preview` |
| Change seats and also change the plan tier | Use [Upgrade & Downgrade](/docs/subscriptions/upgrade-and-downgrade) |
| Give one customer a different per-seat rate | Use [Plan Price Overrides](/docs/subscriptions/plan-price-overrides) with an `amount` override |
| Track seat usage via events (identity-linked) | Use a metered feature with `COUNT_UNIQUE` aggregation - see [Features](/docs/product-catalogue/features/overview) |
## Edge Cases & Gotchas
**`quantity` lives on the line item, not the subscription.**
There is no top-level `quantity` field on a subscription object. It lives at `subscription.line_items[].quantity`. If your plan has multiple prices (e.g. a base fee plus a per-seat charge), only the seat price's line item needs a custom quantity. Leave the base fee line item at `quantity: 1`.
**You need the line item ID, not the price ID.**
The modification API takes `line_items[].id` (format: `li_01...`), not `price_id`. Fetch the subscription first with `GET /subscriptions/{id}` to find it.
**Usage-based prices reject quantity.**
Setting `quantity` on a metered price line item returns an error. Usage is computed from ingested events, not a quantity field.
**Future-dated changes produce two line item records.**
Between the request and `effective_date`, two records exist for the same price: the old one ending at `effective_date` and the new one starting at it. Only one is active at any point in time. Do not delete or modify the old record before the effective date fires.
**Multiple seat changes in one period are fine.**
Each change prorates against the state at the time of the call. The billing engine generates a separate invoice per change.
**Setting quantity to zero stops billing for that line item.**
The remaining period's charge is credited in full. Use with care.
## Related Topics
* [Creating Subscriptions](/docs/subscriptions/customers-create-subscription)
* [Plan Price Overrides](/docs/subscriptions/plan-price-overrides)
* [Subscription Line Item Overrides](/docs/subscriptions/subscription-line-item-overrides)
* [Understanding Proration](/docs/subscriptions/understanding-proration)
* [Upgrade & Downgrade](/docs/subscriptions/upgrade-and-downgrade)
* [Plan Pricing Models](/docs/product-catalogue/plans/pricing)
# Subscription Line Item Overrides
Source: https://docs.flexprice.io/docs/subscriptions/subscription-line-item-overrides
Add, update, or remove individual charges on a live subscription, and push plan pricing changes to existing subscribers using the price sync workflow.
A subscription's line items are the actual billing records — one per price. After a subscription is live, you can add new line items, update existing ones, remove them, or push plan-level pricing changes to all subscribers using the price sync workflow.
## What Is a Line Item?
Each line item on a subscription:
* References one price (`price_id`)
* Has its own `start_date` and `end_date`
* Is what the billing engine uses to generate invoice charges
* Can point to a plan price (standard) or a subscription-scoped price (overridden)
## Adding a Line Item
Use this when you want to add a charge that wasn't part of the original subscription — for example, an add-on price or a usage meter you've just enabled.
```
POST /subscriptions/{id}/line-items
```
```json theme={null}
{
"price_id": "price_new_feature",
"quantity": "1.0",
"start_date": "2026-04-01T00:00:00Z"
}
```
If the price is usage-based, `quantity` is automatically set to `0` — usage is computed from metered events.
## Updating a Line Item
Use this to change the pricing or quantity of an existing line item.
```
PATCH /subscriptions/{id}/line-items/{line_item_id}
```
When you change pricing fields (amount, tiers, billing model), the system:
1. Terminates the old line item at `effective_from`
2. Creates a new subscription-scoped price with the updated values
3. Creates a new line item starting at `effective_from`
When you only change metadata or commitment fields, the line item is updated in-place — no new price is created.
## Removing a Line Item
Use this to stop billing for a specific charge.
```
DELETE /subscriptions/{id}/line-items/{line_item_id}
```
```json theme={null}
{
"effective_from": "2026-04-01T00:00:00Z"
}
```
This sets `end_date` on the line item. The line item record is retained for billing history. Billing stops at the effective date.
## Pushing Plan Price Changes to Existing Subscriptions (Price Sync)
When you update a plan's prices, existing subscriptions are **not automatically updated**. The price sync workflow propagates plan changes to all active subscribers on a plan.
### What Sync Does
**Phase 1 — Terminate expired line items:**
Finds line items for plan prices that have an `end_date` in the past. Sets `line_item.end_date = price.end_date`. Those charges stop billing.
**Phase 2 — Create missing line items:**
Finds plan prices that do not have a corresponding line item on each active subscription. Creates the missing line items with values from the plan price.
| Field set by sync | Value |
| ---------------------- | ------------------------------------ |
| `price_id` | Plan price ID |
| `quantity` | `0` for usage prices; `1` for fixed |
| `start_date` | `price.start_date` (from plan price) |
| `end_date` | `price.end_date` (from plan price) |
| `metadata["added_by"]` | `"plan_sync_api"` |
**What sync does NOT do:**
* Does not update line items that already exist
* Does not overwrite line items with overridden (subscription-scoped) prices
* Does not delete line items for plan prices that were removed (set `price.end_date` to stop billing for removed prices)
### Recommended Sequence for Plan Pricing Changes
**Step 1 — Update plan prices**
```
POST /plans/{id}/prices ← add new price
PUT /plans/{id}/prices/{id} ← update existing price
```
**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. Wait for it before triggering a new one.
**Step 3 — Trigger sync**
```
POST /plans/{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. Check `status` in the response.
**Step 5 — Handle failure**
On `Failed`, call `GET /workflows/{workflow_id}/{run_id}` to review the error context and re-trigger after fixing the root cause.
### Workflow Status Values
| Status | Meaning |
| ------------ | ----------------------------------- |
| `Running` | Currently executing |
| `Completed` | Finished successfully |
| `Failed` | Terminated with an error |
| `Canceled` | Manually canceled |
| `Terminated` | Force-terminated |
| `TimedOut` | Exceeded the 1-hour timeout |
| `Unknown` | Status not yet synced from Temporal |
## Date Handling
### Line Item Start Date
```
line_item.start_date = max(
subscription.start_date,
price.start_date (if set),
request.start_date (if set)
)
```
The latest of the three applies. Timestamps are stored in UTC, truncated to milliseconds.
### Line Item End Date
```
line_item.end_date = request.end_date (if provided)
= subscription.end_date (if subscription has one)
= nil (open-ended)
```
### When Sync Creates Line Items
Sync uses the plan price's dates directly: `start_date = price.start_date`, `end_date = price.end_date`. If the plan price has no dates, the created line item also has no dates (open-ended).
### All Combinations
| Scenario | `line_item.start_date` | `line_item.end_date` |
| -------------------------------------- | -------------------------- | ------------------------------ |
| Price has no start/end dates | `subscription.start_date` | `subscription.end_date` or nil |
| `price.start_date` \< `sub.start_date` | `sub.start_date` | `sub.end_date` or nil |
| `price.start_date` > `sub.start_date` | `price.start_date` | `sub.end_date` or nil |
| `price.end_date` set | per above | `price.end_date` (via sync) |
| Request includes `start_date` | `max(sub, price, request)` | per above |
### Validation
* `line_item.start_date >= subscription.start_date`
* `line_item.end_date <= subscription.end_date` (when subscription has one)
* `line_item.start_date <= line_item.end_date` (when both set)
## Decision Guide
| Situation | Right Approach |
| ----------------------------------------------------------------- | --------------------------------------------------------------- |
| Give one customer a different rate at subscription creation | [Plan price override](/docs/subscriptions/plan-price-overrides) |
| Change pricing for all subscribers on a plan | Update plan price → run price sync |
| Add a new charge to one live subscription | `POST /subscriptions/{id}/line-items` |
| Add a new price to a plan and push to all subscribers | Add price to plan → run price sync |
| Remove a charge from one subscription | `DELETE /subscriptions/{id}/line-items/{id}` |
| Remove a price from a plan and stop billing everyone | Set `price.end_date` → run price sync |
| A subscriber has an override — push global pricing update to them | Update their line item directly (sync won't touch overrides) |
| Change the amount on one subscription's charge | Update line item with new amount |
## Edge Cases & Gotchas
**Overrides survive sync.**
Sync only creates items for missing `(subscription_id, price_id)` pairs. An overridden line item already exists (pointing to a subscription-scoped price), so sync never replaces it.
**Removing a plan price does not stop billing automatically.**
Sync has no delete phase. To stop billing for a plan price, set `price.end_date`. The sync termination phase will then close the line items.
**Prices without dates don't expire.**
If a plan price has no `start_date` or `end_date`, line items created by sync have no date bounds and remain active until the subscription ends or you manually terminate them.
**Concurrent sync protection.**
The sync API uses a Redis distributed lock (2-hour TTL). Always check for a running workflow before triggering to avoid redundant runs.
**Usage prices always get quantity = 0.**
Setting `quantity` on a usage-based price override is rejected. Usage is computed from ingested events, not the line item quantity.
## Related Topics
* [Plan Price Overrides](/docs/subscriptions/plan-price-overrides)
* [Creating Subscriptions](/docs/subscriptions/customers-create-subscription)
* [Plan Pricing](/docs/product-catalogue/plans/pricing)
* [Event Ingestion](/docs/event-ingestion/overview)
* [Pause & Resume Workflows](/docs/subscriptions/workflows/pause)
# Understanding Proration
Source: https://docs.flexprice.io/docs/subscriptions/understanding-proration
Learn how proration works when changing subscription plans and how it affects your billing
## What is Proration?
Proration is the process of calculating proportional charges and credits when you change your subscription plan mid-billing period. It ensures you only pay for what you use and get credited for unused time.
## How Proration Works
### Basic Concept
When you change plans, Flexprice:
1. **Calculates unused time** on your current plan
2. **Credits the unused portion** back to your account
3. **Charges for the new plan** from the change date
4. **Generates a single invoice** with the net amount
### Example Scenario
You have a monthly plan that costs `$100/month`, and you want to upgrade to a `$200/month` plan on day 15 of your billing cycle:
* **Days used**: 15 days
* **Days remaining**: 15 days
* **Credit for unused time**: `$50` (50% of `$100`)
* **Charge for new plan**: `$100` (50% of `$200`)
* **Net amount due**: \$50
## Proration Calculations
### Time-Based Proration
Proration is calculated based on the time remaining in your current billing period:
```
Credit Amount = (Days Remaining / Total Days) × Current Plan Price
Charge Amount = (Days Remaining / Total Days) × New Plan Price
Net Amount = Charge Amount - Credit Amount
```
### Billing Period Considerations
* **Monthly Plans**: Proration based on days in the month
* **Quarterly Plans**: Proration based on days in the quarter
* **Yearly Plans**: Proration based on days in the year
* **Leap Years**: February 29th is included in calculations
## Proration Examples
### Example 1: Monthly Upgrade
**Current Plan**: Basic Monthly (`$50`/month)
**New Plan**: Premium Monthly (`$100`/month)
**Change Date**: Day 10 of 30-day month
**Calculation:**
* Days used: 10
* Days remaining: 20
* Credit: (20/30) × $50 = $33.33
* Charge: (20/30) × $100 = $66.67
* Net amount: $66.67 - $33.33 = \$33.34
### Example 2: Quarterly Downgrade
**Current Plan**: Premium Quarterly (`$300`/quarter)
**New Plan**: Basic Quarterly (`$150`/quarter)
**Change Date**: Day 45 of 90-day quarter
**Calculation:**
* Days used: 45
* Days remaining: 45
* Credit: (45/90) × $300 = $150.00
* Charge: (45/90) × $150 = $75.00
* Net amount: $75.00 - $150.00 = -\$75.00 (credit)
### Example 3: Yearly Plan Change
**Current Plan**: Basic Yearly (`$600`/year)
**New Plan**: Premium Yearly (`$1200`/year)
**Change Date**: Day 100 of 365-day year
**Calculation:**
* Days used: 100
* Days remaining: 265
* Credit: (265/365) × $600 = $435.62
* Charge: (265/365) × $1200 = $871.23
* Net amount: $871.23 - $435.62 = \$435.61
# Upgrade & Downgrade
Source: https://docs.flexprice.io/docs/subscriptions/upgrade-and-downgrade
Learn how to upgrade or downgrade subscription plans with automatic proration and billing adjustments
## Overview
Flexprice allows you to change subscription plans seamlessly with automatic proration calculations, billing adjustments, and invoice generation. Whether you're upgrading to a higher tier or downgrading to a more cost-effective plan, the system handles all the complex billing logic for you.
## Key Features
* **Automatic Proration**: Calculate credits and charges for unused time
* **Immediate Billing**: Generate invoices for plan changes instantly
* **Preview Changes**: See the impact before making changes
* **Flexible Billing**: Support for different billing cycles and periods
* **Audit Trail**: Complete history of all subscription changes
## How It Works
### The Process
1. **Preview the Change**: Use the preview endpoint to see the financial impact
2. **Execute the Change**: Confirm the change to apply it immediately
3. **Automatic Processing**: The system handles proration, billing, and invoice generation
4. **Subscription Update**: Your subscription is updated to the new plan
### API Endpoints
* [**Preview Change**](/api-reference/subscriptions/preview-subscription-plan-change) - See the financial impact before making changes
* [**Execute Change**](/api-reference/subscriptions/execute-subscription-plan-change) - Apply the subscription plan change
## API Reference
### 1. Preview Subscription Plan Change
**Endpoint:** `POST /subscriptions/{id}/change/preview`
Preview the financial impact of changing a subscription plan before making the actual change.
#### Request Fields
| Field | Type | Required | Description |
| ---------------------- | ------- | -------- | ------------------------------------------------------------------------------------- |
| `target_plan_id` | string | ✅ | The ID of the new plan you want to change to |
| `billing_cadence` | enum | ✅ | How often billing occurs: `RECURRING` or `ONETIME` |
| `billing_cycle` | enum | ✅ | When billing cycles start: `anniversary` or `calendar` |
| `billing_period` | enum | ✅ | Billing frequency: `MONTHLY`, `ANNUAL`, `WEEKLY`, `DAILY`, `QUARTERLY`, `HALF_YEARLY` |
| `billing_period_count` | integer | ✅ | Number of billing periods for the new subscription |
| `proration_behavior` | enum | ✅ | How to handle proration: `create_prorations` (default) or `none` |
| `metadata` | object | ❌ | Additional key-value pairs for storing extra information |
### 2. Execute Subscription Plan Change
**Endpoint:** `POST /subscriptions/{id}/change/execute`
Execute the subscription plan change after previewing the impact.
#### Request Fields
Uses the same `SubscriptionChangeRequest` as the preview endpoint.
### Field Explanations
#### Billing Configuration
* **`billing_cadence`**: Whether this is a one-time charge (`ONETIME`) or recurring billing (`RECURRING`)
* **`billing_cycle`**:
* `anniversary`: Billing starts from the subscription start date
* `calendar`: Billing aligns with calendar periods (e.g., monthly on the 1st)
* **`billing_period`**: How often the customer is billed (daily, weekly, monthly, etc.)
* **`billing_period_count`**: How many periods the subscription lasts (e.g., 12 for annual)
#### Proration Behavior
* **`create_prorations`**: Calculate and apply credits/charges for unused time
* **`none`**: No proration
### What Happens During a Plan Change
When you change your subscription plan, Flexprice:
1. **Archives the old subscription** and marks it as cancelled
2. **Calculates proration** for unused time on the current plan
3. **Creates a new subscription** with the target plan
4. **Generates an invoice** with credits and charges
# View subscription
Source: https://docs.flexprice.io/docs/subscriptions/view
View a customer’s subscription details, active plans, and upcoming invoices.
Once a customer is assigned a subscription, you can view their active plans and upcoming invoices.
* In **Customer Management > Customers**, select the customer profile.
* The **Subscriptions** section lists all active plans assigned to the customer.
* Clicking on a plan will open the **Subscription Details** page.
* Here, you can view:
* **Plan Name**
* **Billing Period**
* **Subscription Status**
* **Start Date**
* **Upcoming Invoice**
* If the customer is on a usage-based plan, you can refresh the invoice to see real-time calculations based on their latest consumption.
**View Upcoming Invoices**
* Whenever a customer starts using usage-based features, any additional charges associated with it will reflect in the upcoming invoice.
* Click the **Refresh Icon** to update and view real-time billing calculations.
# Pause subscription
Source: https://docs.flexprice.io/docs/subscriptions/workflows/pause
There are scenarios where customers may need to temporarily **pause their subscription** instead of canceling it. This feature ensures they are not billed during inactivity while allowing for a seamless reactivation when needed. Common use cases include:
* **Seasonal Usage**: A customer only requires the service at certain times of the year.
* **Budget Constraints**: A temporary pause allows cost control without losing their setup.
* **Operational Downtime**: Businesses undergoing internal transitions can pause subscriptions until they resume operations.
* **Trial Extension**: Giving users extra time before fully committing to a plan.
With Flexprice, pausing a subscription **stops all billing for the selected period** while keeping the account intact.
**Steps to Pause a Subscription:**
* Select the customer whose subscription needs to be paused from Customers tab
* Under the Subscriptions section, find the active subscription.
* Click on the Options Menu (⋮) next to the subscription.
* Choose Pause Subscription from the dropdown.
* Select the Pause Start Date and enter the number of days to pause the subscription.
* A confirmation message will summarize the pause period.
* Click Schedule Pause to apply changes.
Once paused, the subscription status updates to **Paused**, and the customer will not be charged until the subscription resumes.
**Steps to Resume a Subscription:**
A paused subscription can either **automatically resume** after the scheduled pause period or can be **resumed manually** at any time before the scheduled resume date.
* Under **Subscriptions**, locate the paused subscription.
* Click on the **Options Menu (⋮)** next to the subscription.
* Select "Resume Subscription”
* A confirmation message appears, indicating that resuming will **restart the billing cycle**.
* Click **Yes, Resume** to reactivate the subscription.
Once resumed, billing will continue based on the subscription’s original schedule.
**Points to consider:**
* Billing automatically resumes once the pause period ends unless manually resumed earlier.
* For usage-based subscriptions, any usage before pausing will still be billed.
* Customers using advance payment plans will be charged immediately upon resuming the subscription.
# Trialing
Source: https://docs.flexprice.io/docs/subscriptions/workflows/trialing
Offer free trial periods on subscriptions before billing begins
## Overview
A **trial** is a window where customers are on the paid plan schedule but **billing has not begun** yet in the recurring sense described below.
**Where `trial_period_days` comes from**
* Pass **`trial_period_days`** on **`POST /v1/subscriptions`** to override the plan for that subscription only.
* **Or omit it.** Every recurring **fixed** price on the linked plan contributes the **same** **`trial_period_days`** (otherwise create fails validation).
**Two invoices developers care about**
* **Trial start (`SUBSCRIPTION_TRIAL_START`):** **`$0`**, finalized at subscribe time **with** the same **line-item shape** as real advance billing for **`trial_start` → `trial_end`**, but all amounts forced to **`0`** (preview + integrations). **`collection_method`** controls **`SUCCEEDED`** vs **`PENDING`** here; details in **[Trial-start invoice](#trial-start-invoice)**.
* **Trial end (`SUBSCRIPTION_TRIAL_END`):** first payable billing window **`trial_end`** → **`trial_end` + `billing_period`**. **`payment_behavior`** drives failure handling here; details in **[What happens at trial end](#what-happens-at-trial-end)**.
**Typical reasons to use trials**
* Product-led growth and self-serve upgrade paths
* Freemium-style evaluation before committing
* Time-boxed enterprise or partner pilots
## How It Works
Trial subscriptions move through three statuses:
```text theme={null}
trialing -> (trial_end reached) -> incomplete -> (invoice paid / zero amount) -> active
```
| Status | What it means | Billing |
| ------------ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `trialing` | Trial window is active | **`SUBSCRIPTION_TRIAL_START`**: **`$0`** finalized invoice for **`trial_start` → `trial_end`** (preview line items); subscription **stays** `trialing` |
| `incomplete` | Trial ended; first paid invoice issued | Awaiting **`SUBSCRIPTION_TRIAL_END`** payment (unless amount is **`$0`**) |
| `active` | Trial converted or non-trial start | Normal billing cycle |
**Period fields while trialing**
* **`current_period_start`** = **`trial_start`**
* **`current_period_end`** = **`trial_end`**
After **`trial_end`**, the billing anchor moves to **`trial_end`**. The first **paid** period is **full-length**: **`[trial_end, trial_end + billing_period]`**. There is **no shortened** first billing period after a trial.
**Automation:** trial-end processing is handled by Flexprice workflows. **No manual cron or API** is required for the **`trial_end`** transition itself.
## Trial-start invoice
**Billing reason (API string):** **`SUBSCRIPTION_TRIAL_START`**.
Applies only when the subscription is **created as non-draft** **`trialing`** (trial window > 0 days).
What you get on create:
* **Invoice period:** `period_start` = `trial_start` (same as `start_date`), `period_end` = `trial_end`, matching **`current_period_*`** while **`subscription_status`** is **`trialing`**.
* **Amounts:** **`subtotal`**, **`total`**, **`amount_due`**, and **each line item `amount`** are **`0`** after compute. Quantity, price linkage, and display metadata stay, so customers see **what will charge** later.
* **Subscription status:** Remains **`trialing`**. **`SUBSCRIPTION_TRIAL_START` does not gate activation.** Moving to **`active`** still depends on **`SUBSCRIPTION_TRIAL_END`** (or a non-trial create path).
### `collection_method` and `payment_behavior`
Use these fields deliberately: they clarify how **trial start** differs from **trial end**.
| Topic | Role |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`payment_behavior`** | Applies when generating / paying **`SUBSCRIPTION_TRIAL_END`** and broader subscription invoice payment flows (`default_active`, `allow_incomplete`, …). See **[What happens at trial end](#what-happens-at-trial-end)**. |
| **`collection_method`** | After finalize, **`$0`** trial-start invoices are **`SUCCEEDED`** under **`charge_automatically`** or left **`PENDING`** under **`send_invoice`** so outbound invoice sync can capture a card **without `active`**. |
| `collection_method` | Trial-start invoice `payment_status` (typical) |
| -------------------------- | ----------------------------------------------------------------- |
| **`charge_automatically`** | **`SUCCEEDED`** at **`$0`** |
| **`send_invoice`** | **`PENDING`** until host / PSP completes the synced **`$0`** flow |
On **`POST /v1/subscriptions`**, **`latest_invoice`** is this document when **`subscription_status`** is **`trialing`**.
## Configuring a Trial Period
1. Define **`trial_period_days`** on the plan’s recurring **FIXED** prices **or**
2. Override on **`POST /v1/subscriptions`**.
There is **no `trial_enabled` flag.** If **`trial_period_days` > 0** after resolve (inherit vs override), the subscription enters **`trialing`**; **`0`** at subscription level **disables trial** even if the plan carries trial days.
### Step 1: Set `trial_period_days` on the price
**Constraints:**
* Only **`billing_cadence: RECURRING`** + **`price_type: FIXED`** combinations accept **`trial_period_days`**. Usage-only or unsupported tier geometries fail validation upstream.
* Omitted ⇒ **`0`**. That price contributes **no inherited trial**.
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/prices \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 4900,
"currency": "USD",
"billing_cadence": "RECURRING",
"billing_period": "MONTHLY",
"billing_period_count": 1,
"price_type": "FIXED",
"trial_period_days": 14
}'
```
Response (abbreviated):
```json theme={null}
{
"id": "price_01abc123",
"billing_cadence": "RECURRING",
"price_type": "FIXED",
"trial_period_days": 14
}
```
### Step 2: Create a subscription (trial inherited automatically)
When all recurring fixed prices on a plan share the same `trial_period_days`, that value is inherited by the subscription automatically. No trial fields are required in the subscription request.
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/subscriptions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "cust_01xyz",
"plan_id": "plan_01abc",
"currency": "USD",
"billing_cadence": "RECURRING",
"billing_period": "MONTHLY",
"billing_period_count": 1,
"start_date": "2025-05-01T00:00:00Z"
}'
```
Response (abbreviated; real payloads also include **`plan`**, **`customer`**, line items with full pricing metadata, and other subscription fields):
```json theme={null}
{
"id": "sub_01def456",
"subscription_status": "trialing",
"customer_id": "cust_01xyz",
"plan_id": "plan_01abc",
"trial_start": "2025-05-01T00:00:00Z",
"trial_end": "2025-05-15T00:00:00Z",
"current_period_start": "2025-05-01T00:00:00Z",
"current_period_end": "2025-05-15T00:00:00Z",
"collection_method": "charge_automatically",
"latest_invoice": {
"id": "inv_trial_start_abc",
"billing_reason": "SUBSCRIPTION_TRIAL_START",
"invoice_type": "SUBSCRIPTION",
"invoice_status": "FINALIZED",
"payment_status": "SUCCEEDED",
"currency": "USD",
"period_start": "2025-05-01T00:00:00Z",
"period_end": "2025-05-15T00:00:00Z",
"subtotal": "0",
"total": "0",
"amount_due": "0",
"amount_remaining": "0",
"line_items": [
{
"display_name": "Pro · monthly (trial preview)",
"amount": "0",
"quantity": "1"
}
]
}
}
```
Notes on the abbreviated response:
* **`subscription_status: "trialing"`** is the canonical lifecycle enum; the payload may also expose a separate **`status`** field (`SubscriptionResponse`).
* **`latest_invoice.billing_reason`** is **`SUBSCRIPTION_TRIAL_START`** on this **`$0`** opening invoice. It stays **visible / finalized**, not **`SKIPPED`**, unlike typical zero-amount **`SUBSCRIPTION_CREATE`** invoices.
* If you use **`send_invoice`** instead of **`charge_automatically`**, **`payment_status`** on that same invoice will usually be **`PENDING`** until PSP / outbound sync settles the **`$0`** path.
### Overriding trial days at subscription creation
Pass `trial_period_days` in the subscription request to override the plan-level value.
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/subscriptions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "cust_01xyz",
"plan_id": "plan_01abc",
"currency": "USD",
"billing_cadence": "RECURRING",
"billing_period": "MONTHLY",
"billing_period_count": 1,
"start_date": "2025-05-01T00:00:00Z",
"trial_period_days": 0
}'
```
| Value | Effect |
| ------- | ----------------------------------------------------------- |
| Omitted | Inherits `trial_period_days` from plan prices |
| `0` | Disables trial; subscription starts as `active` immediately |
| `> 0` | Overrides plan-level value with this number of days |
To shorten or lengthen the plan-level trial, pass a positive integer:
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/subscriptions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "cust_01xyz",
"plan_id": "plan_01abc",
"currency": "USD",
"billing_cadence": "RECURRING",
"billing_period": "MONTHLY",
"billing_period_count": 1,
"start_date": "2025-05-01T00:00:00Z",
"trial_period_days": 30
}'
```
## What Happens at Trial End
Trial end is detected automatically. When `trial_end <= now`:
1. Subscription status changes to `incomplete`.
2. Billing anchor resets to `trial_end`.
3. A `SUBSCRIPTION_TRIAL_END` invoice is created covering `[trial_end, trial_end + billing_period]`.
4. If the invoice amount is **zero**: subscription immediately converts to `active`. No payment event needed.
5. If the invoice amount is **non-zero**: subscription stays `incomplete` until the invoice is paid.
An `incomplete` subscription does not auto-expire. It remains `incomplete` until the trial-end invoice is paid or the subscription is cancelled.
**`payment_behavior` at trial end:**
| Value | Behavior if payment fails |
| -------------------------- | ---------------------------------------------------------------------------------- |
| `default_active` (default) | Subscription activates regardless of payment result |
| `allow_incomplete` | Subscription stays `incomplete` if payment fails |
| `error_if_incomplete` | Subscription stays `incomplete` if payment fails; wallet fallback is not attempted |
| `default_incomplete` | Subscription stays `incomplete`; only valid with `send_invoice` collection method |
Credit grants held pending activation are applied automatically when the subscription becomes `active`.
## API Reference
### Price fields
| Field | Type | Required | Description |
| ------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `trial_period_days` | integer | No | Trial length inherited by subscriptions linking this recurring fixed price. Default `0` (no trial). Only valid for `billing_cadence: RECURRING` + `price_type: FIXED`. Must be `>= 0`. |
### Subscription create fields
| Field | Type | Required | Description |
| ------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `trial_period_days` | integer | No | Override plan-level trial length in days. `0` disables trial. Omit to inherit from plan prices (all recurring fixed plan prices must share the same value). Must be `>= 0`. |
| `payment_behavior` | enum | No | **`SUBSCRIPTION_TRIAL_END`** and broader subscription payment semantics when attempts fail (`default_active`, `allow_incomplete`, `error_if_incomplete`, `default_incomplete`). API normalizes unspecified creates toward **`default_active`** (not all combinations are valid with every `collection_method`). |
| `collection_method` | enum | No | **`charge_automatically`** vs **`send_invoice`**. For **`SUBSCRIPTION_TRIAL_START`**, controls whether the **`$0`** invoice is **`succeeded` immediately** or left **`pending`** for outbound invoicing/card capture. |
### Subscription response (trial-related fields)
| Field | Type | Description |
| ------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `subscription_status` | string | **`trialing`** while the trial window is open. Prefer this for branching; **`status`** may also exist on **`SubscriptionResponse`**. |
| `trial_start` | timestamp | Trial start (= **`start_date`** on create). |
| `trial_end` | timestamp | When paid billing anchors; first **`SUBSCRIPTION_TRIAL_END`** period starts here. |
| `current_period_start` | timestamp | During trial = **`trial_start`**. |
| `current_period_end` | timestamp | During trial = **`trial_end`**. |
| `latest_invoice` | object | On non-draft **`trialing`** creates, populated with **`SUBSCRIPTION_TRIAL_START`**. |
| `latest_invoice.billing_reason` | string | **`SUBSCRIPTION_TRIAL_START`**, the **`$0`** trial preview invoice. |
## Webhooks & Events
**Trial → paid conversion:**
* **`subscription.activated`** when **`subscription_status`** flips **`trialing` → `active`**.
* Raised after **`SUBSCRIPTION_TRIAL_END`** settles (**paid**) **or** auto-completes at **`amount_due = 0`** (no PSP event required).
* **`SUBSCRIPTION_TRIAL_START`** emits **no** **`subscription.activated`**. **`subscription_status`** remains **`trialing`** until **`SUBSCRIPTION_TRIAL_END`** finishes, even if the opening invoice shows **`payment_status: SUCCEEDED`**.
There is no dedicated **`subscription.trialing`**, **`subscription.trial_ended`**, or **`subscription.trial_ending`** webhook. Use **`subscription.activated`** (and your own trial clock if needed) to detect conversion.
Example payload shape (**truncated**; your handler may receive more fields):
```json theme={null}
{
"subscription": {
"id": "sub_01def456",
"status": "active",
"trial_start": "2025-05-01T00:00:00Z",
"trial_end": "2025-05-15T00:00:00Z",
"current_period_start": "2025-05-15T00:00:00Z",
"current_period_end": "2025-06-15T00:00:00Z"
}
}
```
For information on configuring webhook endpoints, see [Webhooks](/docs/webhook/webhooks).
## Edge Cases
| Scenario | Behavior |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `trial_period_days = 0` at subscription level | Trial disabled even if plan price has `trial_period_days > 0` |
| Plan prices have mismatched `trial_period_days` | Subscription creation fails: `"all recurring fixed plan prices must have the same trial_period_days"` |
| Cancellation during trial | Standard cancellation applies. Trial-end processing skips subscriptions not in `trialing` status. |
| Paused subscription when `trial_end` is reached | Trial-end processing is skipped. Runs when the subscription is unpaused. |
| Zero-amount trial-end invoice | Subscription auto-activates immediately without waiting for a payment event. |
| Trial-start **`$0`** invoice | Always created for **`trialing`** creates; **`SUBSCRIPTION_TRIAL_START`** stays visible (not **`SKIPPED`**) unlike other **`$0`** subscription invoices |
| Inherited (child) subscriptions | Trial status cascades from parent. Children are not processed independently. |
| Re-trialing | Not supported. Each subscription has one trial window. To extend a trial, cancel and recreate the subscription. |
## Best Practices
* **Payment ahead of trial end.** You can onboard trialing subscriptions without storing a card, but **`SUBSCRIPTION_TRIAL_END`** still expects a collectible path whenever **`amount_due > 0`**.
* **`send_invoice` + outbound PSP sync.** Keeps **`SUBSCRIPTION_TRIAL_START`** in **`PENDING`** until the hosted **`$0`** handshake finishes. Prefer this pattern when the gateway captures cards solely behind invoice artifacts.
* **Pin `payment_behavior` in production.** **`SUBSCRIPTION_TRIAL_END`** obeys it when autopay retries fail (**`allow_incomplete`** vs **`default_active`**, etc.).
* **Split entitlements cleanly.** Fire **`subscription.activated`** once **`SUBSCRIPTION_TRIAL_END`** succeeded (or **`$0`** short-circuit). **`SUBSCRIPTION_TRIAL_START`** leaves **`subscription_status`** at **`trialing`** even when **`payment_status`** reads **`SUCCEEDED`**.
* **Cheap staging loops.** Truly **`$0`** plan prices accelerate **`trialing → active`** because **`SUBSCRIPTION_TRIAL_END`** can finalize with nothing to capture.
* **Single trial episode.** No built-in replay; cancel + recreate is the sanctioned reset.
Configure webhook destinations in **[Webhooks](/docs/webhook/webhooks)** (events: **[Webhooks & Events](#webhooks--events)** above).
# Auto Top-Up
Source: https://docs.flexprice.io/docs/wallet/auto-top-up
Automatically recharge customer wallets when balance falls below a configured threshold to ensure uninterrupted service.
The Auto Top-Up feature in Flexprice automatically adds credits to a customer's wallet when the ongoing balance falls below a predefined threshold. This ensures continuous service availability and eliminates the need for manual monitoring and intervention.
**Benefits of Auto Top-Up:**
* **Uninterrupted Service**: Prevents service disruptions due to insufficient wallet balance
* **Automated Management**: Eliminates manual monitoring and top-up operations
* **Improved Cash Flow**: Ensures timely wallet replenishment
* **Flexible Configuration**: Choose between immediate credits or invoiced top-ups
* **Proactive Balance Management**: Maintains wallet balance above critical thresholds
## How Auto Top-Up Works
Auto top-up monitors the **ongoing balance** (real-time credit balance) of a wallet, which accounts for:
* Current wallet credit balance
* Pending invoice amounts
* Current period usage
When the ongoing balance drops to or below the configured threshold, Flexprice automatically triggers a top-up operation if configured, based on your configuration.
## Auto Top-Up Modes
Flexprice supports two modes for auto top-up:
### 1. Direct Top-Up
Credits are immediately added to the wallet when the threshold is reached. This mode provides instant balance replenishment.
**When to Use:**
* When you want immediate credit availability
* For customers with reliable payment methods
* When you need to prevent any service interruption
### 2. Invoiced Top-Up
An invoice is created when the threshold is reached, and credits are only added to the wallet after the invoice is paid. This mode provides better cash flow control.
**When to Use:**
* When you want payment confirmation before adding credits
* For customers who prefer invoiced billing
* When you need to track top-up transactions separately
## Configuring Auto Top-Up
### During Wallet Creation
You can enable auto top-up when creating a new wallet:
Go to **Billing** → **Customers** → Select customer → **Wallet** tab
Click the **three-dot menu (⋮)** in the top right corner of the wallet section
Select **Auto Topup** from the dropdown menu to enable auto top-up
Toggle the **Enable Auto Top-Up** switch to activate automatic wallet recharging
Configure the following settings:
* **Threshold**: Enter the minimum ongoing balance amount that triggers auto top-up
* **Top-Up Amount**: Enter the number of credits to add when triggered
* **Invoicing Mode**: Toggle the switch to choose between:
* **Direct Credit**: Credits added immediately
* **Invoiced**: Invoice created, credits added after payment
Click **Save Changes** to apply the auto top-up configuration
### Updating Auto Top-Up Settings
You can modify auto top-up configuration for existing wallets:
Go to **Billing** → **Customers** → Select customer → **Wallet** tab
Click the **three-dot menu (⋮)** in the top right corner of the wallet section
Select **Auto Topup** from the dropdown menu to enable auto top-up
Configure the following settings:
* **Threshold**: Enter the minimum ongoing balance amount that triggers auto top-up
* **Top-Up Amount**: Enter the number of credits to add when triggered
* **Invoicing Mode**: Toggle the switch to choose between:
* **Direct Credit**: Credits added immediately
* **Invoiced**: Invoice created, credits added after payment
Click **Save Changes** to apply the auto top-up configuration
**Note**: Partial updates are supported. You can update only specific fields without changing others.
## Auto Top-Up Configuration Options
### Threshold
The minimum ongoing balance that triggers auto top-up. When the ongoing balance falls to or below this value, the top-up is automatically triggered.
### Top-Up Amount
The number of credits to add to the wallet when auto top-up is triggered.
### Invoicing Mode
Determines how credits are added:
* **Direct Credit** (`invoicing: false`): Credits are immediately added to the wallet
* **Invoiced** (`invoicing: true`): An invoice is created, and credits are added after payment successful
## Auto Top-Up Workflow
### Direct Top-Up Flow
When `invoicing: false` is configured, credits are added immediately:
The wallet's ongoing balance falls to or below the configured threshold value
Flexprice automatically detects the low balance and triggers the top-up process
Credits are immediately added to the wallet balance
A wallet transaction is created with status `completed` and transaction reason `PURCHASED_CREDIT_DIRECT`
The wallet balance is updated instantly, and the transaction appears in the wallet transaction history
### Invoiced Top-Up Flow
When `invoicing: true` is configured, an invoice is created and credits are added after payment:
The wallet's ongoing balance falls to or below the configured threshold value
Flexprice automatically detects the low balance and triggers the top-up process
An invoice is created with status `pending` and linked to the wallet transaction
A wallet transaction is created with status `pending` and transaction reason `PURCHASED_CREDIT_INVOICED`
The wallet balance remains unchanged until the invoice is paid
When the invoice is paid:
* Transaction status changes to `completed`
* Credits are added to the wallet
* Wallet balance is updated
**Note**: If your account has `AutoCompletePurchasedCreditTransaction` enabled, invoiced top-ups may complete immediately without waiting for payment.
## Use Cases
### SaaS Companies
* **Usage-Based Billing**: Automatically top up wallets when usage depletes balance
* **Prepaid Plans**: Maintain minimum balance for prepaid customers
* **Service Continuity**: Prevent service interruptions for high-value customers
### API Providers
* **API Usage**: Maintain wallet balance for API consumption
* **Rate Limiting**: Prevent service disruption due to insufficient credits
* **Enterprise Customers**: Provide automated balance management for enterprise accounts
## Best Practices
💡 **Set Realistic Thresholds**: Choose threshold values that provide sufficient buffer before balance reaches zero. Consider typical usage patterns and top-up frequency.
💡 **Match Top-Up Amount to Usage**: Set top-up amounts that align with typical customer usage. Too small may trigger frequently; too large may tie up unnecessary funds.
💡 **Consider Invoicing Mode**: Use direct top-up for trusted customers with reliable payment methods. Use invoiced mode when you need payment confirmation before adding credits.
💡 **Monitor Transaction History**: Regularly review auto top-up transactions to understand patterns and optimize configuration.
💡 **Combine with Low Balance Alerts**: Use auto top-up alongside low balance alerts for comprehensive wallet management.
## Troubleshooting
### Auto Top-Up Not Triggering
**Possible Causes:**
* Auto top-up is disabled
* Ongoing balance is above threshold
**Solutions:**
* Verify auto top-up is enabled in wallet settings
* Check real-time balance vs. threshold value
### Credits Not Added (Invoiced Mode)
**Possible Causes:**
* Invoice is still pending payment
* Invoice payment failed
* Auto-complete setting is disabled
**Solutions:**
* Check invoice payment status
* Verify invoice payment was successful
* Review transaction status in wallet history
### Frequent Top-Ups
**Possible Causes:**
* Threshold set too high
* Top-up amount too small
* High usage depleting balance quickly
**Solutions:**
* Adjust threshold to a lower value
* Increase top-up amount
* Review customer usage patterns
## Related Documentation
* [Wallet Transactions](/docs/wallet/transactions) - Track and monitor all wallet transactions
* [Top-Up Wallet](/docs/wallet/top-up) - Learn about manual wallet top-ups
* [Low Balance Alert](/docs/wallet/low-balance-alert) - Configure balance alerts
# Conversion rate
Source: https://docs.flexprice.io/docs/wallet/conversion-rate
Conversion rate links your unit (e.g. credits) to base currency (e.g. USD). Billing stays in one currency.
## Overview
### What is wallet conversion rate?
**Wallet conversion rate** tells you how much money one credit is worth in your wallet's currency.
* The wallet **stores** balance in **credits** only. Currency is used only for **calculation and display**. Formula: **amount in currency = credits × conversion rate**.
* **Example:** Conversion rate **0.01**, wallet in USD. Then 1 credit = 0.01 USD. So 100 credits = 1 USD, and 1000 credits show as a 10 USD balance.
* You can set **two** rates on a wallet:
* **Conversion rate** — used when you **spend** (debit) and when balance is shown in currency.
* **Top-up conversion rate** — used only when you **add** credits (top-up). If you don’t set it, top-ups use conversion rate.
***
### What is conversion rate?
It is the number that links your unit to base currency. It appears in **two** places:
| Field | Purpose |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `conversion_rate` | Links stored credits to currency (for display and when user spends). Wallet stores balance in **credits**; currency is worked out from this. Formula: `amount in currency = credits × conversion_rate`. Default: `"1"`. |
| `topup_conversion_rate` | Same formula; used **only for top-ups**. If you do not set it, the wallet uses `conversion_rate` for top-ups. |
So: **1 credit = conversion\_rate in currency.** Examples: rate 1 → 1 USD = 1 credit; rate 2 → 1 USD = 0.5 credits; rate 0.5 → 1 USD = 2 credits.
***
## How it works internally
* **Price unit:** Has `base_currency` and `conversion_rate`. Billing converts price unit amounts into that base currency for invoicing.
* **Wallet:** Balance is stored in **credits**. To show it in currency: amount = credits × conversion rate.
### When conversion is used
Uses `topup_conversion_rate` (or `conversion_rate` if not set). If you send `amount` and not `credits_to_add`, credits = amount ÷ rate. If both are sent, **credits\_to\_add** wins and amount is ignored.
Uses `conversion_rate` (e.g. invoice payment). Manual debit uses `credits` directly; no conversion.
Uses conversion\_rate: currency = credits × rate; credits = currency ÷ rate.
***
## Conversion during credit
Top-up uses **topup\_conversion\_rate**. If not set, it uses **conversion\_rate**.
Send `amount` (in wallet currency). System works out `credits_to_add = amount ÷ topup_conversion_rate` and adds that many credits to the wallet.
Send `credits_to_add`. That many credits are added to the wallet.
Wallet in USD, conversion rate (1 credit = 2 USD). Top-up with amount 10 USD → credits added = 10 ÷ 2 = 5. Wallet balance goes up by 5 credits.
***
## Conversion during debit
Debits use **conversion\_rate**. Not the top-up rate.
Amount in currency is turned into credits: `credits = amount ÷ conversion_rate`. That many credits are taken out.
Send `credits`. No conversion. That many credits are taken out.
Same wallet (rate 2). Debit 5 credits → 10 USD worth taken out (5 × 2 = 10).
***
## Configuration
### Set conversion rate on a wallet
Go to **Customers** → select the customer → **Wallets** tab.
Click **Create Wallet** (or **+ Add**). For **Custom** currency, select a price unit — **Currency** and **Conversion rate** are set from that unit. For fiat, enter **Conversion rate** and optionally **Top-up conversion rate**.
### Set conversion rate via API
`POST /v1/wallets` — send `currency` (required), `conversion_rate` (optional, default `"1"`), `topup_conversion_rate` (optional), or `price_unit` (then currency and conversion\_rate come from the price unit).
**Create wallet with explicit rates:**
```json theme={null}
{
"customer_id": "cust_xxx",
"currency": "usd",
"conversion_rate": "0.01",
"topup_conversion_rate": "0.01"
}
```
`POST /v1/wallets/{id}/top-up` — send `amount` (converted with top-up rate) or `credits_to_add`; if both, credits are used.
**Top-up by amount** (uses top-up rate):
```json theme={null}
{
"amount": "10.00",
"transaction_reason": "PURCHASED_CREDIT",
"idempotency_key": "topup-uniq-123"
}
```
### Validation
* **Wallet:** `conversion_rate` defaults to `"1"`. If you set it, it must be > 0. `topup_conversion_rate` must be > 0 if you set it. When you use `price_unit`, that price unit must be Active.
***
## Use cases
Show and charge in credits. Invoice in one currency (e.g. USD).
Set `topup_conversion_rate` so users get more credits per 1 USD. `conversion_rate` is still used for spending and for showing balance.
Set **Top-up conversion rate** lower than **Conversion rate** so 1 USD buys more credits when users top up. Spending and balance still use **Conversion rate**. Example: conversion rate 0.01, top-up rate 0.008 → 1 USD = 125 credits on top-up.
***
## Edge cases
| Case | What happens |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Missing or zero rate** | Price unit: request fails. Wallet: `conversion_rate` can default to `"1"`. Best: send a valid rate or use `price_unit`. |
| **Invalid or negative rate** | Request fails. Rate must be > 0. |
| **Top-up: amount and credits both sent** | **credits\_to\_add** is used. **amount** is ignored. |
| **Wallet with price\_unit** | `conversion_rate` in the request is ignored. Currency and rate come from the price unit. |
***
## Examples
`conversion_rate = "0.01"`. So 1 credit = 0.01 USD. Top-up 10 USD (same top-up rate) adds 1000 credits. Manual debit 500 credits takes out 5 USD worth.
`conversion_rate = "0.01"`, `topup_conversion_rate = "0.008"`. Normal spend: 1 credit = 0.01 USD. Top-up: 1 USD = 125 credits.
***
# Creating a wallet
Source: https://docs.flexprice.io/docs/wallet/create
A wallet in Flexprice acts as a virtual account linked to a customer. It allows for storing and managing credits that can be applied toward subscription payments, usage-based charges, or other transactions.
Flexprice enable you to manage prepaid credits for your customers. These are credits added to a wallet and can be consumed for billing purposes. Wallets ensure an efficient and transparent mechanism for credit handling, aligning with subscription and usage-based pricing models. Wallets operate in a defined currency and can be topped up manually or automatically.
**Creating a Wallet**
* Go to the **Customers** section in the dashboard.
* Select the desired customer.
* Navigate to the **Wallet** tab in the customer’s profile.
* If no wallet exists for the customer, you will see a message: *“No wallet linked to the customer yet.”*
* Click the **Add Wallet** button to open the wallet creation form.
* Fill out the following details:
* **Wallet Name**: Assign a name to the wallet. By default, it will be “Prepaid Wallet.”
* **Currency**: Select the currency in which this wallet will operate (e.g., USD, INR).
Note: Only **one active wallet per currency** is allowed for a customer. If a wallet for the selected currency already exists, you must deactivate it before creating a new one.
* Currently, only **Free Credits** can be added. These credits do not generate an invoice, as they are granted without charge. Also, the credit-to-currency ratio is **1:1** (e.g., 1 credit equals 1 USD or 1 INR)
* Click **Save Wallet** to finalise the creation process.
* The wallet will now appear in the Wallet tab for the customer, showing the balance and available credits.
# Low Balance Alert
Source: https://docs.flexprice.io/docs/wallet/low-balance-alert
Configure Wallet Sentinel Alerts to get notified when a customer's wallet balance crosses configurable Info, Warning, or Critical thresholds.
Wallet Sentinel Alerts in Flexprice let you monitor wallet balance with **three alert levels** so you can act before balance becomes critical. You can enable or disable alerts per wallet and set a custom threshold and trigger condition for each level.
**Benefits:**
* **Proactive management** — Get notified at Info and Warning levels before balance reaches critical
* **Granular control** — Separate thresholds for informational, warning, and critical states
* **Flexible conditions** — Trigger when balance is above or below a value (e.g. "below" for low balance)
* **Per-wallet settings** — Enable or disable alerts and configure thresholds per wallet
## Alert Levels
| Level | Purpose |
| ------------ | ------------------------------------------------------------ |
| **Info** | Informational alert when balance crosses a defined threshold |
| **Warning** | Early warning level before critical |
| **Critical** | Critical alert level requiring immediate action |
Each level can be configured with:
* A **threshold value** (in the wallet’s currency, e.g. USD)
* A **trigger condition** (e.g. **below** — alert when balance is below the threshold).
Flexprice supports two conditions for thresholds: **Below** and **Above**.
Alerts are turned on or off for a wallet using **Enable Alerts** (`alert_enabled` in the API).
## Configuring Wallet Sentinel Alerts
* Go to **Billing** in the main navigation
* Select **Customers**, then the desired customer
* Open the **Wallet** tab in the customer profile
* Click the **three-dot menu (⋮)** in the top right corner of the wallet section
* Select **Alert Settings** from the dropdown
In the **Wallet Alert Settings** dialog, turn **Enable Alerts** **on** to activate monitoring against your configured thresholds
* Set **Critical**, **Warning**, and **Info** thresholds. For each level set **Threshold Value** and **Condition** (e.g. **Below** — alert when balance is below the threshold).
* Use **Remove** to drop a level you don't need.
* Click **Save Changes** to apply.
## Alert Behavior
Once configured and enabled, the sentinel will:
* **Monitor balance** — Evaluate wallet balance against your thresholds (e.g. after top-ups, debits, and accounting for usage)
* **Trigger by level** — Raise **Info**, **Warning**, or **Critical** alert based on which threshold is crossed
* **Update state** — Reflect current state (e.g. ok, info, warning, in\_alarm) on the wallet and in alert history
* **Notify** — Send webhooks when alert state changes
Webhooks are sent when alert state changes (e.g. ok → warning, warning → in\_alarm). Configure your webhook endpoint to receive these notifications.
## Managing Alert Settings
### Viewing Current Configuration
Open the customer’s **Wallet** tab, click the **three-dot menu (⋮)** → **Alert Settings**. The dialog shows **Enable Alerts** and all threshold levels (Critical, Warning, Info) with their values and conditions.
### Changing Thresholds or Condition
In the **Alert Settings** dialog box, adjust **Enable Alerts**, any threshold value, or condition per level, then click **Save Changes** to apply.
### Disabling Alerts
Set **Enable Alerts** to **off** in the **Alert Settings** dialog and click **Save Changes** (or set `alert_enabled` to `false` via API).
The wallet will no longer generate real time balance alerts until you enable them again.
## Use Cases
### SaaS and Usage-Based Billing
* **Low balance warnings** — Info at 5, Warning at 0, Critical below 0 so you can top up before going negative
* **Prepaid credits** — Notify when credits are running low at multiple levels to avoid running out of credits unexpectedly
* **Service Continuity**: Prevent service interruptions due to insufficient funds
### E-commerce Platforms
* **Wallet monitoring** — Different severities for different teams or workflows
* **Promotional or prepaid wallets** — Alerts when balance is low relative to typical usage
* **Payment Processing**: Ensure sufficient funds for transactions
### API and Credit-Based Products
* **Usage vs balance** — Early Info/Warning so support or automation can act before Critical
* **Multi-wallet** — Per-wallet sentinel settings so each wallet has its own thresholds
* **Rate Limiting**: Prevent service disruption due to insufficient credits
* **Customer Support**: Proactive customer communication
## Best Practices
💡 **Set realistic thresholds** — Choose values that give enough time to top up (e.g. Warning above zero if you want to avoid negative balance)
💡 **Use all three levels** — Use Info for early notice, Warning for action soon, Critical for immediate action
💡 **Match condition to goal** — Use **below** for “low balance” alerts; use **above** if you want alerts when balance exceeds a cap
💡 **Review periodically** — Adjust thresholds based on usage patterns and top-up behavior
💡 **Combine with auto top-up** — Use sentinel alerts together with auto top-up so critical levels can trigger automatic top-up when configured
# Manual Debit
Source: https://docs.flexprice.io/docs/wallet/manual-debit
Manually deduct credits from customer wallets for refunds, adjustments, or administrative corrections.
The Manual Wallet Debit feature allows you to manually deduct credits from a customer's wallet. This is essential for handling refunds, billing corrections, chargebacks, and other administrative adjustments.
**Common Use Cases:**
* **Refunds**: Process customer refunds for overcharges or billing errors
* **Billing Adjustments**: Correct incorrect credit allocations
* **Chargebacks**: Handle payment disputes and chargebacks
* **Administrative Corrections**: Fix manual entry errors or system issues
* **Wallet Termination**: Deduct remaining balance when closing a wallet
## How Manual Debit Works
When you debit a wallet, Flexprice automatically:
1. **Validates Balance**: Ensures sufficient credits are available before processing
2. **Consumes Credits**: Deducts credits using a priority-based algorithm (see below)
3. **Updates Balance**: Reduces the wallet balance by the debited amount
4. **Creates Transaction Record**: Maintains a complete audit trail
5. **Triggers Alerts**: Notifies you if the balance falls below configured thresholds
## Credit Consumption Algorithm
Flexprice uses a smart **Priority-Based FIFO** algorithm to determine which credits to consume when processing a debit:
### Consumption Order
Credits are consumed in the following order:
1. **Priority** (Lower number = Higher priority)
* Credits with priority 1 are consumed before priority 2
* Credits without priority are consumed last
2. **Expiry Date** (Earlier expiry first)
* Credits expiring sooner are consumed first to prevent waste
* Credits without expiry dates are consumed last
3. **Credit Amount** (Larger amounts first)
* Among credits with the same priority and expiry, larger amounts are consumed first
* This optimizes the number of transaction updates required
### Example Scenario
Suppose a wallet has the following credits:
| Credits | Priority | Expiry Date | Status |
| ------- | -------- | ----------- | --------- |
| 50 | 1 | 2024-03-01 | Available |
| 30 | 1 | 2024-03-01 | Available |
| 100 | 1 | 2024-03-15 | Available |
| 75 | 2 | 2024-02-20 | Available |
| 200 | None | None | Available |
**If you debit 150 credits**, they will be consumed in this order:
1. **50 credits** from priority 1, expiring 2024-03-01
2. **30 credits** from priority 1, expiring 2024-03-01
3. **70 credits** from priority 1, expiring 2024-03-15 (partial)
The wallet will have 30 credits remaining from the third transaction, plus the untouched 75 and 200 credits.
## Making a Manual Debit via Dashboard
You can easily debit credits from a customer's wallet through the Flexprice dashboard:
### Step 1: Navigate to the Wallet
* Go to **Billing** in the main navigation
* Select **Customers** from the billing section
* Select the customer whose wallet you want to debit
* Navigate to the **Wallet** tab
### Step 2: Access Manual Debit
* Click the **three-dot menu (⋮)** in the top right corner of the wallet section
* Select **Manual Debit** from the dropdown menu
### Step 3: Enter Debit Details
A "Manual Debit" dialog will appear with the following fields:
* **Credits to Deduct**: Enter the number of credits you want to debit from the wallet
* The system will show a preview: "\$X will be debited from the wallet"
* **Reference ID (Optional)**: Enter a unique reference ID for this transaction
* This serves as the idempotency key to prevent duplicate debits
* Useful for linking to invoices, tickets, or other reference systems
* Click **Submit** to process the debit
### Step 4: Verify the Transaction
After submitting:
* The wallet balance will be updated immediately
* The debit transaction will appear in the Transactions list
* You'll see "-X credits" in the transaction history
## Making a Manual Debit via API
### Endpoint
```http theme={null}
POST https://api.cloud.flexprice.io/v1/wallets/{wallet_id}/debit
```
### Request Parameters
| Parameter | Type | Required | Description |
| -------------------- | ------ | -------- | ----------------------------------------------------------- |
| `credits` | string | Yes | Amount of credits to debit (decimal format, e.g., "100.50") |
| `transaction_reason` | string | Yes | Reason code for the debit (see Transaction Reasons below) |
| `idempotency_key` | string | Yes | Unique key to prevent duplicate debits |
| `description` | string | No | Human-readable description of the debit |
| `metadata` | object | No | Custom key-value pairs for additional context |
### Transaction Reasons
Use appropriate transaction reason codes to categorize debits:
| Reason Code | Use Case |
| ---------------------- | ------------------------------------------------- |
| `MANUAL_BALANCE_DEBIT` | Administrative corrections and manual adjustments |
### Example Request
```bash theme={null}
curl --request POST \
--url https://api.cloud.flexprice.io/v1/wallets/{id}/debit \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"credits": 123,
"description": "",
"idempotency_key": "",
"metadata": {},
"transaction_reason": "MANUAL_BALANCE_DEBIT"
}'
```
### Success Response
```json theme={null}
{
"id": "wallet_0123",
"customer_id": "cust_0123",
"name": "Prepaid Wallet",
"currency": "usd",
"balance": "1587",
"credit_balance": "1587",
"wallet_status": "active",
"auto_topup_trigger": "disabled",
"auto_topup_min_balance": "0",
"auto_topup_amount": "0",
"wallet_type": "PRE_PAID",
"config": {
"allowed_price_types": [
"ALL"
]
},
"conversion_rate": "1",
"alert_enabled": true,
"alert_state": "ok",
"created_at": "2025-11-05T13:41:59.256406Z",
"updated_at": "2025-11-14T15:55:38.19095Z"
}
```
### Error Responses
| Error Code | HTTP Status | Description |
| ------------------------- | ----------- | ---------------------------------- |
| `INSUFFICIENT_BALANCE` | 400 | Wallet doesn't have enough credits |
| `INVALID_CREDITS` | 400 | Credits amount must be positive |
| `MISSING_IDEMPOTENCY_KEY` | 400 | Idempotency key is required |
| `WALLET_NOT_FOUND` | 404 | Wallet doesn't exist |
| `WALLET_NOT_ACTIVE` | 400 | Wallet is not in active status |
### Example Error Response
```json theme={null}
{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "insufficient balance",
"hint": "Insufficient balance to process debit operation",
"details": {
"wallet_id": "wallet_abc123",
"amount": "150.50",
"available_balance": "100.00"
}
}
}
```
## Idempotency
The `idempotency_key` parameter is **required** and ensures that duplicate API requests (e.g., due to network timeouts) don't result in multiple debits.
### Best Practices for Idempotency Keys
* Use a **unique identifier** for each logical debit operation
* Include context in the key, e.g., `refund_invoice_123_20240115`
* **Never reuse** idempotency keys across different operations
## Use Case Examples
### Example: Administrative Correction
```bash theme={null}
curl --location 'https://api.cloud.flexprice.io/v1/wallets/{id}/debit' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'x-api-key: sk_xxxxxxxxxxxx' \
--data '{
"idempotency_key": "Manual-adjustment-123",
"transaction_reason": "MANUAL_BALANCE_DEBIT",
"credits": 1
}'
```
## Important Considerations
### Balance Validation
Before processing a debit, Flexprice:
* Checks that the wallet has sufficient **available credits**
* Excludes expired credits from the calculation
* Returns an error if insufficient balance is found
### Transaction Atomicity
All debit operations are **atomic**:
* Credits are consumed and wallet balance is updated in a single database transaction
* If any step fails, all changes are rolled back
* This ensures data consistency and prevents partial debits
### Audit Trail
Every manual debit creates a complete audit trail:
* Transaction record with type `DEBIT`
* Before and after balances
* Description and metadata
* Transaction reason
* Idempotency key for tracking
* Timestamp and user information
### Alert Integration
After a debit, Flexprice automatically:
* Checks the wallet's low balance alert threshold
* Triggers an alert if the new balance is below the threshold
* Notifies you through configured alert channels
## Best Practices
💡 **Always Use Idempotency Keys**: Protect against duplicate debits by using unique idempotency keys for each operation
💡 **Provide Clear Descriptions**: Include descriptive text and metadata to make audit trails meaningful
💡 **Choose Appropriate Reasons**: Use the correct transaction reason code to categorize debits properly
💡 **Validate Before Debiting**: Check wallet balance before attempting large debits to avoid errors
💡 **Handle Errors Gracefully**: Implement proper error handling, especially for `INSUFFICIENT_BALANCE` errors
💡 **Monitor Alert Notifications**: Watch for low balance alerts after processing debits
## Troubleshooting
### Insufficient Balance Error
**Problem**: API returns `INSUFFICIENT_BALANCE` error
**Solution**:
* Check the wallet's current balance before attempting the debit
* Verify that you're not including expired credits in your calculation
* Consider splitting large debits across multiple operations if needed
### Duplicate Idempotency Key Error
**Problem**: API rejects request due to duplicate idempotency key
**Solution**:
* Ensure each unique debit operation has a unique idempotency key
* Check if the previous operation with the same key completed successfully
* Generate new idempotency keys for each distinct operation
### Wallet Not Active Error
**Problem**: API returns `WALLET_NOT_ACTIVE` error
**Solution**:
* Verify the wallet status is `ACTIVE`
* Check if the wallet has been closed or suspended
* Reactivate the wallet if necessary before debiting
## Related Features
* [Creating a Wallet](/docs/wallet/create) - Learn how to create wallets for customers
* [Top-up Wallet](/docs/wallet/top-up) - Add credits to customer wallets
* [Low Balance Alert](/docs/wallet/low-balance-alert) - Configure balance threshold alerts
* [Prepaid and Promotional Credits](/docs/wallet/prepaid-and-promotional) - Understand credit types
* [Wallet Transactions](/docs/wallet/transactions) - Track and monitor all wallet transactions
# Prepaid & Promotional credits
Source: https://docs.flexprice.io/docs/wallet/prepaid-and-promotional
Description of your new file.
Flexprice provides a flexible credit system that allows businesses to configure **prepaid and promotional credits** for customers. These credits can be used to offset recurring charges, usage-based charges, or both, depending on how the credit wallet is configured.
Credits are a powerful tool in pricing and customer retention. They allow businesses to:
* **Offer promotional incentives** → Free trial credits, signup bonuses, or customer loyalty rewards.
* **Enable prepaid usage models** → Let customers buy credits upfront instead of committing to a fixed plan.
**Defining usage of charges**
When a customer has available credits, Flexprice will **automatically deduct credits** based on the applicable charges:
1. **Recurring charges**
* If the wallet allows **subscription-only credits**, they will be used to pay for the customer’s recurring plan.
* If the credits are not enough, the remaining balance will be charged via the default payment method.
2. **Usage-based charges**
* If the wallet allows **usage-based credits**, they will be used before any other payment method.
* If usage exceeds the available credits, standard billing rules apply (e.g., pay-per-use pricing).
3. **Both subscription & usage-based charges**
* If the wallet allows **both types of charges**, credits will be used wherever applicable.
# Top-up wallet
Source: https://docs.flexprice.io/docs/wallet/top-up
Wallet top-ups allow you to add credits to customer wallets, ensuring continuous service availability. Flexprice supports two top-up methods: **manual top-up** for on-demand credit additions and **automatic top-up** for maintaining minimum balance thresholds.
***
## Manual Top-Up
Manual top-ups provide flexibility to add credits on demand. You can issue free credits for promotions and compensations, or process purchased credits through standard payment methods.
### Adding Credits Manually
Navigate to the customer's wallet and click **Top-up Wallet** in the top right corner
In the top-up dialog, select your credit type and enter the amount to add to the wallet
Click **Add Credits** to complete the top-up
***
## Automatic Top-Up
Automatic top-up ensures uninterrupted service by automatically recharging wallets when they fall below a specified threshold. This eliminates manual monitoring and prevents service disruptions due to insufficient balance.
### Configuring Auto Top-Up
During wallet creation, toggle **Recharge Wallet Automatically**
Define when auto top-up should activate (e.g., when balance falls below \$100)
Enter the minimum balance threshold that triggers automatic recharge
Specify the amount to add when auto top-up is triggered (e.g., $100, $500)
Click **Save changes** to activate auto top-up
***
## Credit Types Supported
Flexprice supports multiple credit types to accommodate different billing scenarios:
### Free Credits Top-Up
Promotional or complimentary credits added without requiring payment.
**Common use cases:**
* Welcome bonuses for new customers
* Promotional campaigns and special offers
* Service credits for issue resolution
* Customer loyalty and retention programs
### Purchased Credits Top-Up
Purchased credit top-up refers to credits that a customer buys. We support two types:
* **Purchased Credits** - Immediate credit addition
* **Purchased Credits (With Invoice)** - Invoice-based top-up with payment tracking
***
## Purchased Credit Top-Up (with Invoice)
The invoice-based top-up workflow provides comprehensive payment tracking and record-keeping for credit purchases.
### How It Works
When you select **Purchased Credits** and enable **Generate Invoice**, Flexprice initiates the following flow:
You select **Purchased Credits** and enable **Generate Invoice** in the top-up dialog, then click **Add Credits**
An invoice is automatically generated and finalized with payment status `Pending`. The invoice contains the credit purchase details and is visible in your invoice list
A wallet transaction is created with status `pending` and transaction reason `PURCHASED_CREDIT_INVOICED`. The transaction is linked to the generated invoice
Credits remain unavailable and are not added to the wallet balance. The wallet balance remains unchanged until payment is confirmed
Credits get available only after the payment is followed through. When the invoice payment status becomes `Succeeded`, the wallet balance gets topped-up.
You can either `Record Payment` or `Update Payment Status` directly to confirm payment.
* **Record Payment** - A payment record is created for the invoice supporting tracking and reconciliation. Payment can be recorded through following methods:
* **Offline Payment** - Record payment that was processed outside the system.
* **Payment Link** - Generate a payment link for online payment. Payment status gets updated when payment is successfully processed through the payment gateway. If you want to know more about the `payment_link` behavior, check out [Stripe Payment Link & Checkout](/integrations/stripe/payment-links) for configuration instructions.
* **Update Payment Status** - Invoice payment status changes from `Pending` to `SUCCEEDED` for API responses)
After the payment is completed, the invoice status is updated to confirm successful payment.
Once payment is confirmed and the invoice is marked as paid:
* Transaction status changes from `pending` to `completed`
* Credits are immediately added to the customer's wallet
* Wallet balance is updated and credits become available for use
For complete transaction lifecycle details, check out [Purchased Credits (Invoiced)](/docs/wallet/transactions#purchased-credits-invoiced) in the Wallet Transactions documentation.
**Payment Gateway Required** — Payment recording requires a configured payment gateway. For Example, Refer to [Stripe Connection Setup](/integrations/stripe/connection-setup) for configuration instructions.
### Credit Activation
Once payment is recorded and the invoice is marked as paid:
* Credits are immediately added to the customer's wallet
* Transaction status updates to `completed` in wallet history
* Credits become available for immediate use
### When to Use Invoice-Based Top-Ups
**Financial Record Keeping**
* Maintain detailed audit trails for all credit purchases
* Separate wallet top-up revenue from service charges
* Generate comprehensive transaction histories for accounting
**Enterprise Workflows**
* Support purchase order and approval processes
* Enable invoice-based payment for corporate customers
* Provide formal documentation for business expense tracking
**Compliance and Reporting**
* Meet regulatory requirements with detailed financial records
* Generate reports for credit purchase analysis
* Track customer acquisition and spending patterns
***
## Related Documentation
* [Wallet Transactions](/docs/wallet/transactions) - Monitor and manage all wallet transactions
* [Auto Top-Up Wallet](/docs/wallet/auto-top-up) - Configure automatic wallet recharging
* [Low Balance Alert](/docs/wallet/low-balance-alert) - Set up balance threshold notifications
***
# Wallet Transactions
Source: https://docs.flexprice.io/docs/wallet/transactions
Track and monitor all wallet activity including credits added, debits, and transaction status
## Overview
Every action that affects a wallet balance is recorded as a transaction. The **Transactions** section provides a complete audit trail of all wallet activity, making it easy to track credits, debits, and monitor account balances.
## Transaction Types
Wallet transactions fall into three main categories based on how they affect the wallet balance:
### Free Credits Added
Promotional or complimentary credits added directly to the wallet:
**Characteristics:**
* Credits are immediately available for use
* Balance updates instantly
* No payment or invoice required
* Reflected with a positive amount (e.g., +100 \$)
**Common Use Cases:**
* Welcome bonuses for new customers
* Promotional campaigns
* Service compensation
* Customer loyalty rewards
* Trial credits
**Example**: You add \$100 in free credits as a welcome bonus for a new customer. The credits are immediately available, and the customer can start using them right away.
***
### Purchased Credits (Invoiced)
Credits purchased by the customer that are linked to an invoice:
**Characteristics:**
* Transaction remains in **pending state** until invoice is paid
* Balance is **not updated** while pending
* Credits become available only after invoice payment
* Reflected with a positive amount (e.g., +20 \$)
**Payment Flow:**
Customer purchases credits and transaction is created in pending state
An invoice is automatically created for the purchased amount
Transaction remains pending while waiting for invoice payment
When the invoice is paid, transaction status updates to completed
Wallet balance is updated and credits become available for use
**Common Use Cases:**
* Enterprise customers with NET payment terms
* Credit packages sold with invoicing
* Prepaid purchases requiring payment confirmation
* Customers who prefer invoiced billing
**Example**: A customer purchases 500 credits worth \$500. The transaction shows as "Purchased Credits (Invoiced)" and remains pending. Once the customer pays the invoice, the transaction completes and the credits are added to their wallet balance.
**Pending State**: While a purchased credit transaction is pending, the wallet balance remains unchanged. This ensures credits are only added after payment confirmation.
#### Auto-Complete Purchased Credit Transactions
For automated payment flows, you can enable the `AutoCompletePurchasedCreditTransaction` setting for your account. When enabled:
* Purchased credit transactions are created directly as **completed** instead of pending
* Credits are added to the wallet **immediately** upon transaction creation
* Invoice is still generated but marked as paid automatically
* Useful for automated payment systems where payment is guaranteed
**When to Enable:**
* Integration with pre-authorized payment methods
* Automated billing systems with payment confirmation
* Trust-based credit purchases for verified enterprise customers
**Note**: This setting bypasses the pending state entirely. Only enable if you have a reliable payment confirmation mechanism in place.
***
### Debited
Credits deducted from the wallet for usage, payments, or manual adjustments:
**Characteristics:**
* Credits are immediately removed from wallet
* Balance updates instantly
* Reflected with a negative amount (e.g., -30 \$)
* Cannot be reversed once completed
**Common Use Cases:**
* Feature usage charges (API calls, storage, etc.)
* Subscription fees paid from wallet
* Invoice payments using wallet credits
* Manual balance adjustments by admins
* Service consumption
**Example**: A customer uses 30 credits during the billing period for API calls. The wallet is debited by 30 credits, reducing their available balance.
## Transaction Details
Each transaction record in the Transactions section includes:
| Field | Description |
| ---------------- | --------------------------------------------------------------------- |
| **Transactions** | Type and description of the transaction |
| **Payment Date** | Date when the transaction was created or completed |
| **Expiry Date** | When credits expire (if applicable, otherwise shown as `--`) |
| **Priority** | Processing priority for credit consumption (shown as `--` if not set) |
| **Amount** | Transaction amount in currency and credits |
### Understanding Transaction Amounts
Transaction amounts are displayed in two formats:
* **Currency Amount**: Dollar value (e.g., +100, -30)
* **Credit Amount**: Credit units (e.g., +100 credits, -30 credits)
**Positive amounts** (+) indicate credits **added** to the wallet\
**Negative amounts** (-) indicate credits **removed** from the wallet
## Accessing Transaction History
View complete transaction history for any customer wallet:
Go to **Billing** → **Customers** → Select customer → **Wallet** tab
Scroll down to the **Transactions** section below the wallet details
View all transactions with type, date, amount, and status information
## Transaction States
Understanding transaction states helps you monitor payment flows and credit availability:
### Completed Transactions
* Credits are available for use
* Balance has been updated
* Transaction is finalized and cannot be changed
* Applies to: Free credits, paid invoiced credits, and debits
### Pending Transactions
* Credits are not yet available
* Balance has not been updated
* Waiting for invoice payment
* Applies to: Purchased credits (invoiced) only
**Monitoring Pending Transactions**: Keep an eye on pending transactions to ensure invoices are paid promptly. Pending credits won't be available until payment is received.
## Use Cases
### For Finance Teams
💡 **Audit Trail**: Complete transaction history for accounting and compliance
💡 **Reconciliation**: Match wallet transactions with invoices and payments
💡 **Revenue Tracking**: Monitor purchased credit transactions and payment flow
### For Customer Support
💡 **Quick Reference**: Review transaction history when resolving customer inquiries
💡 **Balance Investigation**: Understand why a customer's balance changed
💡 **Payment Status**: Check if purchased credits are pending or completed
### For Analytics
💡 **Usage Patterns**: Analyze customer spending and credit consumption trends
💡 **Credit Utilization**: Track how customers use free vs. purchased credits
💡 **Payment Behavior**: Monitor invoice payment timelines for purchased credits
## Related Documentation
* [Top-Up Wallet](/docs/wallet/top-up) - Learn about manual wallet top-ups
* [Auto Top-Up](/docs/wallet/auto-top-up) - Configure automatic wallet recharging
* [Low Balance Alert](/docs/wallet/low-balance-alert) - Set up balance alerts
# Webhooks
Source: https://docs.flexprice.io/docs/webhook/webhooks
Learn about webhook events in Flexprice
Webhooks let you receive real-time notifications about events in your Flexprice account, such as invoice creation, payment status updates, subscription changes, and more.
There are **two ways** webhooks can be delivered:
* **Native Flexprice Webhooks**: Simple, direct POST to your endpoint.
* **Svix Webhooks**: Advanced, robust delivery with retries, history, signatures, and more (powered by [Svix](https://docs.svix.com/)).
**Flexprice Cloud uses Svix for webhook delivery. For self hosting, if you haven't explicitly enabled Svix, you're using native webhooks by default.**
## Webhook Event Types
You can receive webhook notifications for a range of events:
### Invoice Events
* `invoice.create.drafted` - Fired when a new invoice is created in draft state
* `invoice.update.finalized` - Fired when an invoice is finalized and ready for payment
* `invoice.update.payment` - Fired when a payment is recorded on an invoice
* `invoice.update.voided` - Fired when an invoice is voided
* `invoice.payment.overdue` - Fired when an invoice payment is overdue
### Subscription Events
* `subscription.created` - Fired when a new subscription is created
* `subscription.updated` - Fired when a subscription is updated
* `subscription.paused` - Fired when a subscription is paused
* `subscription.cancelled` - Fired when a subscription is cancelled
* `subscription.resumed` - Fired when a paused subscription is resumed
### Feature Events
* `feature.created` - Fired when a new feature is created
* `feature.updated` - Fired when feature details are updated
* `feature.deleted` - Fired when a feature is deleted
### Entitlement Events
* `entitlement.created` - Fired when a new entitlement is created
* `entitlement.updated` - Fired when entitlement details are updated
* `entitlement.deleted` - Fired when an entitlement is deleted
### Wallet Events
* `wallet.created` - Fired when a new wallet is created
* `wallet.updated` - Fired when wallet details are updated
* `wallet.terminated` - Fired when a wallet is terminated
* `wallet.transaction.created` - Fired when a new wallet transaction is created
### Payment Events
* `payment.created` - Fired when a new payment is created
* `payment.updated` - Fired when payment details are updated
* `payment.failed` - Fired when a payment attempt fails
* `payment.success` - Fired when a payment is successful
* `payment.pending` - Fired when a payment is awaiting processing
### Customer Events
* `customer.created` - Fired when a new customer is created
* `customer.updated` - Fired when customer information is updated
* `customer.deleted` - Fired when a customer is deleted
### Checkout Events
* `checkout.session.initiated` - Fired when a Checkout Session is created and the payment link is returned to the caller
* `checkout.session.completed` - Fired when payment is confirmed; the subscription is activated and the invoice is finalized
* `checkout.session.failed` - Fired when the payment fails or the provider cancels the payment link
* `checkout.session.expired` - Fired when the session window elapses without a completed payment
For more on how Checkout Sessions work and how to configure the payment provider webhook events that trigger these, see [Checkout Sessions](/docs/checkout/checkout-sessions).
*(More event types may be added in the future.)*
## Webhook Payload Format
Every webhook payload follows this structure:
```json theme={null}
{
"subscription": ""
}
```
* All event types use this base schema
* The payload field contains event-specific data
## How Webhook Delivery Works
### Native Webhook System
* **Default mode.**
* When a subscribed event occurs, Flexprice makes a simple HTTP POST to your provided webhook URL.
* A JSON payload with event data is sent.
* If your endpoint responds with a `2xx` status, the event is marked as delivered.
* If your endpoint fails (non-2xx, timeout, etc.), Flexprice attempts retries with the following configuration:
* Maximum retries: 3 (default)
* Initial interval: 1 second
* Maximum interval: 10 seconds
* Multiplier: 2.0
* Maximum elapsed time: 2 minutes
**Summary:**
Fast, direct, and simple. Good for lightweight use-cases or internal integrations.
### Svix Webhook System
* **Advanced mode** (opt-in; see [Svix docs](https://docs.svix.com/) for more on their delivery system).
* When enabled, Flexprice hands off event delivery to Svix.
* Svix provides:
* **Robust retries:** Automatic, exponential backoff, no event lost due to transient issues
* **Delivery guarantees:** At-least-once delivery, dead-letter queues for persistent failures
* **Message history and logs:** Track every delivery, status, and error online
* **Signature verification:** Every request is signed with HMAC-SHA256 for security
* **Versioning & secret management:** Change secrets or endpoint URLs safely without missed events
The retry schedule for failed deliveries is:
* 5 minutes after the first failure
* 30 minutes after the second failure
* 2 hours after the third failure
* 5 hours after the fourth failure
* 10 hours after the fifth failure
**Summary:**
Svix is the recommended choice for production, customer-facing, or high-uptime integrations. If you want reliability, observability, and security, use Svix.
## Setting Up Webhooks
To enable webhooks on flexprice:
1. Go to the **Webhooks** section in your Flexprice dashboard
2. Click the **Add Endpoint** button
3. Enter your endpoint URL (e.g., `https://play.svix.com/in/e_...`)
4. The endpoint will be created with a 0.0% error rate initially
5. You can monitor message delivery, view logs, and track activity in the dashboard
Your configured webhook endpoint will be displayed in the **Endpoints** tab, where you can:
* View the error rate for message delivery
* Access message logs with detailed delivery information
* See historical delivery attempts and statistics
* Monitor webhook activity in real-time
The **Event Catalog** tab shows all available event types you can subscribe to, with detailed descriptions of when each event is fired.
The **Logs** tab provides a comprehensive view of message delivery attempts, including:
* Event type
* Message ID
* Timestamp
* Delivery status
* Detailed error information if delivery fails
The **Activity** tab shows historical delivery metrics and attempts over time.
## Security & Verification
* Every payload is signed using HMAC-SHA256 (default) or Ed25519
* Verify signatures using:
* Svix's official SDKs
* Manual verification using `svix-signature`, `svix-id`, and `svix-timestamp` headers
* Automatic timestamp validation prevents replay attacks
* Optional IP address validation
For detailed signature verification instructions, see the [Svix signature docs](https://docs.svix.com/receiving/verifying-payloads).
## Best Practices
1. **Use Svix for Production**: If you're building a production system, use Svix for its reliability and security features.
2. **Always Verify Signatures**: Whether using native or Svix webhooks, always verify the authenticity of incoming webhooks.
3. **Process Asynchronously**: Handle webhook processing in the background to avoid timeouts.
4. **Be Idempotent**: Design your handlers to safely process duplicate events.
5. **Use HTTPS**: Always use HTTPS endpoints for webhook delivery.
6. **Monitor Deliveries**: Regularly check webhook logs for failed deliveries.
7. **Quick Response**: Your endpoint should respond within 5 seconds to avoid timeouts.
# Welcome to Flexprice
Source: https://docs.flexprice.io/docs/welcome-to-flexprice
Developers should focus on building, not billing
## What is Flexprice?
**Flexprice** is an open-source platform for metering, billing and feature management, built by developers, for developers. It handles the heavy lifting of usage tracking, feature access, and billing logic — so you don’t have to build them from scratch.
To get a quick overview of the product, watch this quick demo:
## Why Flexprice?
Traditional billing services are often rigid. They handle basic subscriptions or payments well, but anything beyond that (credit management, feature gating, custom invoice logic) forces developers to create workarounds. This means engineering teams end up writing and maintaining tons of custom code for things like prorated subscriptions, usage limits, time zone differences and tax calculations.
In short, billing becomes a developer problem when the tools don’t flex to your product’s needs, eating up valuable development time and causing endless maintenance headaches.
Plug-and-play SDKs for popular languages make integration seamless. Just send usage events, and Flexprice handles aggregation, metering, and billing logic in real time.
Self-host Flexprice on your own infrastructure for full transparency and control. There's no vendor lock-in or surprise fees.
Seat-based, usage-based, or hybrid models, Flexprice handles it all.
You can handle credit grants, tokens or any usage metric.
## Core Features
Define custom events like API calls or compute time. Aggregate usage in real-time with low-latency tracking. Built to handle countless events at peak load and more.
Grant prepaid or promotional credits programmatically. Set auto top-ups and expiration rules. Configure credit-based workflows without extra logic.
Define seat-based, usage-based, or hybrid models. Launch custom plans and per-customer overrides. Adapt pricing over time without new code.
Define features, set limits, and control access per customer. Enforce limits and gate access by usage. Enable or restrict features without manual code changes.
Generate accurate invoices from usage, subscriptions, or credits. Simplify reconciliation for finance teams. Provide customers full visibility into charges.
## Our SDKs
# Chargebee Connection Setup
Source: https://docs.flexprice.io/integrations/chargebee/connection-setup
Complete guide to setting up and configuring Chargebee connections in Flexprice
## Overview
A Chargebee connection in Flexprice stores encrypted credentials that allow the system to interact with your Chargebee account for:
* Creating item families, items, and item prices in Chargebee
* Synchronizing customers to Chargebee
* Syncing invoices to Chargebee for payment collection
* Receiving webhook notifications from Chargebee
## Prerequisites
Before setting up your Chargebee connection, ensure you have:
1. **Active Chargebee Account** - Sign up at [chargebee.com](https://chargebee.com)
2. **API Key** - Available in your Chargebee Dashboard
3. **Webhook Endpoint** - Configured in Chargebee Dashboard
4. **Flexprice Environment** - Valid tenant and environment IDs
## Step 1: Gather Chargebee Credentials
### Required Credentials
| Credential | Location in Chargebee Dashboard | Required | Purpose |
| -------------------- | ------------------------------------ | -------- | ------------------------------ |
| **Site Name** | Settings → API & Webhooks → Site | ✅ | Chargebee site identifier |
| **API Key** | Settings → API & Webhooks → API Keys | ✅ | API authentication |
| **Webhook Username** | Settings → API & Webhooks → Webhooks | ❌ | Basic Auth username (optional) |
| **Webhook Password** | Settings → API & Webhooks → Webhooks | ❌ | Basic Auth password (optional) |
### Finding Your Credentials
1. **Site Name:**
* Go to Chargebee Dashboard → Settings → API & Webhooks
* Your site name is displayed at the top (e.g., `acme-test`)
2. **API Key:**
* Go to Chargebee Dashboard → Settings → API & Webhooks → API Keys
* Copy your API key (starts with your site name)
* Use test API keys for development
3. **Webhook Credentials (Optional):**
* Go to Chargebee Dashboard → Settings → API & Webhooks → Webhooks
* Configure Basic Authentication for webhook security
* Copy the username and password
## Step 2: Configure Webhook Endpoint in Chargebee
### Webhook URL Format
```
https://api.cloud.flexprice.io/v1/webhooks/chargebee/tenant_01K1TJDVNSN7TWY8CZY870QMNV/env_01K1TJJF0CJR410C6QVPYQTNV0
```
### Required Webhook Events
Configure these events in your Chargebee webhook endpoint:
| Event Type | Purpose |
| ------------------- | ------------------------------------ |
| `payment_succeeded` | Track successful payment completions |
### Setting Up the Webhook
1. Go to Chargebee Dashboard → Settings → API & Webhooks → Webhooks
2. Click **+ Add New Webhook**
3. Enter your webhook URL
4. Select the required events listed above
5. Configure Basic Authentication (optional but recommended)
6. Click **Save**
## Step 3: Create Chargebee Connection
### Using Flexprice Dashboard
You can create a Chargebee connection directly from the Flexprice dashboard.
### API Request
**Endpoint:** `POST /api/v1/connections`
**Headers:**
```http theme={null}
Content-Type: application/json
Authorization: Bearer your_api_key
X-Tenant-ID: your_tenant_id
X-Environment-ID: your_environment_id
```
**Request Body:**
```json theme={null}
{
"name": "Chargebee Production",
"provider_type": "chargebee",
"encrypted_secret_data": {
"site": "acme-production",
"api_key": "acme-production_api_key_...",
"webhook_username": "webhook_user",
"webhook_password": "webhook_password"
}
}
```
### Response
```json theme={null}
{
"id": "conn_1234567890abcdef",
"name": "Chargebee Production",
"provider_type": "chargebee",
"status": "active",
"created_at": "2024-01-20T10:30:00Z",
"updated_at": "2024-01-20T10:30:00Z"
}
```
### Editing an Existing Connection
You can edit an existing Chargebee connection to update credentials, sync configuration, or webhook settings.
**Editable Fields:**
* **Connection Name**: Update the friendly name for the connection
* **API Key**: Update Chargebee API key if rotated
* **Site**: Update Chargebee site name
* **Sync Configuration**: Enable or disable invoice sync
* **Webhook Configuration**: Update webhook username and password
## Security Best Practices
### Credential Management
1. **Environment Separation**: Use different API keys for test/production
2. **Key Rotation**: Regularly rotate your Chargebee API keys
3. **Access Control**: Limit API key permissions in Chargebee
4. **Encryption**: All credentials are encrypted at rest in Flexprice
### Webhook Security
1. **HTTPS Only**: Always use HTTPS for webhook endpoints
2. **Basic Authentication**: Use Basic Auth for webhook verification (recommended)
3. **Secret Management**: Keep webhook credentials secure and rotate regularly
4. **Rate Limiting**: Implement rate limiting on webhook endpoints
## Troubleshooting
### Common Issues
| Issue | Cause | Solution |
| --------------------- | ---------------------------- | ----------------------------------------------- |
| Connection test fails | Invalid API key or site name | Verify credentials in Chargebee Dashboard |
| Webhook not received | Incorrect webhook URL | Check URL format and endpoint |
| Authentication fails | Wrong webhook credentials | Update webhook username/password in connection |
| API calls fail | Invalid site name | Verify site name matches your Chargebee account |
### Debug Steps
1. **Check Chargebee Dashboard**: Verify API key and site name
2. **Test API Key**: Use Chargebee API to test key validity
3. **Monitor Webhooks**: Check webhook delivery logs in Chargebee
4. **Review Logs**: Check Flexprice application logs for errors
## Next Steps
After setting up your Chargebee connection:
1. **Create Item Family**: Create an item family in Chargebee (required for plan sync)
2. **Sync Plans**: Sync your Flexprice plans to Chargebee
3. **Test Invoice Sync**: Create a test invoice and sync to Chargebee
4. **Monitor Webhooks**: Ensure webhook events are being received
5. **Go Live**: Switch to production credentials when ready
# Chargebee Customer Synchronization
Source: https://docs.flexprice.io/integrations/chargebee/customer-sync
Complete guide to customer synchronization between Flexprice and Chargebee
## Overview
Customer synchronization in Flexprice ensures that customers exist in Chargebee before invoice sync. Unlike other integrations, **customers are only synced when an invoice is being synced to Chargebee**, not during payment link creation or other operations.
### Key Concepts
* **On-Demand Sync**: Customers are synced only when invoice sync is triggered
* **Invoice-Triggered**: Sync happens automatically during invoice sync
* **Entity Integration Mapping**: Links Flexprice customers to Chargebee customer IDs
* **Metadata Storage**: Customer metadata stores cross-platform references
* **Duplicate Prevention**: Intelligent checks prevent duplicate customer creation
## Synchronization Flow
Customer sync is **invoice-triggered** and happens automatically:
```mermaid theme={null}
graph LR
A[Invoice Sync Request] --> B[Ensure Customer Synced to Chargebee]
B --> C{Check Metadata}
C -->|Has ID| D[Return Existing Customer]
C -->|No ID| E[Check Integration Mapping]
E --> F{Mapping Exists?}
F -->|Yes| G[Update Customer Metadata]
F -->|No| H[Create Customer in Chargebee]
G --> I[Return Customer Info]
H --> J[Update Metadata & Mapping]
J --> I
I --> K[Continue Invoice Sync]
```
### When Customer Sync Happens
Customer sync is triggered **only** when:
* An invoice is being synced to Chargebee
* Invoice outbound sync is enabled in the Chargebee connection
* Invoice status changes from `DRAFT` to `FINALIZED`
**Customer sync does NOT happen for:**
* Payment link creation
* Manual customer operations
* Other payment flows
## Customer Data Model
### Flexprice Customer Structure
```json theme={null}
{
"id": "cust_1234567890abcdef",
"external_id": "customer_external_123",
"name": "John Doe",
"email": "john@example.com",
"address_line1": "123 Main St",
"address_line2": "Apt 4B",
"address_city": "New York",
"address_state": "NY",
"address_postal_code": "10001",
"address_country": "US",
"metadata": {
"chargebee_customer_id": "cust_chargebee123abc"
},
"environment_id": "prod",
"tenant_id": "tenant_123",
"status": "active",
"created_at": "2024-01-20T10:30:00Z"
}
```
### Customer Creation in Chargebee
When creating a customer in Chargebee:
**Required Fields:**
* `email`: Customer email address
* `auto_collection`: Set to `"on"` (enables automatic payment collection)
**Optional Fields:**
* `first_name`: Customer name (from Flexprice customer name)
* `billing_address`: Address information if available
**Address Mapping:**
* `line1` → `billing_address.line1`
* `line2` → `billing_address.line2`
* `city` → `billing_address.city`
* `state` → `billing_address.state`
* `postal_code` → `billing_address.zip`
* `country` → `billing_address.country`
## Customer Linking
### Metadata Storage
Customer linking is maintained through metadata fields:
**In Flexprice Customer:**
```json theme={null}
{
"metadata": {
"chargebee_customer_id": "cust_chargebee123abc",
"chargebee_sync_timestamp": "2024-01-20T10:30:00Z"
}
}
```
### Entity Integration Mapping
Customer links are also stored in the entity integration mapping table:
* **Entity Type**: `customer`
* **Entity ID**: Flexprice customer ID
* **Provider Type**: `chargebee`
* **Provider Entity ID**: Chargebee customer ID
This provides a reliable lookup mechanism even if metadata is missing.
## Duplicate Prevention
### Email-Based Deduplication
The system prevents duplicate customers using email addresses:
1. **Check Existing**: Look for customers with same email in Chargebee
2. **Reuse Existing**: If found, use existing Chargebee customer
3. **Link Creation**: Create cross-platform links
4. **Audit Trail**: Log all sync operations
### External ID Matching
For customers with external IDs:
1. **Priority Matching**: External ID takes precedence over email
2. **Fallback to Email**: If no external ID match, use email
3. **Manual Resolution**: Conflicts require manual intervention
## Auto Collection
### Automatic Payment Collection
When creating customers in Chargebee, `auto_collection` is set to `"on"`:
**Benefits:**
* Chargebee automatically attempts to collect payment when invoices are due
* Reduces manual payment processing
* Improves cash flow
**Note**: This setting is required for Chargebee to process payments automatically.
## Error Handling
### Common Sync Errors
| Error | Cause | Resolution |
| ---------------------- | ---------------------------- | ----------------------------------- |
| "Customer not found" | Invalid customer ID | Verify customer exists in Flexprice |
| "Email already exists" | Duplicate email in Chargebee | System will reuse existing customer |
| "Invalid email format" | Malformed email address | Fix email format in Flexprice |
| "Chargebee API error" | Chargebee connection issue | Check Chargebee credentials |
### Sync Failure Recovery
When customer sync fails:
1. **Log Error**: Record failure with details
2. **Invoice Sync Fails**: Invoice sync cannot proceed without customer
3. **Manual Intervention**: Admin can retry customer sync
4. **Notification**: Alert on persistent failures
## Testing Customer Sync
### Test Environment Setup
1. **Test Chargebee Account**: Use Chargebee test mode
2. **Test Customers**: Create test customers in Flexprice only
3. **Test Invoice Sync**: Trigger invoice sync to test customer sync
### Testing Flow
1. **Create Test Customer**: Add customer in Flexprice (no sync yet)
2. **Verify No Sync**: Customer should NOT exist in Chargebee initially
3. **Create Invoice**: Create and finalize a test invoice
4. **Sync Invoice**: Trigger invoice sync to Chargebee
5. **Verify Auto-Sync**: Customer should now exist in Chargebee
6. **Verify Reuse**: Sync another invoice - should use existing customer
## Next Steps
After understanding customer sync:
1. **Create Customers**: Create customers in Flexprice as normal
2. **Create Invoices**: Create and finalize invoices
3. **Sync Invoices**: Invoice sync will automatically handle customer sync
4. **Verify Sync**: Check customers in Chargebee dashboard
For detailed information about invoice sync, see [Invoice Sync Documentation](/integrations/chargebee/invoice-sync).
# Chargebee Invoice Sync & Webhook Handling
Source: https://docs.flexprice.io/integrations/chargebee/invoice-sync
Complete guide to syncing invoices to Chargebee and handling webhooks
## Overview
Invoice sync in Flexprice sends invoices to Chargebee for payment processing. The sync process handles different pricing models (flat fee, per unit, tiered) with precision-aware amount handling to ensure accurate billing.
### Key Features
* **Automatic Sync**: Invoices sync automatically when finalized
* **Precision Handling**: Different strategies for flat fee vs tiered pricing
* **Customer Auto-Sync**: Customers are synced automatically during invoice sync
* **Webhook Processing**: Payment webhooks update invoice status automatically
* **Amount Accuracy**: Ensures exact amount matching with Flexprice calculations
## Invoice Sync Flow
```mermaid theme={null}
graph TD
A[Invoice Finalized] --> B[Check Chargebee Connection]
B --> C{Connection Enabled?}
C -->|No| D[Skip Sync]
C -->|Yes| E[Check Invoice Sync Enabled]
E --> F{Invoice Sync Enabled?}
F -->|No| D
F -->|Yes| G[Check Existing Mapping]
G --> H{Already Synced?}
H -->|Yes| I[Return Existing Invoice]
H -->|No| J[Ensure Customer Synced]
J --> K[Build Line Items]
K --> L[Create Invoice in Chargebee]
L --> M[Save Entity Mapping]
M --> N[Sync Complete]
```
## Invoice Sync Timing
Invoices are automatically synced to Chargebee when:
1. **Invoice Finalization**: Invoice status changes from `DRAFT` to `FINALIZED`
2. **Invoice Sync Enabled**: Invoice outbound sync is enabled in Chargebee connection
3. **Connection Active**: Chargebee connection exists and is active
**Manual Sync:**
* Invoices can also be synced manually via API if needed
## Line Item Handling
### Critical: Different Strategies by Pricing Model
Flexprice uses **different strategies** for sending line items to Chargebee based on the pricing model. This is crucial for maintaining precision and accuracy.
### Flat Fee / Per Unit / Package Pricing
For **non-tiered** pricing models (flat\_fee, per\_unit, package):
**Strategy:** Send `quantity=1` + exact `unit_amount`
```json theme={null}
{
"item_price_id": "price_123",
"quantity": 1,
"unit_amount": 1000 // Exact amount in smallest currency unit (cents)
}
```
**Why This Approach:**
* Chargebee has **2 decimal precision** limitations
* Sending exact amount as `unit_amount` ensures **perfect accuracy**
* Quantity=1 means "charge this exact amount once"
* Prevents rounding errors from quantity × unit\_price calculations
**Example:**
* Flexprice amount: \$10.50
* Sent to Chargebee: `quantity=1, unit_amount=1050` (cents)
* Result: Exact \$10.50 charge
### Tiered Pricing
For **tiered** pricing models (tiered, volume, stairstep):
**Strategy:** Send **only quantity**, let Chargebee calculate amount
```json theme={null}
{
"item_price_id": "price_123",
"quantity": 1500
// NO unit_amount - Chargebee calculates based on tiers
}
```
**Why This Approach:**
* Tier calculations involve complex multi-tier math
* Chargebee's tier calculation may differ slightly due to rounding
* Sending quantity allows Chargebee to apply its tier logic correctly
* Prevents precision mismatches from manual tier calculations
**Important Notes:**
* **DO NOT** set `unit_amount` for tiered pricing (Chargebee will reject it)
* Chargebee calculates the amount based on tier definitions
* There may be **slight differences** (usually \< \$0.01) due to rounding
* This is expected and acceptable for tiered pricing
**Example:**
* Flexprice quantity: 1500 units
* Flexprice calculated amount: \$125.75
* Sent to Chargebee: `quantity=1500` (no unit\_amount)
* Chargebee calculates: \$125.74 (may differ slightly)
* Difference: \$0.01 (acceptable rounding difference)
### Pricing Model Detection
The system automatically detects the pricing model:
1. **Lookup Item Price**: Retrieves Chargebee item price by Flexprice price ID
2. **Check Pricing Model**: Reads `pricing_model` from Chargebee item price
3. **Determine Strategy**:
* If `tiered`, `volume`, or `stairstep` → Send quantity only
* Otherwise → Send quantity=1 + unit\_amount
## Amount Conversion
### Currency Precision
All amounts are converted to smallest currency unit:
| Currency | Precision | Smallest Unit | Example |
| -------- | --------- | ------------- | -------------------- |
| USD | 2 | Cents | \$10.50 → 1050 cents |
| EUR | 2 | Cents | €10.50 → 1050 cents |
| INR | 2 | Paise | ₹10.50 → 1050 paise |
| JPY | 0 | Yen | ¥100 → 100 yen |
**Conversion Formula:**
```
amount_in_smallest_unit = round(amount × 10^precision)
```
### Why 2 Decimal Precision Matters
Chargebee stores all monetary values as integers in the smallest currency unit. This means:
* **2 decimal currencies** (USD, EUR, INR): Stored as cents/paise
* **0 decimal currencies** (JPY): Stored as whole units
**Precision Issues:**
* If we send `quantity × unit_price` for flat fees, rounding errors can occur
* Example: quantity=1, unit\_price=10.505 → rounds to 10.51 (wrong!)
* Solution: Send exact amount as `unit_amount` with `quantity=1`
## Invoice Creation
### Request Structure
```json theme={null}
{
"customer_id": "cust_chargebee123",
"auto_collection": "on",
"line_items": [
{
"item_price_id": "price_123",
"quantity": 1,
"unit_amount": 1000
}
],
"invoice_date": 1642680600,
"due_date": 1642767000
}
```
### Line Item Fields
| Field | Required | Description |
| --------------- | -------- | ----------------------------------------------------------- |
| `item_price_id` | ✅ | Chargebee item price ID (from entity mapping) |
| `quantity` | ✅ | Quantity (1 for flat fees, actual quantity for tiers) |
| `unit_amount` | ❌ | Unit amount in smallest currency unit (only for non-tiered) |
| `date_from` | ❌ | Period start date (Unix timestamp) |
| `date_to` | ❌ | Period end date (Unix timestamp) |
| `description` | ❌ | Line item description |
### Auto Collection
Invoices are created with `auto_collection: "on"`:
* Chargebee automatically attempts payment collection
* Uses customer's default payment method
* Sends payment reminders if needed
## Entity Mapping
### Invoice Mapping Storage
After syncing an invoice, Flexprice stores the mapping:
* **Entity Type**: `invoice`
* **Entity ID**: Flexprice invoice ID
* **Provider Type**: `chargebee`
* **Provider Entity ID**: Chargebee invoice ID
This mapping is used for:
* Webhook processing (finding Flexprice invoice from Chargebee invoice)
* Duplicate prevention (checking if invoice already synced)
* Status updates (updating Flexprice invoice from Chargebee)
## Webhook Handling
### Supported Webhooks
| Event Type | Purpose |
| ------------------- | ------------------------------------ |
| `payment_succeeded` | Track successful payment completions |
### Webhook URL Format
```
https://api.cloud.flexprice.io/v1/webhooks/chargebee/{tenant_id}/{environment_id}
```
### Webhook Authentication
Chargebee webhooks support Basic Authentication:
1. **Configure in Chargebee**: Set username and password in webhook settings
2. **Store in Connection**: Save credentials in Chargebee connection
3. **Verify on Receipt**: Flexprice verifies Basic Auth credentials
4. **Optional**: If not configured, webhooks are still accepted (less secure)
### Payment Succeeded Webhook
When a payment succeeds in Chargebee:
**Webhook Payload:**
```json theme={null}
{
"event_type": "payment_succeeded",
"content": {
"transaction": {
"id": "txn_123",
"amount": 1000,
"currency_code": "USD",
"payment_method": "card"
},
"invoice": {
"id": "inv_chargebee123"
}
}
}
```
**Processing Flow:**
1. **Receive Webhook**: Flexprice receives the webhook
2. **Verify Auth**: Verify Basic Authentication (if configured)
3. **Find Invoice**: Look up Flexprice invoice ID from Chargebee invoice ID
4. **Check Duplicate**: Verify payment doesn't already exist
5. **Create Payment**: Create payment record in Flexprice
6. **Update Invoice**: Update invoice payment status and amounts
7. **Reconcile**: Reconcile invoice with payment
### Payment Record Creation
When processing a payment webhook:
**Payment Record:**
```json theme={null}
{
"id": "pay_123",
"destination_type": "INVOICE",
"destination_id": "inv_flexprice123",
"amount": "10.00",
"currency": "USD",
"payment_status": "SUCCEEDED",
"payment_gateway": "chargebee",
"gateway_payment_id": "txn_123",
"succeeded_at": "2024-01-20T10:30:00Z"
}
```
**Idempotency:**
* Payments are checked for duplicates using `gateway_payment_id`
* Prevents duplicate payment records from webhook retries
## Error Handling
### Common Sync Errors
| Error | Cause | Resolution |
| ------------------------ | ----------------------------- | ----------------------------------- |
| "Customer not found" | Customer not synced | Customer sync happens automatically |
| "Item price not found" | Price not synced to Chargebee | Sync plan to Chargebee first |
| "Invalid amount" | Amount precision issue | Check amount conversion |
| "Invoice already synced" | Duplicate sync attempt | Use existing Chargebee invoice |
### Webhook Errors
| Error | Cause | Resolution |
| ------------------------ | ---------------------------- | ------------------------------------- |
| "Authentication failed" | Wrong Basic Auth credentials | Update webhook credentials |
| "Invoice not found" | Invoice mapping missing | Check entity integration mapping |
| "Payment already exists" | Duplicate webhook | Idempotency check prevents duplicates |
## Best Practices
### Invoice Sync
1. **Sync Plans First**: Always sync plans to Chargebee before creating invoices
2. **Verify Items**: Ensure item prices exist in Chargebee
3. **Check Amounts**: Verify amounts match between Flexprice and Chargebee
### Amount Precision
1. **Flat Fees**: Always use quantity=1 + exact unit\_amount
2. **Tiered Pricing**: Accept small rounding differences (\< \$0.01)
3. **Currency Awareness**: Be aware of currency precision (2 vs 0 decimals)
4. **Testing**: Test with various amounts to verify precision
## Testing
### Test Invoice Sync
1. **Create Test Invoice**: Create invoice in Flexprice
2. **Finalize Invoice**: Change status to FINALIZED
3. **Verify Sync**: Check Chargebee dashboard for invoice
4. **Verify Amounts**: Compare amounts between Flexprice and Chargebee
5. **Test Payment**: Process test payment in Chargebee
6. **Verify Webhook**: Check payment record in Flexprice
## Next Steps
After setting up invoice sync:
1. **Sync Plans**: Ensure all plans are synced to Chargebee
2. **Create Invoices**: Create and finalize invoices
3. **Monitor Sync**: Check sync status and logs
4. **Test Payments**: Process test payments and verify webhooks
5. **Go Live**: Switch to production when ready
For detailed information about item sync, see [Item Sync Documentation](/integrations/chargebee/item-sync).
# Chargebee Item Sync
Source: https://docs.flexprice.io/integrations/chargebee/item-sync
Complete guide to syncing item families, items, and item prices to Chargebee
## Overview
Item sync in Flexprice creates the necessary Chargebee infrastructure (item families, items, and item prices) to support invoice synchronization. This enables Flexprice plans and prices to be represented in Chargebee's item-based pricing model.
### Key Concepts
* **Item Family**: A container for related items in Chargebee (required before syncing plans)
* **Item**: Represents a chargeable entity (one per price/currency combination)
* **Item Price**: Defines the pricing model and rates for an item
* **One Item Per Currency**: Chargebee only allows one item price per currency per item, so we create separate items for each price
## Item Family
### Overview
An item family is a container that groups related items together in Chargebee. You must create at least one item family in Chargebee before syncing plans.
### Creating an Item Family
**In Chargebee Dashboard:**
1. Go to Products → Item Families
2. Click **+ Add Item Family**
3. Enter a name (e.g., "Flexprice Charges")
4. Click **Save**
**Via API:**
```json theme={null}
{
"id": "item_family_123",
"name": "Flexprice Charges"
}
```
### Item Family Selection
When syncing plans, Flexprice automatically:
1. Retrieves all item families from Chargebee
2. Selects the **latest** item family (most recently created)
3. Uses it for all synced items
**Note**: Ensure you have at least one item family in Chargebee before syncing plans.
## Item Sync Process
### Plan Sync Flow
When you sync a Flexprice plan to Chargebee:
```mermaid theme={null}
graph TD
A[Sync Plan Request] --> B[Get Latest Item Family]
B --> C{Item Family Exists?}
C -->|No| D[Error: Create Item Family First]
C -->|Yes| E[For Each Price in Plan]
E --> F[Create Item]
F --> G[Create Item Price]
G --> H[Save Entity Mapping]
H --> I{More Prices?}
I -->|Yes| E
I -->|No| J[Sync Complete]
```
### Item Creation
For each price in a plan, Flexprice creates:
1. **One Item** (type: `charge`)
* ID format: `charge_{uuid}`
* Name: Same as ID (must be unique)
* Type: `charge` (for one-time/recurring charges)
* Item Family: Latest item family from Chargebee
* External Name: `{display_name} - {currency}` (e.g., "API Calls - USD")
2. **One Item Price** (linked to the item)
* ID: Flexprice price ID (for easy mapping)
* Item ID: The created item ID
* External Name: `{display_name} - {currency}`
* Currency: Price currency
* Pricing Model: Mapped from Flexprice billing model
### Why Separate Items Per Price?
Chargebee has a limitation: **only one item price per currency per item**. To support multiple currencies or multiple prices per plan, we create separate items for each price.
**Example:**
* Plan: "Pro Plan" with prices in USD and EUR
* Result: 2 items created (`charge_uuid1` for USD, `charge_uuid2` for EUR)
## Item Price Sync
### Pricing Model Mapping
Flexprice billing models are mapped to Chargebee pricing models:
| Flexprice Billing Model | Flexprice Type | Chargebee Pricing Model |
| ----------------------- | -------------- | ----------------------- |
| `FLAT_FEE` | `FIXED` | `flat_fee` |
| `FLAT_FEE` | `USAGE` | `per_unit` |
| `PACKAGE` | - | `package` |
| `TIERED` | `VOLUME` | `volume` |
| `TIERED` | `SLAB` | `tiered` |
### Flat Fee / Per Unit Pricing
For flat fee and per unit pricing:
```json theme={null}
{
"id": "price_123",
"item_id": "charge_uuid1",
"pricing_model": "flat_fee",
"price": 1000, // Amount in smallest currency unit (cents for USD)
"currency_code": "USD"
}
```
**Details:**
* `price`: Amount converted to smallest currency unit (cents for USD, paise for INR)
* No tiers required
* Simple fixed pricing
### Package Pricing
For package pricing:
```json theme={null}
{
"id": "price_123",
"item_id": "charge_uuid1",
"pricing_model": "package",
"price": 1000,
"currency_code": "USD",
"period": 10, // Optional: package size
"period_unit": "month"
}
```
**Details:**
* `price`: Package price in smallest currency unit
* `period`: Optional package size (e.g., 10 units per package)
* Used for bulk pricing models
### Tiered Pricing
For tiered/volume pricing:
```json theme={null}
{
"id": "price_123",
"item_id": "charge_uuid1",
"pricing_model": "volume",
"currency_code": "USD",
"tiers": [
{
"starting_unit": 1,
"ending_unit": 1000,
"price": 100 // Price per unit in this tier (in cents)
},
{
"starting_unit": 1001,
"ending_unit": 10000,
"price": 80
},
{
"starting_unit": 10001,
"ending_unit": null, // Last tier has no ending
"price": 60
}
]
}
```
**Details:**
* `tiers`: Array of tier definitions
* `starting_unit`: Minimum quantity for this tier (1-based)
* `ending_unit`: Maximum quantity for this tier (null for last tier)
* `price`: Price per unit in this tier (in smallest currency unit)
* Tiers are converted from Flexprice tier format to Chargebee format
### Tier Conversion
Flexprice tiers are converted to Chargebee format:
**Flexprice Tier:**
```json theme={null}
{
"up_to": 1000,
"unit_amount": 1.00
}
```
**Chargebee Tier:**
```json theme={null}
{
"starting_unit": 1,
"ending_unit": 1000,
"price": 100 // 1.00 USD = 100 cents
}
```
**Conversion Rules:**
1. `unit_amount` converted to smallest currency unit (e.g., \$1.00 → 100 cents)
2. `up_to` becomes `ending_unit`
3. `starting_unit` calculated from previous tier's `ending_unit + 1`
4. First tier always starts at 1
5. Last tier has `ending_unit: null`
## Amount Conversion
### Currency Precision
All amounts are converted to the smallest currency unit based on currency precision:
| Currency | Precision | Smallest Unit | Example |
| -------- | --------- | ------------- | ------------------ |
| USD | 2 | Cents | \$1.00 → 100 cents |
| EUR | 2 | Cents | €1.00 → 100 cents |
| INR | 2 | Paise | ₹1.00 → 100 paise |
| JPY | 0 | Yen | ¥100 → 100 yen |
**Formula:**
```
amount_in_smallest_unit = amount × 10^precision
```
### Why Convert to Smallest Unit?
Chargebee stores all prices as integers in the smallest currency unit to avoid floating-point precision issues. This ensures accurate calculations and prevents rounding errors.
## Entity Mapping
### Mapping Storage
After syncing items and item prices, Flexprice stores entity mappings:
**Price → Item Price Mapping:**
* Entity Type: `item_price`
* Entity ID: Flexprice price ID
* Provider Entity ID: Chargebee item price ID
* Provider Type: `chargebee`
This mapping is used during invoice sync to find the correct Chargebee item price for each Flexprice price.
## Display Names
### External Names
Items and item prices use external names for better invoice display:
**Format:** `{display_name} - {currency}`
**Examples:**
* "API Calls - USD"
* "Pro Plan - EUR"
* "Storage - INR"
**Display Name Priority:**
1. Feature name (if price has a meter/feature)
2. Meter name (if feature not found)
3. Plan name (fallback)
## Error Handling
### Common Sync Errors
| Error | Cause | Solution |
| ------------------------ | --------------------------- | --------------------------------------------- |
| "Item family not found" | No item family in Chargebee | Create an item family first |
| "Item already exists" | Duplicate item ID | Item IDs are auto-generated, should not occur |
| "Invalid pricing model" | Unsupported billing model | Check billing model mapping |
| "Currency not supported" | Invalid currency code | Verify currency is supported by Chargebee |
### Sync Failure Recovery
When item sync fails:
1. **Partial Sync**: Some items may be created even if others fail
2. **Retry**: Re-sync the plan to create missing items
3. **Manual Cleanup**: Remove failed items from Chargebee if needed
4. **Logs**: Check application logs for detailed error messages
## Best Practices
### Item Family Management
1. **Single Family**: Use one item family for all Flexprice items (simplifies management)
2. **Naming**: Use descriptive names (e.g., "Flexprice Charges")
3. **Organization**: Group related items in the same family
### Price Sync Strategy
1. **Sync Before Invoicing**: Always sync plans before creating invoices
2. **Currency Separation**: Each currency requires a separate item
3. **Tier Precision**: Be aware that tier calculations may have slight differences due to rounding
### Testing
1. **Test Environment**: Use Chargebee test mode for development
2. **Verify Items**: Check created items in Chargebee dashboard
3. **Test Invoices**: Create test invoices to verify item prices work correctly
## Next Steps
After syncing items:
1. **Verify Items**: Check created items in Chargebee dashboard
2. **Test Invoice Sync**: Create a test invoice and sync to Chargebee
3. **Monitor Mappings**: Ensure entity mappings are created correctly
4. **Go Live**: Switch to production when ready
For detailed information about invoice sync, see [Invoice Sync Documentation](/integrations/chargebee/invoice-sync).
# Introduction
Source: https://docs.flexprice.io/integrations/integrations
Connect Flexprice with your favorite tools and platforms to streamline your pricing and revenue operations
## Overview
Flexprice integrations allow you to seamlessly connect with third-party platforms and services to automate your pricing workflows, synchronize customer data, and optimize revenue operations. Our robust integration ecosystem ensures that Flexprice works harmoniously with your existing technology stack.
## Why Use Integrations?
Automate pricing updates, customer synchronization, and billing processes across platforms.
Keep customer data, pricing, and billing information synchronized in real-time.
Reduce manual work and eliminate data silos between your business tools.
Get comprehensive insights by combining data from multiple platforms.
## Available Integrations
Connect with Stripe for payment processing, customer sync, and billing workflows.
Connect with Razorpay for payment processing, customer sync, and billing workflows.
Connect with Chargebee for payment processing, item sync, customer sync, and invoice management.
Connect with QuickBooks for item sync, customer sync, invoice management, and payment reconciliation.
Connect with Nomod to generate payment links for invoices, push invoices to Nomod for collection, and receive payment events via Zapier.
Connect with Moyasar to generate invoices, push invoices along with payment sync to Moyasar for collection.
Connect with Paddle for payment processing, customer sync, invoice sync, and Flexprice Checkout (Paddle-hosted overlay).
Connect with Zoho Books for OAuth, invoice sync to Zoho, webhooks for paid and voided invoices, and optional inbound customer sync from contacts.
# How to Set Up Charge Automatically
Source: https://docs.flexprice.io/integrations/moyasar/autopay
Save a customer card via Flexprice checkout and charge invoices automatically
Autopay lets Flexprice charge a customer's saved card whenever a new invoice is due, without any action from the customer. The card is captured once through the Flexprice checkout page, stored as a payment method, and reused for all subsequent invoice payments.
Autopay requires an active Moyasar connection with a valid Secret Key, Publishable Key, and Webhook Secret. See [Moyasar Connection Setup](/integrations/moyasar/connection-setup) before proceeding.
## How it Works
Flexprice creates a payment record to track the card save attempt and returns a secure checkout URL.
The customer is redirected to the Flexprice checkout page and enters their card number, expiry, and CVC.
Flexprice runs a 1 SAR verification charge to confirm the card is valid. Once confirmed, the charge is voided immediately. If the void fails, Flexprice issues a refund automatically. The customer is not charged.
Flexprice receives confirmation, extracts the reusable card token, and saves it as an active payment method linked to the customer.
When a new invoice is generated for the customer, Flexprice charges the saved card directly. No redirect or manual payment step is required.
***
## Setting Up Autopay for a Customer
### Using the Flexprice Dashboard
Go to **Billing** → **Customers** and select the customer.
In the **Integrations** section, locate the Moyasar entry and click **Setup Autopay**.
Flexprice opens the checkout page in the current tab. The customer sees a card entry form.
### Using the API
```bash theme={null}
POST /v1/payments/customers/{customer_id}/setup/intent
```
**Request body:**
```json theme={null}
{
"provider": "moyasar",
"success_url": "https://yourapp.com/customers/cust_01KVZ9HE6XC2AH7CXE6WK8SXK8"
}
```
| Field | Type | Required | Description |
| ------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------- |
| `provider` | string | Yes | Must be `moyasar` |
| `success_url` | string | No | URL the customer is redirected to after the card is saved. Must be on the same origin as your application. |
**Response:**
```json theme={null}
{
"status": "pending_card_entry",
"customer_id": "cust_01KVZ9HE6XC2AH7CXE6WK8SXK8",
"checkout_url": "https://admin.flexprice.io/checkout?provider=moyasar&token=eyJhbGci..."
}
```
Redirect the customer to `checkout_url`. The JWT embedded in the URL expires after 2 hours.
***
## Card Entry
The customer is taken to the Flexprice checkout page where they enter their card details.
If the card issuer requires 3DS authentication, the customer is redirected to their bank's verification page and returned automatically after completing it.
***
## Payment Tracking
Every autopay attempt is tracked as a payment record in Flexprice from the moment the setup intent is created. Each invoice charge attempt also creates its own record, so you can see exactly which invoices were collected automatically.
### Payment Status
| Status | When it occurs |
| ----------- | ---------------------------------------------------------------------------- |
| `INITIATED` | Setup intent is created. Waiting for the customer to complete the card form. |
| `SUCCEEDED` | Card confirmed. Token saved. |
| `VOIDED` | The 1 SAR verification charge was voided successfully. |
| `REFUNDED` | The 1 SAR charge could not be voided and was refunded instead. |
| `FAILED` | Card authentication or the verification charge failed. |
***
## Automatic Invoice Charging
Once a customer has a saved payment method, Flexprice charges it automatically when a new invoice is generated.
A new invoice is created for the customer.
Flexprice looks for the customer's default saved payment method. If no default is set, the most recently added active payment method is used.
If a valid saved method is found, Flexprice creates a charge request using the stored token. The customer does not need to take any action.
If no active saved payment method exists, Flexprice falls back to the invoice-link flow. See [Moyasar Invoice Synchronization](/integrations/moyasar/invoice-sync).
When the charge is confirmed, Flexprice marks the invoice as `PAID`.
***
## Troubleshooting
| Issue | Likely Cause | Resolution |
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| Customer completed the card form but invoices are still not being charged automatically | The card save did not complete successfully | Run the setup autopay flow again for the customer |
| A 1 SAR charge appeared on the customer's card but was not returned | The reversal is processed automatically and typically settles within 1-5 business days depending on the card issuer | No action needed. If it does not settle, contact Moyasar support with the payment reference |
| Autopay is charging the wrong card | The customer has more than one saved card | Make sure the correct card is set as the default payment method |
| A card was saved but automatic charging has stopped working | The Moyasar connection credentials may have changed or the connection is inactive | Go to **Integrations** → **Moyasar** and verify the connection is active. Re-enter credentials if needed |
| The checkout URL expired before the customer completed the form | The link is valid for 2 hours from the time it was created | Run the setup autopay flow again to generate a new link |
# Moyasar Connection Setup
Source: https://docs.flexprice.io/integrations/moyasar/connection-setup
Complete guide to setting up and configuring Moyasar connections in Flexprice
## Overview
A Moyasar connection in Flexprice stores encrypted credentials so Flexprice can sync invoices to Moyasar, generate payment links, and enable autopay via the Flexprice checkout page.
### Steps required
Get your Secret Key, Publishable Key, and Webhook Secret from the Moyasar Dashboard.
Add the webhook URL in Moyasar, select `payment_paid`, and copy the Webhook Secret.
In the Flexprice dashboard (Integrations → Moyasar → Add a connection), enter your Secret Key, Publishable Key, and Webhook Secret.
## Gather Moyasar Credentials
### Required Credentials
| Credential | Location in Moyasar Dashboard | Required | Purpose |
| ------------------- | -------------------------------------- | -------- | ------------------------------------------------------------- |
| **Secret Key** | Settings → API Keys | ✅ | API authentication for all server-side operations |
| **Publishable Key** | Settings → API Keys | ✅ | Required for the Flexprice checkout page (autopay card entry) |
| **Webhook Secret** | Settings → Webhooks → \[Your Endpoint] | ✅ | Webhook signature verification |
### Finding Your Credentials
Go to Moyasar Dashboard → Settings → API Keys. Copy both your **Secret Key** (starts with `sk_`) and your **Publishable Key** (starts with `pk_`). Both are required.
Go to Moyasar Dashboard → Settings → Webhooks. Create or select your webhook endpoint and copy the webhook secret (used to verify that events are from Moyasar).
### Required Webhook Events
Configure the following events so Flexprice can update invoice status when payments occur:
| Event Type | Purpose |
| -------------- | ------------------------------------ |
| `payment_paid` | Track successful payment completions |
### Webhook configuration endpoint URL
In the Moyasar Dashboard, use this Flexprice webhook URL when creating your webhook.
```
https://api.cloud.flexprice.io/v1/webhooks/moyasar/tenant_YOUR_TENANT_ID/env_YOUR_ENV_ID
```
*(If you use a different Flexprice region, use the base URL for your region instead of `https://api.cloud.flexprice.io`.)*
### Setting Up the Webhook
Go to Moyasar Dashboard → Settings → Webhooks and click **+ Add New Webhook**.
Enter the Flexprice webhook endpoint URL (see **Webhook configuration endpoint URL** above) and select `payment_paid`.
Click **Create Webhook**, then copy the **Webhook Secret** and save it for your Flexprice connection.
## Create Moyasar Connection
### Using Flexprice Dashboard
Go to **Flexprice dashboard** → **Integrations** → **Moyasar** → **Add a connection**.
Enter your Secret Key, Publishable Key, and Webhook Secret.
Click to create the connection. Flexprice will store your credentials securely.
## Connection Configuration
### Default Currency (required in Moyasar)
The default currency for Moyasar is **SAR** (Saudi Riyal).
* **Supported Currencies**: SAR, USD, EUR, KWD, etc.
* **Currency Precision**: Moyasar requires amounts in the **smallest currency unit** (e.g., Halalah for SAR). Flexprice handles this conversion automatically.
### Currency Precision Table
| Currency | Precision | Unit | Example |
| -------- | --------- | ------- | ------------------ |
| SAR | 2 | Halalah | 10.00 SAR = 1000 |
| USD | 2 | Cents | 10.00 USD = 1000 |
| KWD | 3 | Fils | 10.000 KWD = 10000 |
| JPY | 0 | Yen | 1000 JPY = 1000 |
## Security Best Practices
Webhooks must use HTTPS.
Always configure the `webhook_secret` to enable HMAC-SHA256 signature verification.
Rotate API keys periodically from the Moyasar dashboard.
## Troubleshooting
| Issue | Cause | Solution |
| ---------------------------- | ---------------------- | ------------------------------------------------- |
| Connection test fails | Invalid `Secret Key` | Verify the key in Moyasar Dashboard. |
| Webhook not updating invoice | Event type mismatch | Ensure specific `payment_paid` event is selected. |
| Webhook signature error | Wrong `Webhook Secret` | Update the secret in Flexprice connection. |
| Invoice not syncing | Missing credentials | Ensure connection is active and valid. |
# Moyasar Invoice Synchronization
Source: https://docs.flexprice.io/integrations/moyasar/invoice-sync
Complete guide to invoice synchronization between Flexprice and Moyasar
## Overview
Moyasar integration in Flexprice syncs invoices to Moyasar and generates a payment link (Moyasar invoice URL) for each synced invoice.
## Synchronization Flow
The sync process is **one-way** (Flexprice → Moyasar) and works as follows:
```mermaid theme={null}
graph LR
A[Flexprice Invoice] -->|Sync Request| B[Moyasar API]
B -->|Create Invoice| C[Moyasar Invoice]
C -->|Return URL| A
C -->|Payment Link| D[Customer Payment]
D -->|Webhook (payment_paid)| E[Flexprice Updates Status]
```
### How it Works
An invoice sync is triggered (automatically).
Flexprice creates an invoice object in Moyasar using the `secret_key`.
Moyasar returns a `url` (the payment page).
Flexprice saves the Moyasar invoice ID and payment URL in the invoice metadata.
## Invoice Data Model
### Flexprice Invoice Structure
```json theme={null}
{
"id": "inv_123",
"amount": "100.00",
"currency": "SAR",
"metadata": {
"moyasar_invoice_id": "inv_moy_xyz",
"moyasar_invoice_url": "https://moyasar.com/i/inv_moy_xyz"
}
}
```
### Moyasar Invoice Structure
```json theme={null}
{
"id": "inv_moy_xyz",
"status": "initiated",
"amount": 10000,
"currency": "SAR",
"description": "Invoice for Premium Plan",
"url": "https://moyasar.com/i/inv_moy_xyz",
"metadata": {
"flexprice_invoice_id": "inv_123"
}
}
```
## Sync Configuration
The following fields are mapped during sync:
| Field | Flexprice → Moyasar | Notes |
| ------------- | ------------------- | -------------------------------------------- |
| `amount` | ✅ | Converted to smallest unit (e.g., Halalah) |
| `currency` | ✅ | Passed as-is (e.g., SAR) |
| `description` | ✅ | Summary of line items |
| `metadata` | ✅ | Includes `flexprice_invoice_id` for tracking |
## Payment Flow & Webhooks
### Payment Journey
User clicks the `moyasar_invoice_url`.
Payment is processed on Moyasar's hosted page.
Moyasar sends a `payment_paid` webhook.
Flexprice receives the webhook, finds the invoice via the metadata/mapping, and updates the invoice status to `PAID`.
## Error Handling
| Error | Cause |
| ----------------------------------- | ------------------------------------------------------------------- |
| `Moyasar connection not configured` | No active Moyasar connection found for the tenant/env. |
| `Currency not supported` | Invoice currency (e.g., INR) not supported by your Moyasar account. |
| `Invalid amount` | Amount must be positive. |
## Testing
Create a Flexprice invoice.
Trigger the sync to Moyasar.
Check invoice metadata for `moyasar_invoice_url`.
Open the URL and pay using a **Test Card**.
### Test Cards (Moyasar Sandbox)
| Number | Expiry | CVC | Result |
| --------------------- | ------ | --- | ------- |
| `4111 1111 1111 1111` | Future | 123 | Success |
| `4000 0000 0000 0002` | Future | 123 | Failure |
# Nomod Connection Setup
Source: https://docs.flexprice.io/integrations/nomod/connection-setup
Complete guide to setting up and configuring Nomod connections in Flexprice
## Overview
A Nomod connection in Flexprice lets you:
* **Sync customers** from Flexprice to Nomod
* **Sync invoices** from Flexprice to Nomod
* **Create payment links** for invoices so customers can pay via Nomod
* **Receive payment** (e.g. link paid, invoice paid) in Flexprice
* **Process payments** through Nomod’s secure checkout
## Prerequisites
Before you start, have:
1. **Nomod account** – [Nomod app](https://nomod.com) ([Play Store](https://nomod.app/g) / [App Store](https://nomod.app/a))
2. **Flexprice account** – [https://flexprice.io/](https://flexprice.io/)
## Keys you need
| Key | Where to get it | What it’s for |
| ------------------ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| **Nomod API key** | Nomod app → **Connect and Manage Integrations** | Payment links, invoices, and Nomod integration in Flexprice |
| **Zapier API key** | Nomod app - **Settings** → **Business** → **Connect apps and manage keys** → **Zapier** | Contact Flexprice team to set up the Zapier Zap and webhook configuration |
## Create Nomod Connection
### Using Flexprice Dashboard
Go to **Flexprice dashboard** → **Integrations** → **Nomod** → **Add a connection**.
### Setup at a glance
Choose a name for this Nomod connection so you can identify it in Flexprice.
Get your **Nomod API key** from the Nomod app (Connect and Manage Integrations).
**Invoice sync = true** means Flexprice pushes invoices to Nomod.
**Invoice sync = false** means Flexprice does not push invoices to Nomod (invoices stay only in Flexprice).
Use any secure random name or string for webhook verification and provide it to the Flexprice team as well.
Nomod does not support webhooks. For webhook configuration, contact us.
### Why Zapier?
Nomod does not support webhooks directly.
Zapier offers a pre-built Nomod app for webhook events.
Provide your **Zapier API key** and **webhook secret** to Flexprice so it can receive Nomod events (e.g. Link Paid, Invoice Paid).
Flexprice creates and manages the Zapier connection for you. Contact the Flexprice team to set up the Zapier Zap and webhook configuration.
## Configure Connection Settings
### Editing a Connection
Navigate to the Nomod integration page
Find your connection in the "Connected Accounts" list
Click the edit icon (pencil)
Modify settings as needed
Click **"Update Connection"**
**Note**: API Key and Webhook Secret cannot be edited after creation for security reasons.
### Deleting a Connection
Navigate to the Nomod integration page
Find your connection in the "Connected Accounts" list
Click the delete icon (trash) for the connection you want to remove.
Confirm deletion in the dialog. The connection will be removed and Nomod payment processing for it will stop.
**Warning:** Deleting a connection will disable all Nomod payment processing for that connection.
## Security Best Practices
### Credential Management
1. **Environment Separation**: Use different keys for test/production
2. **Key Rotation**: Regularly rotate your Nomod API keys
3. **Access Control**: Limit API key permissions in Nomod
4. **Encryption**: All credentials are encrypted at rest in Flexprice
## Troubleshooting
### Common Issues
| Issue | Cause | Solution |
| ----------------------------- | --------------------------------- | ---------------------------------------------------------------------------------- |
| Connection creation fails | Invalid Nomod API key | Verify key in Nomod app → Connect and Manage Integrations |
| Events not reaching Flexprice | Invalid or missing Zapier API key | Verify Zapier API key in the connection; Flexprice uses it for webhook integration |
| Invoice sync fails | Sync not enabled | Enable invoice outbound sync in the connection |
## Next steps
After your Nomod connection is set up:
1. **Test payment links** – Create a test payment link and confirm the flow works.
2. **Check events** – Ensure your Zapier API key is correct and events reach Flexprice (Flexprice handles the Zapier integration).
3. **Enable invoice sync** – If you want invoices pushed to Nomod, turn on **Push to Nomod** in the connection.
4. **Go live** – When ready, switch to production keys in Nomod and Flexprice.
# Nomod Customer Synchronization
Source: https://docs.flexprice.io/integrations/nomod/customer-sync
Complete guide to customer synchronization between Flexprice and Nomod
## Overview
Customer synchronization in Flexprice ensures that customers exist in Nomod before invoice sync and payment link. **Customers are synced to Nomod when invoices are being synced**, ensuring proper invoice-to-customer association in Nomod.
### Key Concepts
* **Invoice- and Payment-Link-Triggered Sync**: Customers are synced only when invoice sync or payment link is triggered
* **On-Demand Sync**: Sync happens automatically during invoice sync
* **One-Way Sync**: Customers are synced from Flexprice to Nomod only (not bidirectional)
* **Entity Integration Mapping**: Links Flexprice customers to Nomod customer IDs
* **Metadata Storage**: Customer metadata stores cross-platform references
* **Duplicate Prevention**: Intelligent checks prevent duplicate customer creation in Nomod
## Synchronization Flow
Customer sync is **invoice- or payment-link-triggered** and happens automatically:
```mermaid theme={null}
graph LR
A[Invoice Sync or Payment Link Request] --> B[Ensure Customer Synced to Nomod]
B --> C{Check Metadata}
C -->|Has ID| D[Return Existing Customer]
C -->|No ID| E[Check Integration Mapping]
E --> F{Mapping Exists?}
F -->|Yes| G[Update Customer Metadata]
F -->|No| H[Create Customer in Nomod]
G --> I[Return Customer Info]
H --> J[Update Metadata & Mapping]
J --> I
I --> K[Continue Invoice Sync or Payment Link]
```
### When Customer Sync Happens
Customer sync is triggered **only** when:
* An invoice is being synced to Nomod
* A payment link is requested for an invoice
* Invoice outbound sync is enabled in the Nomod connection (`invoice_sync.outbound = true`) when syncing invoices
* Invoice status changes from `DRAFT` to `FINALIZED` (for invoice sync)
**Customer sync does NOT happen for:**
* Manual customer operations
* Other flows that do not involve invoice sync or payment link
## Customer Data Model
### Flexprice Customer Structure
```json theme={null}
{
"id": "cust_1234567890abcdef",
"name": "John Doe",
"email": "john@example.com",
"metadata": {
"nomod_customer_id": "cust_nomod123abc",
"nomod_sync_timestamp": "2024-01-20T10:30:00Z"
},
"environment_id": "prod",
"tenant_id": "tenant_123",
"status": "active",
"created_at": "2024-01-20T10:30:00Z"
}
```
## Sync Configuration
### Required Fields for Sync
| Field | Flexprice → Nomod | Notes |
| ------- | ----------------- | ------------------------------------------------------------ |
| `name` | ✅ | Customer full name (required for Nomod customer creation) |
| `email` | ✅ | Primary email address (required for Nomod customer creation) |
### Optional Fields
| Field | Description |
| ------------- | ---------------------------- |
| `phone` | Phone number (E.164 format) |
| `address` | Customer address information |
| `external_id` | Your internal customer ID |
| `metadata` | Custom key-value pairs |
## Sync Process Details
### Invoice Sync and Payment Link Flow (Customer-Triggered)
```mermaid theme={null}
graph TD
A[Invoice Finalized or Payment Link Request] --> B{Invoice Sync / Payment Link Enabled?}
B -->|No| C[Skip Sync]
B -->|Yes| D[Ensure Customer Synced to Nomod]
D --> E{Check Metadata}
E -->|Has nomod_customer_id| F[Return Existing Customer]
E -->|No ID| G[Check Integration Mapping]
G --> H{Mapping Exists?}
H -->|Yes| I[Update Customer Metadata]
H -->|No| J[Create Customer in Nomod]
I --> K[Return Customer Info]
J --> L[Update Metadata & Mapping]
L --> K
K --> M[Create Invoice in Nomod or Return Payment Link]
```
## Customer Linking
### Metadata Storage
Customer linking is maintained through metadata fields:
**In Flexprice Customer:**
```json theme={null}
{
"metadata": {
"nomod_customer_id": "cust_nomod123abc",
"nomod_sync_timestamp": "2024-01-20T10:30:00Z"
}
}
```
**In Nomod Customer:**
```json theme={null}
{
"metadata": {
"flexprice_customer_id": "cust_1234567890abcdef",
"flexprice_environment": "prod",
"flexprice_sync_timestamp": "2024-01-20T10:30:00Z"
}
}
```
### Link Verification
You can verify customer links using the API:
**Endpoint:** `GET /api/v1/customers/{customer_id}/integrations`
**Headers:**
```http theme={null}
Authorization: Bearer your_api_key
X-Tenant-ID: your_tenant_id
X-Environment-ID: your_environment_id
```
**Response:**
```json theme={null}
{
"customer_id": "cust_1234567890abcdef",
"integrations": {
"nomod": {
"nomod_customer_id": "cust_nomod123abc",
}
}
}
```
## Error Handling
### Common Sync Errors
| Error | Cause | Resolution |
| ------------------------- | ------------------------ | --------------------------------------------------- |
| "Customer not found" | Invalid customer ID | Verify customer exists in Flexprice |
| "Email already exists" | Duplicate email in Nomod | Check for existing customer in Nomod dashboard |
| "Invalid email format" | Malformed email address | Fix email format in Flexprice customer |
| "Missing required fields" | Name or email missing | Ensure customer has `name` and `email` in Flexprice |
| "Nomod API error" | Nomod connection issue | Check Nomod connection credentials and status |
## Testing Customer Sync
### Test Environment Setup
Configure Nomod connection with test API keys (`sk_test_...`)
Create test customers in Flexprice with `name` and `email` (no sync yet)
Customers should NOT exist in Nomod initially
### Invoice Sync Testing Flow
Add a customer in Flexprice with name and email (no sync yet)
Customer should NOT exist in Nomod initially
Ensure invoice outbound sync is enabled in the Nomod connection
Create an invoice for the customer and finalize it (status changes to `FINALIZED`)
Customer should now exist in Nomod with linked metadata (`nomod_customer_id` in Flexprice customer metadata)
Create another invoice for the same customer and verify it uses the existing Nomod customer (no duplicate created)
# Nomod Payment Links & Invoice Sync
Source: https://docs.flexprice.io/integrations/nomod/payment-links
Complete guide to creating payment links and handling the Nomod checkout flow
## Overview
Payment links in Flexprice provide a secure, hosted checkout experience powered by Nomod. Customers click a payment URL and are redirected to Nomod's secure checkout page to complete their payment.
### Key Features
* **Secure checkout**: Hosted by Nomod with PCI compliance.
* **Multiple Payment Methods**: Supports various payment methods available in Nomod
* **Real-time Updates**: Webhook-driven status updates
* **Auto-generated Links**: Nomod automatically generates payment link URLs
## Payment Link Lifecycle
```mermaid theme={null}
graph TD
A[Create Payment Request] --> B[Validate Invoice & Customer]
B --> C[Create Payment Record INITIATED]
C --> D[Generate Nomod Payment Link]
D --> E{Link Created?}
E -->|Success| F[Update Status PENDING]
E -->|Failure| G[Keep Status INITIATED]
F --> H[Return Payment URL]
H --> I[Customer Clicks Link]
I --> J[Nomod Checkout]
J --> K[Enter Payment Details]
K --> L{Payment Successful?}
L -->|Yes| M[Webhook: payment_link.paid]
L -->|No| N[Webhook: payment_link.unpaid]
M --> O[Status: SUCCEEDED]
O --> Q[Reconcile Invoice]
```
## Payment Status Flow
### Status Transitions
```
INITIATED → PENDING → SUCCEEDED
```
### Status Definitions
| Status | Description | Triggers |
| ----------- | -------------------------------------------------- | ---------------------------------- |
| `INITIATED` | Payment record created, Nomod link not yet created | Initial payment creation |
| `PENDING` | Nomod payment link created successfully | Successful Nomod API call |
| `SUCCEEDED` | Payment completed successfully | Nomod webhook: `payment_link.paid` |
## Creating Payment Links
### Prerequisites
1. **Nomod Connection**: Active Nomod connection configured
2. **Invoice Status**: Invoice must be finalized and unpaid
3. **Valid Amount**: Payment amount must match invoice outstanding
4. **Connection Status**: Connection must be in "Published" status
### Recording Payments with Nomod
You can use Nomod to generate payment links when recording payments:
Navigate to the invoice or customer page, click **"Record Payment"**, and enter the payment amount.
In the **Payment Method** dropdown, select **"Payment Link"**. In the **Payment Provider** dropdown, select your Nomod connection. Optionally add a description.
Click **"Record"** to create the payment. A payment link will be generated via Nomod and a success dialog will display the payment URL. You can copy the URL to share with the customer, open the link in a new tab, or close the dialog.
Share the payment link with your customer. The customer clicks the link and completes payment through Nomod. Payment status updates automatically via webhooks.
### API Request
**Endpoint:** `POST /api/v1/payments`
**Headers:**
```http theme={null}
Content-Type: application/json
Authorization: Bearer your_api_key
X-Environment-ID: your_environment_id
```
**Request Body:**
```json theme={null}
{
"amount": 100.00,
"currency": "USD",
"destination_id": "inv_123",
"destination_type": "invoice",
"payment_method_type": "payment_link",
"payment_gateway": "nomod",
"metadata": {
"connection_id": "conn_123",
"connection_name": "Nomod Production",
"success_url": "https://app.flexprice.com/billing/invoices/inv_123",
"cancel_url": "https://app.flexprice.com/billing/invoices/inv_123"
}
}
```
### Response
```json theme={null}
{
"id": "pay_1234567890abcdef",
"payment_status": "pending",
"payment_url": "https://nomod.com/pay/abc123",
"amount": "100.00",
"currency": "USD",
"gateway_tracking_id": "plink_1234567890abcdef",
"created_at": "2024-01-20T10:30:00Z",
"metadata": {
"nomod_payment_link_id": "plink_1234567890abcdef"
}
}
```
**Note**: The `payment_url` is automatically generated by Nomod and returned as a URL.
### Payment Link Configuration
### Required Fields
| Field | Type | Required | Description |
| --------------------- | ------ | -------- | ------------------------------------------ |
| `destination_type` | string | ✅ | Must be `"invoice"` |
| `destination_id` | string | ✅ | Valid invoice ID |
| `payment_method_type` | string | ✅ | Must be `"payment_link"` |
| `payment_gateway` | string | ✅ | Must be `"nomod"` |
| `amount` | string | ✅ | Payment amount (must match invoice) |
| `currency` | string | ✅ | Valid currency code (USD, etc.) |
| `success_url` | string | ❌ | Callback URL after payment (optional) |
| `cancel_url` | string | ❌ | Callback URL after cancellation (optional) |
## Payment Link Features
* **Secure**: All payments processed through Nomod's secure infrastructure
* **Multiple Payment Methods**: Supports various payment methods available in Nomod
* **Automatic Updates**: Payment status updates automatically when customer pays
* **Success/Cancel URLs**: Automatically configured to redirect back to Flexprice
### API reference
**Payment Link:** [Create a new payment](https://docs.flexprice.io/api-reference/payments/create-payment#create-a-new-payment)
### Invoice Sync
Whenever an invoice is generated in Flexprice it's synced to Nomod in realtime and Nomod generates a payment link for the invoice.
1. **Payment Link Reuse**: If a payment link is requested for a synced invoice, the existing Nomod invoice payment URL is returned
2. **Automatic Association**: Payments made through the URL are automatically associated with the invoice
3. **Metadata**: Once synced, you get the Nomod invoice details and payment URL in the Flexprice invoice metadata
### Invoices are automatically synced to Nomod when:
* Invoice is finalized (status changes from `DRAFT` to `FINALIZED`)
* Invoice outbound sync is enabled in the Nomod connection
* Customer is successfully synced to Nomod (if applicable)
**API reference:** [Get an invoice by ID](https://docs.flexprice.io/api-reference/invoices/get-invoice)
## Error Handling
### Common Payment Errors
| Error | Cause | Resolution |
| ------------------------------ | ------------------------ | --------------------------------------- |
| "Payment links not generating" | Connection not published | Verify connection status is "Published" |
| "Invalid invoice amount" | Amount mismatch | Ensure amount matches invoice |
| "Invoice already paid" | Invoice status issue | Check invoice payment status |
| "Nomod link creation failed" | API error | Check Nomod connection |
## Security Considerations
### Payment Link Security
* **HTTPS Required**: All payment links use HTTPS
* **Link Expiry**: Links expire based on Nomod settings
* **One-time Use**: Each link can only be used once
* **Signature Verification**: All webhooks verified
### Data Protection
* **PCI Compliance**: Nomod handles sensitive payment data
* **No Card Storage**: Card details never stored in Flexprice
* **Encrypted Storage**: All payment data encrypted at rest
## Webhook Endpoints
* `POST /api/v1/webhooks/nomod/{tenant_id}/{environment_id}` - Nomod webhook handler
For complete API documentation, see the [API Reference](/api-reference/payments/create-payment).
# Paddle Connection Setup
Source: https://docs.flexprice.io/integrations/paddle/connection-setup
Complete guide to setting up and configuring Paddle connections in Flexprice
## Overview
A Paddle connection in Flexprice stores encrypted credentials that allow the system to interact with your Paddle account for:
* Creating customers and addresses in Paddle
* Syncing subscriptions to Paddle for payment method collection via hosted checkout
* Syncing invoices to Paddle as transactions for automatic charging
* Receiving webhook notifications from Paddle
* Automatic reconciliation of payments between Paddle and Flexprice
## Prerequisites
Before setting up your Paddle connection, ensure you have:
1. **Active Paddle Billing Account** - Sign up at [paddle.com](https://www.paddle.com)
2. **API Key** - Available in your Paddle Dashboard (Developers → Authentication)
3. **Webhook Secret** - Configured in Paddle Dashboard (Developers → Notifications)
4. **Client-Side Token** - For Paddle.js when a Flexprice-hosted checkout page loads with a signed `token` query param (see Step 2); customers saving a card via the subscription API use `paddle_checkout_url` from the [integration workflow](/integrations/paddle/integration-workflow)
5. **Flexprice Environment** - Valid Flexprice environment
6. **Default Payment Link** - Required by Paddle on your seller account; set the Flexprice checkout base URL in Paddle (see Step 2)
7. **Domain Approval** - flexprice.io domain approved in Paddle (see Step 2)
## Step 1: Gather Paddle Credentials
### Required Credentials
| Credential | Location in Paddle Dashboard | Required | Purpose |
| --------------------- | --------------------------------------------- | ----------- | ---------------------------------------------------------------------------- |
| **API Key** | Developers → Authentication | ✅ | API authentication |
| **Webhook Secret** | Developers → Notifications → \[Your Endpoint] | ✅ | Webhook signature verification |
| **Client-Side Token** | Developers → Authentication | ✅ | Paddle.js on hosted checkout when the URL includes Flexprice’s `token` param |
| **Redirect URL** | Connection metadata (not Paddle) | Recommended | Success URL after payment |
### Finding Your Credentials
1. **API Key:**
* Go to Paddle Dashboard → Developers → Authentication
* Copy your API key (use `pdl_sdbx_` prefix for sandbox, live prefix for production)
2. **Webhook Secret:**
* Go to Paddle Dashboard → Developers → Notifications
* Create or select your webhook endpoint
* Copy the webhook secret (used for signature verification)
3. **Client-Side Token:**
* Go to Paddle Dashboard → Developers → Authentication
* Copy the client-side token (e.g. `test_xxx` or `live_xxx`) for Paddle.js
## Step 2: Configure Paddle Checkout & Webhooks
### Default Payment Link
Paddle requires a default payment link on your seller account to create transactions. Flexprice still relies on this as part of the integration.
**Card capture for subscriptions:** In the current integration, you usually send customers the **`paddle_checkout_url`** returned when you create a Flexprice subscription (see [Paddle Integration Workflow](/integrations/paddle/integration-workflow)). The steps below configure Paddle’s **required** default payment link and domain — they do not replace sharing that API-provided checkout URL.
1. Go to Paddle Dashboard → Checkout → Checkout settings
2. Set **Default payment link** to: `https://admin.flexprice.io/checkout`
3. Submit your domain for approval via **Request website approval** if not already approved
### Website Approval
Your domain must be approved before checkout works in production.
1. Go to Paddle Dashboard → Checkout → Request website approval
2. Add and approve the flexprice.io domain (or your production domain)
### Setting Up the Webhook
1. Go to Paddle Dashboard → Developers → Notifications
2. Click **Add notification destination**
3. Enter your webhook URL (see format below)
4. Select the required events listed below
5. Copy the **Webhook Secret** for your connection
**Webhook URL Format:**
```
https://api.cloud.flexprice.io/v1/webhooks/paddle/tenant_01K1TJDVNSN7TWY8CZY870QMNV/env_01K1TJJF0CJR410C6QVPYQTNV0
```
*(Replace `tenant_01K1TJDVNSN7TWY8CZY870QMNV` and `env_01K1TJJF0CJR410C6QVPYQTNV0` with your tenant and environment IDs. If you use a different Flexprice region, use the base URL for your region instead of `https://api.cloud.flexprice.io`.)*
**Required Webhook Events** – Configure these events in your Paddle webhook endpoint:
| Event Type | Purpose |
| ------------------------ | ------------------------------------------------------------- |
| `transaction.completed` | Reconcile invoice payments when Paddle charges the saved card |
| `subscription.activated` | Detect when a customer has saved their payment method |
## Step 3: Create Paddle Connection
### Using Flexprice Dashboard
You can create a Paddle connection directly from the Flexprice dashboard:
Go to **Flexprice dashboard** → **Integrations** → **Paddle** → **Add a connection**.
Enter your API Key, Webhook Secret, and Client-Side Token (recommended). Optionally add a Redirect URL in metadata for post-payment redirects.
Click to create the connection. Flexprice will store your credentials securely.
```json theme={null}
{
"name": "Paddle Production",
"provider_type": "paddle",
"encrypted_secret_data": {
"api_key": "pdl_live_...",
"webhook_secret": "...",
"client_side_token": "live_..."
},
"metadata": {
"redirect_url": "https://your-app.com/billing/success"
}
}
```
## Subscriptions, invoices, and Paddle
1. **Subscription setup:** When you create a Flexprice subscription, Flexprice creates a matching Paddle subscription and returns checkout metadata so the customer can save a payment method. See [Paddle Integration Workflow](/integrations/paddle/integration-workflow) for the full flow (`paddle_checkout_url`, `subscription.activated`, card capture, and **customer/address sync**).
2. **Invoicing:** Flexprice syncs each invoice to Paddle as a transaction. Paddle charges the saved payment method automatically; `transaction.completed` reconciles the invoice in Flexprice.
3. **Tax:** If Paddle adds tax on top of the invoice amount and the charged total exceeds the invoice total, Flexprice may mark the invoice as **overpaid** — align tax mode (internal vs. external) in Paddle with your pricing setup.
## Security Best Practices
### Credential Management
1. **Environment Separation**: Use sandbox keys (`pdl_sdbx_`) for development
2. **Key Rotation**: Regularly rotate your Paddle API keys
3. **Encryption**: All credentials are encrypted at rest in Flexprice
### Webhook Security
1. **HTTPS Only**: Always use HTTPS for webhook endpoints
2. **Signature Verification**: Flexprice verifies all webhook signatures
3. **Secret Management**: Keep webhook secrets secure and rotate regularly
## Troubleshooting
| Issue | Cause | Solution |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No `paddle_checkout_url` in subscription metadata | Subscription not linked to Paddle or no active connection | Confirm an active Paddle connection under **Integrations → Paddle**; follow [Paddle Integration Workflow](/integrations/paddle/integration-workflow) |
| Invoice sync fails – "Paddle address ID not found" | Customer missing country or address not synced | Add country to customer address; ensure address has valid country code |
| Connection test fails | Invalid API keys | Verify keys in Paddle Dashboard |
| Webhook not received (`transaction.completed` / `subscription.activated`) | Incorrect webhook URL or required events not selected | Check URL format; in Paddle → Developers → Notifications, select both required events |
| Paddle.js won’t load or hosted checkout page errors | Missing **Client-Side Token** on the connection, or the URL lost Flexprice’s `token` query param | Add **Client-Side Token** in the connection. For subscriptions, customers should open **`paddle_checkout_url`** from the API ([workflow](/integrations/paddle/integration-workflow)). For other Flexprice-hosted checkout links, keep the `token` param for Paddle.js. |
| Invoice marked overpaid | Paddle added tax on top of invoice amount | Align tax mode (internal vs external) in Paddle with your invoice pricing |
## Next Steps
After setting up your Paddle connection:
1. **Follow the Integration Workflow**: See the [Paddle Integration Workflow](/integrations/paddle/integration-workflow) for the complete end-to-end guide — subscription creation, customer checkout, and invoice auto-charge.
2. **Test in Sandbox**: Use `pdl_sdbx_` keys and Paddle's sandbox environment before going live.
3. **Test customer sync and checkout**: Create a test customer with address, sync an invoice, and complete a test payment via Flexprice Checkout.
4. **Monitor Webhooks**: Ensure `transaction.completed` and `subscription.activated` events are being received.
5. **Go Live**: Switch to production keys when ready.
For detailed API documentation, see the [API Reference](/api-reference/introduction).
# Paddle Integration Workflow
Source: https://docs.flexprice.io/integrations/paddle/integration-workflow
End-to-end guide to the Paddle integration: subscription sync, customer checkout, and invoice auto-charge
## Overview
This guide walks you through the complete Paddle integration workflow in Flexprice.
**Division of responsibilities:**
* **Flexprice** handles usage metering, billing logic, plan configuration, and invoice generation.
* **Paddle** handles payment method collection, secure card storage, and automatic charging.
**What you'll achieve:**
* Subscriptions created in Flexprice automatically sync to Paddle as \$0 subscriptions
* Customers save their payment method via a Paddle-hosted checkout overlay
* Every invoice Flexprice generates is automatically charged against the saved card via Paddle
## Prerequisites
Before following this guide:
1. Complete the [Paddle Connection Setup](/integrations/paddle/connection-setup) — you need an active Paddle connection in your Flexprice environment.
2. Ensure your Paddle webhook destination is configured with both required events: `transaction.completed` and `subscription.activated`.
***
## How It Works
The integration runs in two phases:
**Phase 1 — Subscription & Card Setup**
```mermaid theme={null}
graph TD
A[Create Flexprice subscription] --> B["Flexprice creates a zero-amount Paddle subscription"]
B --> C["paddle_checkout_url and paddle_transaction_id in metadata"]
C --> D[Share checkout URL with customer]
D --> E[Customer saves card via Paddle overlay]
E --> F[Paddle fires subscription.activated webhook]
F --> G[Card is on file — ready to charge]
```
**Phase 2 — Invoice Auto-Charge**
```mermaid theme={null}
graph TD
A[Flexprice billing cycle runs] --> B[Invoice generated]
B --> C[Invoice synced to Paddle as a transaction]
C --> D[Paddle auto-charges saved card]
D --> E[Paddle fires transaction.completed webhook]
E --> F[Flexprice reconciles invoice as paid]
```
***
## Step 1: Create a Subscription
Create a subscription in Flexprice as you normally would. Flexprice automatically creates a corresponding \$0 subscription in Paddle and returns the checkout URL in the response metadata.
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/subscriptions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "cust_01abc",
"plan_id": "plan_01xyz",
"currency": "USD",
"billing_cadence": "RECURRING",
"billing_period": "MONTHLY",
"billing_period_count": 1,
"start_date": "2025-06-01T00:00:00Z"
}'
```
**Response** (abbreviated):
```json theme={null}
{
"id": "sub_01def456",
"status": "active",
"customer_id": "cust_01abc",
"plan_id": "plan_01xyz",
"metadata": {
"paddle_checkout_url": "https://checkout.paddle.com/checkout/custom/...",
"paddle_transaction_id": "txn_01abc123def"
}
}
```
The same metadata fields are also included in the `subscription.created` webhook payload.
`paddle_checkout_url` and `paddle_transaction_id` are only present when your Flexprice environment has an active Paddle connection. If these fields are missing, verify the connection in **Integrations → Paddle**.
### With a Trial Period
To give the customer a free trial before billing begins, pass `trial_period_days`:
```bash theme={null}
curl -X POST https://api.flexprice.io/v1/subscriptions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "cust_01abc",
"plan_id": "plan_01xyz",
"currency": "USD",
"billing_cadence": "RECURRING",
"billing_period": "MONTHLY",
"billing_period_count": 1,
"start_date": "2025-06-01T00:00:00Z",
"trial_period_days": 14
}'
```
The `paddle_checkout_url` is still returned immediately. Prompt the customer to save their card during the trial window — if no card is on file when the trial ends and the first invoice is generated, the auto-charge will fail.
For full details on trial behavior, see [Trialing](/docs/subscriptions/workflows/trialing).
***
## Step 2: Customer Saves Their Card
Extract `paddle_checkout_url` from the subscription response and share it with your customer — embed it in your app, send it via email, or redirect to it after signup.
When the customer opens the URL, they see a Paddle-hosted overlay to enter and save their payment method. No card data passes through Flexprice or your servers.
Once the card is saved:
* Paddle fires a `subscription.activated` webhook to your Flexprice webhook endpoint.
* Flexprice records that the payment method is on file.
* The customer is ready to be charged automatically on the next invoice.
The customer should save their card before Flexprice raises the first invoice, or the auto-charge will fail.
`subscription.activated` here is a **Paddle webhook event** — it fires when the Paddle-side \$0 subscription is activated after card capture. This is separate from the Flexprice `subscription.activated` event, which fires when a Flexprice subscription transitions from `trialing` to `active`.
***
## Step 3: Invoice Auto-Charge
No action is required from you for recurring charges. When Flexprice generates an invoice on the billing cycle:
1. The invoice is synced to Paddle as a transaction against the customer's account.
2. Paddle automatically charges the saved card.
3. Paddle fires a `transaction.completed` webhook to Flexprice.
4. Flexprice marks the invoice as paid and reconciles the amounts.
If Paddle applies tax on top of the invoice amount and the charged total exceeds the invoice total, Flexprice marks the invoice as **overpaid**. To avoid this, align your Paddle tax mode (internal vs. external) with your Flexprice pricing setup.
***
## Customer & Address Sync
Customer sync is **on-demand** and **Flexprice → Paddle** only. Customers are synced to Paddle automatically when a subscription or invoice operation requires it.
Paddle requires a customer address (at minimum: a country code) to create transactions. If a customer is missing a country, invoice sync will fail with `"Paddle address ID not found"`. Add the country to the customer record in Flexprice before creating a subscription.
***
## Troubleshooting
| Issue | Cause | Solution |
| -------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| No `paddle_checkout_url` in subscription metadata | No active Paddle connection in environment | Verify connection in **Integrations → Paddle** |
| `transaction.completed` webhook not received | Webhook destination not configured or wrong events | Go to Paddle → Developers → Notifications and confirm both `transaction.completed` and `subscription.activated` are selected |
| Invoice not auto-charged | Customer hasn't saved card yet (`subscription.activated` not received) | Share the `paddle_checkout_url` with the customer; wait for card save before expecting auto-charges |
| Invoice sync fails — "Paddle address ID not found" | Customer missing country on their address | Add country to the customer's address in Flexprice |
| Invoice marked overpaid | Paddle applied tax on top of the invoice amount | Align tax mode (internal vs. external) in Paddle settings with your Flexprice pricing |
| Checkout URL expired or invalid | Paddle transaction expired | Re-create the subscription or contact Flexprice support to re-sync the Paddle subscription |
# QuickBooks Connection Setup
Source: https://docs.flexprice.io/integrations/quickbooks/connection-setup
Complete guide to setting up and configuring QuickBooks Online connections in Flexprice
## Overview
A QuickBooks connection in Flexprice stores encrypted credentials that allow the system to interact with your QuickBooks Online account for:
* Syncing invoices from Flexprice to QuickBooks
* Creating customers in QuickBooks automatically
* Creating items (products/services) for invoice line items
* Inbound Payment sync
## Understanding Sync Configuration
### Invoice Sync (Outbound Only)
When enabled, invoices created in Flexprice are automatically synced to QuickBooks:
* **Outbound**: `true` - Sync invoices from Flexprice to QuickBooks
* **Inbound**: Always `false` - QuickBooks invoices are NOT imported to Flexprice
### Payment Sync (Inbound Only)
When enabled, payment notifications from QuickBooks update invoice status in Flexprice:
* **Inbound**: `true` - Receive payment notifications from QuickBooks webhooks
* **Outbound**: Always `false` - Payments are NOT sent from Flexprice to QuickBooks
**Important**: Payments are recorded in QuickBooks by you or your accounting team. Flexprice only receives notifications when payments are marked as complete.
## Prerequisites
Before setting up your QuickBooks connection, ensure you have:
1. **Intuit Developer Account** - Create an app at [developer.intuit.com](https://developer.intuit.com)
2. **OAuth 2.0 Credentials** - Client ID and Client Secret from your app
3. **Income Account in QuickBooks** - At least one active Income account (for creating items)
## Step 1: Create a QuickBooks App
### Create App in Intuit Developer Portal
1. Go to [Intuit Developer Portal](https://developer.intuit.com/app/developer/myapps)
2. Click **Create an app**
3. Select **QuickBooks Online and Payments**
4. Fill in app details:
* **App Name**: "Flexprice Connection" (or your preferred name) following the naming quickbooks' naming conventions. Go to [View naming guidelines](https://developer.intuit.com/app/developer/qbo/docs/go-live/list-on-the-app-store/naming-and-logo-guidelines)
5. Define the authorization scope of your app
* You will be provided with two options (you can select either or both as per your requirements):
* [com.intuit.quickbooks.accounting](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/account)
* [com.intuit.quickbooks.payment](https://developer.intuit.com/app/developer/qbpayments/docs/api/resources/all-entities/bankaccounts)
6. Click **Create app**
### Configure OAuth Settings
1. In your app dashboard, go to **Settings**
2. Configure redirect URIs:
* Add flexprice resource `https://admin.flexprice.io/tools/integrations/oauth/callback` as one of your redirect uri
3. Go to **Keys & Credentials** and copy your credentials:
* **Client ID**
* **Client Secret**
## Step 2: Configure Webhook (Optional but Recommended)
Webhooks allow Flexprice to receive real-time payment notifications from QuickBooks.
### Webhook URL Format
```
https://api.cloud.flexprice.io/v1/webhooks/quickbooks/{tenant_id}/{environment_id}
```
### Setting Up Webhooks
1. Go to Intuit Developer Portal → Your App → **Webhooks**
2. Set up your webhook endpoint to receive live events.
* Enter the webhook URL you were provided with from flexprice
3. Subscribe to these events:
* `Payment` entity (Create)
4. Copy the **Webhook Verifier Token**
5. Click **Save**
**Note**: Webhook verifier token is used to verify webhook signatures for security.
## Step 3: Create QuickBooks Connection
### Using Flexprice Dashboard
You can create a QuickBooks connection through OAuth flow in the Flexprice dashboard:
Go to **Settings** → **Integrations** → **QuickBooks**
Fill in the connection form:
* **Connection Name**: "QuickBooks Production" (or your preferred name)
* **Client ID**: From Intuit Developer Portal
* **Client Secret**: From Intuit Developer Portal
* **Environment**: Select "Production" or "Sandbox"
* **Webhook Verifier Token**: From webhook configuration (optional)
* **Income Account ID**: Provide any custom income account id that you may have created to track your revenue, by default it is set to `79` (used for creating items)
Enable sync options:
* **Invoice Sync**: Enable to automatically sync invoices to QuickBooks
* **Payment Sync**: Enable to sync payments from QuickBooks
Click **Connect to QuickBooks** to start OAuth flow:
1. You'll be redirected to QuickBooks login
2. Sign in to your QuickBooks account
3. Select the company to connect
4. Authorize Flexprice to access your account
5. You'll be redirected back to Flexprice
After authorization, verify the connection is active and working
## Security Best Practices
### Credential Management
1. **Environment Separation**: Use sandbox for development, production for live data
2. **Token Refresh**: Access tokens expire after 1 hour - Flexprice automatically refreshes them
3. **Access Control**: Limit OAuth app permissions in QuickBooks
4. **Encryption**: All credentials are encrypted at rest in Flexprice
### Webhook Security
1. **HTTPS Only**: Always use HTTPS for webhook endpoints in production
2. **Signature Verification**: Configure webhook verifier token for signature validation
3. **Idempotency**: Flexprice handles duplicate webhook events automatically
4. **Rate Limiting**: Webhooks are rate-limited to prevent abuse
### OAuth Token Management
**Access Token Expiration**:
* Access tokens expire after **1 hour** of inactivity
* Flexprice automatically refreshes tokens using refresh tokens
* No manual intervention required
**Refresh Token Expiration**:
* Refresh tokens expire after **100 days** of inactivity
* If both tokens expire, you'll need to re-authorize the connection
## Troubleshooting
### Common Issues
| Issue | Cause | Solution |
| ------------------------- | ---------------------- | ------------------------------------------------------ |
| Connection test fails | Invalid credentials | Verify Client ID and Secret in Intuit Developer Portal |
| OAuth authorization fails | Incorrect redirect URI | Check redirect URI matches in app settings |
| Invoice sync fails | Missing Income Account | Ensure Income Account ID is configured |
| Webhook not received | Incorrect webhook URL | Verify webhook URL format and subscription |
| Token expired | 1 hour | Re-authorize the connection |
### Debug Steps
1. **Check Intuit Developer Portal**: Verify OAuth credentials and webhook configuration
2. **Verify Realm ID**: Ensure Realm ID matches your QuickBooks company
3. **Test Income Account**: Query QuickBooks to ensure Income Account exists and is active
4. **Monitor Webhooks**: Check webhook delivery logs in Intuit Developer Portal
5. **Review Logs**: Check Flexprice application logs for sync errors
## Income Account Configuration
QuickBooks requires all service items to have an Income Account reference. This is where revenue is tracked in your books.
### Finding Your Income Account ID
1. Go to QuickBooks → **Chart of Accounts**
2. Look for accounts with type **Income**
3. Click on an account to view details
### Setting Default Income Account
During connection setup, you may provide an Income Account ID. This account will be used as the default for all items created from Flexprice line items.
## Next Steps
After setting up your QuickBooks connection:
1. **Test Invoice Sync**: Create a test invoice in Flexprice and verify it appears in QuickBooks
2. **Verify Customer Creation**: Ensure customers are automatically created when needed
3. **Check Item Creation**: Verify line items are creating service items in QuickBooks
4. **Configure Webhooks**: Set up payment webhooks for automated reconciliation
5. **Monitor Sync Status**: Check integration logs for any sync errors
## Related Documentation
* [Item Sync](/integrations/quickbooks/item-sync) - Learn how items are synced to QuickBooks
* [Integration Workflow](/integrations/quickbooks/integration-workflow) - Understand the complete sync workflow
## Additional Resources
* [QuickBooks API Documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account)
* [OAuth 2.0 Guide](https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization)
* [Webhook Documentation](https://developer.intuit.com/app/developer/qbo/docs/develop/webhooks)
# QuickBooks Integration Workflow
Source: https://docs.flexprice.io/integrations/quickbooks/integration-workflow
Complete end-to-end workflow for syncing invoices from Flexprice to QuickBooks Online
## Overview
This guide walks through the complete integration workflow between Flexprice and QuickBooks Online, from initial setup to invoice sync and payment reconciliation.
**Integration Type**: One-way sync (Flexprice → QuickBooks)
**Key Capabilities**:
* ✅ Automatic invoice sync
* ✅ On-demand customer creation
* ✅ Automatic item (product/service) creation
* ✅ Inbound Payment sync
## Step-by-Step Workflow
### Phase 1: Initial Setup
1. Go to [Intuit Developer Portal](https://developer.intuit.com)
2. Create new app for QuickBooks Online
3. Configure OAuth 2.0 settings
4. Copy Client ID and Client Secret
**Duration**: 5-7 minutes
1. In Intuit Developer Portal, go to Webhooks
2. Add webhook endpoint: `https://api.cloud.flexprice.io/v1/webhooks/quickbooks/{tenant_id}/{environment_id}`
3. Subscribe to Payment entity events
4. Copy webhook verifier token
**Duration**: 2-3 minutes
1. Navigate to Integrations
2. Go to QuickBooks and click "Add a connection"
3. Enter credentials and configuration
4. Complete OAuth authorization flow
5. Verify connection is active
**Duration**: 3-5 minutes
**Total Setup Time**: \~10 minutes (one-time)
### Phase 2: Plan and Price Setup
Create a pricing plan with charges:
**Example Plan: "Pro Plan"**
* Recurring charge: \$99/month (Flat fee)
* Metered charge: \$0.01 per API call (Usage-based)
```json theme={null}
{
"name": "Pro Plan",
"interval": "month",
"prices": [
{
"id": "price_recurring_01",
"display_name": "Monthly Fee",
"amount": 99.00,
"type": "recurring"
},
{
"id": "price_metered_02",
"display_name": "API Calls",
"unit_amount": 0.01,
"meter": "api_calls",
"type": "usage"
}
]
}
```
Verify QuickBooks connection:
* Status: Active
* Invoice sync: Enabled
* Income Account: Configured
**No action needed** - Items will be created automatically during first invoice sync
### Phase 3: Customer Subscription
Customer creates a subscription to Pro Plan:
```json theme={null}
{
"customer_id": "cust_abc123",
"plan_id": "plan_pro",
"start_date": "2025-01-01"
}
```
During the subscription period:
* Customer uses 4,500 API calls
* Usage is tracked via events
* Billing period ends on 2025-01-31
### Phase 4: Invoice Generation
At end of billing period, Flexprice generates invoice:
```json theme={null}
{
"id": "inv_xyz789",
"customer_id": "cust_abc123",
"line_items": [
{
"price_id": "price_recurring_01",
"description": "Pro Plan - Monthly Fee",
"amount": 99.00,
"quantity": 1
},
{
"price_id": "price_metered_02",
"description": "API Calls",
"amount": 45.00,
"quantity": 4500,
"unit_price": 0.01
}
],
"total": 144.00,
"status": "open"
}
```
Flexprice checks if invoice should sync:
1. ✅ QuickBooks connection exists
2. ✅ Connection is active
3. ✅ Invoice sync is enabled
4. ✅ Customer has valid data
→ **Proceed with sync**
### Phase 5: Customer Sync
Query entity\_integration\_mapping:
```sql theme={null}
SELECT * FROM entity_integration_mapping
WHERE entity_id = 'cust_abc123'
AND entity_type = 'customer'
AND provider_type = 'quickbooks'
```
**Result**: Not found → Customer needs to be created
Create customer via QuickBooks API:
**Request**:
```json theme={null}
POST /v3/company/{realmId}/customer
{
"DisplayName": "Acme Corporation",
"PrimaryEmailAddr": {
"Address": "billing@acme.com"
},
"BillAddr": {
"Line1": "123 Main St",
"City": "San Francisco",
"CountrySubDivisionCode": "CA",
"PostalCode": "94105",
"Country": "USA"
}
}
```
**Response**:
```json theme={null}
{
"Id": "67",
"DisplayName": "Acme Corporation",
"PrimaryEmailAddr": {
"Address": "billing@acme.com"
}
}
```
Create mapping record:
```json theme={null}
{
"entity_id": "cust_abc123",
"entity_type": "customer",
"provider_type": "quickbooks",
"provider_entity_id": "67",
"metadata": {
"synced_at": "2025-01-31T10:00:00Z",
"quickbooks_customer_display_name": "Acme Corporation",
"quickbooks_customer_primary_email_addr_address": "billing@acme.com"
}
}
```
### Phase 6: Item Creation
**Price**: `price_recurring_01` (Monthly Fee)
1. Check mapping → Not found
2. Query QuickBooks for item → Not found
3. Create service item:
```json theme={null}
POST /v3/company/{realmId}/item
{
"Name": "Pro Plan-Monthly Fee",
"Description": "price_recurring_01",
"Type": "Service",
"IncomeAccountRef": {
"value": "79",
"name": "Services"
}
}
```
4. QB Response: `{"Id": "125"}`
5. Store mapping: `price_recurring_01` → `125`
**Price**: `price_metered_02` (API Calls)
1. Check mapping → Not found
2. Query QuickBooks for item → Not found
3. Create service item:
```json theme={null}
POST /v3/company/{realmId}/item
{
"Name": "Pro Plan-API Calls",
"Description": "price_metered_02",
"Type": "Service",
"IncomeAccountRef": {
"value": "79",
"name": "Services"
}
}
```
4. QB Response: `{"Id": "126"}`
5. Store mapping: `price_metered_02` → `126`
### Phase 7: Invoice Sync
Create invoice with all line items:
```json theme={null}
POST /v3/company/{realmId}/invoice
{
"DocNumber": "INV-001",
"TxnDate": "2025-01-31",
"DueDate": "2025-02-14",
"CustomerRef": {
"value": "67"
},
"Line": [
{
"DetailType": "SalesItemLineDetail",
"Amount": 99.00,
"Description": "Pro Plan - Monthly Fee",
"SalesItemLineDetail": {
"ItemRef": {
"value": "125",
"name": "Pro Plan-Monthly Fee"
},
"UnitPrice": 99.00,
"Qty": 1
}
},
{
"DetailType": "SalesItemLineDetail",
"Amount": 45.00,
"Description": "API Calls",
"SalesItemLineDetail": {
"ItemRef": {
"value": "126",
"name": "Pro Plan-API Calls"
},
"UnitPrice": 0.01,
"Qty": 4500
}
}
],
"TotalAmt": 144.00
}
```
**QB Response**:
```json theme={null}
{
"Id": "219",
"DocNumber": "INV-001",
"TotalAmt": 144.00,
"Balance": 144.00
}
```
Create mapping record:
```json theme={null}
{
"entity_id": "inv_xyz789",
"entity_type": "invoice",
"provider_type": "quickbooks",
"provider_entity_id": "219",
"metadata": {
"synced_at": "2025-01-31T10:05:00Z",
"quickbooks_invoice_doc_number": "INV-001"
}
}
```
Update invoice metadata:
```json theme={null}
{
"id": "inv_xyz789",
"metadata": {
"quickbooks_invoice_id": "219",
"quickbooks_synced_at": "2025-01-31T10:05:00Z",
"quickbooks_status": "pending"
}
}
```
### Phase 8: Payment Recording (in QuickBooks)
Your accounting team records payment in QuickBooks:
1. Open QuickBooks Desktop/Online
2. Navigate to invoice #219
3. Click "Receive Payment"
4. Enter payment details:
* Amount: \$144.00
* Date: 2025-02-05
* Method: Bank Transfer
5. Mark invoice as paid
QuickBooks creates a Payment record:
```json theme={null}
{
"Id": "222",
"TxnDate": "2025-02-05",
"TotalAmt": 144.00,
"CustomerRef": {"value": "67"},
"Line": [
{
"Amount": 144.00,
"LinkedTxn": [
{
"TxnId": "219",
"TxnType": "Invoice"
}
]
}
]
}
```
### Phase 9: Webhook Notification
QuickBooks sends webhook to Flexprice:
**Webhook Payload**:
```json theme={null}
POST /v1/webhooks/quickbooks/{tenant_id}/{environment_id}
{
"eventNotifications": [
{
"realmId": "1234567890",
"dataChangeEvent": {
"entities": [
{
"name": "Payment",
"id": "222",
"operation": "Create",
"lastUpdated": "2025-02-05T14:30:00.000Z"
}
]
}
}
]
}
```
**Headers**:
```
intuit-signature:
```
1. Verify webhook signature (if verifier token configured)
2. Check payment sync is enabled
3. Extract payment ID: "222"
### Phase 10: Payment Reconciliation
Call QuickBooks API to get full payment:
```bash theme={null}
GET /v3/company/{realmId}/payment/222
```
**Response**:
```json theme={null}
{
"Id": "222",
"TotalAmt": 144.00,
"TxnDate": "2025-02-05",
"Line": [
{
"Amount": 144.00,
"LinkedTxn": [
{
"TxnId": "219",
"TxnType": "Invoice"
}
]
}
]
}
```
**Extract**: QuickBooks Invoice ID = "219"
Query entity\_integration\_mapping:
```sql theme={null}
SELECT * FROM entity_integration_mapping
WHERE provider_entity_id = '219'
AND entity_type = 'invoice'
AND provider_type = 'quickbooks'
```
**Result**: Flexprice Invoice ID = `inv_xyz789`
Update invoice in Flexprice:
```json theme={null}
{
"id": "inv_xyz789",
"payment_status": "SUCCEEDED",
"paid_at": "2025-02-05T14:30:00Z",
"amount_paid": 144.00,
"amount_remaining": 0.00,
"metadata": {
"payment_method": "offline",
"payment_recorded_by": "quickbooks",
"quickbooks_payment_id": "222",
"quickbooks_invoice_id": "219",
"payment_synced_at": "2025-02-05T14:30:00Z"
}
}
```
Invoice is now marked as paid in Flexprice:
* ✅ Payment status: SUCCEEDED
* ✅ Amount paid: \$144.00
* ✅ Linked to QuickBooks payment
* ✅ Audit trail maintained
## Next Invoice (Same Customer)
When the next billing cycle arrives:
Second invoice generated for same customer
✅ Mapping exists → Use QB Customer ID "67" (no creation needed)
✅ Mappings exist → Use QB Item IDs "125" and "126" (no creation needed)
Only invoice is created in QuickBooks (reusing customer and items)
**Result**: Subsequent invoices sync much faster since customer and items already exist.
## Key Integration Points
### 1. Entity Integration Mapping
**Purpose**: Track relationships between Flexprice and QuickBooks entities
**Entities Tracked**:
* Customers: `cust_abc123` ↔ QB Customer `67`
* Prices/Items: `price_recurring_01` ↔ QB Item `125`
* Invoices: `inv_xyz789` ↔ QB Invoice `219`
**Why It Matters**:
* Prevents duplicate entity creation
* Enables bidirectional lookups
* Maintains data integrity
### 2. Sync Configuration
**Invoice Sync**:
```json theme={null}
{
"invoice": {
"inbound": false,
"outbound": true
}
}
```
**Payment Sync**:
```json theme={null}
{
"payment": {
"inbound": true,
"outbound": false
}
}
```
### 3. Webhook Processing
**Flow**:
1. QuickBooks sends webhook → Flexprice
2. Verify signature (optional)
3. Fetch full payment details
4. Find linked invoice
5. Update invoice status
6. Return 200 OK immediately
**Security**:
* HMAC-SHA256 signature verification
* HTTPS only
* Idempotency handling
* Rate limiting
## Error Handling
### Common Scenarios
| Scenario | Handling | Result |
| ----------------------- | ------------------------------ | ------------------- |
| Customer already exists | Use existing QB customer | No error |
| Item already exists | Use existing QB item | No error |
| Invoice already synced | Skip sync, return mapping | No error |
| Missing Income Account | Return error, block sync | User must configure |
| QB API rate limit | Retry with exponential backoff | Eventual success |
| OAuth token expired | Auto-refresh token, retry | Transparent to user |
| Webhook signature fail | Log warning, skip processing | Security measure |
### Graceful Degradation
**Philosophy**: Integration failures should NOT break core Flexprice functionality
**Example**:
* Invoice creation in Flexprice: ✅ Success
* QB sync fails due to API error: ⚠️ Logged, not blocking
* Invoice is still accessible in Flexprice
* Manual retry available
## Performance Considerations
### First Invoice Sync
* **Time**: \~5-10 seconds
* **Operations**: Customer creation + 2 item creations + invoice creation
### Subsequent Invoice Syncs
* **Time**: \~2-3 seconds
* **Operations**: Invoice creation only (reuses existing customer and items)
### Optimization Strategies
* ✅ Entity mapping cache
* ✅ Parallel API calls where possible
* ✅ Connection pooling
* ✅ Automatic token refresh
## Troubleshooting Guide
### Invoice Not Syncing
1. Verify QB connection is active
2. Check OAuth token is valid
3. Test API connectivity
1. Verify invoice sync is enabled
2. Check sync\_config.invoice.outbound = true
3. Ensure no sync filters blocking
1. Verify Income Account ID is configured
2. Ensure account exists in QB and is active
3. Query QB to confirm account type
### Payment Not Updating
1. Verify webhook endpoint is correct
2. Check webhook events are subscribed
3. Review webhook delivery logs in Intuit portal
1. Verify payment sync is enabled
2. Check sync\_config.payment.inbound = true
3. Ensure webhook verifier token is configured (optional)
1. Verify invoice was synced to QB
2. Check entity\_integration\_mapping exists
3. Confirm QB invoice ID matches
## Related Documentation
* [Connection Setup](/integrations/quickbooks/connection-setup) - Initial QuickBooks connection setup
* [Item Sync](/integrations/quickbooks/item-sync) - Detailed item creation and mapping
# Item Sync to QuickBooks
Source: https://docs.flexprice.io/integrations/quickbooks/item-sync
Learn how Flexprice automatically creates and manages items in QuickBooks for invoice line items
## Overview
When syncing invoices from Flexprice to QuickBooks, each invoice line item must reference a QuickBooks **Item** (product or service). Flexprice automatically handles item creation and management, ensuring your line items are properly tracked in QuickBooks.
**Key Points**:
* Items are synced when QuickBooks connection is active
* Each Flexprice price is mapped to a QuickBooks service item
* Items are linked to your configured Income Account
* Item mappings are stored to prevent duplicates
### When Items Are Created
Items are created automatically when:
1. **New Price**: A Flexprice price hasn't been synced to QuickBooks before
2. **Active Connection**: QuickBooks connection is active and authenticated
### Why Service Items?
QuickBooks has different item types (Inventory, Non-Inventory, Service). Flexprice uses **Service** items because:
* ✅ Best fit for SaaS and subscription businesses
* ✅ No inventory tracking required
* ✅ Simplifies accounting for digital services
* ✅ Requires only Income Account (no COGS account)
## Entity Integration Mapping
### Mapping Structure
When an item is created, Flexprice stores a mapping record:
```json theme={null}
{
"entity_id": "price_01ABC123",
"entity_type": "price",
"provider_type": "quickbooks",
"provider_entity_id": "125",
"metadata": {
"synced_at": "2025-11-25T09:50:21Z",
"quickbooks_item_name": "Pro Plan-API Calls"
}
}
```
### Why Mapping Matters
**Prevents Duplicates**:
* Before creating an item, Flexprice checks if mapping exists
* If found, uses existing QuickBooks item ID
* Avoids creating duplicate items for the same price
**Enables Sync**:
* Maps Flexprice prices to QuickBooks items
* Allows consistent line item references across invoices
* Maintains data integrity between systems
## Troubleshooting
### Common Issues
| Issue | Cause | Solution |
| ----------------------- | ---------------------- | ------------------------------------------ |
| Item creation fails | Missing Income Account | Configure Income Account ID in connection |
| Item creation fails | Invalid account ID | Verify Income Account exists and is active |
| Duplicate items created | Mapping not found | Check entity\_integration\_mapping table |
## Best Practices
### Income Account
💡 **Use a Dedicated Account**: Create a specific Income account like "Flexprice Revenue" for easy tracking
💡 **Keep Account Active**: Ensure the Income account is always active in QuickBooks
💡 **Document Account ID**: Store the account ID in your configuration documentation
### Item Management
💡 **Don't Manually Edit**: Avoid manually editing Flexprice-created items in QuickBooks
💡 **Use Consistent Naming**: Follow Flexprice naming conventions for clarity
💡 **Monitor Mappings**: Regularly check entity\_integration\_mapping for data integrity
## Related Documentation
* [Connection Setup](/integrations/quickbooks/connection-setup) - Set up QuickBooks connection
* [Integration Workflow](/integrations/quickbooks/integration-workflow) - Complete sync workflow
## QuickBooks Resources
* [QuickBooks Item API](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/item)
* [QuickBooks Account API](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account)
# Razorpay Connection Setup
Source: https://docs.flexprice.io/integrations/razorpay/connection-setup
Complete guide to setting up and configuring Razorpay connections in Flexprice
## Overview
A Razorpay connection in Flexprice stores encrypted credentials that allow the system to interact with your Razorpay account for:
* Creating customers in Razorpay
* Processing payment links via Razorpay Checkout
* Receiving webhook notifications from Razorpay
* Synchronizing customer data between systems
* Syncing invoices to Razorpay for payment collection
## Prerequisites
Before setting up your Razorpay connection, ensure you have:
1. **Active Razorpay Account** - Sign up at [razorpay.com](https://razorpay.com)
2. **API Keys** - Available in your Razorpay Dashboard
3. **Webhook Endpoint** - Configured in Razorpay Dashboard
4. **Flexprice Environment** - Valid tenant and environment IDs
## Step 1: Gather Razorpay Credentials
### Required Credentials
| Credential | Location in Razorpay Dashboard | Required | Purpose |
| ------------------ | -------------------------------------- | -------- | ------------------------------ |
| **Key ID** | Settings → API Keys | ✅ | API authentication |
| **Secret Key** | Settings → API Keys | ✅ | API authentication |
| **Webhook Secret** | Settings → Webhooks → \[Your Endpoint] | ✅ | Webhook signature verification |
### Finding Your Credentials
1. **API Keys:**
* Go to Razorpay Dashboard → Settings → API Keys
* Copy your `Key ID`
* Copy your `Secret Key`
* Use test keys for development
2. **Webhook Secret:**
* Go to Razorpay Dashboard → Settings → Webhooks
* Create or select your webhook endpoint
* Copy the webhook secret (used for signature verification)
## Step 2: Configure Webhook Endpoint in Razorpay
### Webhook URL Format
```
https://api.cloud.flexprice.io/v1/webhooks/razorpay/tenant_01K1TJDVNSN7TWY8CZY870QMNV/env_01K1TJJF0CJR410C6QVPYQTNV0
```
### Required Webhook Events
Configure these events in your Razorpay webhook endpoint:
| Event Type | Purpose |
| ------------------------ | ----------------------------------------------------------------- |
| `payment.captured` | Track successful payment completions |
| `payment.failed` | Handle payment failures and errors |
| `payment_link.paid` | Required for Checkout Sessions — triggers subscription activation |
| `payment_link.cancelled` | Required for Checkout Sessions — triggers draft entity cleanup |
| `payment_link.expired` | Required for Checkout Sessions — triggers draft entity cleanup |
The `payment_link.*` events are required if you use [Flexprice Checkout Sessions](/docs/checkout/checkout-sessions). Without them, subscriptions created via Checkout will remain in `draft` status after payment. If you only use manual invoice payment links (not Checkout Sessions), these three events are optional.
### Setting Up the Webhook
1. Go to Razorpay Dashboard → Settings → Webhooks
2. Click **+ Add New Webhook**
3. Enter your webhook URL
4. Select the required events listed above
5. Click **Create Webhook**
6. Copy the **Webhook Secret** for your connection
## Step 3: Create Razorpay Connection
### Using Flexprice Dashboard
You can create a Razorpay connection directly from the Flexprice dashboard:
### Webhook Configuration
After creating the connection, configure webhooks in the connection settings:
### API Request
### API Request
**Endpoint:** `POST /api/v1/connections`
**Headers:**
```http theme={null}
Content-Type: application/json
Authorization: Bearer your_api_key
X-Tenant-ID: your_tenant_id
X-Environment-ID: your_environment_id
```
**Request Body:**
```json theme={null}
{
"name": "Razorpay Production",
"provider_type": "razorpay",
"encrypted_secret_data": {
"key_id": "rzp_live_...",
"secret_key": "rzp_live_...",
"webhook_secret": "..."
}
}
```
### Response
```json theme={null}
{
"id": "conn_1234567890abcdef",
"name": "Razorpay Production",
"provider_type": "razorpay",
"status": "active",
"created_at": "2024-01-20T10:30:00Z",
"updated_at": "2024-01-20T10:30:00Z"
}
```
## Security Best Practices
### Credential Management
1. **Environment Separation**: Use different keys for test/production
2. **Key Rotation**: Regularly rotate your Razorpay API keys
3. **Access Control**: Limit API key permissions in Razorpay
4. **Encryption**: All credentials are encrypted at rest in Flexprice
### Webhook Security
1. **HTTPS Only**: Always use HTTPS for webhook endpoints
2. **Signature Verification**: Flexprice verifies all webhook signatures
3. **Secret Management**: Keep webhook secrets secure and rotate regularly
4. **Rate Limiting**: Implement rate limiting on webhook endpoints
## Troubleshooting
### Common Issues
| Issue | Cause | Solution |
| ---------------------------- | --------------------------- | ----------------------------------- |
| Connection test fails | Invalid API keys | Verify keys in Razorpay Dashboard |
| Webhook not received | Incorrect webhook URL | Check URL format and endpoint |
| Signature verification fails | Wrong webhook secret | Update webhook secret in connection |
| Customer sync fails | Missing account permissions | Check Razorpay account permissions |
### Debug Steps
1. **Check Razorpay Dashboard**: Verify API keys and webhook configuration
2. **Test API Keys**: Use Razorpay API to test key validity
3. **Monitor Webhooks**: Check webhook delivery logs in Razorpay
4. **Review Logs**: Check Flexprice application logs for errors
## Next Steps
After setting up your Razorpay connection:
1. **Test Customer Sync**: Create a test customer and sync to Razorpay
2. **Create Payment Links**: Test payment link creation and flow
3. **Monitor Webhooks**: Ensure webhook events are being received
4. **Go Live**: Switch to production keys when ready
# Razorpay Customer Synchronization
Source: https://docs.flexprice.io/integrations/razorpay/customer-sync
Complete guide to on-demand customer synchronization between Flexprice and Razorpay
## Overview
Customer synchronization ensures that customers exist in both Flexprice and Razorpay with linked identifiers, enabling:
* **On-demand sync**: Customers are synced to Razorpay only when needed for payments
* Payment link creation for any Flexprice customer
* Invoice sync to Razorpay
* Consistent customer data across platforms
* Efficient payment processing workflows
**Note**: Customer sync is **one-way only** (Flexprice → Razorpay). Customers are automatically synced to Razorpay when:
* Creating payment links
* Syncing invoices to Razorpay
## Synchronization Flow
Customer sync works **on-demand** and is **one-way only** (Flexprice → Razorpay):
```mermaid theme={null}
graph LR
A[Flexprice Customer] -->|On-Demand Sync| B[Razorpay Customer]
C[Payment Link Request] -->|Triggers Sync| B
D[Invoice Sync Request] -->|Triggers Sync| B
B -->|Notes Link| A
```
### Key Concepts
* **One-Way Sync**: Customers are synced from Flexprice to Razorpay only (not bidirectional)
* **On-Demand Sync**: Customers are synced to Razorpay only when payment interactions occur
* **Sync Triggers**: Sync happens automatically when creating payment links or syncing invoices
* **Entity Integration Mapping**: Links Flexprice customers to Razorpay customer IDs
* **Notes Storage**: Customer notes store cross-platform references for linking
* **Duplicate Prevention**: Intelligent checks prevent duplicate customer creation in Razorpay
* **Performance Optimization**: No unnecessary syncs for customers who never make payments
## Customer Data Model
### Flexprice Customer Structure
```json theme={null}
{
"id": "cust_1234567890abcdef",
"metadata": {
"razorpay_customer_id": "cust_razorpay123abc"
},
"environment_id": "prod",
"tenant_id": "tenant_123",
"status": "active",
"created_at": "2024-01-20T10:30:00Z"
}
```
## Sync Configuration
### Required Fields for Sync
| Field | Flexprice → Razorpay | Notes |
| ------- | -------------------- | --------------------- |
| `name` | ✅ | Customer full name |
| `email` | ✅ | Primary email address |
## Sync Process Details
### On-Demand Sync Flow (Payment-Triggered)
```mermaid theme={null}
graph TD
A[Payment Request] --> B[Ensure Customer Synced to Razorpay]
B --> C{Check Metadata}
C -->|Has ID| D[Return Existing Customer]
C -->|No ID| E[Check Integration Mapping]
E --> F{Mapping Exists?}
F -->|Yes| G[Update Customer Metadata]
F -->|No| H[Create Customer in Razorpay]
G --> I[Return Customer Info]
H --> J[Update Metadata & Mapping]
J --> I
I --> K[Process Payment]
```
## Customer Linking
### Notes Storage
Customer linking is maintained through notes fields:
**In Flexprice Customer:**
```json theme={null}
{
"metadata": {
"razorpay_customer_id": "cust_razorpay123abc",
"razorpay_sync_timestamp": "2024-01-20T10:30:00Z"
}
}
```
**In Razorpay Customer:**
```json theme={null}
{
"notes": {
"flexprice_customer_id": "cust_1234567890abcdef",
"flexprice_environment": "prod",
"flexprice_sync_timestamp": "2024-01-20T10:30:00Z"
}
}
```
## Error Handling
### Common Sync Errors
| Error | Cause | Resolution |
| ---------------------- | --------------------------- | --------------------------- |
| "Customer not found" | Invalid customer ID | Verify customer exists |
| "Email already exists" | Duplicate email in Razorpay | Check for existing customer |
| "Invalid email format" | Malformed email address | Fix email format |
| "Razorpay API error" | Razorpay connection issue | Check Razorpay credentials |
### Sync Failure Recovery
When sync fails:
1. **Log Error**: Record failure with details
2. **Retry Logic**: Automatic retry with exponential backoff
3. **Manual Intervention**: Admin can retry failed syncs
4. **Notification**: Alert on persistent failures
## Testing Customer Sync
### Test Environment Setup
1. **Test Razorpay Account**: Use Razorpay test mode
2. **Test Customers**: Create test customers in Flexprice only
3. **Webhook Testing**: Use ngrok for local webhook testing
### On-Demand Sync Testing Flow
1. **Create Test Customer**: Add customer in Flexprice (no sync yet)
2. **Verify No Sync**: Customer should NOT exist in Razorpay initially
3. **Create Payment Link**: Trigger payment link creation
4. **Verify Auto-Sync**: Customer should now exist in Razorpay
5. **Test Invoice Sync**: Sync invoice with same customer
6. **Verify Reuse**: Should use existing Razorpay customer (no duplicate)
# Razorpay Integration Workflow
Source: https://docs.flexprice.io/integrations/razorpay/integration-workflow
Complete guide to integrating Flexprice with Razorpay for payment processing
## Overview
This comprehensive guide walks you through integrating **Flexprice** with **Razorpay** to enable seamless payment processing and advanced usage-based billing. This integration combines Flexprice's powerful usage tracking capabilities with Razorpay's robust billing & payment infrastructure.
**What you'll achieve with this integration:**
* Process payments securely through Razorpay's infrastructure
* Automatically sync customers and invoices
* Handle complex usage-based billing scenarios
* Receive real-time updates via webhooks
## System Architecture
The Flexprice-Razorpay integration follows a clear division of responsibilities, ensuring each system handles what it does best while maintaining seamless data flow between platforms.
### Division of Responsibilities
**Flexprice specializes in:**
* Usage-based charge calculation and metering
* Complex billing logic and usage invoice generation
* Plan configuration with usage components
**Razorpay handles:**
* Secure payment processing and compliance
* Customer payment method storage
* Transaction processing and failure handling
* Payment link generation and checkout
## Prerequisites
Before you begin, ensure the following:
* You have an **active Flexprice account**
* You have a **Razorpay account** (in test or live mode)
* You possess **admin access** to both platforms
* You have a **basic understanding of webhooks** (optional but helpful)
***
## Step 1: Set Up the Connection
Follow the connection setup guide below to link Razorpay with Flexprice:\
👉 [Razorpay Connection Setup Guide](/integrations/razorpay/connection-setup)
### Required Webhook Events
```
payment.captured
payment.failed
payment_link.paid
payment_link.cancelled
payment_link.expired
```
The `payment_link.*` events are required if you use [Flexprice Checkout Sessions](/docs/checkout/checkout-sessions). See [Razorpay Checkout Setup](/docs/checkout/razorpay-checkout) for the full configuration guide.
***
## Step 2: Sync Configuration Details
### Invoice Outbound Sync (Flexprice → Razorpay)
* **What it does:** Sends invoices generated in Flexprice to Razorpay for payment processing
* **When to enable:** You create invoices in Flexprice but want Razorpay to handle payment collection
* **Benefits:** Unified payment experience and centralized transaction history
#### Invoice Sync Timing
Invoices are automatically synced to Razorpay when:
* Invoice is finalized (status changes from `DRAFT` to `FINALIZED`)
* Invoice outbound sync is enabled in the Razorpay connection
* Customer is successfully synced to Razorpay
#### Invoice Sync Process
1. **Invoice Finalization**: Invoice is finalized in Flexprice
2. **Customer Sync Check**: System ensures customer exists in Razorpay
3. **Invoice Creation**: Invoice is created in Razorpay with all line items
4. **Payment URL Generation**: Razorpay automatically generates payment URL
5. **Mapping Storage**: Flexprice stores the mapping between Flexprice and Razorpay invoices
#### Invoice Sync Features
* **Automatic Sync**: Invoices sync automatically when finalized
* **Line Item Preservation**: All invoice line items are preserved in Razorpay
* **Payment URL**: Razorpay generates payment URL automatically
* **Duplicate Prevention**: System prevents duplicate invoice creation
***
## Step 3: Payment Flow Setup
### 3.1 Supported Payment Methods
Flexprice supports multiple Razorpay payment flows:
1. **Payment Links** – For one-time payments via Razorpay Checkout
2. **Invoice Payments** – Payments made through synced invoices
### 3.2 Payment Collection Modes
You can choose how customer payments are collected:
| Mode | Description |
| ----------------- | ----------------------------------------------------------------- |
| **Payment Links** | Customers receive payment links to complete transactions manually |
| **Invoice Sync** | Invoices are synced to Razorpay with auto-generated payment URLs |
### 3.3 Currency Minimums
Razorpay requires minimum amounts in the smallest currency subunit:
| Currency | Minimum amount (in smallest subunit) | Equivalent Minimum Value |
| -------- | ------------------------------------ | ------------------------ |
| INR | 100 | ₹1.00 |
| USD | 1 | \$0.01 |
**Important**:
* Amounts are automatically converted to smallest currency units (paise for INR, cents for USD)
* Ensure all payment amounts meet these minimums
* Invoice amounts must also meet minimum requirements
***
## Step 4: Customer Synchronization
### On-Demand Customer Sync
Customers are automatically synced to Razorpay when needed:
* **Payment Link Creation**: Customer is synced before creating payment link
* **Invoice Sync**: Customer is synced before syncing invoice
**Note**: Customer sync is **one-way only** (Flexprice → Razorpay). Customers are only synced when payment interactions occur.
### Customer Sync Features
* **Automatic Sync**: No manual intervention required
* **Duplicate Prevention**: System prevents duplicate customer creation
* **Metadata Linking**: Customer IDs are stored in metadata for quick lookup
* **Email-Based Matching**: Uses email for customer matching
For detailed information, see [Customer Synchronization Documentation](/integrations/razorpay/customer-sync).
***
## Step 5: Payment Links
### Creating Payment Links
Payment links can be created for any invoice:
**Endpoint:** `POST /api/v1/payments`
**Request:**
```json theme={null}
{
"amount": 100,
"currency": "inr",
"destination_id": "inv_01K34HYZN03V6X8Q6V3T8X6G4F",
"destination_type": "INVOICE",
"payment_method_type": "PAYMENT_LINK",
"process_payment": true,
"payment_gateway": "razorpay",
"success_url": "https://admin.flexprice.io/customer-management/invoices/inv_01K34HYZN03V6X8Q6V3T8X6G4F?page=1"
}
```
### Payment Link Features
* **Auto-generated URLs**: Razorpay automatically generates short payment URLs
* **Multiple Payment Methods**: Supports cards, UPI, netbanking, wallets
* **Mobile Optimized**: Responsive checkout experience
* **Real-time Updates**: Webhook-driven status updates
For detailed information, see [Payment Links Documentation](/integrations/razorpay/payment-links).
***
## Step 6: Webhook Configuration
### Required Webhooks
Configure these webhooks in your Razorpay dashboard:
| Event | Purpose |
| ------------------ | ------------------------------------ |
| `payment.captured` | Track successful payment completions |
| `payment.failed` | Handle payment failures and errors |
### Webhook URL Format
```
https://api.cloud.flexprice.io/v1/webhooks/razorpay/{tenant_id}/{environment_id}
```
### Webhook Processing
* **Signature Verification**: All webhooks are verified for security
* **Idempotency**: Duplicate webhooks are handled gracefully
* **Error Handling**: Errors are logged but don't fail webhook processing
* **Automatic Reconciliation**: Payments are automatically reconciled with invoices
***
## Best Practices
### Currency Handling
1. **Minimum Amounts**: Always ensure amounts meet currency minimums
2. **Currency Conversion**: Amounts are automatically converted to smallest units
3. **Consistent Currency**: Use consistent currency across invoice and payment
### Invoice Sync
1. **Enable Sync**: Enable invoice outbound sync in connection settings
2. **Finalize Invoices**: Only finalized invoices are synced
3. **Customer Sync**: Ensure customers are synced before invoice sync
### Payment Links
1. **Amount Validation**: Validate amounts against invoice remaining balance
2. **Currency Matching**: Ensure payment currency matches invoice currency
3. **Callback URLs**: Provide callback URLs for better user experience
### Error Handling
1. **Monitor Webhooks**: Regularly check webhook delivery logs
2. **Review Logs**: Check application logs for sync errors
3. **Retry Failed Syncs**: Retry failed syncs manually if needed
***
## Troubleshooting
### Common Issues
| Issue | Solution |
| --------------------------- | -------------------------------------------------------------------- |
| Payment link creation fails | Check customer sync status |
| Invoice sync fails | Verify invoice is finalized and customer is synced |
| Webhook not received | Verify webhook URL and events in Razorpay dashboard |
| Amount below minimum | Ensure amount meets currency minimum (₹1.00 for INR, \$0.01 for USD) |
### Debug Steps
1. **Check Connection**: Verify Razorpay connection is active
2. **Check Customer Sync**: Ensure customer is synced to Razorpay
3. **Check Invoice Status**: Verify invoice is finalized
4. **Check Webhooks**: Verify webhook configuration in Razorpay
5. **Review Logs**: Check application logs for detailed error messages
***
## Support
Need assistance? We're here to help.
* **Flexprice Support:** [support@flexprice.com](mailto:support@flexprice.com)
* **Integration Documentation:** [Flexprice Docs](https://docs.flexprice.com)
* **Razorpay Documentation:** [Razorpay Docs](https://razorpay.com/docs)
💡 **Need help setting up?**\
Our team can assist with configuration, testing, and troubleshooting.\
Reach out to **Flexprice Support** to schedule a setup session.
# Razorpay Payment Links & Invoice Sync
Source: https://docs.flexprice.io/integrations/razorpay/payment-links
Complete guide to creating payment links and handling the Razorpay checkout flow
## Overview
Payment links in Flexprice provide a secure, hosted checkout experience powered by Razorpay. Customers click a payment URL and are redirected to Razorpay's secure checkout page to complete their payment.
### Key Features
* **Secure Checkout**: Hosted by Razorpay with PCI compliance
* **Multiple Payment Methods**: Cards, UPI, netbanking, wallets, and more
* **Real-time Updates**: Webhook-driven status updates
* **Invoice Integration**: Automatic invoice reconciliation
* **Auto-generated Links**: Razorpay automatically generates payment link URLs
## Payment Link Lifecycle
```mermaid theme={null}
graph TD
A[Create Payment Request] --> B[Validate Invoice & Customer]
B --> C[Create Payment Record INITIATED]
C --> D[Generate Razorpay Payment Link]
D --> E{Link Created?}
E -->|Success| F[Update Status PENDING]
E -->|Failure| G[Keep Status INITIATED]
F --> H[Return Payment URL]
H --> I[Customer Clicks Link]
I --> J[Razorpay Checkout]
J --> K[Enter Payment Details]
K --> L{Payment Successful?}
L -->|Yes| M[Webhook: payment.captured]
L -->|No| N[Webhook: payment.failed]
M --> O[Status: SUCCEEDED]
N --> P[Status: FAILED]
O --> Q[Reconcile Invoice]
P --> R[Log Failure]
```
## Payment Status Flow
### Status Transitions
```
INITIATED → PENDING → SUCCEEDED
↓ ↓ ↓
FAILED FAILED REFUNDED
```
### Status Definitions
| Status | Description | Triggers |
| ------------ | ----------------------------------------------------- | ------------------------------------ |
| `INITIATED` | Payment record created, Razorpay link not yet created | Initial payment creation |
| `PENDING` | Razorpay payment link created successfully | Successful Razorpay API call |
| `PROCESSING` | Payment is being processed | Temporary status during updates |
| `SUCCEEDED` | Payment completed successfully | Razorpay webhook: `payment.captured` |
| `FAILED` | Payment attempt failed | Razorpay webhook: `payment.failed` |
| `REFUNDED` | Payment was refunded | Manual refund processing |
## Creating Payment Links
### Prerequisites
1. **Razorpay Connection**: Active Razorpay connection configured
2. **Customer Sync**: Customer must have Razorpay customer ID
3. **Invoice Status**: Invoice must be finalized and unpaid
4. **Valid Amount**: Payment amount must match invoice outstanding
5. **Currency Minimums**: Ensure amounts meet Razorpay minimums
### Currency Minimum Amounts
Razorpay requires minimum amounts in the smallest currency subunit:
| Currency | Minimum amount (in smallest subunit) | Equivalent Minimum Value |
| -------- | ------------------------------------ | ------------------------ |
| INR | 100 | ₹1.00 |
| USD | 1 | \$0.01 |
**Important**: Amounts are automatically converted to smallest currency units (paise for INR, cents for USD) when creating payment links.
### API Request
**Endpoint:** `POST /api/v1/payments`
**Headers:**
```http theme={null}
Content-Type: application/json
Authorization: Bearer your_api_key
X-Environment-ID: your_environment_id
```
**Request Body:**
```json theme={null}
{
"amount": 100,
"currency": "inr",
"destination_id": "inv_01K34HYZN03V6X8Q6V3T8X6G4F",
"destination_type": "INVOICE",
"payment_method_type": "PAYMENT_LINK",
"process_payment": true,
"payment_gateway": "razorpay",
"success_url": "https://admin.flexprice.io/customer-management/invoices/inv_01K34HYZN03V6X8Q6V3T8X6G4F?page=1"
}
```
### Response
```json theme={null}
{
"id": "pay_1234567890abcdef",
"payment_status": "pending",
"payment_url": "https://rzp.io/i/abc123",
"amount": "100.00",
"currency": "inr",
"gateway_tracking_id": "plink_1234567890abcdef",
"created_at": "2024-01-20T10:30:00Z",
"metadata": {
"razorpay_payment_link_id": "plink_1234567890abcdef"
}
}
```
**Note**: The `payment_url` is automatically generated by Razorpay and returned as a short URL (e.g., `https://rzp.io/i/abc123`).
### Creating Payment Links via Dashboard
You can also create payment links directly from the Flexprice dashboard:
## Payment Link Configuration
### Required Fields
| Field | Type | Required | Description |
| --------------------- | ------- | -------- | ------------------------------------- |
| `destination_type` | string | ✅ | Must be `"INVOICE"` |
| `destination_id` | string | ✅ | Valid invoice ID |
| `payment_method_type` | string | ✅ | Must be `"PAYMENT_LINK"` |
| `payment_gateway` | string | ✅ | Must be `"razorpay"` |
| `amount` | string | ✅ | Payment amount (must match invoice) |
| `currency` | string | ✅ | Valid currency code (INR, USD, etc.) |
| `process_payment` | boolean | ✅ | Set to `true` to create Razorpay link |
| `success_url` | string | ❌ | Callback URL after payment (optional) |
## Razorpay Payment Link Configuration
### Default Payment Link Settings
When creating payment links, Flexprice automatically configures Razorpay with:
* **Payment Methods**: Cards, UPI, netbanking, wallets, and more
* **Currency**: Matches the payment currency
* **Amount**: Exact payment amount (converted to smallest unit)
* **Customer**: Pre-filled with customer data
* **Description**: Invoice details and line items
* **Notifications**: Email and SMS notifications enabled
* **Reminders**: Payment reminders enabled
### Callback URL
Razorpay uses a single `callback_url` for both successful and cancelled payments:
* **Success**: Customer is redirected to `callback_url` with payment details
* **Cancel**: Customer is redirected to `callback_url` with cancellation status
* **Method**: Only GET method is supported for payment links
## Webhook Processing
### Payment Completion Webhook
When a payment is completed, Razorpay sends a `payment.captured` webhook:
```json theme={null}
{
"event": "payment.captured",
"payload": {
"payment": {
"entity": {
"id": "pay_1234567890abcdef",
"amount": 10000,
"currency": "INR",
"status": "captured",
"invoice_id": "inv_razorpay123",
"notes": {
"flexprice_payment_id": "pay_1234567890abcdef"
}
}
}
}
}
```
### Webhook Processing Flow
1. **Receive Webhook**: Flexprice receives the webhook from Razorpay
2. **Verify Signature**: Validate webhook signature for security
3. **Find Payment**: Locate payment record using payment ID from notes
4. **Update Status**: Change payment status to `SUCCEEDED`
5. **Reconcile Invoice**: Update invoice payment status and amounts
6. **Trigger Events**: Send payment completion events
## Error Handling
### Common Payment Errors
| Error | Cause | Resolution |
| -------------------------------- | ---------------------- | ----------------------------------------------------------- |
| "Customer not found in Razorpay" | Customer not synced | Sync customer to Razorpay first |
| "Invalid invoice amount" | Amount mismatch | Ensure amount matches invoice |
| "Invoice already paid" | Invoice status issue | Check invoice payment status |
| "Amount below minimum" | Below currency minimum | Ensure amount meets minimum (₹1.00 for INR, \$0.01 for USD) |
| "Razorpay link creation failed" | API error | Check Razorpay connection |
### Payment Failure Handling
When payments fail:
1. **Webhook Received**: Razorpay sends `payment.failed` webhook
2. **Status Updated**: Payment status changed to `FAILED`
3. **Error Logged**: Failure reason recorded
4. **Customer Notified**: Optional failure notification
5. **Retry Available**: Customer can create new payment link
## Testing Payment Links
### Test Environment Setup
1. **Use Test Keys**: Configure Razorpay test mode
2. **Test Cards**: Use Razorpay's test card numbers
3. **Test Webhooks**: Use ngrok for local webhook testing
### Test Card Numbers
#### Test Cards for Indian Payments
| Card Network | Card Number | CVV | Expiry Date |
| ------------ | --------------------- | ---------- | --------------- |
| Mastercard | `2305 3242 5784 8228` | Random CVV | Any future date |
| Visa | `4386 2894 0766 0153` | Random CVV | Any future date |
#### Test Cards for International Payments
| Card Network | Card Number | CVV | Expiry Date |
| ------------ | --------------------- | ---------- | --------------- |
| Mastercard | `5421 1393 0609 0628` | Random CVV | Any future date |
| Mastercard | `5105 1051 0510 5100` | Random CVV | Any future date |
| Mastercard | `5104 0600 0000 0008` | Random CVV | Any future date |
| Visa | `4012 8888 8888 1881` | Random CVV | Any future date |
### Test UPI IDs
| UPI ID | Description |
| ------------------ | ------------------ |
| `success@razorpay` | Successful payment |
| `failure@razorpay` | Failed payment |
#### To use the test UPI ID details:
1. At the Checkout, select **UPI** as the payment method
2. Enter the UPI ID
3. **Test payment success flow** using `success@razorpay`
4. **Test payment failure flow** using `failure@razorpay`
**Watch Out!**
In test mode, payment cancellation will result in a successful payment. Use the live mode to test payment cancellation on UPI.
### Testing Flow
1. **Create Test Payment**: Use test invoice and customer
2. **Generate Payment Link**: Create payment with test data
3. **Complete Payment**:
* Use test card numbers listed above in Razorpay checkout
* Or use test UPI ID `success@razorpay` for UPI payments
4. **Verify Webhook**: Check webhook processing
5. **Confirm Status**: Verify payment and invoice updates
## Invoice Integration
### Payment Links from Synced Invoices
When an invoice is synced to Razorpay:
1. **Invoice Sync**: Invoice is created in Razorpay with payment URL
2. **Payment Link Reuse**: If payment link is requested for synced invoice, the existing Razorpay invoice payment URL is returned
3. **Automatic Association**: Payments made through the URL are automatically associated with the invoice
### Invoice Sync Timing
Invoices are automatically synced to Razorpay when:
* Invoice is finalized (status changes from `DRAFT` to `FINALIZED`)
* Invoice outbound sync is enabled in the Razorpay connection
* Customer is successfully synced to Razorpay
## Security Considerations
### Payment Link Security
* **HTTPS Required**: All payment links use HTTPS
* **Link Expiry**: Links expire based on Razorpay settings
* **One-time Use**: Each link can only be used once
* **Signature Verification**: All webhooks verified
### Data Protection
* **PCI Compliance**: Razorpay handles sensitive payment data
* **No Card Storage**: Card details never stored in Flexprice
* **Encrypted Storage**: All payment data encrypted at rest
* **Access Controls**: Role-based access to payment data
## Webhook Endpoints
* `POST /api/v1/webhooks/razorpay/{tenant_id}/{environment_id}` - Razorpay webhook handler
For complete API documentation, see the [API Reference](/api-reference/payments/create-payment).
# Card Payment Checkout Flow
Source: https://docs.flexprice.io/integrations/stripe/card-payment
Complete guide to processing card payments using saved payment methods
## Overview
Card payments in Flexprice allow customers to pay using their previously saved payment methods. This flow is designed for returning customers who have already saved their card details through a payment link with `save_card_and_make_default: true`.
### Key Features
* **Instant Payments**: No redirect to external checkout pages
* **Saved Payment Methods**: Uses customer's default payment method
* **Off-Session Processing**: Payments processed without customer interaction
* **Automatic Reconciliation**: Invoice updates happen automatically
* **Secure Processing**: All card data handled by Stripe
## Card Payment Prerequisites
### Customer Requirements
* **Existing Customer**: Customer must exist in both Flexprice and Stripe
* **Default Payment Method**: Customer must have a default payment method set in Stripe
* **Previous Payment Link**: Default payment method created via payment link with `save_card_and_make_default: true`
### System Requirements
* **Stripe Integration**: Active Stripe connection configured
* **Valid Invoice**: Invoice must be finalized and have outstanding amount
* **Matching Currency**: Payment currency must match invoice currency
## Payment Status Flow
### Status Transitions for Card Payments
```
INITIATED → PROCESSING → SUCCEEDED
↓ ↓ ↓
FAILED FAILED REFUNDED
```
### Status Definitions
| Status | Description | Triggers |
| ------------ | ------------------------------------------ | ------------------------------------------ |
| `INITIATED` | Payment record created, validation pending | Initial payment creation |
| `PROCESSING` | Payment intent created and being processed | Stripe payment intent confirmation |
| `SUCCEEDED` | Payment completed successfully | Successful charge confirmation |
| `FAILED` | Payment attempt failed | Stripe payment failure or validation error |
| `REFUNDED` | Payment was refunded | Manual refund processing |
## Creating Card Payments
### API Request
**Endpoint:** `POST /api/v1/payments`
**Headers:**
```http theme={null}
Content-Type: application/json
Authorization: Bearer your_api_key
X-Environment-ID: your_environment_id
```
**Request Body:**
```json theme={null}
{
"amount": 150.00,
"currency": "usd",
"destination_id": "inv_01K34HYZN03V6X8Q6V3T8X6G4F",
"destination_type": "INVOICE",
"payment_method_type": "CARD",
"process_payment": true,
"payment_gateway": "stripe"
}
```
### Response
**Success Response:**
```json theme={null}
{
"id": "pay_1234567890abcdef",
"payment_status": "succeeded",
"amount": "150.00",
"currency": "usd",
"payment_method_id": "pm_1234567890abcdef",
"gateway_tracking_id": "pi_1234567890abcdef",
"created_at": "2024-01-20T10:30:00Z",
"metadata": {
"stripe_payment_intent_id": "pi_1234567890abcdef",
"card_last4": "4242",
"card_brand": "visa"
}
}
```
**Error Response:**
```json theme={null}
{
"error": {
"code": "no_default_payment_method",
"message": "Customer must have a default payment method to make card payments. Please make a payment using payment link with save_card_and_make_default=true first.",
"details": {
"customer_id": "cus_1234567890abcdef",
"payment_id": "pay_1234567890abcdef"
}
}
}
```
## Card Payment Configuration
### Required Fields
| Field | Type | Required | Description |
| --------------------- | ------- | -------- | ------------------------------------ |
| `destination_type` | string | ✅ | Must be `"INVOICE"` |
| `destination_id` | string | ✅ | Valid invoice ID |
| `payment_method_type` | string | ✅ | Must be `"CARD"` |
| `payment_gateway` | string | ✅ | Must be `"stripe"` |
| `amount` | decimal | ✅ | Payment amount (must match invoice) |
| `currency` | string | ✅ | Valid currency code |
| `process_payment` | boolean | ✅ | Set to `true` to process immediately |
### Optional Fields
| Field | Type | Required | Description |
| ------------------- | ------ | -------- | -------------------------------------------------- |
| `payment_method_id` | string | ❌ | Specific payment method ID (uses default if empty) |
| `metadata` | object | ❌ | Additional payment metadata |
### Payment Method Selection
When creating a card payment, you can specify which payment method to use:
* **No `payment_method_id` provided**: Uses the customer's default payment method
* **`payment_method_id` provided**: Uses the specified payment method for this payment
**Example with specific payment method:**
```json theme={null}
{
"amount": 150.00,
"currency": "usd",
"destination_id": "inv_01K34HYZN03V6X8Q6V3T8X6G4F",
"destination_type": "INVOICE",
"payment_method_type": "CARD",
"payment_method_id": "pm_1234567890abcdef",
"process_payment": true,
"payment_gateway": "stripe"
}
```
**cURL Example:**
```bash theme={null}
curl -X POST https://api.cloud.flexprice.io/v1/payments \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_api_key" \
-H "X-Environment-ID: your_environment_id" \
-d '{
"amount": 150.00,
"currency": "usd",
"destination_id": "inv_01K34HYZN03V6X8Q6V3T8X6G4F",
"destination_type": "INVOICE",
"payment_method_type": "CARD",
"payment_method_id": "pm_1234567890abcdef",
"process_payment": true,
"payment_gateway": "stripe"
}'
```
## Payment Processing Flow
### Payment Validation
Before processing, the system validates:
* **Invoice Status**: Must be finalized and have outstanding amount
* **Customer Existence**: Customer must exist in both systems
* **Amount Matching**: Payment amount must match invoice outstanding
* **Currency Matching**: Payment and invoice currencies must match
### Default Payment Method Retrieval
```mermaid theme={null}
graph TD
A[Check Payment Method ID] --> B{ID Provided?}
B -->|Yes| C[Use Provided ID]
B -->|No| D[Get Customer Default Payment Method]
D --> E{Default Exists?}
E -->|Yes| F[Use Default Payment Method]
E -->|No| G[Return Error]
C --> H[Process Payment]
F --> H[Process Payment]
```
### Stripe Payment Intent Creation
The system creates a Stripe Payment Intent with:
* **Amount**: Exact payment amount in smallest currency unit
* **Currency**: Payment currency
* **Customer**: Stripe customer ID
* **Payment Method**: Default payment method ID
* **Confirmation**: Immediate confirmation (`confirm: true`)
* **Off Session**: Set to `true` for saved payment methods
### Payment Confirmation
Payment intents are automatically confirmed with the following parameters:
```json theme={null}
{
"amount": 15000,
"currency": "usd",
"customer": "cus_stripe123abc",
"payment_method": "pm_1234567890abcdef",
"off_session": true,
"confirm": true
}
```
## Error Handling
### Common Card Payment Errors
| Error Code | Cause | Resolution |
| --------------------------- | -------------------------------------- | ----------------------------------------------------------- |
| `no_default_payment_method` | Customer has no default payment method | Create payment link with `save_card_and_make_default: true` |
| `card_declined` | Card was declined by issuer | Customer should contact their bank |
| `insufficient_funds` | Insufficient funds on card | Customer should use different payment method |
| `authentication_required` | 3D Secure authentication needed | Use payment link flow for authentication |
| `expired_card` | Card has expired | Customer needs to update payment method |
### Error Response Format
> **Note:** All card payment errors follow this standardized format for consistent error handling.
```json theme={null}
{
"error": {
"code": "error_code",
"message": "Human-readable error message",
"details": {
"customer_id": "cus_1234567890abcdef",
"payment_id": "pay_1234567890abcdef",
"stripe_error_code": "card_declined"
}
}
}
```
### Recovery Flow
1. **Immediate Failure**: Payment status set to `FAILED`
2. **Error Logging**: Detailed error information recorded
3. **Customer Notification**: Optional failure notification sent
4. **Alternative Options**:
* Retry with payment link flow
* Update payment method via payment link with `save_card_and_make_default: true`
* Use different payment method
## Security Considerations
### Payment Security
* **Off-Session Payments**: All card payments are off-session using saved methods
* **PCI Compliance**: No card data stored in Flexprice systems
* **Stripe Security**: All sensitive operations handled by Stripe
* **Authentication**: Strong Customer Authentication (SCA) compliance
### Data Protection
* **Encrypted Storage**: All payment data encrypted at rest
* **Access Controls**: Role-based access to payment information
* **Audit Logging**: All payment operations logged for compliance
* **Data Retention**: Payment data retained per compliance requirements
## Testing Card Payments
### Test Environment Setup
1. **Stripe Test Mode**: Use Stripe test keys
2. **Test Customer**: Create customer with saved payment method
3. **Test Payment Link**: First create payment link with `save_card_and_make_default: true`
4. **Test Card Payment**: Then test card payment flow
### Test Scenarios
| Scenario | Test Card | Expected Result |
| ----------------------- | ------------------ | ------------------------------------- |
| Successful Payment | `4242424242424242` | Payment succeeds |
| Declined Card | `4000000000000002` | Payment fails with decline |
| Insufficient Funds | `4000000000009995` | Payment fails with insufficient funds |
| Authentication Required | `4000002760003184` | Payment fails, requires payment link |
## API Reference
For complete API documentation, see the [API Reference](/api-reference/payments/create-payment).
# Stripe Connection Setup
Source: https://docs.flexprice.io/integrations/stripe/connection-setup
Complete guide to setting up and configuring Stripe connections in Flexprice
## Overview
A Stripe connection in Flexprice stores encrypted credentials that allow the system to interact with your Stripe account for:
* Creating customers in Stripe
* Processing payment links via Stripe Checkout
* Receiving webhook notifications from Stripe
* Synchronizing customer data between systems
## Video Walkthrough
Watch this step-by-step guide to setting up your Stripe connection:
## Prerequisites
Before setting up your Stripe connection, ensure you have:
1. **Active Stripe Account** - Sign up at [stripe.com](https://stripe.com)
2. **API Keys** - Available in your Stripe Dashboard
3. **Webhook Endpoint** - Configured in Stripe Dashboard
4. **Flexprice Environment** - Valid tenant and environment IDs
## Step 1: Gather Stripe Credentials
### Required Credentials
| Credential | Location in Stripe Dashboard | Required | Purpose |
| ------------------ | ---------------------------------------- | -------- | ------------------------------ |
| **Secret Key** | Developers → API Keys | ✅ | Server-side API calls |
| **Webhook Secret** | Developers → Webhooks → \[Your Endpoint] | ✅ | Webhook signature verification |
### Finding Your Credentials
1. **API Keys:**
* Go to Stripe Dashboard → Developers → API Keys
* Copy your `Secret key` (starts with `sk_`)
* Use test keys (`sk_test_`) for development
2. **Webhook Secret:**
* Go to Stripe Dashboard → Developers → Webhooks
* Create or select your webhook endpoint
* Click "Reveal signing secret"
* Copy the secret (starts with `whsec_`)
## Step 2: Configure Webhook Endpoint in Stripe
### Webhook URL Format
```
https://api.cloud.flexprice.io/v1/webhooks/stripe/tenant_01K1TJDVNSN7TWY8CZY870QMNV/env_01K1TJJF0CJR410C6QVPYQTNV0
```
### Required Webhook Events
Configure these events in your Stripe webhook endpoint:
| Event Type | Purpose |
| -------------------- | --------------------------------------------------------------------- |
| `checkout.session *` | Track payment link completions from Stripe Checkout |
| `customer *` | Synchronize customer data changes between Stripe and Flexprice |
| `payment_intent *` | Monitor payment processing status and handle payment lifecycle events |
### Setting Up the Webhook
1. Go to Stripe Dashboard → Developers → Webhooks
2. Click **+ Add endpoint**
3. Enter your webhook URL
4. Select the required events listed above
5. Click **Add endpoint**
6. Copy the **Signing secret** for your connection
## Step 3: Create Stripe Connection via API
### Using Flexprice Dashboard
You can create a Stripe connection directly from the Flexprice dashboard:
### API Request
**Endpoint:** `POST /api/v1/connections`
**Headers:**
```http theme={null}
Content-Type: application/json
Authorization: Bearer your_api_key
X-Tenant-ID: your_tenant_id
X-Environment-ID: your_environment_id
```
**Request Body:**
```json theme={null}
{
"name": "Stripe Production",
"provider_type": "stripe",
"encrypted_secret_data": {
"secret_key": "sk_live_...",
"webhook_secret": "whsec_...",
}
}
```
### Response
```json theme={null}
{
"id": "conn_1234567890abcdef",
"name": "Stripe Production",
"provider_type": "stripe",
"status": "active",
"created_at": "2024-01-20T10:30:00Z",
"updated_at": "2024-01-20T10:30:00Z"
}
```
## Security Best Practices
### Credential Management
1. **Environment Separation**: Use different keys for test/production
2. **Key Rotation**: Regularly rotate your Stripe API keys
3. **Access Control**: Limit API key permissions in Stripe
4. **Encryption**: All credentials are encrypted at rest in Flexprice
### Webhook Security
1. **HTTPS Only**: Always use HTTPS for webhook endpoints
2. **Signature Verification**: Flexprice verifies all webhook signatures
3. **Secret Management**: Keep webhook secrets secure and rotate regularly
4. **Rate Limiting**: Implement rate limiting on webhook endpoints
## Troubleshooting
### Common Issues
| Issue | Cause | Solution |
| ---------------------------- | --------------------------- | ----------------------------------- |
| Connection test fails | Invalid API keys | Verify keys in Stripe Dashboard |
| Webhook not received | Incorrect webhook URL | Check URL format and endpoint |
| Signature verification fails | Wrong webhook secret | Update webhook secret in connection |
| Customer sync fails | Missing account permissions | Check Stripe account permissions |
### Debug Steps
1. **Check Stripe Dashboard**: Verify API keys and webhook configuration
2. **Test API Keys**: Use Stripe CLI to test API key validity
3. **Monitor Webhooks**: Check webhook delivery logs in Stripe
4. **Review Logs**: Check Flexprice application logs for errors
## Next Steps
After setting up your Stripe connection:
1. **Test Customer Sync**: Create a test customer and sync to Stripe
2. **Create Payment Links**: Test payment link creation and flow
3. **Monitor Webhooks**: Ensure webhook events are being received
4. **Go Live**: Switch to production keys when ready
# Stripe Customer Synchronization
Source: https://docs.flexprice.io/integrations/stripe/customer-sync
Complete guide to on-demand customer synchronization between Flexprice and Stripe
## Overview
Customer synchronization ensures that customers exist in both Flexprice and Stripe with linked identifiers, enabling:
* **On-demand sync**: Customers are synced to Stripe only when needed for payments
* Payment link creation for any Flexprice customer
* Card payment processing with automatic customer sync
* Automatic customer creation from Stripe webhooks
* Consistent customer data across platforms
* Efficient payment processing workflows
## Synchronization Flow
Customer sync now works **on-demand** with bidirectional support:
```mermaid theme={null}
graph LR
A[Flexprice Customer] -->|On-Demand Sync| B[Stripe Customer]
C[Payment Link Request] -->|Triggers Sync| B
D[Card Payment Request] -->|Triggers Sync| B
E[Stripe Customer] -->|Webhook Sync| F[Flexprice Customer]
B -->|Metadata Link| A
F -->|Metadata Link| E
```
### Key Concepts
* **On-Demand Sync**: Customers are synced to Stripe only when payment interactions occur
* **Entity Integration Mapping**: Links Flexprice customers to Stripe customer IDs
* **Metadata Storage**: Customer metadata stores cross-platform references
* **Payment-Triggered Sync**: Sync happens automatically during payment flows
* **Duplicate Prevention**: Intelligent checks prevent duplicate customer creation
* **Performance Optimization**: No unnecessary syncs for customers who never make payments
## Customer Data Model
### Flexprice Customer Structure
```json theme={null}
{
"id": "cust_1234567890abcdef",
"external_id": "customer_external_123",
"name": "John Doe",
"email": "john@example.com",
"address_line1": "123 Main St",
"address_line2": "Apt 4B",
"address_city": "New York",
"address_state": "NY",
"address_postal_code": "10001",
"address_country": "US",
"metadata": {
"stripe_customer_id": "cus_stripe123abc"
},
"environment_id": "prod",
"tenant_id": "tenant_123",
"status": "active",
"created_at": "2024-01-20T10:30:00Z"
}
```
### Stripe Customer Structure
```json theme={null}
{
"id": "cus_stripe123abc",
"name": "John Doe",
"email": "john@example.com",
"address": {
"line1": "123 Main St",
"line2": "Apt 4B",
"city": "New York",
"state": "NY",
"postal_code": "10001",
"country": "US"
},
"metadata": {
"flexprice_customer_id": "cust_1234567890abcdef",
"flexprice_environment": "prod",
"external_id": "customer_external_123"
},
"created": 1642680600
}
```
## Synchronization Methods
### 1. On-Demand Sync (Payment-Triggered)
**Primary Method**: Customers are automatically synced to Stripe when payment interactions occur.
#### Payment Link Creation
When creating a payment link, the system automatically ensures the customer exists in Stripe:
**Endpoint:** `POST /api/v1/payments`
**Request:**
```json theme={null}
{
"amount": 20,
"currency": "usd",
"destination_id": "inv_01K3K2N0FY0Z7Z6RHR6EMGJ8AC",
"destination_type": "INVOICE",
"payment_method_type": "PAYMENT_LINK",
"process_payment": true,
"payment_gateway": "stripe",
"success_url": "https://admin.flexprice.io/customer-management/invoices/inv_01K3K2N0FY0Z7Z6RHR6EMGJ8AC?page=1",
"cancel_url": "https://admin.flexprice.io/customer-management/invoices/inv_01K3K2N0FY0Z7Z6RHR6EMGJ8AC?page=1"
}
```
**Internal Flow:**
1. Check if customer has `stripe_customer_id` in metadata
2. Check entity integration mapping table
3. If not synced → Create customer in Stripe automatically
4. Create payment link with synced customer
#### Card Payment Processing
When processing card payments, the system ensures customer sync:
**Endpoint:** `POST /api/v1/payments`
**Request:**
```json theme={null}
{
"amount": 20,
"currency": "usd",
"destination_id": "inv_01K3K2N0FY0Z7Z6RHR6EMGJ8AC",
"destination_type": "INVOICE",
"payment_method_type": "CARD",
"process_payment": true,
"payment_gateway": "stripe",
"success_url": "https://admin.flexprice.io/customer-management/invoices/inv_01K3K2N0FY0Z7Z6RHR6EMGJ8AC?page=1",
"cancel_url": "https://admin.flexprice.io/customer-management/invoices/inv_01K3K2N0FY0Z7Z6RHR6EMGJ8AC?page=1"
}
```
**Internal Flow:**
1. Validate payment amount against invoice remaining balance
2. Ensure customer is synced to Stripe (auto-sync if needed)
3. Process payment with Stripe customer using saved payment method
### 2. Manual Sync (Optional)
For advanced use cases, manual sync is still available:
**Endpoint:** `POST /api/v1/integrations/sync/customer`
**Request:**
```json theme={null}
{
"customer_id": "cust_1234567890abcdef",
"provider": "stripe",
"sync_direction": "flexprice_to_provider"
}
```
### 3. Automatic Sync (Stripe → Flexprice)
Customers created in Stripe are automatically synced via webhooks.
**Webhook Event:** `customer.created`
**Webhook Payload:**
```json theme={null}
{
"id": "evt_1234567890abcdef",
"type": "customer.created",
"data": {
"object": {
"id": "cus_stripe123abc",
"name": "John Doe",
"email": "john@example.com",
"address": {
"line1": "123 Main St",
"city": "New York",
"state": "NY",
"postal_code": "10001",
"country": "US"
},
"metadata": {
"flexprice_environment": "prod"
}
}
}
}
```
## Sync Configuration
### Required Fields for Sync
| Field | Flexprice → Stripe | Stripe → Flexprice | Notes |
| --------------------- | ------------------ | ------------------ | --------------------- |
| `name` | ✅ | ✅ | Customer full name |
| `email` | ✅ | ✅ | Primary email address |
| `address_line1` | ✅ | ✅ | Street address |
| `address_city` | ✅ | ✅ | City |
| `address_state` | ✅ | ✅ | State/province |
| `address_postal_code` | ✅ | ✅ | ZIP/postal code |
| `address_country` | ✅ | ✅ | Country code |
### Optional Fields
| Field | Description |
| --------------- | ------------------------- |
| `address_line2` | Apartment/suite number |
| `phone` | Phone number |
| `external_id` | Your internal customer ID |
| `metadata` | Custom key-value pairs |
## Sync Process Details
### On-Demand Sync Flow (Payment-Triggered)
```mermaid theme={null}
graph TD
A[Payment Request] --> B[EnsureCustomerSyncedToStripe]
B --> C{Check Metadata}
C -->|Has stripe_customer_id| D[Return Existing Customer]
C -->|No stripe_customer_id| E[Check Integration Mapping]
E --> F{Mapping Exists?}
F -->|Yes| G[Update Customer Metadata]
F -->|No| H[CreateCustomerInStripe]
G --> I[Return Customer Info]
H --> J[Update Metadata & Mapping]
J --> I
I --> K[Process Payment]
```
### Stripe → Flexprice Sync Flow (Unchanged)
```mermaid theme={null}
graph TD
A[Webhook Received] --> B[Verify Signature]
B --> C[Parse Customer Data]
C --> D[Check Existing Flexprice Customer]
D --> E{Exists in Flexprice?}
E -->|Yes| F[Update Flexprice Customer]
E -->|No| G[Create Flexprice Customer]
F --> H[Update Metadata]
G --> H
H --> I[Return 200 Success]
```
## Customer Linking
### Metadata Storage
Customer linking is maintained through metadata fields:
**In Flexprice Customer:**
```json theme={null}
{
"metadata": {
"stripe_customer_id": "cus_stripe123abc",
"stripe_sync_timestamp": "2024-01-20T10:30:00Z"
}
}
```
**In Stripe Customer:**
```json theme={null}
{
"metadata": {
"flexprice_customer_id": "cust_1234567890abcdef",
"flexprice_environment": "prod",
"flexprice_sync_timestamp": "2024-01-20T10:30:00Z"
}
}
```
### Link Verification
You can verify customer links using the API:
**Endpoint:** `GET /api/v1/customers/{customer_id}/integrations`
**Response:**
```json theme={null}
{
"customer_id": "cust_1234567890abcdef",
"integrations": [
{
"provider": "stripe",
"external_id": "cus_stripe123abc",
"sync_status": "linked",
"last_sync": "2024-01-20T10:30:00Z"
}
]
}
```
## Duplicate Prevention
### Email-Based Deduplication
The system prevents duplicate customers using email addresses:
1. **Check Existing**: Look for customers with same email
2. **Merge Strategy**: Update existing customer with new data
3. **Link Creation**: Create cross-platform links
4. **Audit Trail**: Log all sync operations
### External ID Matching
For customers with external IDs:
1. **Priority Matching**: External ID takes precedence over email
2. **Fallback to Email**: If no external ID match, use email
3. **Manual Resolution**: Conflicts require manual intervention
## Error Handling
### Common Sync Errors
| Error | Cause | Resolution |
| ---------------------- | ------------------------- | --------------------------- |
| "Customer not found" | Invalid customer ID | Verify customer exists |
| "Email already exists" | Duplicate email in Stripe | Check for existing customer |
| "Invalid email format" | Malformed email address | Fix email format |
| "Stripe API error" | Stripe connection issue | Check Stripe credentials |
### Sync Failure Recovery
When sync fails:
1. **Log Error**: Record failure with details
2. **Retry Logic**: Automatic retry with exponential backoff
3. **Manual Intervention**: Admin can retry failed syncs
4. **Notification**: Alert on persistent failures
## Testing Customer Sync
### Test Environment Setup
1. **Test Stripe Account**: Use Stripe test mode
2. **Test Customers**: Create test customers in Flexprice only
3. **Webhook Testing**: Use ngrok for local webhook testing
### On-Demand Sync Testing Flow
1. **Create Test Customer**: Add customer in Flexprice (no sync yet)
2. **Verify No Sync**: Customer should NOT exist in Stripe initially
3. **Create Payment Link**: Trigger payment link creation
4. **Verify Auto-Sync**: Customer should now exist in Stripe
5. **Test Card Payment**: Process card payment with same customer
6. **Verify Reuse**: Should use existing Stripe customer (no duplicate)
# Invoice Sync & Save Card
Source: https://docs.flexprice.io/integrations/stripe/examples/invoice-sync-workflow
This guide walks through the complete integration workflow between Flexprice and Stripe, from initial setup to automated invoice sync and payment processing.
**Integration Type**: Bidirectional sync (Flexprice ↔ Stripe)
**Key Capabilities**:
* ✅ Automatic invoice sync to Stripe
* ✅ Secure payment method storage
* ✅ Automated payment processing
* ✅ Real-time webhook reconciliation
* ✅ Customer and subscription sync
## Step-by-Step Workflow
### Phase 1: Initial Setup
Set up your Stripe connection with invoice sync enabled. This allows Flexprice to automatically sync invoices to Stripe and process payments.
**📚 Complete Guide:** [Stripe Connection Setup](/integrations/stripe/connection-setup)
***
### Phase 2: Product Catalog Setup
Define a metered feature for usage-based billing:
**Endpoint:** `POST /api/v1/features`
```json theme={null}
{
"name": "API Calls",
"key": "api_calls",
"feature_type": "metered",
"meter_id": "meter_api_requests",
"description": "Number of API requests made"
}
```
**Response:**
```json theme={null}
{
"id": "feat_1234567890abcdef",
"name": "API Calls",
"key": "api_calls",
"feature_type": "metered",
"status": "active"
}
```
Create a subscription plan with pricing:
**Endpoint:** `POST /api/v1/plans`
```json theme={null}
{
"name": "Professional Plan",
"key": "professional_plan",
"billing_period": "monthly",
"currency": "USD",
"plan_type": "recurring",
"prices": [
{
"feature_id": "feat_1234567890abcdef",
"pricing_model": "tiered",
"unit_price": "0.01",
"tiers": [
{
"up_to": 1000,
"unit_price": "0.01",
"flat_fee": "10.00"
},
{
"up_to": 10000,
"unit_price": "0.008"
},
{
"up_to": null,
"unit_price": "0.005"
}
]
}
]
}
```
**📚 Documentation:**
* [Create a Feature](/docs/product-catalogue/features/create)
* [Create a Plan](/docs/product-catalogue/plans/create)
**🔗 API Reference:**
* [Create Feature API](/api-reference/features/create-feature)
* [Create Plan API](/api-reference/plans/create-plan)
***
### Phase 3: Customer Onboarding
Create a customer in Flexprice (automatically synced to Stripe):
**Endpoint:** `POST /api/v1/customers`
```json theme={null}
{
"name": "Acme Corporation",
"email": "billing@acme.com",
"customer_id": "acme_corp_001",
"metadata": {
"company_size": "enterprise",
"industry": "technology"
}
}
```
**Response:**
```json theme={null}
{
"id": "cus_1234567890abcdef",
"name": "Acme Corporation",
"email": "billing@acme.com",
"status": "active"
}
```
**Note:** Customer is automatically created in Stripe when connection is configured.
Generate a secure link for customer to save their card:
**Endpoint:** `POST /api/v1/payments/customers/{customer_id}/setup/intent`
**Request:**
```json theme={null}
{
"success_url": "https://yourapp.com/payment/success",
"cancel_url": "https://yourapp.com/payment/cancel",
"provider": "stripe",
"set_default": true
}
```
**Response:**
```json theme={null}
{
"setup_intent_id": "seti_1234567890abcdef",
"checkout_session_id": "cs_test_1234567890abcdef",
"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_...",
"status": "requires_payment_method",
"usage": "off_session",
"customer_id": "cus_1234567890abcdef",
"created_at": 1765623942,
"expires_at": 1765710343
}
```
**Integration Steps:**
1. Send `checkout_url` to customer via email or display in your application
2. Customer enters card details on Stripe's hosted page
3. Card is saved and set as default payment method
4. Flexprice receives webhook confirmation from Stripe
**📚 Documentation:** [Create a Customer](/docs/customers/create)
**🔗 API Reference:** [Create Customer API](/api-reference/customers/create-customer)
**Alternative: Save Card During Payment**
You can also save cards during invoice payment by using the payment creation endpoint with `save_card_and_make_default` flag:
**Endpoint:** `POST /api/v1/payments`
```json theme={null}
{
"destination_type": "invoice",
"destination_id": "inv_1234567890abcdef",
"payment_method_type": "payment_link",
"payment_gateway": "stripe",
"amount": "100.00",
"currency": "USD",
"save_card_and_make_default": true,
"success_url": "https://yourapp.com/payment/success",
"cancel_url": "https://yourapp.com/payment/cancel",
"process_payment": true
}
```
**📚 Documentation:** [Save Payment Method](/integrations/stripe/payment-links)
**🔗 API Reference:** [Create Payment API](/api-reference/payments/create-payment)
***
### Phase 4: Subscription Creation
Create a subscription that will generate invoices automatically:
**Endpoint:** `POST /api/v1/subscriptions`
```json theme={null}
{
"customer_id": "cus_1234567890abcdef",
"plan_id": "plan_1234567890abcdef",
"start_date": "2024-01-20T00:00:00Z",
"billing_anchor": "subscription_start",
"collection_method": "charge_automatically",
"metadata": {
"contract_id": "CNT-2024-001"
}
}
```
**Response:**
```json theme={null}
{
"id": "sub_1234567890abcdef",
"customer_id": "cus_1234567890abcdef",
"plan_id": "plan_1234567890abcdef",
"status": "active",
"current_period_start": "2024-01-20T00:00:00Z",
"current_period_end": "2024-02-20T00:00:00Z",
"collection_method": "charge_automatically"
}
```
**📚 Documentation:** [Create a Subscription](/docs/subscriptions/customers-create-subscription)
**🔗 API Reference:** [Create Subscription API](/api-reference/subscriptions/create-subscription)
***
### Phase 5: Usage Tracking
Send usage events throughout the billing period:
**Single Event:**
**Endpoint:** `POST /api/v1/events`
```json theme={null}
{
"event_id": "evt_1234567890abcdef",
"event_name": "api.request",
"customer_id": "acme_corp_001",
"timestamp": "2024-01-21T14:30:00Z",
"properties": {
"endpoint": "/api/v1/users",
"method": "GET"
},
"idempotency_key": "unique_key_12345"
}
```
**Bulk Events:**
**Endpoint:** `POST /api/v1/events/bulk`
```json theme={null}
{
"events": [
{
"event_id": "evt_001",
"event_name": "api.request",
"customer_id": "acme_corp_001",
"timestamp": "2024-01-21T14:30:00Z"
}
]
}
```
Query aggregated usage data anytime:
**Endpoint:** `POST /api/v1/events/usage/meter`
```json theme={null}
{
"meter_id": "meter_api_requests",
"customer_id": "acme_corp_001",
"start_time": "2024-01-20T00:00:00Z",
"end_time": "2024-02-20T00:00:00Z"
}
```
**Response:**
```json theme={null}
{
"data": [
{
"customer_id": "acme_corp_001",
"value": 15420,
"start_time": "2024-01-20T00:00:00Z",
"end_time": "2024-02-20T00:00:00Z"
}
]
}
```
**📚 Documentation:**
* [Sending Events](/docs/event-ingestion/sending-events)
* [Event Ingestion Overview](/docs/event-ingestion/overview)
**🔗 API Reference:**
* [Bulk Ingest Events API](/api-reference/events/bulk-ingest-events)
***
### Phase 6: Invoice Generation & Sync
At end of billing period, Flexprice automatically:
1. Creates draft invoice with usage charges
2. Calculates pricing based on usage
3. Finalizes invoice
4. Syncs to Stripe (if connection configured)
**Example Invoice:**
```json theme={null}
{
"id": "inv_01KCBAVXJC34H8JJGEJBD6F5MV",
"customer_id": "cust_01KCB8BKDZ5D1VN0J1M7J404ZH",
"subscription_id": "subs_01KCBAVKS24PJKT2GCDQ0GK8R6",
"invoice_number": "INV-202512-00039",
"invoice_status": "FINALIZED",
"payment_status": "SUCCEEDED",
"currency": "usd",
"amount_due": "118.5",
"total": "118.5",
"amount_paid": "118.5",
"line_items": [
{
"display_name": "WindowEdge Plan",
"price_type": "FIXED",
"amount": "10",
"quantity": "1"
},
{
"display_name": "Call Duration",
"price_type": "USAGE",
"amount": "108.5",
"quantity": "1085"
}
],
"invoice_pdf_url": "https://pay.stripe.com/invoice/acct_1Rg0lDBVFqEMJNuG/test_YWNjdF8x.../pdf?s=ap",
"metadata": {
"stripe_hosted_invoice_url": "https://invoice.stripe.com/i/acct_1Rg0lDBVFqEMJNuG/test_YWNjdF8x...?s=ap"
},
"period_start": "2025-12-13T07:47:24.721Z",
"period_end": "2025-12-13T07:47:38.139Z",
"paid_at": "2025-12-13T07:48:15.771Z"
}
```
Flexprice checks if invoice should sync:
1. ✅ Stripe connection exists
2. ✅ Connection is active
3. ✅ Invoice sync is enabled
4. ✅ Customer synced to Stripe
→ **Proceed with sync**
Invoice is automatically synced to Stripe:
**Response:**
```json theme={null}
{
"stripe_invoice_id": "in_1234567890abcdef",
"status": "open",
"payment_intent_id": "pi_1234567890abcdef",
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_.../test_...",
"invoice_pdf": "https://pay.stripe.com/invoice/.../pdf"
}
```
**📚 Documentation:**
* [Invoice Overview](/docs/invoices/overview)
* [Stripe Invoice Sync](/integrations/stripe/invoice-sync)
***
### Phase 7: Payment Processing
When `collection_method: "charge_automatically"`:
1. Stripe automatically attempts to charge customer's default payment method
2. Payment intent is created and confirmed
3. Charge is processed immediately
**If successful:**
* Stripe sends `invoice.payment_paid` webhook
* Flexprice updates invoice status to "paid"
* Customer receives payment confirmation email
Flexprice receives webhook at:
```
/api/v1/webhooks/stripe/{tenant_id}/{environment_id}
```
**Webhook Flow:**
1. Stripe sends webhook → Flexprice
2. Verify webhook signature
3. Extract invoice/payment ID from metadata
4. Update invoice status in Flexprice
5. Return 200 OK immediately
Check invoice payment status:
**Endpoint:** `GET /api/v1/invoices/{invoice_id}`
```json theme={null}
{
"id": "inv_1234567890abcdef",
"payment_status": "paid",
"paid_at": "2024-02-20T00:05:00Z",
"payment_details": {
"payment_method": "card",
"last4": "4242",
"brand": "visa"
}
}
```
**📚 Documentation:**
* [Invoice Overview](/docs/invoices/overview)
* [Stripe Invoice Sync](/integrations/stripe/invoice-sync)
**🔗 API Reference:** [Get Invoice API](/api-reference/invoices/get-invoice)
***
## Payment Collection Methods
Flexprice supports multiple payment collection methods for invoices. For detailed information on all payment options including offline payments, credit/wallet usage, and automated charging:
**📚 Complete Guide:** [Record Payment & Collection Methods](/docs/subscriptions/record-payment)
### Quick Overview
**Charge Automatically (`charge_automatically`)**
* Best for: Subscriptions with saved payment methods
* Stripe automatically charges customer's default payment method
* Immediate payment confirmation via webhooks
**Send Invoice (`send_invoice`)**
* Best for: Manual payment workflows, B2B customers
* Customer receives email with Stripe-hosted payment link
* Flexible payment options
***
## Key Integration Points
### 1. Entity Synchronization
Flexprice maintains bidirectional mapping between entities:
| Flexprice Entity | Stripe Entity | Sync Direction |
| ---------------- | -------------- | ---------------------- |
| Customer | Customer | Bidirectional |
| Plan | Product | Outbound |
| Subscription | Subscription | Bidirectional |
| Invoice | Invoice | Outbound |
| Payment | Payment Intent | Inbound (via webhooks) |
### 2. Invoice Sync Configuration
**Invoice Sync:**
```json theme={null}
{
"invoice": {
"inbound": false,
"outbound": true
}
}
```
**Features:**
* Status synchronization: Payment status updates via webhooks
* PDF generation: Access invoices as PDF from Stripe
* Hosted pages: Use Stripe's branded invoice pages
### 3. Payment Method Management
**Setup Intent Flow:**
1. Create Setup Intent via Flexprice API
2. Customer enters card on Stripe hosted page
3. Card saved and set as default
4. Webhook confirms setup completion
**Payment Method Reuse:**
* Saved cards used for recurring charges
* Off-session payments supported
* Multiple payment methods per customer
* Automatic default payment method selection
***
## Error Handling
### Common Scenarios
| Scenario | Handling | Result |
| ----------------------- | ---------------------------- | --------------------------- |
| Customer already exists | Use existing Stripe customer | No error |
| Invoice already synced | Skip sync, return mapping | No error |
| Payment method missing | Return error, require setup | User must save card |
| Card declined | Stripe retries automatically | Eventual success or failure |
| Webhook signature fail | Log warning, skip processing | Security measure |
***
## Testing
### Test Mode
Use Stripe test keys (`sk_test_...`) for development:
1. **Test Cards**: Use Stripe's test card numbers
* Success: `4242 4242 4242 4242`
* Decline: `4000 0000 0000 0002`
2. **Test Webhooks**: Use Stripe CLI for local testing
```bash theme={null}
stripe listen --forward-to localhost:8080/api/v1/webhooks/stripe/{tenant_id}/{environment_id}
```
3. **Test Events**: Trigger test webhooks from Stripe Dashboard
### Production Checklist
Before going live:
* [ ] Stripe connection created with production keys (`sk_live_...`)
* [ ] Webhook endpoint configured with production URL
* [ ] Webhook secret updated in connection
* [ ] Test customer creation and sync
* [ ] Test card save flow (Setup Intent)
* [ ] Test subscription creation
* [ ] Test usage event ingestion
* [ ] Verify invoice generation and sync
* [ ] Test automatic payment flow
* [ ] Monitor webhook delivery
* [ ] Set up error alerting
* [ ] Document customer support procedures
***
## Security Best Practices
### API Key Management
1. **Never expose API keys** in client-side code
2. **Use environment variables** for key storage
3. **Rotate keys regularly** (quarterly recommended)
4. **Use separate keys** for test and production environments
5. **Implement key scoping** - use least privilege principle
### Webhook Security
1. **Verify webhook signatures** - Flexprice does this automatically
2. **Use HTTPS only** for webhook endpoints
3. **Implement replay protection** via idempotency
4. **Rate limit webhook endpoints**
5. **Log all webhook events** for audit trails
### Customer Data
1. **Never store raw card numbers** - use tokenized references only
2. **PCI compliance** - Stripe handles card data securely
3. **Encrypt sensitive data** at rest and in transit
4. **Implement access controls** for customer information
5. **Regular security audits**
***
### Recommended Dashboards
1. **Billing Overview**: Total revenue, invoice count, payment success rate
2. **Customer Health**: Active subscriptions, churn rate, payment failures
3. **Integration Health**: Sync success rate, webhook delivery, API errors
4. **Usage Analytics**: Event volume by customer, feature adoption, usage trends
***
## Support & Resources
### API Documentation
* [Flexprice API Reference](https://docs.flexprice.io/api-reference)
* [Stripe API Documentation](https://stripe.com/docs/api)
### Code Examples
* [Flexprice SDK Examples](https://github.com/flexprice/examples)
* [Stripe Integration Samples](https://github.com/stripe-samples)
### Getting Help
* **Flexprice Support**: [support@flexprice.io](mailto:support@flexprice.io)
* **Documentation**: [https://docs.flexprice.io](https://docs.flexprice.io)
* **Community**: [https://community.flexprice.io](https://community.flexprice.io)
* **Stripe Support**: [https://support.stripe.com](https://support.stripe.com)
***
Your Flexprice-Stripe integration is now complete and ready to handle automated usage-based billing with seamless payment processing! 🎉
# Stripe Invoice Sync Flow
Source: https://docs.flexprice.io/integrations/stripe/invoice-sync
Comprehensive guide to Stripe invoice synchronization, collection methods, and payment reconciliation
## Overview
The Stripe Invoice Sync Flow enables Flexprice to synchronize invoices with Stripe, allowing for seamless payment processing and reconciliation across both platforms. This system handles different collection methods, webhook processing, and ensures accurate payment tracking.
### Key Features
* **Invoice Synchronization**: Automatic sync of Flexprice invoices to Stripe
* **Collection Methods**: Support for `charge_automatically` and `send_invoice` methods
* **Payment Reconciliation**: Real-time payment tracking across platforms
* **Webhook Processing**: Comprehensive webhook handling for all payment events
* **Credit/Offline Restrictions**: Disabled when invoice sync is enabled
* **Metadata Tracking**: Uses `flexprice_payment_id` and `flexprice_invoice_id` for reconciliation
## Invoice Sync Lifecycle
```mermaid theme={null}
graph TD
A[Invoice Created in Flexprice] --> B{Invoice Sync Enabled?}
B -->|No| C[Skip Sync]
B -->|Yes| D[Check Stripe Connection]
D --> E{Customer Synced?}
E -->|No| F[Sync Customer to Stripe]
E -->|Yes| G[Create Draft Invoice in Stripe]
F --> G
G --> H[Sync Line Items]
H --> I[Finalize Invoice in Stripe]
I --> J{Collection Method?}
J -->|charge_automatically| K[Auto-advance Invoice]
J -->|send_invoice| L[Send Invoice to Customer]
K --> M[Wait for Payment Events]
L --> M
M --> N[Process Webhooks]
N --> O[Reconcile Payments]
```
## Collection Methods
### Charge Automatically
When `collection_method` is set to `charge_automatically`:
1. **Invoice Creation**: Creates a draft invoice in Stripe with `charge_automatically` collection method
2. **Auto-advance**: Stripe automatically creates a PaymentIntent and attempts to charge the customer's default payment method
3. **Payment Processing**: Stripe handles the payment attempt immediately
4. **Webhook Events**: Listen for `invoice_payment.paid` and `payment_intent.succeeded`
**Behavior:**
* Immediate payment attempt upon invoice finalization
* Uses customer's default payment method
* Automatic retry on failure (based on Stripe settings)
* Subscription status depends of payment behavior until payment succeeds
### Send Invoice
When `collection_method` is set to `send_invoice`:
1. **Invoice Creation**: Creates a invoice in Stripe with `send_invoice` collection method
2. **Due Date**: Sets the invoice due date from Flexprice invoice
3. **Send Invoice**: Automatically sends the invoice to the customer via email
4. **Customer Action**: Customer must manually pay using Stripe's hosted invoice page
5. **Webhook Events**: Listen for `invoice_payment.paid` and `payment_intent.succeeded`
**Behavior:**
* Customer receives email with payment link
* Manual payment required by customer
* Subscription status depends on payment behavior setting
* More control over payment timing
## Payment Flows
### Stripe Payment Methods
#### 1. Invoice Link (Stripe Hosted)
**Flow:**
```mermaid theme={null}
graph TD
A[Customer Receives Invoice Email] --> B[Clicks Payment Link]
B --> C[Stripe Hosted Invoice Page]
C --> D[Customer Enters Payment Details]
D --> E{Payment Successful?}
E -->|Yes| F[Stripe: invoice_payment.paid]
E -->|No| G[No Webhook - Failure Handled by Stripe]
F --> H[Flexprice: Process Payment]
G --> I[Customer Can Retry Payment]
```
**Webhook Handler:** `payment_intent.succeeded`
* **Trigger**: Customer pays via Stripe invoice link
* **Action**: Creates payment record in Flexprice
* **Reconciliation**: Uses `flexprice_invoice_id` from invoice metadata
* **Note**: We don't listen to failure webhooks, only success
#### 2. Charge Customer (Stripe Direct)
**Flow:**
```mermaid theme={null}
graph TD
A[Flexprice Initiates Charge] --> B[Stripe: Create PaymentIntent]
B --> C[Stripe: Charge Customer]
C --> D{Charge Successful?}
D -->|Yes| E[Stripe: invoice_payment.paid]
D -->|No| F[No Webhook - Failure Handled by Stripe]
E --> G[Flexprice: Update Payment Status]
```
**Webhook Handler:** `invoice_payment.paid`
* **Trigger**: Direct charge to customer's payment method succeeds
* **Action**: Updates existing payment record
* **Reconciliation**: Uses `flexprice_invoice_id` from invoice metadata
### Flexprice Payment Methods
#### 1. Checkout Link (Flexprice Hosted)
**Flow:**
```mermaid theme={null}
graph TD
A[Create Payment Link] --> B[Generate Stripe Checkout Session]
B --> C[Customer Redirected to Stripe]
C --> D[Customer Completes Payment]
D --> E[Stripe: checkout.session.completed]
E --> F[Flexprice: Process Payment]
F --> G[Update Invoice Status and Reconcile flexprice invoice ]
G --> H[If invoice sync enabled : Reconcile stripe invoice]
```
**Webhook Handler:** `checkout.session.completed`
* **Trigger**: Customer completes payment via Stripe Checkout
* **Action**: Updates payment status to `SUCCEEDED`
* **Reconciliation**: Uses `flexprice_payment_id` from PaymentIntent metadata
* **Note**: We get the invoice ID from the PaymentIntent and don't listen to failure webhooks
#### 2. Charge Card (Flexprice Direct)
**Flow:**
```mermaid theme={null}
graph TD
A[Flexprice: Charge Card API] --> B[Stripe: Create PaymentIntent]
B --> C[Stripe: Charge Card]
C --> D{Charge Successful?}
D -->|Yes| E[Flexprice: Update Payment Status]
D -->|No| F[Flexprice: Return Error]
E --> G[Update Invoice Status]
```
**No Webhook Processing:**
* **Reason**: Payment is processed synchronously
* **Action**: Immediate status update in Flexprice
* **Reconciliation**: Direct API response handling
## Webhook Processing
### Webhook Event Types
| Event Type | Trigger | Action | Reconciliation Method |
| ------------------------------- | -------------------------- | ---------------------------- | -------------------------------------------------- |
| `checkout.session.completed` | Payment link completed | Update payment status | `flexprice_payment_id` from session metadata |
| `payment_intent.succeeded` | Checkout payment succeeded | Update payment status | `flexprice_payment_id` from PaymentIntent metadata |
| `payment_intent.payment_failed` | Direct charge failed | Update payment status | `flexprice_payment_id` from PaymentIntent metadata |
| `invoice_payment.paid` | Invoice payment succeeded | Create/update payment record | `flexprice_invoice_id` from invoice metadata |
### Metadata Requirements
**Critical:** Never delete `flexprice_payment_id` or `flexprice_invoice_id` from Stripe metadata. These are essential for payment reconciliation.
**Required Metadata Fields:**
* `flexprice_payment_id`: Links Stripe payment to Flexprice payment record
* `flexprice_invoice_id`: Links Stripe invoice to Flexprice invoice record
* `flexprice_customer_id`: Links Stripe customer to Flexprice customer record
## Payment Reconciliation
### Reconciliation Process
1. **Webhook Received**: Stripe sends webhook with payment event
2. **Metadata Extraction**: Extract `flexprice_payment_id` or `flexprice_invoice_id`
3. **Record Lookup**: Find corresponding Flexprice record
4. **Status Update**: Update payment/invoice status
5. **Amount Verification**: Ensure payment amounts match
6. **Event Triggering**: Send internal events for downstream processing
## Restrictions and Edge Cases
### When Invoice Sync is Enabled
**Disabled Payment Methods:**
* ❌ **Credit Payments**: Cannot use wallet credits for payment
* ❌ **Offline Payments**: Cannot mark payments as offline
* ❌ **Manual Payment Recording**: Must use Stripe payment methods
**Reasoning:**
* Invoice sync requires payment reconciliation with Stripe
* Credit and offline payments bypass Stripe's payment tracking
* This ensures accurate financial reporting and reconciliation
### Edge Cases
#### 1. Partial Payments
* **Stripe**: Handles partial payments automatically
* **Flexprice**: Syncs partial payment amounts
* **Reconciliation**: Tracks cumulative payment amounts
#### 2. Refunds
* **Stripe**: Process refunds through Stripe dashboard
* **Flexprice**: Manual reconciliation required
#### 3. Failed Payments
* **Retry Logic**: Handled by Stripe's retry mechanism
* **Webhook**: `payment_intent.payment_failed` for direct charges only
* **Flexprice**: Updates payment status to `FAILED`
* **Note**: Invoice payment failures are not tracked via webhooks
## Configuration
### Enabling Invoice Sync
1. **Stripe Connection**: Configure Stripe integration
2. **Enable Sync**: Set `"invoice": {"inbound": false, "outbound": true}`
3. **Customer Sync**: Ensure customers are synced to Stripe
4. **Webhook Setup**: Configure webhook endpoints
### Webhook Endpoints
```
POST /api/v1/webhooks/stripe/{tenant_id}/{environment_id}
```
**Required Events:**
* `checkout.session.completed`
* `payment_intent.succeeded`
* `payment_intent.payment_failed`
* `invoice_payment.paid`
## Error Handling
### Common Errors
| Error | Cause | Resolution |
| ------------------------------ | --------------------------------- | ----------------------------------------- |
| "Invoice not synced to Stripe" | Invoice sync disabled | Enable invoice sync or use manual payment |
| "Customer not found in Stripe" | Customer not synced | Sync customer to Stripe first |
| "Payment already exists" | Duplicate webhook | Skip processing (idempotent) |
| "Metadata missing" | Webhook without required metadata | Log warning and skip |
### Error Recovery
1. **Webhook Failures**: Stripe retries failed webhooks
2. **Sync Failures**: Manual retry via admin interface
## Testing
### Test Scenarios
1. **Invoice Sync**: Create invoice and verify Stripe sync
2. **Payment Link**: Generate link and complete payment
3. **Direct Charge**: Charge customer directly
4. **Webhook Processing**: Simulate webhook events
5. **Reconciliation**: Verify payment amounts match
### Test Data
**Test Cards:**
* `4242424242424242`: Successful payment
* `4000000000000002`: Card declined
* `4000000000009995`: Insufficient funds
**Test Webhooks:**
Use Stripe CLI to forward webhooks to local development environment.
For additional support, refer to the [API Reference](/api-reference) or contact the development team.
# Stripe Payment Links & Checkout Flow
Source: https://docs.flexprice.io/integrations/stripe/payment-links
Complete guide to creating payment links and handling the Stripe checkout flow
## Overview
Payment links in Flexprice provide a secure, hosted checkout experience powered by Stripe. Customers click a payment URL and are redirected to Stripe's secure checkout page to complete their payment.
### Key Features
* **Secure Checkout**: Hosted by Stripe with PCI compliance
* **Multiple Payment Methods**: Cards, bank transfers, digital wallets
* **Mobile Optimized**: Responsive checkout experience
* **Real-time Updates**: Webhook-driven status updates
* **Invoice Integration**: Automatic invoice reconciliation
* **Card Saving**: Optional card storage for future payments
## Payment Link Lifecycle
```mermaid theme={null}
graph TD
A[Create Payment Request] --> B[Validate Invoice & Customer]
B --> C[Create Payment Record: INITIATED]
C --> D[Generate Stripe Checkout Session]
D --> E{Session Created?}
E -->|Success| F[Update Status: PENDING]
E -->|Failure| G[Keep Status: INITIATED]
F --> H[Return Payment URL]
H --> I[Customer Clicks Link]
I --> J[Stripe Checkout Page]
J --> K[Customer Enters Payment Details]
K --> L{Payment Successful?}
L -->|Yes| M[Stripe Webhook: completed]
L -->|No| N[Stripe Webhook: failed]
M --> O[Update Status: SUCCEEDED]
N --> P[Update Status: FAILED]
O --> Q[Reconcile Invoice]
P --> R[Log Payment Failure]
```
## Payment Status Flow
### Status Transitions
```
INITIATED → PENDING → SUCCEEDED
↓ ↓ ↓
FAILED FAILED REFUNDED
```
### Status Definitions
| Status | Description | Triggers |
| ------------ | ------------------------------------------------------ | ---------------------------------------------------------------------------- |
| `INITIATED` | Payment record created, Stripe session not yet created | Initial payment creation |
| `PENDING` | Stripe checkout session created successfully | Successful Stripe API call |
| `PROCESSING` | Payment is being processed | Temporary status during updates |
| `SUCCEEDED` | Payment completed successfully | Stripe webhook: `checkout.session.completed` |
| `FAILED` | Payment attempt failed | Stripe webhooks: `checkout.session.expired`, `payment_intent.payment_failed` |
| `REFUNDED` | Payment was refunded | Manual refund processing |
## Creating Payment Links
### Prerequisites
1. **Stripe Connection**: Active Stripe connection configured
2. **Customer Sync**: Customer must have Stripe customer ID
3. **Invoice Status**: Invoice must be finalized and unpaid
4. **Valid Amount**: Payment amount must match invoice outstanding
### Creating Payment Links via Dashboard
You can create payment links directly from the Flexprice dashboard:
### API Request
**Endpoint:** `POST /api/v1/payments`
**Headers:**
```http theme={null}
Content-Type: application/json
Authorization: Bearer your_api_key
X-Environment-ID: your_environment_id
```
**Request Body (Save Card for Future Use):**
```json theme={null}
{
"amount": 10,
"currency": "usd",
"destination_id": "inv_01K34HYZN03V6X8Q6V3T8X6G4F",
"destination_type": "INVOICE",
"payment_method_type": "PAYMENT_LINK",
"process_payment": true,
"payment_gateway": "stripe",
"success_url": "https://admin-dev.flexprice.io/customer-management/invoices/inv_01K34HYZN03V6X8Q6V3T8X6G4F?page=1",
"cancel_url": "https://admin-dev.flexprice.io/customer-management/invoices/inv_01K34HYZN03V6X8Q6V3T8X6G4F?page=1",
"save_card_and_make_default": true
}
```
**Request Body (One-time Payment Only):**
```json theme={null}
{
"amount": 10,
"currency": "usd",
"destination_id": "inv_01K34HYZN03V6X8Q6V3T8X6G4F",
"destination_type": "INVOICE",
"payment_method_type": "PAYMENT_LINK",
"process_payment": true,
"payment_gateway": "stripe",
"success_url": "https://admin-dev.flexprice.io/customer-management/invoices/inv_01K34HYZN03V6X8Q6V3T8X6G4F?page=1",
"cancel_url": "https://admin-dev.flexprice.io/customer-management/invoices/inv_01K34HYZN03V6X8Q6V3T8X6G4F?page=1"
}
```
### Response
```json theme={null}
{
"id": "pay_1234567890abcdef",
"payment_status": "pending",
"payment_url": "https://checkout.stripe.com/c/pay/cs_test_...",
"amount": "150.00",
"currency": "usd",
"gateway_tracking_id": "cs_test_1234567890abcdef",
"created_at": "2024-01-20T10:30:00Z",
"metadata": {
"stripe_session_id": "cs_test_1234567890abcdef"
}
}
```
## Payment Link Configuration
### Required Fields
| Field | Type | Required | Description |
| ---------------------------- | ------- | -------- | ---------------------------------------------- |
| `destination_type` | string | ✅ | Must be `"invoice"` |
| `destination_id` | string | ✅ | Valid invoice ID |
| `payment_method_type` | string | ✅ | Must be `"payment_link"` |
| `payment_gateway` | string | ✅ | Must be `"stripe"` |
| `amount` | string | ✅ | Payment amount (must match invoice) |
| `currency` | string | ✅ | Valid currency code |
| `process_payment` | boolean | ✅ | Set to `true` to create Stripe session |
| `save_card_and_make_default` | boolean | ❌ | Save card for future payments (default: false) |
## Stripe Checkout Configuration
### Default Checkout Settings
When creating payment links, Flexprice automatically configures Stripe Checkout with:
* **Payment Methods**: Cards, bank transfers, digital wallets
* **Currency**: Matches the payment currency
* **Amount**: Exact payment amount
* **Customer**: Pre-filled with customer data
* **Invoice Details**: Line items from the invoice
### Card Saving Configuration
When `save_card_and_make_default` is set to `true`:
* **Setup Future Usage**: Stripe session configured with `setup_future_usage: "off_session"`
* **Card Storage**: Payment method automatically saved to customer
* **Default Assignment**: Saved card becomes the customer's default payment method
* **Future Payments**: Enables subsequent card payments without re-entering details
**Important Notes:**
* Card saving requires customer consent and should comply with local regulations
* Saved cards can be used for future `CARD` payment method type payments
* Only one default payment method per customer is supported
## Webhook Processing
### Payment Completion Webhook
When a payment is completed, Stripe sends a `checkout.session.completed` webhook:
```json theme={null}
{
"id": "evt_1234567890abcdef",
"type": "checkout.session.completed",
"data": {
"object": {
"id": "cs_test_1234567890abcdef",
"payment_status": "paid",
"amount_total": 15000,
"currency": "usd",
"customer": "cus_stripe123abc",
"metadata": {
"flexprice_payment_id": "pay_1234567890abcdef"
}
}
}
}
```
### Webhook Processing Flow
1. **Receive Webhook**: Flexprice receives the webhook from Stripe
2. **Verify Signature**: Validate webhook signature for security
3. **Find Payment**: Locate payment record using session ID
4. **Check Card Saving**: Read `save_card_and_make_default` from payment metadata
5. **Set Default Card**: If flag is true, set payment method as default in Stripe
6. **Update Status**: Change payment status to `SUCCEEDED`
7. **Reconcile Invoice**: Update invoice payment status and amounts
8. **Trigger Events**: Send payment completion events
## Error Handling
### Common Payment Errors
| Error | Cause | Resolution |
| -------------------------------- | -------------------- | ----------------------------- |
| "Customer not found in Stripe" | Customer not synced | Sync customer to Stripe first |
| "Invalid invoice amount" | Amount mismatch | Ensure amount matches invoice |
| "Invoice already paid" | Invoice status issue | Check invoice payment status |
| "Stripe session creation failed" | API error | Check Stripe connection |
### Payment Failure Handling
When payments fail:
1. **Webhook Received**: Stripe sends failure webhook
2. **Status Updated**: Payment status changed to `FAILED`
3. **Error Logged**: Failure reason recorded
4. **Customer Notified**: Optional failure notification
5. **Retry Available**: Customer can create new payment link
## Testing Payment Links
### Test Environment Setup
1. **Use Test Keys**: Configure Stripe test mode
2. **Test Cards**: Use Stripe's test card numbers
3. **Test Webhooks**: Use ngrok for local webhook testing
### Test Card Numbers
| Card Number | Description |
| ------------------ | ------------------ |
| `4242424242424242` | Successful payment |
| `4000000000000002` | Card declined |
| `4000000000009995` | Insufficient funds |
| `4000000000009987` | Expired card |
### Testing Flow
1. **Create Test Payment**: Use test invoice and customer
2. **Generate Payment Link**: Create payment with test data
3. **Complete Payment**: Use test card in Stripe checkout
4. **Verify Webhook**: Check webhook processing
5. **Confirm Status**: Verify payment and invoice updates
## Relationship with Card Payments
### Enabling Future Card Payments
Payment links with `save_card_and_make_default: true` enable future card payments:
1. **First Payment**: Customer uses payment link to pay and save card
2. **Card Storage**: Payment method saved and set as default in Stripe
3. **Future Payments**: Customer can use `CARD` payment method type
4. **Seamless Experience**: No redirect required for subsequent payments
### Payment Method Hierarchy
```mermaid theme={null}
graph TD
A[Customer First Payment] --> B[Payment Link with save_card_and_make_default: true]
B --> C[Card Saved as Default]
C --> D[Future Payments Available]
D --> E[CARD Payment Method Type]
D --> F[PAYMENT_LINK Payment Method Type]
E --> G[Uses Saved Default Card]
F --> H[Creates New Payment Session]
```
### Integration Strategy
**Recommended Flow:**
1. **New Customers**: Always use payment links for first payment
2. **Set Save Flag**: Include `save_card_and_make_default: true` for returning customers
3. **Future Payments**: Use card payment method for faster checkout
4. **Fallback**: Provide payment link option if card payment fails
For detailed information about card payments, see [Card Payment Documentation](/integrations/stripe/card-payment).
## Security Considerations
### Payment Link Security
* **HTTPS Required**: All payment links use HTTPS
* **Session Expiry**: Links expire after 24 hours
* **One-time Use**: Each link can only be used once
* **Signature Verification**: All webhooks verified
### Data Protection
* **PCI Compliance**: Stripe handles sensitive payment data
* **No Card Storage**: Card details never stored in Flexprice
* **Encrypted Storage**: All payment data encrypted at rest
* **Access Controls**: Role-based access to payment data
### Webhook Endpoints
* `POST /api/v1/webhooks/stripe/{tenant_id}/{environment_id}` - Stripe webhook handler
For complete API documentation, see the [API Reference](/api-reference/payments/create-payment).
# Whop Connection Setup
Source: https://docs.flexprice.io/integrations/whop/connection-setup
Connect Flexprice to Whop to sync invoices and reconcile payments automatically
## Overview
A Whop connection in Flexprice lets you:
* **Sync invoices** - push `finalized` Flexprice invoices to Whop
* **Generate payment links** - every synced invoice gets a Whop-hosted payment URL
* **Reconcile payments** - payment status is kept in sync between Whop and Flexprice automatically
## Prerequisites
Before you start, have:
1. **Whop business account** - [whop.com](https://whop.com)
2. **Flexprice account** - [flexprice.io](https://flexprice.io)
## Keys you need
| Key | Where to get it | What it's for |
| --------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| **API Key** | Whop Dashboard → **Developer** → **API Keys** → **Create** → select **Admin** role | Authenticating all Whop API calls from Flexprice |
| **Company ID** | View your business profile on Whop → click the three-dot menu (**...**) → **Copy ID** | Your Whop business identifier (`biz_...`) |
| **Product ID** *(optional)* | Whop Dashboard → **Products** | Which Whop product invoices are grouped under - leave blank and Flexprice creates one automatically |
## How to connect your Whop account
Go to **Flexprice Dashboard** → **Settings** → **Integrations** → **Whop** → **Add a connection**.
Enter a name to identify this connection, e.g. `Whop Production`.
* **API Key** - starts with `apik_`. Go to Whop Dashboard → **Developer** → **API Keys** → **Create**, enter a name, and select the **Admin** role.
* **Company ID** - starts with `biz_`. Open your business profile on Whop, click the three-dot (**...**) menu in the top right, and select **Copy ID**.
If you have an existing Whop product, enter its ID (`prod_...`).
Leave blank - Flexprice will automatically create a product called **Flexprice Billing Product** on the first invoice sync and reuse it for all future invoices.
Toggle **Invoice Sync** on to push finalized invoices to Whop. With this off, invoices stay only in Flexprice.
At the bottom of the drawer, copy your webhook URL. You will need it in the next step.
The URL format depends on your region:
| Region | Webhook URL |
| ------ | ------------------------------------------------------------------------------ |
| US | `https://us.api.flexprice.io/v1/webhooks/whop/{tenant_id}/{environment_id}` |
| India | `https://api.cloud.flexprice.io/v1/webhooks/whop/{tenant_id}/{environment_id}` |
## Configure Webhook in Whop
For Flexprice to mark invoices as paid when payment is completed through Whop, register your webhook URL in Whop.
Go to your Whop Dashboard → **Settings** → **Webhooks** → **Add Endpoint**.
Paste the URL copied from the Flexprice connection drawer.
Select the following events and save:
| Event | Purpose |
| ------------------- | ---------------------------------------------------------- |
| `invoice_paid` | Marks the Flexprice invoice as paid when payment completes |
| `payment_succeeded` | Enables Charge Automatically for future invoices |
Without the webhook configured, invoices synced to Whop will not automatically mark as paid in Flexprice and Charge Automatically will not activate.
## Troubleshooting
| Issue | Cause | Solution |
| --------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Invoice not syncing to Whop | Invoice sync toggle is off | Enable outbound sync in connection settings |
| Sync fails - customer error | Customer has no email address | Add an email to the customer in Flexprice before finalizing the invoice |
| Webhook events not received | Wrong URL or events not subscribed | Re-copy the URL from the drawer and confirm both `invoice_paid` and `payment_succeeded` are selected in Whop |
## Next Steps
1. Finalize a test invoice in Flexprice and verify it appears in your Whop dashboard
2. Check the invoice in Flexprice for `whop_checkout_url` in the metadata
3. Pay the Whop invoice and confirm the Flexprice invoice is marked paid
## Related Documentation
* [Invoice Sync](/integrations/whop/invoice-sync) - How invoices are synced and payments reconciled
# Whop Invoice Sync
Source: https://docs.flexprice.io/integrations/whop/invoice-sync
How Flexprice pushes invoices to Whop and reconciles payment status automatically
The Flexprice–Whop integration is bidirectional and fully asynchronous. Invoice creation and finalization in Flexprice are never blocked by Whop sync.
* **Whop to Flexprice:** When a customer pays through Whop, Flexprice is notified and marks the invoice paid automatically.
* **Flexprice to Whop:** Finalized invoices are pushed to Whop with one of two collection methods:
* **Send Invoice** (default): Customer receives a hosted checkout link and pays on their own timeline.
* **Charge Automatically:** After the first payment, subsequent invoices are charged automatically using the saved payment method.
***
## Pushing Invoices to Whop
Every finalized Flexprice invoice is automatically pushed to Whop. This applies to all invoice types: subscription billing, one-off invoices, and credit topup invoices.
**Conditions for sync:**
1. Invoice status transitions to **finalized**
2. Whop connection is **active**
3. **Invoice Sync** toggle is **enabled** on the connection
**What gets created in Whop:**
| Field | Value |
| ----------------- | -------------------------------------------------------------------------------------------- |
| Product | From connection config, or auto-created `Flexprice Billing Product` |
| Plan type | `one_time` |
| Amount | Invoice `amount_due`, rounded to 2 decimal places |
| Collection method | `send_invoice` or `charge_automatically` (see [Charge Automatically](#charge-automatically)) |
| Payment method | Only set when using `charge_automatically`; omitted otherwise |
| Due date | Invoice due date if in the future; defaults to 30 days from now |
| Customer name | Customer name from Flexprice |
| Email | Customer email from Flexprice |
After the Whop invoice is created, Flexprice fetches the hosted checkout URL and stores it under `invoice.metadata["whop_checkout_url"]`. Share this link with your customer for payment.
**Product handling:** If no product ID is configured on the connection, Flexprice auto-creates a `Flexprice Billing Product` (visibility: `quick_link`). Its ID is stored on the connection and reused for all future invoices.
***
## Charge Automatically
For returning customers, Flexprice skips the checkout link and charges their saved payment method directly.
**First invoice:** Customer receives a hosted checkout link and pays through Whop.
**All subsequent invoices:** Whop charges the saved payment method automatically. No action required from the customer.
If no saved payment method is found (e.g. customer has not paid through Whop before), the invoice falls back to a hosted checkout link.
***
## Reconciling Payments from Whop
On payment through Whop, an `invoice.paid` webhook is fired to Flexprice. Flexprice identifies the corresponding invoice and, if not already paid, marks the payment status as `succeeded`.
For credit topup invoices, the customer's wallet is credited immediately on reconciliation.
Without the webhook configured in Whop, payments will not automatically reconcile in Flexprice and Charge Automatically will not activate. See [Connection Setup](/integrations/whop/connection-setup) for instructions.
***
## Marking an Invoice Paid Offline
When a Flexprice invoice is marked paid via an admin action or an offline payment, Flexprice automatically marks the corresponding Whop invoice as paid. If the Whop invoice is already paid, the call is skipped.
***
## Troubleshooting
| Scenario | Behavior |
| ------------------------------------------------ | ---------------------------------------------------------------------------- |
| No Whop connection configured | Sync skipped silently |
| Invoice sync toggle off | Sync skipped silently |
| Customer has no email address | Sync fails immediately. Add an email to the customer and re-sync |
| Invoice already synced to Whop | Skipped; duplicate creation is prevented |
| Whop invoice already paid | Skipped; duplicate mark-paid call is prevented |
| First-time customer with no Whop payment history | Falls back to hosted checkout link |
| Customer has no saved payment methods | Falls back to hosted checkout link |
| Payment method fetch fails | Falls back to hosted checkout link; Whop API errors are retried with backoff |
| Whop API temporarily unavailable | Retried automatically with backoff |
Whop sync failures never block invoice creation or finalization in Flexprice. All sync happens asynchronously.
***
## Related Documentation
* [Connection Setup](/integrations/whop/connection-setup): Set up your Whop connection and configure the webhook
# Zoho Books Connection Setup
Source: https://docs.flexprice.io/integrations/zohobooks/connection-setup
Complete guide to setting up and configuring Zoho Books connections in Flexprice
## Overview
A Zoho Books connection in Flexprice stores encrypted credentials that allow the system to interact with your Zoho Books organization for:
* Connecting to Zoho Books using OAuth
* Syncing invoices from Flexprice to Zoho Books
* Creating and matching contacts in Zoho Books when invoices sync
* Receiving webhook notifications from Zoho Books
* Reconciling invoice payments and voids between Zoho Books and Flexprice
## Prerequisites
Before setting up your Zoho Books connection, ensure you have:
1. **Zoho Books Organization** - Admin access to a Zoho Books organization with API access
2. **Zoho API Console Application** - Client ID and Client Secret from [Zoho API Console](https://api-console.zoho.com/) (or your region’s console)
3. **OAuth Redirect URI** - The callback URL Flexprice uses for OAuth, registered exactly under your app’s authorized redirect URIs in Zoho
4. **Flexprice Environment** - Valid Flexprice tenant and environment for the connection
5. **Webhook Secret** - A strong value **you invent** (Zoho does **not** provide one by default). **Type the exact same string** in Zoho’s webhook settings **and** in Flexprice (recommended; used to verify `X-Zoho-Webhook-Signature`)
## Step 1: Gather Zoho Books Credentials
### Required Credentials
| Credential | Location | Required | Purpose |
| ------------------- | ------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------- |
| **Client ID** | Zoho API Console | ✅ | OAuth app identifier; starts the Zoho sign-in flow |
| **Client Secret** | Zoho API Console | ✅ | Completes token exchange after user consent |
| **Redirect URI** | Zoho API Console → Authorized Redirect URIs | ✅ | Must match Flexprice’s OAuth callback URL for your deployment and region |
| **Organization ID** | Zoho Books (organization settings / URL) | ✅ | Identifies which Zoho Books org Flexprice calls |
| **Accounts server** | Zoho Accounts region | ✅ | OAuth and API host context (for example `https://accounts.zoho.in`, `https://accounts.zoho.com`) |
| **Webhook Secret** | **You type it** in Zoho Books (webhook action) **and** again in Flexprice—same string both places | Recommended | Zoho does **not** auto-generate this; both sides must match for `X-Zoho-Webhook-Signature` verification |
### Finding Your Client ID and Client Secret
1. Go to [Zoho API Console](https://api-console.zoho.com/) (or your Zoho region’s API console).
2. Create or open an application for **Server-based Applications** (or the application type your organization uses for Zoho Books).
3. Copy the **Client ID** and **Client Secret**.
4. Under **Authorized Redirect URIs**, add Flexprice’s OAuth callback (for example `https://admin.flexprice.io/tools/integrations/oauth/` for Flexprice Cloud—use the exact URL your deployment shows if it differs).
5. Save the application.
## Step 2: Configure Zoho Books Webhooks
Zoho Books uses **webhook rules** (when something happens) and **actions** (for example **notify URL** / POST to an endpoint). For a full integration, configure **two** webhooks:
1. **Invoices** - So Flexprice can process **paid** and **voided** invoice events.
2. **Customers** - So Flexprice can create **customers** when a **customer is created** in Zoho Books (workflow on the **Customers** module; **customer inbound** sync is **on by default** on the connection).
Both webhooks should use the **same** Flexprice URL pattern below. Use the **tenant** and **environment** IDs from the Flexprice dashboard for the workspace where the connection will live.
**Webhook URL Format:**
```
https:///v1/webhooks/zoho_books//
```
For Flexprice Cloud, the host is often `https://api.cloud.flexprice.io`. Replace `` and `` with your Flexprice IDs. For other regions or deployments, use the API base URL your organization was given (for example `https://us.api.flexprice.io`).
If webhooks return success but Flexprice does not update data, confirm the **tenant** and **environment** in the path match the environment where Zoho Books is connected.
### Webhook secret (type the same value in Zoho and Flexprice)
Zoho Books does **not** give you a webhook signing secret to copy. The field is empty unless **you** fill it: you choose a strong secret (for example a long random string), then **type that exact same characters** everywhere it is asked for.
1. **Choose one secret** and use it for **both** workflow webhooks (invoice and customer), so Flexprice only stores a single value.
2. When you add the **webhook** action in Zoho, **type** that secret into Zoho’s webhook / signing secret field (wording varies by screen)—do not look for a value Zoho generated; there isn’t one by default.
3. In Flexprice (**Step 3**), **type the same secret** into the **Webhook secret** field on the Zoho Books connection.
There is nothing to “copy from Zoho”—only the same string **typed** in Zoho and in Flexprice. It does not matter whether you enter it in Zoho or in Flexprice first, as long as both match. If you change the secret, update **both** places.
### Webhook 1 — Invoices (Paid and Voided)
Create a **workflow** on the **Invoice** module (for example when an invoice is **edited** and **status** is updated) so changes such as **Paid** or **Void** are sent to Flexprice. Add a **webhook** immediate action that **POST**s to your Flexprice webhook URL.
### Webhook 2 — Customers (Inbound Customer Sync)
Create a second workflow on the **Customers** module for **customer created** (all customers or your criteria), with a webhook action posting to the **same** Flexprice URL as the invoice rule. **Customer inbound** sync is **on by default** on the connection; turn it off only if you do not want new Zoho customers to create Flexprice customers.
## Step 3: Create Zoho Books Connection
### Using Flexprice Dashboard
You can create a Zoho Books connection from the Flexprice dashboard:
Go to **Flexprice dashboard** → **Integrations** → **Zoho Books** (or **Accounting**) → **Connect** / **Add connection**.
Fill in **Connection name**, **Client ID**, and **Client Secret** from the Zoho API Console. Add your Zoho Books **organization ID** and the **accounts server** for your region (for example `https://accounts.zoho.in` or `https://accounts.zoho.com`). Copy the **webhook URL** from the modal into each Zoho Books webhook. In **Webhook secret**, **type the same value** you use in Zoho’s webhook / signing-secret field (**character-for-character**). You pick that value yourself—Zoho does not provide one. You can update this field later if you rotate the secret.
Complete sign-in and consent in Zoho. Flexprice exchanges the authorization code for tokens, stores them encrypted, and associates the connection with your Zoho **organization**. Access tokens refresh automatically.
**Invoice** outbound sync and **customer** inbound sync are **on by default** for a new connection. Change them only if you do not want invoices pushed to Zoho or new Zoho contacts imported into Flexprice.
## Customer Sync
Flexprice can keep customers aligned **toward Zoho** (with invoice sync) and **from Zoho** (via webhooks). On the connection, **invoice outbound** and **customer inbound** sync are **on by default**; you can turn either off if you do not want that behavior.
### Flexprice → Zoho Books (contacts on the Zoho side)
**Default:** **Invoice outbound** sync is **on by default** (see [Sending invoices to Zoho](#sending-invoices-to-zoho-outbound)). If someone turned it off, enable it again on the connection to push invoices—and the related contacts—to Zoho.
**What happens:** Whenever Flexprice **creates an invoice in Zoho Books**, it also **creates or matches a contact** in Zoho for that invoice’s Flexprice customer. You do not maintain that contact separately before the invoice sync runs.
### Zoho Books → Flexprice (new customers in Flexprice)
**Default:** **Customer inbound** sync is **on by default** on the connection.
**Also required in Zoho:** Inbound only works when Zoho can reach Flexprice. In Zoho Books, configure the **customer created** webhook from [Step 2](#step-2-configure-zoho-books-webhooks) so Zoho notifies Flexprice when customers are created.
**What happens:** A **new customer** in Zoho Books can become a **new customer** in Flexprice. **Vendor** records are ignored. If the customer’s **email** already exists on a Flexprice customer, Flexprice **links** to that customer instead of creating a second one.
## Invoice Sync
### Sending invoices to Zoho (outbound)
**Default:** **Invoice outbound** sync is **on by default** on a new connection. Disable it only if you do not want Flexprice to create invoices in Zoho Books.
**What happens:** After a Flexprice invoice is **eligible to sync** to your accounting system (same rules as other Flexprice accounting integrations), Flexprice **creates a corresponding invoice** in Zoho Books. You do not manually re-enter that invoice in Zoho.
### How line items appear in Zoho
Zoho Books receives one line per Flexprice line so **totals stay aligned**:
* **Quantity** is always **1** per line.
* **Rate** is set to the **full amount** of that Flexprice line (so the line total in Zoho equals the Flexprice line amount).
### Currency and exchange rates
Zoho Books uses an organization **base currency**. If the Flexprice invoice uses a **different currency**:
1. In Zoho Books, open **Settings → Currencies**.
2. **Add** that invoice currency if it is missing.
3. Set a **positive exchange rate** from that currency to your **base** currency.
If this is missing or invalid, Zoho may **reject** the invoice Flexprice tries to create.
### Invoice updates from Zoho (webhooks)
This applies when the Zoho invoice was **originally created from Flexprice** (so both systems are linked).
| Event in Zoho | What Flexprice does |
| ------------------ | ---------------------------------------------------------------------------------------------------------------- |
| Invoice **paid** | Records payment and updates the Flexprice invoice status (safe to retry if Zoho sends the event more than once). |
| Invoice **voided** | Voids the linked Flexprice invoice when the workflow allows it. |
Other changes in Zoho (edits, partial updates, unrelated fields) are **not** continuously copied back into Flexprice; only these webhook-driven outcomes are handled.
## Security Best Practices
### Credential Management
1. **Environment separation** - Use separate Zoho API applications or organizations for development and production where possible.
2. **Secret rotation** - Rotate the Zoho **Client Secret** in the API Console if needed. For webhooks, set a **new** secret in both Zoho workflow actions and the Flexprice connection together.
3. **Encryption** - Flexprice stores connection secrets encrypted at rest.
### Webhook Security
1. **HTTPS only** - Use HTTPS for the webhook URL.
2. **Signature verification** - Store the **same** user-defined webhook secret on the Flexprice connection as in Zoho so Flexprice can verify `X-Zoho-Webhook-Signature`.
3. **Correct URL path** - Incorrect **tenant** or **environment** in the URL prevents Flexprice from loading the connection even when the HTTP request succeeds.
## Troubleshooting
| Issue | Cause | Solution |
| ---------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Webhook does not update Flexprice | Wrong tenant or environment in URL | Use the exact **tenant** and **environment** IDs from Flexprice for the org where the connection exists |
| Invalid webhook signature | Secret mismatch | The string in Zoho’s webhook action and in Flexprice must be **identical**; update both sides if you rotate the secret |
| Invoice fails to sync to Zoho | Currency or exchange rate | In **Settings → Currencies**, add the invoice currency with a **positive** rate against **base** currency |
| New Zoho customer does not create Flexprice customer | Inbound off or missing webhook | Confirm **customer inbound** is on (default is on); ensure the **customer created** webhook in Zoho targets the correct Flexprice URL |
| Paid in Zoho but Flexprice invoice unpaid | Missing entity mapping | The Zoho invoice typically must have been created from Flexprice so entity integration mapping links both sides |
## Next Steps
After setting up your Zoho Books connection:
1. **Verify the connection** - Confirm the connection shows as active in Flexprice.
2. **Test invoice sync** - With outbound invoice sync on, create or finalize a test invoice and confirm it appears in Zoho Books.
3. **Test payment reconciliation** - Mark the invoice paid in Zoho and confirm Flexprice reflects payment status.
4. **Test customer sync (optional)** - With inbound customer sync on, create a customer in Zoho Books and confirm a customer appears in Flexprice.