# 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 alert settings Source: https://docs.flexprice.io/api-reference/alertsettings/create-alert-settings /api-reference/openapi.json post /alerts/setting Configure a subscription, line item, or group spend alert. # Delete alert settings Source: https://docs.flexprice.io/api-reference/alertsettings/delete-alert-settings /api-reference/openapi.json delete /alerts/setting/{id} Soft delete an alert setting. # Get alert settings Source: https://docs.flexprice.io/api-reference/alertsettings/get-alert-settings /api-reference/openapi.json get /alerts/setting/{id} Fetch a single alert setting by id. # Query alert settings Source: https://docs.flexprice.io/api-reference/alertsettings/query-alert-settings /api-reference/openapi.json post /alerts/setting/search List or search alert settings. Returns a paginated list. # Update alert settings Source: https://docs.flexprice.io/api-reference/alertsettings/update-alert-settings /api-reference/openapi.json put /alerts/setting/{id} Patch an alert setting's config; omitted fields keep their stored value. # Cancel checkout session Source: https://docs.flexprice.io/api-reference/checkout/cancel-checkout-session /api-reference/openapi.json post /checkout/sessions/{id}/cancel # 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 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 addon credit grants Source: https://docs.flexprice.io/api-reference/credit-grants/get-addon-credit-grants /api-reference/openapi.json get /addons/{id}/creditgrants Use when listing credits attached to an addon (e.g. included prepaid or promo credits). # 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 subscriptions for customer by external ID Source: https://docs.flexprice.io/api-reference/customers/get-subscriptions-for-customer-by-external-id /api-reference/openapi.json get /customers/external/{external_id}/subscriptions Returns all subscriptions for a customer looked up by external_id, with line-item meters and entitlements attached (no pagination). # 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/lookup Use when debugging a specific event (e.g. why it failed or how it was aggregated). Reads the meter-usage pipeline; includes processing status and step-by-step debug tracker when unprocessed. Uses ?id= query param because event IDs can contain "/". # 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). Reads the meter-usage pipeline. # 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: Region Selector ## 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. Pass a `checkout` object to gate the invoice behind a hosted payment session: the invoice stays DRAFT with no invoice number, and the response carries `checkout_session.payment_action.url` for the customer to pay. It finalizes only when the payment webhook lands; if the session expires the invoice is voided and archived. Poll `GET /checkout/sessions/{id}` until `terminal` is true. One-off invoices only. # Execute invoice modification Source: https://docs.flexprice.io/api-reference/invoices/execute-invoice-modification /api-reference/openapi.json post /invoices/{id}/modify/execute Execute a modification on a draft or finalized invoice. Supports line item changes: add (bulk), update (one line item per call; the edit is versioned, so the line item id changes), and remove (bulk, soft delete). Totals are recalculated from the remaining line items; a manual edit marks the invoice as manually edited, which disables recompute. Modifying a FINALIZED invoice voids it and recreates it as a draft copy carrying all current data (description, billing period, due date, metadata, line items); the modification lands on the copy and the response returns the new draft — chain subsequent calls to the returned invoice id; a call that still targets the voided original is rejected with an error naming the replacement. # 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), or when recalculating this draft invoice's discount from its current standing coupon associations via apply_discount:true (idempotent, does not attach a new coupon). Allowed for invoices in draft or finalized status. # 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. # Register an AWS Marketplace agreement Source: https://docs.flexprice.io/api-reference/marketplace/register-an-aws-marketplace-agreement /api-reference/openapi.json post /marketplace/agreements Registers an AWS Marketplace buyer agreement against an existing Flexprice subscription, upserting plan/subscription/customer integration mappings in one call. # 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. # Get refund Source: https://docs.flexprice.io/api-reference/refunds/get-refund /api-reference/openapi.json get /refunds/{id} Use to inspect a single refund: its destination, settled amount and failure reason. # List refunds Source: https://docs.flexprice.io/api-reference/refunds/list-refunds /api-reference/openapi.json get /refunds Use to see where refunded money actually went and whether it has settled. Filter by invoice_ids to get every settlement row for one invoice. # Retry refund Source: https://docs.flexprice.io/api-reference/refunds/retry-refund /api-reference/openapi.json post /refunds/{id}/retry Use when a refund failed or is stuck pending. A failed gateway refund is retried into the customer's wallet; an already-settled refund is rejected. # 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 Deprecated: use POST /subscriptions/{id}/modify/execute with type "addon" and action "add", which also supports previewing the proration charge first. 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 /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 a plan change (v2, swap in place) Source: https://docs.flexprice.io/api-reference/subscriptions/execute-a-plan-change-v2-swap-in-place /api-reference/openapi.json post /subscriptions/{id}/change/v2/execute Change a subscription's plan in place. Subscription id, billing anchor and period bounds are preserved; line items are sliced and settled in one transaction. change_at controls timing. Omitted or 'immediate' applies the change now. 'end_of_period' records a pending schedule that executes at the subscription's current period end: the response returns is_scheduled, schedule_id and scheduled_at instead of a completed change, and nothing is swapped or billed until the boundary. scheduled_at is resolved from the subscription's current period end at request time. If that period end is already in the past (a backdated start date, a resumed pause, or worker downtime can all leave a subscription behind), the change is due immediately and fires on the next billing scan rather than a period away — inspect scheduled_at to see this. Only one plan change may be pending per subscription. By default (on_conflict_policies.on_pending_schedule = 'reject') a second request returns 400; cancel the existing schedule via POST /subscriptions/schedules/{schedule_id}/cancel first. Pending schedules are listable via GET /subscriptions/{id}/schedules. Set on_conflict_policies.on_pending_schedule to 'supersede' to replace the queued change instead: the pending schedule is cancelled and this request applied in the same transaction, so both land or neither does. The cancelled schedule ids are returned in superseded_schedules, and preview reports the same list without writing. # 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, tax, or addon add/remove). # 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 /subscriptions/schedules/{schedule_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 /subscriptions/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 /subscriptions/{id}/schedules Use when listing scheduled changes for a subscription (e.g. upcoming plan change or renewal). Returns all schedules for that subscription. # Preview a plan change (v2, swap in place) Source: https://docs.flexprice.io/api-reference/subscriptions/preview-a-plan-change-v2-swap-in-place /api-reference/openapi.json post /subscriptions/{id}/change/v2/preview Preview a subscription plan change without writing. Swap-in-place: subscription id, billing anchor and period bounds are preserved. # 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, tax, or addon add/remove) 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 Deprecated: use POST /subscriptions/{id}/modify/execute with type "addon" and action "remove", which also supports previewing the proration credit first. 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. # 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. # Import a CSV of usage events Source: https://docs.flexprice.io/api-reference/tasks/import-a-csv-of-usage-events /api-reference/openapi.json post /tasks Use to submit a CSV of usage events for async ingestion. The CSV must already have been uploaded to the Flexprice-managed imports bucket (currently via CSV Box) — pass the upload_id and the backend fetches the file from S3 and streams rows into ClickHouse. Returns the task ID and Temporal workflow IDs for polling. # 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. # List usage records Source: https://docs.flexprice.io/api-reference/usage-records/list-usage-records /api-reference/openapi.json post /usage-records/search Lists usage records. Also accepts filters/sort for a filtered query. # 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. # Remove user from tenant Source: https://docs.flexprice.io/api-reference/users/remove-user-from-tenant /api-reference/openapi.json post /users/{id}/remove Remove a human user (type=user) from the current tenant. Not supported for service accounts; use DELETE /users/{id} for those. # 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). # Update user roles Source: https://docs.flexprice.io/api-reference/users/update-user-roles /api-reference/openapi.json put /users/{id}/roles Update the roles of a user account (not service accounts — their roles are fixed at creation). Restricted to super_admin; a caller cannot update their own roles. Blocked with a 400 if the user has any active (published, unexpired) API key in any environment, since a key's permissions are snapshotted at creation time and would otherwise silently keep running on the old roles; the error lists the active keys grouped by environment ID so the caller can prompt to expire them first, then retry. # 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). # checkout.session.completed Source: https://docs.flexprice.io/api-reference/webhook-events/checkoutsessioncompleted /api-reference/openapi.json post /webhook-events/checkout.session.completed Fired when payment is confirmed; the subscription is now active and the invoice is finalized. Doc-only for parsing. # checkout.session.expired Source: https://docs.flexprice.io/api-reference/webhook-events/checkoutsessionexpired /api-reference/openapi.json post /webhook-events/checkout.session.expired Fired when a Checkout Session times out without payment; draft records are cleaned up. Doc-only for parsing. # checkout.session.failed Source: https://docs.flexprice.io/api-reference/webhook-events/checkoutsessionfailed /api-reference/openapi.json post /webhook-events/checkout.session.failed Fired when payment fails or the provider cancels the payment link. Doc-only for parsing. # checkout.session.initiated Source: https://docs.flexprice.io/api-reference/webhook-events/checkoutsessioninitiated /api-reference/openapi.json post /webhook-events/checkout.session.initiated Fired when a Checkout Session is created and a payment URL is returned to the customer. Doc-only for parsing. # 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. # event.rejected Source: https://docs.flexprice.io/api-reference/webhook-events/eventrejected /api-reference/openapi.json post /webhook-events/event.rejected Fired when an ingested usage event produces no meter usage — either no meter is registered for its event name, or meters exist for the name but the event matched none of their filters. Throttled to at most once per configured window per event name. 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. `invoice.line_items` omits line items with neither an amount nor a quantity (period fan-out emits one per window whether or not usage landed in it), and `invoice.subscription.plan.prices` carries only the prices this invoice references, not the plan's full catalogue. Doc-only for parsing. # invoice.create.drafted Source: https://docs.flexprice.io/api-reference/webhook-events/invoicecreatedrafted /api-reference/openapi.json post /webhook-events/invoice.create.drafted Fired when a new invoice is created in draft state. `invoice.line_items` omits line items with neither an amount nor a quantity (period fan-out emits one per window whether or not usage landed in it), and `invoice.subscription.plan.prices` carries only the prices this invoice references, not the plan's full catalogue. 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. `invoice.line_items` omits line items with neither an amount nor a quantity (period fan-out emits one per window whether or not usage landed in it), and `invoice.subscription.plan.prices` carries only the prices this invoice references, not the plan's full catalogue. 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. `invoice.line_items` omits line items with neither an amount nor a quantity (period fan-out emits one per window whether or not usage landed in it), and `invoice.subscription.plan.prices` carries only the prices this invoice references, not the plan's full catalogue. 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. `invoice.line_items` omits line items with neither an amount nor a quantity (period fan-out emits one per window whether or not usage landed in it), and `invoice.subscription.plan.prices` carries only the prices this invoice references, not the plan's full catalogue. 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. `invoice.line_items` omits line items with neither an amount nor a quantity (period fan-out emits one per window whether or not usage landed in it), and `invoice.subscription.plan.prices` carries only the prices this invoice references, not the plan's full catalogue. 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). `invoice.line_items` omits line items with neither an amount nor a quantity (period fan-out emits one per window whether or not usage landed in it), and `invoice.subscription.plan.prices` carries only the prices this invoice references, not the plan's full catalogue. 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. # refund.created Source: https://docs.flexprice.io/api-reference/webhook-events/refundcreated /api-reference/openapi.json post /webhook-events/refund.created Fired when a refund is planned against an invoice, before the money moves. Doc-only for parsing. # refund.failed Source: https://docs.flexprice.io/api-reference/webhook-events/refundfailed /api-reference/openapi.json post /webhook-events/refund.failed Fired when a refund fails. A gateway refund that fails is retried into the customer's wallet. Doc-only for parsing. # refund.succeeded Source: https://docs.flexprice.io/api-reference/webhook-events/refundsucceeded /api-reference/openapi.json post /webhook-events/refund.succeeded Fired when a refund settles, to the original payment gateway or to a wallet. 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.group_spend.threshold_reached Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptiongroup_spendthreshold_reached /api-reference/openapi.json post /webhook-events/subscription.group_spend.threshold_reached Fired when a feature group's total metered spend on a subscription crosses an alert threshold (critical, warning, or info) for the current billing period. Doc-only for parsing. # subscription.group_spend.threshold_recovered Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptiongroup_spendthreshold_recovered /api-reference/openapi.json post /webhook-events/subscription.group_spend.threshold_recovered Fired when a feature group's total metered spend on a subscription falls back below all configured alert thresholds for the current billing period. Doc-only for parsing. # subscription.line_item_spend.threshold_reached Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptionline_item_spendthreshold_reached /api-reference/openapi.json post /webhook-events/subscription.line_item_spend.threshold_reached Fired when a subscription line item's metered spend crosses an alert threshold (critical, warning, or info) for the current billing period. Doc-only for parsing. # subscription.line_item_spend.threshold_recovered Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptionline_item_spendthreshold_recovered /api-reference/openapi.json post /webhook-events/subscription.line_item_spend.threshold_recovered Fired when a subscription line item's metered spend falls back below all configured alert thresholds for the current billing period. 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.plan_changed Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptionplan_changed /api-reference/openapi.json post /webhook-events/subscription.plan_changed Fired when a subscription plan changes in place (id/anchor preserved; not cancelled+created). 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.spend.threshold_reached Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptionspendthreshold_reached /api-reference/openapi.json post /webhook-events/subscription.spend.threshold_reached Fired when a subscription's total metered spend crosses an alert threshold (critical, warning, or info) for the current billing period. Doc-only for parsing. # subscription.spend.threshold_recovered Source: https://docs.flexprice.io/api-reference/webhook-events/subscriptionspendthreshold_recovered /api-reference/openapi.json post /webhook-events/subscription.spend.threshold_recovered Fired when a subscription's total metered spend falls back below all configured alert thresholds for the current billing period. 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.ongoing_balance.updated Source: https://docs.flexprice.io/api-reference/webhook-events/walletongoing_balanceupdated /api-reference/openapi.json post /webhook-events/wallet.ongoing_balance.updated Fired when a wallet's ongoing (real-time) balance changes. 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.transaction.updated Source: https://docs.flexprice.io/api-reference/webhook-events/wallettransactionupdated /api-reference/openapi.json post /webhook-events/wallet.transaction.updated Fired when an existing wallet transaction is updated (e.g. pending to completed). 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) Push flow — LiteLLM pushes a per-request webhook to the Flexprice Collector, which transforms it to ai.usage and forwards it through metering and pricing to wallets, alerts, and analytics; a budget breach calls back to LiteLLM via the management API. 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) Managed pull flow — a scheduled trigger reads each source since the last watermark via API, SQL, or object-store readers, transforms the result to ai.usage server-side, dedupes by request_id, and ingests into Flexprice. 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. How cost is attached — if the ai.usage event already carries reported_cost, Flexprice uses the source cost; otherwise (or when markup or normalised pricing is needed) it prices from the model pricing repository and applies overrides and markup, producing the charged cost on usage. ## 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. Flexprice AI Cost Tracking architecture 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 Credit-based 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** Create Features (metered) for credit-based pricing 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** Add Subscription and credit grants **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). Add Subscription and credit grants 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** Create Customer in Flexprice **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 Resend 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** Create a feature 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** Create a plan 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. Add entitlement to 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**. Add subscription to customer * **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. usage Events * **Invoices:** Generated at the end of each billing cycle; they include the recurring fee and usage-based line items. Subscription Invoice 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 Feature
Module ## 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
Module **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:** Feature
Module **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 ## Cancel checkout sessions In-flight checkout sessions can now be cancelled through the API, releasing everything the session was holding in one step. * **Endpoint**: `POST /checkout/sessions/{id}/cancel` marks a `pending` or `initiated` session as `expired` and returns the same pollable response as `GET /checkout/sessions/{id}`. Completed sessions are rejected, and sessions that are already `failed` or `expired` are returned unchanged * **Atomic cleanup**: The draft subscription and its addons, coupons, credit grants, and tax associations, any pending addon attachments, and the payment and invoice are archived in a single transaction. A pending wallet top-up transaction, and any pending bonus credit earned from it, is marked failed with the reason `checkout session expired before payment`. A funded invoice is voided first so prepaid credits are returned. If any step fails, nothing is cleaned up and the session stays active * **Concurrency**: The terminal transition is claimed atomically, so a cancel racing the expiry job cleans up once. Cancelled sessions emit `checkout.session.expired` * **Customer portal**: Cancelling from the portal routes through the same path

**Other changes** * The event debugger returns every ingested copy of an event ID, newest first and up to 50, in a new `events` array on `GET /events/{id}`, and each event carries `ingested_at`, so duplicate ingestions of the same ID are visible * The Helm chart drops the unused `POSTGRES_AUTO_MIGRATE` config key; deployments use versioned migrations * Zero commitments no longer report usage as commitment utilization in analytics, and out-of-bucket usage on a line item with time-of-day buckets bills at the base rate without counting as utilized * Subscription schedules keep their `configuration` and `execution_result` when read through the TypeScript SDK, which previously returned them as empty objects, and the `subscription_id` query parameter on `GET /subscriptions/schedules` now filters instead of returning every schedule in the tenant * Grouped invoicing child subscriptions roll their current period together with the parent subscription * Razorpay autopay authorization links carry the mandate expiry, which was previously written to a field the gateway ignored * The dashboard credit grant dialog accepts conversion rates with more than three decimals, matching wallet creation, and saves both rates as numbers * New `POST /checkout/sessions/{id}/cancel` endpoint * `GET /events/{id}` responses include `events` and `ingested_at` * Subscription schedule `configuration` and `execution_result` are typed as open objects in the OpenAPI spec, so generated SDKs preserve their contents * The TypeScript SDK generation source now declares bearer JWT authentication with an optional `X-Environment-ID` header alongside API keys, so browser applications can authenticate with a session token. The published SDK picks this up on its next release
## Custom currencies Define your own currencies, such as credits or tokens, price your catalogue in them, and let Flexprice settle invoices in fiat. * **Configuration**: Add currencies to the `custom_currency_config` setting with a 3-character code, a name, a symbol, and a fiat conversion factor per settlement currency. Every custom currency must map the same set of fiat currencies * **Enforcement**: Prices, subscriptions, wallets, addons, and coupons can be created in a configured custom currency or in the default fiat currency * **Fiat settlement**: Invoices are denominated in the default fiat currency using the conversion factor, which stays live while the invoice is draft and is frozen at finalization. The custom currency amounts are retained on the invoice and on each line item * **Dashboard**: Configure currencies from Settings under Billing, and pick them from the currency selectors for charges, coupons, invoices, and wallets
Currency dropdown showing custom and standard currencies

## Payment-gated one-off invoices One-off invoices can now be created behind a hosted checkout session, and checkout sessions can be polled for their result. * **Checkout on create**: Pass a `checkout` object to `POST /invoices`. The invoice stays draft with no invoice number, the response carries the payment URL in `checkout_session.payment_action.url`, and the invoice finalizes when the payment webhook lands. If the session expires, the invoice is voided and archived * **Guards**: Invoices created this way carry `source_type: checkout` and cannot be finalized or voided by another caller while the session is active. Zero-amount invoices, including invoices fully covered by prepaid credits, are rejected * **Reconciliation on read**: `GET /checkout/sessions/{id}` checks non-terminal Razorpay sessions against the gateway and completes them when the customer has paid, covering late or dropped webhooks. Responses include `terminal`, `next_poll_after_ms`, `stale`, and a `payment` block, so clients poll until `terminal` is true. Chargebee sessions continue to reconcile through webhooks

## Invoice line item grouping Subscriptions can now collapse charges billed on a shorter cadence than the billing period into one line item per billing period. * **Grouping modes**: Set `line_item_grouping` on `POST /subscriptions` to `per_charge_period` (default) or `per_billing_period`. A monthly charge on a quarterly subscription bills as three line items or one, with the same total either way * **Merged rows**: Quantities are summed across windows, so five seats over three months bills as 15 seat-months at the unit price. Plan-level overage rows from cumulative commitments are not merged * **Dashboard**: The create subscription form shows an invoice line items toggle whenever a charge is shorter than the billing period; the setting is written at create time
## Percentage fee charges Usage and fixed charges can now be entered as a percentage in the plan builder. * **Charge type**: Select Percentage Fee when adding a usage or fixed charge and enter the rate, such as 2.5 for 2.5% * **Display and editing**: Charge tables and previews display the percentage rate, and the update and override dialogs let you edit it as a percentage
**Other changes** * Zoho Books invoice sync settings accept `payment_mode`, `deposit_to_account_id`, and `global_custom_fields`, and recorded payments carry the gateway payment ID as the reference number * Subscription price overrides now reuse the plan price's Stripe product, preventing duplicate products during sync * Paddle invoice sync builds charges as non-catalog prices, so syncing an invoice no longer adds prices to the Paddle catalog; plan change invoices with credit lines are charged as one net amount labelled with the plan change * Scheduled export creation fails immediately when the destination cannot be resolved, GCS destinations no longer require a region, and export validation errors are not retried * Addon quantities can be set while adding addons during subscription creation * The invoice edit page uses the invoice details as the editing surface, with inline row editing for line items, a clear action on the calendar, and the reapply coupon action below the line items table * Wallet alert settings now include a Currency or Percentage unit selector in the wallet dialog and tenant settings; balance alerts always evaluate a falling balance * Tenant onboarding requires an organization URL * Helm chart supports a replicated ClickHouse cluster through `clickhouse.altinity.replicasCount` and `clickhouse.altinity.keeper.hosts`, spreads replicas across nodes by default, and the worker HPA honors `targetMemoryUtilizationPercentage` * The Flexprice CLI ships an install script that downloads the latest release for macOS and Linux * Immediate cancellation of a subscription whose period has not started is clamped to the period start, and the open period is closed at the cancellation date * Cancelling a subscription terminates line items that start in the future at their start date rather than the cancellation date * Prepaid credit grants are not expired while the invoice for their period is still being finalized, so finalization consumes the correct grant * Proration for line items on a different cadence from the subscription is computed against the line item's own period, fixing skipped or inflated proration on multi-cadence addons and calendar stubs * Opening invoices generated at subscription start use the `SUBSCRIPTION_CREATE` billing reason and stay draft until they are paid through checkout or processed * Usage analytics CSV exports no longer group by source, which applied line item commitments once per source and overstated true-ups * Scheduled exports to S3-compatible destinations use the endpoint URL and path-style setting from the export configuration * HubSpot invoice sync resolves the contact from the invoice's customer and skips unmapped customers instead of falling back to another mapping * Razorpay customer creation is idempotent, and Paddle webhooks for environments without a Paddle connection are ignored instead of failing * One-time charges synced to Zoho Books show the correct service date instead of the previous day * Tax associations created without a `tax_behavior` default to `exclusive` for every currency, where previously USD and CAD defaulted to exclusive and all other currencies to inclusive; associations stored without a behavior read back as exclusive * Dashboard addon tables show overridden addon prices instead of the catalog default, nested dialogs no longer close the dialog beneath them, pressing Escape on a dropdown inside a dialog closes only the dropdown, the Add Credits dialog and update toast are readable in dark mode, spend alert dialog labels render correctly, and the manual export date picker opens inside its dialog * `POST /invoices` accepts a `checkout` object; the response includes `checkout_session`, and gated invoices carry `source_type` * `GET /checkout/sessions/{id}` responses include `terminal`, `next_poll_after_ms`, `stale`, and `payment` * `POST /subscriptions` accepts `line_item_grouping` * New `custom_currency_config` setting; invoice and line item responses include `custom_currency` amounts * Invoice line item amounts may be negative on any invoice type; invoice metadata accepts `collapsed_invoice_display_name` for collectors that club charges into one item
## Customer portal payments and saved cards Customers can now pay invoices, top up wallets, and manage saved cards from the customer portal. * **Payments and top-ups**: Pay outstanding invoices through a payment link, top up through checkout or a saved card, and configure auto top-up * **Saved cards**: Add, remove, and choose a default payment method through supported gateways * **Gateway support**: Available actions adapt to connected providers, with checkout and auto charge through Chargebee and Razorpay, and saved-card management through Chargebee * **Portal updates**: The Overview tab brings together wallet balance, payment methods, and subscriptions; payment flows open in a new tab and refresh the portal on completion

## Gateway refunds Refund credit notes can now return money through Razorpay or Chargebee, with each refund tracked in a settlement ledger. * **Refund destination**: Set `refund_target` to `BACK_TO_SOURCE` to refund the original payment; `PREPAID_WALLET` remains the default * **Status tracking**: Gateway webhooks update refund status, and duplicate deliveries cannot settle the same refund twice * **Wallet fallback**: Failed gateway refunds can be retried into the wallet, and paid invoice voids use the same refund ledger
## Invoice line item editing Invoice line items can now be added, updated, or removed, with totals recalculated after each edit. * **Draft editing**: Update amounts, quantities, descriptions, and service periods; descriptions and periods also appear on the PDF * **Finalized invoices**: Editing line items or applying discounts voids the original and creates a linked draft replacement * **Manual edit protection**: Edited invoices are marked with `is_manually_edited` so automated recomputation cannot overwrite them; line item updates create a new item ID

## Percentage-based wallet alerts Wallet alerts now support percentage thresholds alongside absolute balances. * **Percentage thresholds**: Set `alert_threshold_type: percentage` and a threshold from 0 to 100 for ongoing balance alerts * **Calculation**: Evaluates `100 × ongoing balance / sum of completed credit amounts`, using credits since the earliest current period start across active or trialing subscriptions. Without those subscriptions, it uses wallet history; a zero base skips evaluation * **More webhook states**: Balance alerts now emit warning and info transitions, plus recovery events when the balance returns to `ok`

## Mixed-cadence prices on subscription create You can now select compatible shorter-cadence prices when creating a subscription through the API or dashboard. * **Price selection**: `include_price_ids` selects specific plan prices. Omit it to include matching-cadence and one-time prices, or pass an empty list to include none * **Dashboard controls**: The "Also available on this plan" section adds shorter-cadence charges with the usual price overrides, commitments, coupons, and quantity controls * **Addon compatibility**: Addon dialogs filter charges by the subscription's cadence and currency
## Chargebee invoice sync improvements Synced invoices now use the Chargebee invoice number and can serve Chargebee's own PDF. * **Invoice matching**: The original Flexprice invoice number is preserved in metadata, and payment IDs are recorded in Chargebee notes * **Pending collections**: ACH and SEPA payments stay pending while processing, avoiding premature invoice voids * **Itemized checkout**: Checkout shows individual line items and handles zero-decimal currencies correctly

**Other changes** * Subscription spend and entitlement grant alerts now default to off; set `alert_enabled: true` in `subscription_alert_config` and `entitlement_alert_config` to continue evaluation * Off-session auto top-ups default to a one-hour cooloff when none is configured * Team settings now support removing members * CSV imports show a sample file and a 5 MB limit; imports continue to use S3 on GCS deployments * Wallet top-ups support per-currency minimums through `min_topup_amount_per_currency` (default `USD: 1`); end-customer credits always wait for payment * Customer search includes archived customers when requested or when no status filter is set * Invoice reads bypass stale cached data, and smaller webhook payloads avoid delivery size limits * Subscription line item updates lock rows before writing, and clearing commitment buckets removes stale values * Mixed-cadence billing aligns charge windows to the billing anchor and removes stray trailing line items * Subscription creation includes one-time prices again, and addon filtering respects `billing_period_count` * Portal payments handle missing `customer_not_present` settings correctly; currency display, saved-card payments, and return URLs were fixed * ClickHouse and integration HTTP clients require TLS 1.2 or later * Redis certificate verification is configurable: set `redis.tls_skip_verify: false` and `redis.tls_server_name` to enable it; skip-verify still defaults to `true` * Containers run as non-root, file permissions were tightened, and published images are signed * CI permissions and action pinning were hardened, integer bounds checks were added, and vulnerable dependencies were updated * New portal invoice payment, wallet top-up, auto top-up, payment method, and integration discovery endpoints * New refund list, get, and retry endpoints; refund webhooks emit `refund.created`, `refund.succeeded`, and `refund.failed` * New `POST /invoices/{id}/modify/execute` endpoint for line item edits * Chargebee connections accept `gateway_account_id` to choose where new cards are stored * Portal wallet top-up `idempotency_key` is optional; checkout configuration replaces `customer_present` with `customer_not_present`
## Inclusive and exclusive tax Taxes can now be included in the price or added on top, and customers can be marked tax exempt. * **Tax behavior**: Set `tax_behavior` to `inclusive` or `exclusive`. Subscription defaults are exclusive for USD and CAD, and inclusive for other currencies * **Exempt customers**: Set `tax_treatment: exempt` to skip tax and remove included tax from the total, starting with the next invoice * **Invoice breakdown**: `tax_summary` shows inclusive and exclusive totals and explains why no tax was charged * **Percentage-only rates**: `percentage_value` is now required; `fixed_value` has been removed

## Price-level usage bucketing Usage windows can now be configured on individual prices, so the same meter can power charges with different bucket sizes. * **Price configuration**: Set `bucket_size` on usage prices with `MAX` or `SUM` aggregation; existing meter-level buckets remain supported and prevent setting a second bucket on the price * **Versioned updates**: Changing the bucket creates a new price or line item version; use `none` to clear it * **Dashboard support**: Charge tables and price dialogs now show bucket sizes, with validation for window commitments
## Mixed-cadence charges on a subscription Subscriptions can now include shorter-cadence charges, such as monthly prices on a quarterly subscription. * **Compatibility**: Billing period and count must match exactly, or the price's effective months must divide the subscription's. Daily and weekly prices require an exact match; one-time prices remain supported * **Separate billing windows**: A quarterly invoice with monthly charges gets three line items, with usage and pricing tiers reset per month * **Threshold billing**: Auto-invoice threshold checks use the same billing windows
## Entitlement grant proration on addon changes Adding or removing an addon mid-cycle now adjusts its entitlement quota for the time remaining. * **Proportional grants**: Applies to features whose entitlements all use additive, subscription-period grants * **Quota updates**: Adding an addon grants the remaining period's share; removing it revokes the unused share * **Proration control**: Set `proration_behavior: none` to add the full quota

## Google Cloud Storage support Invoice PDFs and exports can now use Google Cloud Storage. CSV Box imports still require S3. * **Provider selection**: Set `storage.provider` to `s3` or `gcs`, or use cloud auto-detection * **Cloud credentials**: GCS supports Workload Identity and application default credentials; S3 exports support static keys or AWS role credentials * **GCS exports**: Require Flexprice-managed buckets and a signer service account with token creator permissions for signed URLs * **Configuration change**: `sync_config.s3` is now `sync_config.storage`

## Versioned database migrations Postgres schema changes now use versioned SQL migrations through dbmate, replacing Ent auto-migration. * **Existing databases**: Run `make migrate-adopt url="postgres://..."` before upgrading to chart 1.4.0; add `dry=1` to preview adoption * **Deployment controls**: Helm uses `migration.steps.dbmate` instead of `migration.steps.ent`; ECS migrations run only with `DB_MIGRATIONS_ENABLED=true` * **Migration checks**: Unadopted databases and invalid indexes block migration, and checksums protect applied files from edits
## Usage syncs report A new Usage Syncs page under Tools shows which subscription usage records have synced to cloud marketplaces. * **Provider status**: See AWS, GCP, and Azure syncs alongside usage amounts and billing periods * **Filters**: Find records by customer, subscription, or period

## Remove users from a tenant Tenant super admins can now remove users through the API. * **User removal**: Removes a human user from the tenant and auth provider; service accounts retain their existing delete endpoint * **Access protection**: Self-removal, cross-tenant removal, and removal of the last human user are blocked

**Other changes** * CSV event imports now use a tenant-scoped CSV Box `upload_id` instead of a caller-supplied URL * Free credit top-ups support a per-transaction limit through `wallet_topup_config.free_credit_limit_per_transaction` (default `0`, unlimited) * Meter usage records can optionally publish to an analytics Kafka topic * Meter lookup caching reduces database load, and Temporal startup retries transient failures * Wallet balance alert webhooks include the customer's `external_id` * Addon dialogs support price overrides and commitments; charge menus support editing and termination, and commitments can run daily * Helm supports separate consumer deployments and an optional Bento collector * Single-tenant deployments can show SSO login through `VITE_SSO_TENANT_ID` * Credit grant expiry respects the subscription's timezone * Price overrides preserve bucket sizes and validate commitment windows * Subscription creation for tax-exempt customers succeeds; their tax associations are skipped instead of rejected * Long charge amounts no longer hide row actions, and pagination stays stable while loading * Kafka topics default to three partitions when unspecified; Helm migration jobs retain logs * Tenant-scoped user administration no longer requires environment selection * User removal now enforces tenant boundaries and prevents auth-provider path injection * Dashboard prototype-pollution issues were fixed, and CI permissions were tightened * Backend and dashboard pull requests now run additional security scans, with grouped weekly dependency updates * New `POST /users/{id}/remove` endpoint * `POST /tasks` requires `upload_id` and `file_provider: csvbox`; `file_url` was removed, and only CSV event imports are supported * Addon filters accept `addon_statuses` (default `active`) and `active_at` * Meter-level `bucket_size` is deprecated in favor of price-level configuration
## Scheduled plan changes and billing anchor control Plan change v2 now supports changes at period end and more control over the billing anchor. * **Scheduled changes**: Set `change_at: end_of_period` to queue a plan change; pending changes can be listed and cancelled * **Billing anchor**: Keep the current anchor or use `billing_period_behaviour: anchor_at_effect` to start a new period when the change takes effect * **Conflict handling**: Choose whether a new request rejects or supersedes an existing scheduled change * **Grant updates**: Plan grants and entitlements switch at the change instant, preserving addon-sourced grants

## Permission-aware dashboard Dashboard actions and pages now respect each user's role. * **Action permissions**: Users without write access see disabled controls with an explanation * **Page access**: Restricted routes show a 403 page, and saving or resetting settings requires Super Admin * **Immediate access checks**: Permissions load at login without needing a page refresh

## Zoho Books invoice sync improvements Zoho Books sync now supports GST details, custom fields, customer updates, and invoice approval. * **GST support**: Customer tax details and price HSN/SAC codes sync to Zoho, with place of supply determining IGST versus CGST and SGST * **Custom fields**: Map customer and invoice metadata, plus service period dates, to Zoho custom fields * **Approval flow**: Optionally submit invoices for approval before marking them paid; pending or rejected approvals leave payment unrecorded in Zoho * **Customer updates**: Changes to a Flexprice customer update the linked Zoho contact

## Recalculate finalized invoices Finalized subscription invoices can now be recalculated in one step. * **Automatic replacement**: Recalculation validates and voids the original, then creates a replacement linked through `recalculated_invoice_id` * **Dashboard action**: Recalculate Invoice is available on finalized invoices; drafts and invoices with an existing replacement do not qualify
## HubSpot deal line item sync HubSpot deal line items now stay aligned with subscription changes. * **Ongoing reconciliation**: Creates, updates, and removes deal line items after quantity changes, addon changes, and completed checkouts * **Billing details**: Synced items include billing dates, frequency, discounts, and descriptions
**Other changes** * The dashboard supports Pylon in-app chat with verified user identity * Prices can expand feature reporting units, and plans can report unsynced subscription counts * Checkout records declined payment attempts without closing the payment; invoices finalize only after successful payment * Draft invoice finalization runs every 30 minutes and stops retrying invalid-state failures * OpenTelemetry adds optional HTTP metrics, replica identifiers, and cache tracing * Helm adds KEDA autoscaling, configurable Kafka topics, and dashboard builds from a pinned ref * Helm removes Sentry and Pyroscope integrations in favor of OpenTelemetry exception capture * Postgres and ClickHouse schema baselines simplify fresh installations * Invoice previews include coupon discounts, and recomputation no longer duplicates coupon applications or PDF discount lines * Usage queries respect timezones and stop when the subscription's customer cannot be resolved * Tabs invoice sync matches by billing period start, falling back to issue date for one-off and credit invoices * SAML tokens validate correctly, and background role polling failures no longer sign users out * User search requires read permission instead of Super Admin * Meter aggregation fields trim whitespace, and migration index creation is safe to repeat * Account creation requires an email verified by the auth provider * Dashboard routes now send security headers to restrict framing, content sniffing, and referrer exposure * Plan change v2 accepts scheduling, billing anchor, and conflict options; new schedule list, get, and cancel endpoints * `expand=features` on prices and `expand=price_sync_status` on plans * New `POST /users/chat/verify` and `POST /usage-records/search` endpoints * Zoho invoice sync settings accept approval and custom field configuration
## Role-based access for dashboard users Tenant super admins can now assign roles from Team settings to control what users can read and change. * **User roles**: Choose `super_admin`, `all_writer`, or `all_reader`; service accounts have a separate role catalog * **Role management**: The Edit roles action replaces a user's roles, with active API keys listed when they block a change * **Grant restrictions**: Admins can grant only permissions they hold, and invitations require Super Admin

## SAML single sign-on Self-managed deployments can now connect a SAML identity provider such as Okta, Azure AD, or Google Workspace. * **Tenant settings**: Super admins configure the identity provider, default role, and SSO enforcement from the dashboard * **Account provisioning**: Users are created on their first SSO login with the configured role * **Recovery access**: Enforced SSO blocks password login for regular users while keeping super admins exempt * **Deployment setup**: Enable `auth.saml.enabled` with Redis and an auth signing secret; tenant login links show the SSO button
## Plan change v2: swap plans in place Subscriptions can now switch plans while keeping the same subscription ID, billing anchor, and period boundaries. * **Preview and apply**: Preview prorated charges or credits before applying the change * **Addon handling**: Choose which attached addons to carry over or drop * **Change event**: `subscription.plan_changed` replaces the cancel and create events used by the earlier flow * **Supported changes**: Interval, currency, hierarchy, and phased changes continue to use the existing endpoints

## Addon changes through subscription modification Addon attachment and removal now use the subscription modification flow, so you can preview charges or credits first. * **Preview support**: Review proration, affected line items, and invoices before applying a change * **Checkout support**: Responses include checkout details when payment is required * **Endpoint migration**: The existing addon create and delete endpoints are deprecated in favor of modification endpoints
## Stripe product sync Stripe invoices and payment links can now show product-backed, itemized charges. * **Price sync**: Creates a Stripe product for each Flexprice price when outbound price sync is enabled * **Itemized payments**: Invoice and checkout lines use synced products where available * **Dashboard toggle**: Enable Prices in the Stripe connection drawer; outbound price sync is off by default

## Exportable usage and credits widgets The `@flexprice/ui` package now includes six widgets for embedding usage and credit information in your app. * **Usage widgets**: `UsageQuota`, `MetricCards`, `UsageTrendChart`, and `UsageBreakdown` * **Credit widgets**: `CreditBalance` and `CreditHistory`, with wallet switching and transaction history * **Bring your own data**: Widgets accept props, with adapters for portal responses and built-in loading and empty states * **Package rename**: `@flexprice/flexprice-ui` is now `@flexprice/ui`, with the bundle reduced from 17.5 MB to 1.35 MB

## Entitlement grant allocation Entitlement grants can now cover a full subscription period or align usage windows to fixed intervals. * **Full-period grants**: `grant_duration_unit: subscription_period` opens a grant without waiting for usage; omit duration value and allocation behavior for this unit * **Window alignment**: Choose `first_usage` (default) or `unit_start` to anchor windows to usage or intervals within the billing period * **Multi-unit windows**: Supports intervals such as six-hour buckets, calculated in the subscription's timezone

**Other changes** * Stripe Checkout promo discounts are reflected on Flexprice invoices, with payments matching the captured amount * Child subscription coupons apply to grouped invoices, and line item coupons can target a subscription line item * Zoho sync includes discounts before tax and marks already-paid invoices as paid after sync * Invoice webhooks include `environment_id`, and subscription webhooks include the customer * Kafka consumers support Kafka 4 offset commits, with configurable retention (default `168h`) * Onboarding is now a two-step flow, with refreshed login and dark theme styling * Dashboard refresh actions preserve filters, and tiered pricing forms allow editing the final tier's upper bound * Usage charges no longer offer one-time billing; legacy one-time values load as monthly * Concurrent wallet transactions no longer corrupt balances, and void refunds cannot exceed the amount paid * API key issuance and role updates are atomic, and alert transitions stay in order during concurrent evaluations * Subscription creation no longer freezes when applying a line item discount * Zoho item and invoice sync no longer sends per-line tax IDs * Deleting a setting works again after it was deleted and recreated, and archiving a missing setting reports not found * Nested dropdowns work inside dialogs, and searchable pickers no longer share unrelated results * Helm topology selectors and Kafka broker configuration were corrected * Event ingestion enforces request size limits, and outbound requests reject unsafe URLs and destinations * Payment validation, OAuth redirects, login responses, tenant isolation, and QuickBooks queries were hardened * Payment updates no longer accept client-set amounts; deletion is restricted to initiated, pending, or failed payments * Environment selection, API key issuance, portal session creation, and workflow monitoring enforce caller permissions * Whop requires a webhook signing secret, webhook verification fails closed, and the portal no longer issues already-expired session tokens * Vulnerable `dompurify` and `nanoid` dependencies were updated * New `PUT /users/{id}/roles` endpoint; `GET /rbac/roles` accepts `user_type` * New SAML metadata, login, and assertion endpoints under `/auth/saml/{tenant}` * New plan change v2 preview and execute endpoints under `/subscriptions/{id}/change/v2` * Subscription modification accepts `type: "addon"`; `POST /subscriptions/addon` and `DELETE /subscriptions/addon` are deprecated * Payment creation accepts `gateway_options.stripe.tax_id_collection_enabled`; customer responses include `environment_id` * `GET /taxes/rates` returns `items` and `pagination`
## Subscription creation via checkout Subscriptions can now be created behind a hosted checkout flow, so a subscription only activates once the customer has completed payment. * **Payment-gated create**: Start a subscription through a checkout session and Flexprice defers activation until the checkout is paid * **Unified checkout actions**: Subscription create shares the same checkout session action pipeline used for wallet top-ups and addon attachments * **Configurable behavior**: New checkout configuration fields control payment gating on subscription create requests

## Addon attachment via checkout Addons can now be attached to a subscription through a checkout session, matching the payment-first flow used for new subscriptions. * **Payment-gated attach**: Addon activation waits until the checkout session is paid, then the addon is applied to the target subscription * **Proration preserved**: Line-item proration continues to work when the addon lands mid-cycle after checkout completes * **Checkout DTOs**: Addon attach requests accept the same checkout session fields as subscription create

## Draft invoice discount management Draft invoices now expose a first-class way to apply and recalculate discounts through the standard update endpoint, replacing the earlier dedicated recalculate route. * **`apply_discount` flag**: Set `apply_discount` on an `UpdateInvoice` request to recompute coupon and credit adjustments against the current line items * **Single locked path**: Updates run inside one locked transaction that gates discount recalculation on the new flag, keeping totals consistent * **Tax reset on removal**: Removing a discount association now clears stale tax lines and clamps any negative amount-remaining values * **Endpoint consolidation**: The separate `POST /invoices/:id/apply-discount` and recalculate-discount endpoints have been removed in favor of `UpdateInvoice`

## Credit note idempotency Credit note creation now guarantees safe retries with deterministic idempotency keys and database-level uniqueness. * **Deterministic keys**: Idempotency keys are derived from stable request content, so client retries collapse to a single credit note * **DB-level uniqueness**: `idempotency_key` is enforced by a unique constraint, closing races between concurrent create calls * **Draft retry coverage**: Draft retries and `process_credit_note` calls now participate in idempotency deduplication * **Cache-hit safety**: Idempotent cache hits no longer re-publish webhooks or re-run finalization

**Other changes** * ClickHouse client now supports the HTTP wire protocol — deployments behind TLS-terminating load balancers on port 8443 no longer fail with `unexpected packet [72] from server` * Credit note finalize and void recalculations run inside the same transaction as the state change and propagate their errors instead of being swallowed * Credit note finalize now locks the invoice row and re-checks refund capacity before committing, preventing over-refund on concurrent finalizes * Export job timeouts have been extended to accommodate larger tenants * Coupon redemption limits are now enforced on the subscription modify path, matching the checks already applied on create * Coupon `max_redemptions` is capped at creation time so unbounded values can no longer be persisted * On-off invoice generation now respects coupon max redemption counts * Credit note recalculation ordering and capacity-check typing have been corrected, and a same-credit-note double-finalize race has been closed * Webhook delivery via Svix now uses a stable idempotency key so retried deliveries deduplicate correctly * Billing calculations use higher decimal precision, and max-credit adjustments on invoices now clamp correctly * Failure response codes returned by validations have been aligned with the documented set * HTTP client and file-import paths now reject cross-origin redirects, closing SSRF vectors through retryable and OTel HTTP clients * Chargebee webhooks fail closed on signature or decrypt failures, return JSON `401` bodies, and log rejections with request context; the Chargebee connection create endpoint now requires basic auth * Payment create uses a deterministic, tenant-scoped idempotency key that includes `payment_method_id` and intent fields, and idempotency conflicts are distinguished from other constraint errors * Credit note create locks the target invoice row before validating amounts, and void refetches state to avoid stale reads * Bulk ingest now enforces a per-request cap and API responses include a standard set of security headers * The dashboard no longer exposes an API catalog discovery surface
## Bulk event ingestion A new bulk ingestion path lets you submit many events in a single request with batching, deduplication, meter processing, and optional post-insert usage alerts. * **Batched submission**: Multiple events are ingested together and empty batches are handled without error * **Redis deduplication**: New configuration options control deduplication and post-insert processing so retried batches do not double-count * **Post-insert alerts**: Usage alert evaluation can now run immediately after bulk inserts to keep threshold notifications timely

## Wallet top-up via checkout with mandate autocharge Wallets can now be topped up through a hosted checkout flow, and subsequent top-ups can be charged automatically against a saved payment mandate. * **Checkout top-up**: Kick off a wallet top-up from a checkout session instead of only through direct payment APIs * **Autocharge from mandate**: Once a mandate is on file, auto top-ups charge without another manual approval * **Cooloff on the dashboard**: The wallet auto top-up modal now accepts a cooloff duration and unit so repeat charges are spaced out

## Customer subscriptions API A new endpoint returns the full list of subscriptions for a customer in one call, replacing manual filtering on the subscriptions list. * **Single call**: Fetch all subscriptions attached to a customer without paging through the global subscriptions list * **Consistent shape**: Response matches the standard subscription payload used elsewhere in the API

## Daily invoice draft and recompute schedule A new Temporal schedule runs a daily pass that creates draft invoices and recomputes existing drafts so upcoming charges stay current without manual triggers. * **Draft creation**: Draft invoices are opened on schedule for subscriptions that need them * **Recompute**: Existing drafts are recomputed against the latest usage, entitlements, and price sync state * **Cron consolidation**: HTTP cron handlers duplicated by Temporal schedules were removed, so scheduling now lives in one place
## Backdating terminated subscription line items You can now edit the end date of an already-terminated recurring line item on a subscription to correct historical billing without recreating the subscription. * **Backdated end date**: Move a terminated line item's end date earlier or later within its subscription window * **Preserves termination**: Replacing a backdated line item keeps its existing termination date instead of overwriting it * **One-time guardrails**: Invalid forward-dating or backdating of one-time items is now rejected
## Revenue feature toggle A new platform configuration flag controls whether the Revenue area of the dashboard is available in a deployment. * **Configurable in platform config**: Turn Revenue on or off through platform configuration; it remains enabled by default * **Dynamic UI**: Revenue navigation, command palette entries, and routes appear only when the feature is on
**Other changes** * Subscription overage minimum is now `1`, so a single unit of overage is billable * Cost sheet requests now accept property filters, matching the filtering available elsewhere in analytics * Addon prices and quantities can be overridden per subscription, and the override is applied inside the persistence transaction * Grouped invoicing now honors per-subscription price and quantity overrides * Coupon associations with zero duration are excluded, so no-op coupons no longer appear on invoices * Price sync indexes were tuned to reduce the time spent syncing plan prices * Bulk event publish uses a batched producer path to cut Kafka publish overhead on the hot ingest path * ClickHouse `max_open_conns` now applies as configured, replacing the silent no-op that pinned the pool at defaults * Temporal client SDK metrics are exported for monitoring worker and workflow health * Signup can be disabled through platform configuration, redirecting new sign-up attempts to login with a localized message * AWS Marketplace connection setup now generates a deterministic External ID and shows clearer error messaging * `formatPayableAmount` preserves precision on sub-cent amounts across invoice and payment displays * Node.js 22 is now the runtime for the dashboard build and application * Coupon add and remove now honor the requested start and end dates, falling back to the current UTC time only when unspecified * Wallet balance calculations fall back to the most recently cached balance when real-time computation is canceled or times out * Wallet balance calculations now respect the shorter of the request deadline and the configured service timeout * Currency selection in the detailed cost analytics response was returning the wrong currency in some cases * Service-account requests without sufficient permissions now stop with HTTP `403` and a clearer error message * API key authentication now identifies the correct account owner * Whop webhook events are verified against the signature header before processing * Addon idempotency is preserved when the same addon request is retried * Cache reads treat unmarshal errors as a miss instead of surfacing a corrupt entry * Billing usage calculations now consistently use subscription entitlement data * Invoice and subscription screens refresh related information more consistently after updates * Billing-period date formatting is correct across time zones * Portaled comboboxes are clickable inside modal sheets, so selects inside drawers register clicks again * Modal sheets no longer close when focus returns to their trigger * The "Add Customer" drawer no longer closes on clicks inside it, and the customer form starts fresh when creating a new customer * Date pickers hard-refresh their internal state and enforce min and max limits more consistently * Event debugger passes the event ID as a URL-safe query parameter * `cel-go` bumped to `v0.29.0` to address GHSA-gcjh-h69q-9w9g * New `POST /v1/events/bulk` path for bulk event ingestion with deduplication and optional post-insert alerting * New endpoint to fetch all subscriptions for a customer in a single call * Subscription line item edit now accepts an updated end date for already-terminated recurring items * Supabase auth tokens now include an `environment_id` claim * New `clickhouse.tls_skip_verify` option skips TLS certificate and hostname verification for self-signed development ClickHouse; defaults `false`
## GCP Marketplace integration Marketplace usage reporting now extends to Google Cloud — connect a GCP Marketplace producer project and Flexprice reports computed usage against your listed offer the same way it already does for AWS. * **Agreement registration**: Register agreements and report usage against a GCP Marketplace metered offer * **Shared usage pipeline**: Backed by a refactored `UsageRecord` schema and cron workflow shared across all three marketplace providers
Connect to GCP Marketplace from the Flexprice dashboard

## Azure Marketplace integration Marketplace usage reporting now also extends to Microsoft Azure — connect an Azure Marketplace transactable SaaS offer using Microsoft Entra app credentials and Flexprice reports computed usage the same way it already does for AWS and GCP. * **Entra app credentials**: Authenticate with a dedicated Azure Marketplace client using a Microsoft Entra tenant ID, client ID, and client secret registered in your own tenant * **Secret redaction**: A shared `RedactSecrets` utility sanitizes error messages from AWS, GCP, and Azure integrations so failure reasons stay visible without leaking credentials
Connect to Azure Marketplace from the Flexprice dashboard

## Entitlement grant overage and alerting Entitlement grants now handle usage beyond the quota and notify you as windows fill up, completing the rolling-quota model introduced for metered features. * **Overage billing**: Usage beyond a grant's quota is billed as overage on the invoice instead of being silently dropped * **Grant window coverage**: New grants are clipped to the current cycle end and opened ahead of time so a window is always active to absorb incoming usage * **Exhaustion alerting**: Alert evaluation now tracks grant consumption and fires when a window's quota is used up * **Quota comparison fix**: Corrected a comparison that could misclassify a grant as exhausted before its quota was actually consumed

## Custom onboarding workflows Customer onboarding now supports multiple named workflows instead of a single fixed sequence — pass a workflow name at customer creation to run a specific onboarding path. * **`onboarding_workflow_name`**: New optional field on customer create resolves to a named entry in `custom_workflows`; unrecognized names fall back to the default onboarding actions * **Default tenant name**: Onboarding configuration now accepts a default tenant name, used when provisioning a tenant during onboarding * **Dashboard editor**: A new workflow card and action-set editor let you build and manage custom onboarding workflows from the settings area
## One-time charges on subscriptions Subscription plans can now include one-time, non-recurring charges alongside their regular recurring line items. * **Plan price resolution**: Subscription creation now fetches both the subscription's billing-period prices and any `ONETIME` billing-period prices configured on the plan * **Entity type validation**: Subscription pricing now validates that only supported price entity types are used, rejecting unsupported combinations up front
## Zoho Books invoice mark-paid sync Flexprice invoices synced to Zoho Books are now marked paid in Zoho automatically once payment succeeds, closing the loop on the existing Zoho invoice sync. * **Automatic dispatch**: When a Zoho-synced invoice's payment status becomes `succeeded`, a Temporal workflow marks the corresponding invoice as paid in Zoho Books * **New Zoho client methods**: `GetInvoice` and `CreateCustomerPayment` support fetching invoice state and recording the payment directly against the Zoho customer * **Non-syncable line items skipped**: Line items that Zoho can't represent are excluded from the outbound sync instead of failing the whole invoice

## Redis Sentinel support Self-hosted deployments can now run Redis behind Sentinel for high availability and automatic failover, as an alternative to cluster mode. * **Sentinel mode**: Setting `FLEXPRICE_REDIS_SENTINEL_MASTER_NAME` and `FLEXPRICE_REDIS_SENTINEL_ADDRS` switches the client to Sentinel mode, taking precedence over cluster and standalone * **Replica read routing**: `FLEXPRICE_REDIS_ROUTE_READS_TO_REPLICAS` spreads reads across the master and replicas while keeping writes on the master * **Fail-fast validation**: Sentinel master name and addresses must be configured together — a partial config now returns a clear startup error instead of silently falling back to standalone or losing HA
**Other changes** * Webhook delivery to Svix now filters events to only those an endpoint is actually subscribed to, instead of delivering every event type * Webhooks dashboard adds an activity overview with delivery stats and Svix Play support for testing endpoints end-to-end * `subscription.updated` webhook event propagation refined for more accurate change tracking * Billing now supports multiple charges against the same line item in meter usage calculation * Wallet auto-topup gains a configurable cooloff period, with a matching provider control in the topup modal * Temporal client SDK metrics added for monitoring workflow and activity execution * Cost sheets now accept property filters in usage requests * Supabase auth tokens now include `environment_id` in their claims * Helm chart supports an optional dedicated namespace for in-cluster ClickHouse * Customer onboarding pins reads to the writer to avoid replication-lag races right after signup * AWS Marketplace connections now generate their external ID deterministically from account details instead of leaving it free-form * Whop webhook handler now verifies the webhook signature before processing events * Commitment math is now applied once per line item instead of once per split charge, preventing overcounting * Fixed a nil-pointer panic when a line item referenced a meter that no longer existed * Azure Marketplace usage records with zero quantity are now handled correctly instead of being rejected * GCP Marketplace usage report rejections now log a clearer error reason * Paddle invoice payment updates now skip only the transaction linked to subscription activation, not all transactions * Event debugger URLs now use a URL-safe event ID * Subscription overage is now allowed to be exactly `1` instead of being rejected as invalid * `cel-go` and `grpc` dependencies bumped to patch known CVEs (GHSA-gcjh-h69q-9w9g, GHSA-hrxh-6v49-42gf) * Plans and customers now load correctly on subscription and invoice pages with no existing records (frontend) * Subscription form no longer loops indefinitely fetching prices when a plan has no published charges (frontend) * Connection API accepts marketplace `provider: gcp` and `provider: azure` alongside the existing `aws` value * Customer create request accepts an `onboarding_workflow_name` field to select a named onboarding workflow
## AWS Marketplace integration Flexprice can now report usage directly to a seller's AWS Marketplace listing, joining the existing GCP integration — connect an AWS account, register each buyer's agreement, and let Flexprice-computed usage flow through to AWS billing. * **Marketplace client & connection verification**: A new AWS Marketplace client assumes a tenant's IAM role via STS to verify and report usage; connection creation is now bounded by a timeout so a misconfigured or unreachable AWS credential chain fails fast instead of hanging the request * **Agreement registration**: Link a buyer's AWS Marketplace agreement to a Flexprice subscription so usage is attributed to the right AWS account and reported through `BatchMeterUsage` * **Dashboard connection management**: A new AWS Marketplace connection drawer lets you configure and manage the connection directly from the dashboard, with improved UI and localization
AWS Bills page showing a Marketplace charge reported by Flexprice

## Grouped-invoicing children created inline with the parent Parent subscriptions can now create their grouped-invoicing children in the same request instead of a separate step, with each child's first-period charges folded into the parent's opening invoice. * **`grouped_invoicing_children_to_create`**: New field on subscription creation accepts child seat specs (plan, external customer, start date) to create alongside the parent; mutually exclusive with converting existing standalone subscriptions into the group * **Unified opening invoice**: Each inline child's period-1 charges are folded into the parent's single opening invoice rather than generating a separate invoice per child * **Aligned billing start**: Grouped-invoicing children created this way always start on the parent's billing date, keeping the group's billing periods in sync

## Zero-quantity subscription line items Subscription line items now accept an explicit quantity of `0` — useful for provisioning a seat-based or fixed charge at zero scale and ramping it up later — instead of silently defaulting to the price's minimum quantity. * **Pointer-based quantity**: `CreateSubscriptionLineItemRequest.Quantity` and `LineItemQuantityChange.Quantity` are now pointers, distinguishing an explicit `0` from an omitted field (which still defaults to `min_quantity`) * **Dashboard support**: The subscription quantity-modify dialog and price table now accept and display a typed `0` instead of coercing it back to the minimum quantity
## Razorpay card auto-charge and payment status sync Razorpay auto-charge mandates now support card in addition to UPI, and payment status is actively reconciled against the gateway instead of relying on webhooks alone. * **Card mandate registration**: Razorpay mandate registration accepts `card` as a method alongside `upi`, chosen by a new token-selection helper that tries UPI first and falls back to card * **Gateway status sync on read**: `GetPayment` now performs a best-effort status sync against Stripe or Razorpay for any payment still `PENDING` or `PROCESSING`, reconciling stuck statuses without waiting on a webhook * **Payment link tracking**: Razorpay payment links can now be polled by tracking ID to resolve their current status
## Wallet bonus credits and payment sync Wallet top-ups and onboarding-created wallets gain a dedicated expiry for bonus and initial credits, and purchased-credit invoices can be force-synced to Moyasar with connection-configured redirect URLs. * **`bonus_credits_expiry_date_utc`**: Top-up requests can set an expiry for bonus credits that's independent of the purchase credits' own expiry * **Onboarding wallet initial credits**: The onboarding workflow's wallet-creation action can now apply an optional expiration duration to the initial credits it loads * **Moyasar redirect URLs**: `force_sync_invoice` on wallet top-ups now forwards the connection-configured success/cancel URLs to Moyasar, mirroring the existing Paddle pattern, and is configurable from the Moyasar connection drawer
**Other changes** * Meter usage queries now support `group_by` on commitments for exports, and parent-child subscription usage is handled correctly when computing meter usage * Customer analytics requests can now include child customer costs alongside the parent's own * `formatPayableAmount` gives precise monetary formatting on the subscription addon table, replacing lossy rounding (frontend) * Storage-span sampling and app-level DB metrics added for tracing, giving SigNoz trace-cost control (`FLE-1003`) * Tax is now correctly applied when creating a subscription through checkout * Coupon redemption race condition closed with an atomic DB-level guard — concurrent requests could previously all pass validation and push `total_redemptions` past `max_redemptions` * Razorpay: mandate limit capping is now scoped to UPI only, `short_url`/`id` are validated on mandate registration responses, and `RefundLateCapturedPayment` failures now surface to the caller instead of being swallowed * `MandateLimit.Currency` is now validated as a 3-letter currency code * Inherited subscription revenue calculation corrected in meter usage computation * `EnvironmentSelector` button opacity behavior fixed in the dashboard (frontend) * `svix` browser bundle polyfill issue fixed in the self-hosted webhook dashboard (frontend) * New `wallet.transaction.updated` and `wallet.ongoing_balance.updated` webhook events; wallet transaction payloads now include the associated customer object * Subscription search now supports expanding `entitlements` * `Customer` entity gains a `contact` field, used for Razorpay customer sync * `PaymentResponse` now includes `tenant_id` and `environment_id`
## Spend alert settings Subscriptions and line items now support configurable, threshold-based spend alerts backed by a dedicated alert-settings entity and a real-time alert-log pipeline that evaluates billed spend as usage events arrive. * **AlertSettings API**: New endpoints let you create and update spend-alert thresholds for a subscription or line item, with `info`, `warning`, and `critical` levels * **Alert log pipeline**: Alert logs now track the originating `alert_setting_id` and flow through a dedicated consumer pipeline, driven by config rather than hardcoded thresholds * **Dashboard support**: A new alert settings dialog lets you configure and save spend-alert thresholds for a subscription or line item directly from the dashboard

## Razorpay UPI Autopay and auto-charge Razorpay checkout sessions now support mandate-based recurring payments — a customer authorizes a UPI Autopay or card mandate once, and every invoice after that is auto-charged with no further action needed. * **Mandate registration at checkout**: Set `payment_provider_config.collection_method` to `charge_automatically` to register a UPI or card mandate instead of a one-time payment link * **Automatic invoice charging**: Finalizing an invoice now triggers a best-effort `AutoChargeInvoice` call against any usable saved token, guarded by per-invoice and per-token-cycle idempotency claims to guarantee at-most-once charging * **Mandate ceiling**: A configurable `max_mandate_limit` caps the size of any single auto-charge; invoices above it fall back automatically to a normal payment link * **Reconciliation and webhook handling**: A reconciliation sweep recovers stuck charge claims, and the Razorpay webhook handler now processes `token.confirmed`, `token.rejected`, and `token.cancelled` events

## Tabs integration Flexprice now connects to Tabs, syncing finalized invoices to its revenue recognition platform through a dedicated background workflow. * **Connection setup**: Register and configure a Tabs connection from the dashboard's integrations catalog, alongside existing providers like Stripe and Razorpay * **Invoice sync workflow**: A new Temporal workflow pushes finalized invoices to Tabs for revenue recognition as they're generated
## Credit grants on addons Addons can now carry their own credit grants, matching the behavior already available on plans. * **Addon credit grants**: Attach credit grants directly to an addon — applying the addon to a subscription grants credits the same way a plan-level credit grant does * **Dashboard parity**: Addon detail pages now surface credit grants alongside other addon configuration, matching the plan detail experience
## Wallet bonus credit grants Wallet top-ups now support slab-based bonus credits — an extra credit grant calculated automatically from the size of a purchased top-up. * **Slab-based resolution**: A configurable slab table matches the top-up amount to a threshold and computes the bonus as either a flat amount or a percentage of the purchase * **Linked bonus transaction**: The bonus is recorded as its own wallet transaction, linked back to the originating purchase via a new `parent_transaction_id` field, and shares its completion state with the purchase
**Other changes** * Table row dropdown menus across the dashboard (addons, cost sheets, coupons, credit grants, customers, entitlement overrides) now include a copy ID action * Environment creation UI now supports platform-specific configuration, improving the setup flow for self-hosted deployments * `ManualBalanceDebit` requests now accept a `metadata` field for recording debit context * Scheduled subscription cancellations now correctly terminate line items, addons, and credit grants when the cancellation takes effect — previously only the subscription-level status was updated * Reverting a scheduled cancellation on a parent subscription now cascades the reverted state to inherited child subscriptions — previously children kept stale `CancelAtPeriodEnd`/`CancelAt`/`CancelledAt` values * Fixed a credit-grant application cancellation ordering bug, and cancellation failures are now propagated instead of silently swallowed * `GET /v1/tenants/{id}` now rejects cross-tenant reads * Meter usage charge calculations now include `SubscriptionLineItemID`, fixing incorrect spend-breach checks * New `event.unmatched` webhook (opt-in, throttled per tenant/environment/event name) fires when an ingested event produces no meter usage — either no meter is registered, or filters excluded it — replacing a previously silent drop * `ManualBalanceDebit` requests now accept a `metadata` field
## Self-hosted webhooks portal The webhooks dashboard now ships a full self-hosted management portal instead of a docs stub — manage endpoints, inspect message logs, and debug delivery attempts directly from Flexprice. * **Endpoint and message detail views**: New endpoint overview, advanced, and testing tabs, plus a message detail view with per-attempt status and response bodies * **Auto-switching portal**: The dashboard automatically renders the self-hosted Svix portal or the hosted Svix iframe depending on which URL your environment is configured with * **Event type picker**: Endpoint filters now page through the full event catalog instead of stopping at the first 250 event types * **Paddle event coverage**: Paddle connections can now subscribe to the full set of Paddle webhook event types, including `transaction.completed`

## Settings redesign The settings area is now a single tabbed dashboard, replacing the previous set of standalone pages for billing, team, and alerts. * **Tabbed navigation**: Billing, Team, Alerts, and Customer Portal are now tabs on one settings page with shared routing and i18n * **Billing tab**: Invoice numbering and subscription configuration moved into a dedicated tab with live invoice preview * **Team tab**: Organization info and member management consolidated with filtering and invite flows * **Alerts tab**: Wallet balance alert thresholds configurable with ordering validation to prevent conflicting rules * **Customer Portal tab**: Toggle which sections and visibility options are exposed on the customer-facing portal

## Customer onboarding automation You can now configure a default onboarding workflow that runs automatically whenever a new customer is created — provisioning a wallet, subscription, or custom action set without manual setup. * **Action sets**: Define default actions (create wallet with an initial credit load, create subscription against a published plan) that fire on customer creation * **Custom workflows**: Add named custom workflows alongside the default action set for different onboarding paths * **Validation**: Wallet credit expiration and workflow name conflicts are validated before saving
## Usage analytics discounts Usage analytics now account for active percentage coupons — the detailed analytics endpoint reports gross cost, discount, and net cost per line item and per time-series point instead of just gross cost. * **Discount-aware response**: `UsageAnalyticItem` and `UsageAnalyticPoint` now include `subtotal`, `total_discount`/`discount`, and a discount-adjusted `total_cost`/`cost` * **Subscription and line-item coupons**: Percentage coupons with `forever` or `repeated` cadence are resolved and applied across the requested time range, including ranges that straddle a coupon's active window
## Customer entitlements by external ID A new endpoint resolves a customer's entitlements using your own external customer ID, making it easier to check feature access at the edge without an internal Flexprice lookup first. * **New endpoint**: `GET /customers/external/{external_id}/entitlements` returns a customer's entitlements, with optional `feature_ids` and `subscription_ids` filters
**Other changes** * Meter creation now supports a `+ Custom Expression` toggle that swaps the aggregation field for a CEL formula input, with inline help for supported functions * Meters gain a monthly bucket size option alongside existing bucket granularities * Meter usage analytics support `expand: "source"` to return distinct event sources per bucketed meter * Cost Sheet details support filtering and sorting * Entitlement config-value editing now uses a dedicated JSON editor with validation, and overridden config values show a tooltip in the entitlements table * Repository-layer DB calls for Postgres and ClickHouse are now traced as proper DB client spans, and high-volume info logs were downgraded to debug to cut log volume * Entity caching gains ID-only keys, hit/miss/set observability, and tuned TTLs (2m in-memory, 30m Redis) * Kafka consumer groups now each have a dedicated DLQ topic for events ingestion * Credit grants support updating `end_date` after creation * Settings pages adopt a shared info-icon and tooltip pattern across wallet alerts, subscription, portal, and onboarding fields * Fixed a wallet balance alert double-firing bug by removing a redundant Kafka publish path * Stripe invoice line items now preserve and display the original quantity in the line item description instead of losing it when only an amount is synced * Paddle `transaction.completed` webhooks are now correctly handled against subscription activation * `UpdateEntitlement` now rejects requests that try to enable a config-type feature or corrupt its `config_value` * Fixed several `/users/me` session sync bugs — stale cached user data could overwrite newer state across tabs, and tenant updates weren't consistently reflected in the session * Team members list now shows correct singular/plural counts and auto-refreshes * Invoice configuration settings now save and reset to defaults correctly * Webhook endpoint advanced settings no longer lose disabled status or duplicate custom headers on save * New `GET /customers/external/{external_id}/entitlements` endpoint for looking up entitlements by external customer ID * `UsageAnalyticItem`/`UsageAnalyticPoint` response fields renamed: `net_cost` is now `total_cost`/`cost` (post-discount), with gross cost moved to a new `subtotal` field * `UpdateCreditGrantRequest` accepts an `end_date` field * `OverrideEntitlementRequest` accepts a `config_value` field for config-type feature overrides * `/webhooks/dashboard` returns `token` and `app_id` for the self-hosted Svix portal in addition to the existing hosted URL
## Checkout Sessions Flexprice now offers a hosted checkout flow that creates a subscription and finalizes payment end-to-end, without you building any payment UI. The initial release supports Razorpay, behind a pluggable provider architecture designed to add more gateways later. * **Session lifecycle**: Create a session via `POST /v1/checkout/sessions` and redirect the customer to the returned payment URL; sessions move through `initiated`, `pending`, `completed`, `failed`, and `expired` states * **Automatic cleanup**: If a session fails or expires, Flexprice automatically cleans up the draft payment, invoice, and subscription it created * **Automatic expiry**: A Temporal workflow sweeps and expires stale sessions every 30 minutes so abandoned checkouts don't linger

## Per-subscription timezones Timezone-aware billing now extends below the customer level — subscriptions carry their own `timezone` field (renamed from `customer_timezone`), and it threads through every date calculation that touches the subscription. * **Timezone validation**: Customer and subscription timezone fields are validated as IANA timezone names (e.g. `Asia/Kolkata`) on create and update, instead of accepting arbitrary strings * **Timezone-aware billing math**: Credit grant periods, proration, and ClickHouse usage aggregation now consistently use the subscription's timezone for period boundaries * **Timezone-aware analytics**: Detailed usage analytics now bucket time-series data in the customer's timezone instead of hardcoded UTC * **Dashboard**: Subscription and customer timezone are now visible in the Customer Information and Subscription Details views

## Custom expressions for meters Metered features can now compute their per-event quantity from a CEL formula instead of a single event property — useful when the billable quantity is derived from multiple fields, needs a unit conversion, or needs to be rounded or clamped. * **CEL math helpers**: Expressions support `max`, `min`, `abs`, `ceil`, `floor`, `round`, `pow`, `sqrt`, and `log` alongside standard arithmetic and comparison operators * **Validation**: Expressions are only accepted on SUM, AVG, MAX, and LATEST aggregations, and a meter can't set both a field and an expression at the same time * **Dashboard**: A "+ Custom Expression" toggle on feature creation swaps the aggregation field input for a CEL formula input, with inline help text and examples
Aggregation section with the Custom expression toggle highlighted

## Config feature type for entitlements Entitlements can now carry an arbitrary JSON `config_value`, so you can deliver feature flags, rate limits, or tenant settings to customers through the standard entitlements pipeline instead of a boolean or numeric limit. * **Config feature type**: A new `config` feature type accepts a JSON object as its `config_value` when the feature is attached to a plan's entitlements * **Dashboard JSON editor**: A JSON editor with syntax highlighting validates the config payload on entry, and a side sheet displays the full stored value from the plan's Entitlements tab * **Customer entitlements**: Config values are returned on the customer's entitlements as soon as they subscribe to a plan that includes the feature
Add Entitlement with Config feature JSON editor

## Redesigned settings dashboard Settings are now organized into a single tabbed dashboard instead of scattered pages, covering team, billing, alerts, and customer portal configuration. * **Team tab**: Manage tenant members and pending invites in one place, with an option to show pending invites alongside published members * **Billing tab**: Configure invoice and subscription settings — auto-cancellation and invoice numbering — directly from the dashboard * **Alerts tab**: Configure wallet balance alert thresholds from Settings instead of a separate page * **Customer portal tab**: New tab for configuring customer-facing portal settings

**Other changes** * Customer Analytics tab now surfaces COGS and margin metrics alongside usage and cost data * Subscription, payment, and customer-information list views now default to filtering on `PUBLISHED` status, hiding draft and pending records * Wallet balance endpoint now falls back to the last cached balance — flagged with a new `is_cached_fallback` field — when real-time computation fails or exceeds a 30s timeout; a 5xx is only returned when no cached balance is available either * Price entity limit increased from 3,000 to 5,000 * Customer entitlements response no longer includes subscription line items, reducing payload size for entitlement checks * Service account archive action now shows the correct delete label and icon * Moyasar webhook handler no longer errors when the referenced payment record can't be found — logs and returns gracefully instead * Moyasar payments now sync their status into the linked invoice's `moyasar_invoice_status` metadata field on completion * Credit note and tax association creation now return an `ErrAlreadyExists` error instead of a generic database error when the record already exists * CEL expressions now reject non-finite property values (`NaN`, `±Inf`) and no longer leak raw property values in boundary error messages * Svix webhook auth token environment variable is now bound correctly — webhooks were previously failing with 401s in production * JsonEditor now uses a proper typed value for config editing and fixes text-selection handling * Customer entitlements response now includes a `subscriptions` field listing which subscriptions grant each entitlement * Subscription's `customer_timezone` field renamed to `timezone` * Coupon and line-item responses now support `expand` for richer nested data * `includeAllStatuses` param added to the tenant members endpoint, returning pending invites alongside published members
## Customer timezone-aware billing Customers can now be assigned a timezone, which Flexprice uses to align billing periods, usage windows, entitlement resets, and credit/coupon expirations to their local calendar instead of UTC. * **Timezone field on customers**: Set `timezone` (any IANA name) when creating or updating a customer — subscriptions inherit it at creation time * **Timezone-aware billing periods**: Daily, weekly, monthly, quarterly, half-yearly, and annual periods now start and end at local midnight rather than UTC midnight * **Timezone-aware usage and resets**: Usage aggregation windows, entitlement resets, credit grant expirations, and coupon end dates now compute from the customer's local time

## Moyasar autopay Flexprice can now save a customer's card through Moyasar and charge it automatically on future invoices, with a hosted checkout page, 3DS verification, and lifecycle handling for reconciliation, voids, and refunds. * **Checkout page**: New Flexprice-hosted checkout page tokenizes the customer's card and redirects through 3DS verification when the issuer requires it * **Automatic invoice charging**: Once a card is saved, new invoices are charged directly against it without any customer action * **Cron reconciliation and void/refund lifecycle**: A scheduled workflow reconciles pending card-verification charges — voiding the verification charge automatically, or issuing a refund if the void fails
Setup Autopay button on the customer page

## Coupon and tax associations on subscriptions The subscription details page now has a dedicated dashboard flow for applying and removing coupons and tax rates on individual line items, mid-cycle, without needing to call the API directly. * **Apply/Remove dialogs**: New `ApplyCouponDialog`, `RemoveCouponDialog`, `ApplyTaxDialog`, and `RemoveTaxDialog` walk through a preview step showing affected resources before executing the change * **Association tables**: New `CouponAssociationTable` and `TaxAssociationTable` components list active associations with valid-from/to ranges, filtered to active, published associations only * **Line item actions**: Apply and remove coupon/tax actions are now available directly from each line item's dropdown menu * **Coupon codes**: Coupons now require a human-readable `coupon_code` on creation, shown as a monospace badge on the coupon details page

**Other changes** * Addons can now be added to draft subscriptions, and addon line item dates shift to match the new start date when a draft is activated * Subscription entitlement overrides can now be edited directly from the subscription page via a new edit drawer * Webhook requests arriving at inbound integration endpoints are now logged and persisted for auditing, with oversized bodies skipped from persistence * Tenant auth middleware reworked to support tenant-scoped access checks across permission and API key validation * Service account name is now included in the list API keys response * Zoho invoice sync no longer sends the Flexprice invoice number to Zoho, and instead updates the Flexprice invoice with the Zoho-generated number * Recurring charges form now synchronizes billing model and tier mapping automatically when switching pricing configurations * Service account role selection now prioritizes `super_admin` in the drawer's role list * Meter usage queries now correctly handle meters with no active subscription and group by properties on bucketed meters * Billing period calculation now enforces an exact match between subscription and plan price billing periods * Analytics event count and bucket line-item queries corrected * `DeleteUser` now correctly enforces tenant-scoped checks on the caller's API key * Arabic characters are now accepted by input sanitization on form fields (previously rejected as invalid) * Broken documentation links fixed across onboarding and integration guides * New `GET /coupons/associations` endpoint to list coupon associations * `subscription_coupons`/`CreateCouponRequest`/`CouponAssociation` payloads now accept `coupon_code` * Subscription details endpoint accepts an `expand` query parameter to include tax rate data * `AssociationID` renamed to `CouponAssociationID`/`TaxAssociationID` on subscription line item associations
## 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
Paddle connection setup

## 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
Create feature with group

**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
Clone plan from plan menu

## 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
Assign group to plan charges

## 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
Wallet alert settings

## 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
Usage breakdown by group in analytics
**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.
Bulk import interface
* **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
Currency-segregated revenue trends
## 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). # Autopay & Mandates Source: https://docs.flexprice.io/docs/checkout/autopay-mandates Collect a recurring payment mandate once at checkout, then let future invoices charge automatically A mandate is a one-time authorization from your customer that lets Flexprice charge their future invoices without asking them to pay each time. The lifecycle has three phases: the customer **registers** the mandate once at checkout, the **first charge** runs, and every invoice after that **auto-debits** in the background. Razorpay Checkout supports three collection methods: | Method | What the customer does | Future invoices | | ---------------------- | -------------------------------------------------- | -------------------------------- | | Payment link (default) | Pays a one-time hosted link | Each one needs a new link | | UPI Autopay mandate | Authorizes a recurring UPI mandate once | Auto-charged, no customer action | | Card mandate | Saves a card and authorizes recurring charges once | Auto-charged, no customer action | Use a mandate when the customer will be billed repeatedly — a subscription, metered usage, or wallet top-ups. Use the [one-time payment link](/docs/checkout/checkout-sessions) when there is a single payment and nothing recurring. ## Prerequisites * **Razorpay connection** with invoice sync enabled. Set `sync_config.invoice.outbound: true` when you [connect Razorpay](/integrations/razorpay/connection-setup) so future invoices sync automatically. * **Customer `contact` (phone number).** A mandate registration is rejected without it (`"The contact field is required"`). The requirement comes from Razorpay's authorization step, so set `contact` when you create the customer if you plan to use autopay. * **A plan and price** in the currency you will charge. The examples below use `INR`. Set `contact` at customer-creation time. Adding it later does not retroactively fix a mandate registration that already failed — you will need to start a new checkout. ## Register a mandate at checkout Create a checkout session and include `payment_provider_config`. This is what switches the session from a one-time payment link to a mandate registration. ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/checkout/sessions \ -H "x-api-key: " \ -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" } }, "payment_provider_config": { "collection_method": "charge_automatically", "payment_method": "UPI", "max_mandate_limit": "5000" }, "success_url": "https://app.example.com/checkout/success", "cancel_url": "https://app.example.com/checkout/cancel" }' ``` The response is the same shape as any checkout session — a `payment_action.url`, this time a Razorpay UPI Autopay authorization link: ```json theme={null} { "id": "chk_01HXXX", "checkout_status": "pending", "payment_action": { "type": "payment_link", "url": "https://rzp.io/l/XXXXXXX" } } ``` Redirect the customer to `payment_action.url`. They authorize the mandate once in their UPI app. ### payment\_provider\_config fields | Field | Required | Description | | ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `collection_method` | Yes | Set to `charge_automatically` to register a mandate. Omit `payment_provider_config` entirely for the default one-time payment link. | | `payment_method` | Yes | `UPI` for UPI Autopay, `CARD` for a card mandate. | | `max_mandate_limit` | Yes | Ceiling, in the major currency unit (e.g. rupees), for any single future auto-charge. Invoices above this are not auto-charged. | ### Card mandate A card mandate uses the same request. Change `payment_method` to `CARD`: ```json theme={null} "payment_provider_config": { "collection_method": "charge_automatically", "payment_method": "CARD", "max_mandate_limit": "5000" } ``` The response is the same — a Razorpay-hosted link where the customer enters card details and authorizes recurring charges. Everything after authorization works identically to UPI. ## What happens after authorization Once the customer authorizes, Razorpay confirms the mandate (`token.confirmed`) and captures the first payment (`payment.captured`). Flexprice marks the checkout session `completed`, activates the subscription, and finalizes the invoice — the same completion path as a one-time payment link. From then on, every new invoice for that customer is checked for a usable saved token and charged automatically, with no further customer action. This applies to subscription renewals, and also to one-off invoices and wallet top-up invoices. An invoice falls back to a normal Razorpay payment link when there is no usable saved token, or when the invoice amount exceeds `max_mandate_limit`. The fallback is automatic and safe to rely on — the customer receives a link to pay that invoice manually. `max_mandate_limit` is a hard ceiling, not a soft warning. An invoice above it never auto-charges; it always falls back to a payment link. Set the limit high enough to cover your largest expected invoice. ## Gotchas * **`contact` is mandatory** to register a mandate. Collect it at customer creation — see [Prerequisites](#prerequisites). * **`max_mandate_limit` is a hard ceiling.** Invoices above it fall back to a payment link rather than auto-charging. This is an intentional safety cap. * **Currency casing.** Send `currency` in any casing — Flexprice normalizes it to uppercase before calling Razorpay. Enable the webhook events, including the mandate events autopay needs. Full field reference for the create session call. End-to-end integration with webhook handling. # 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, or was cancelled | ## Create a Checkout Session ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/checkout/sessions \ -H "x-api-key: " \ -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` | Yes | `monthly`, `quarterly`, `annual`, etc. | | `configuration.create_subscription_params.start_date` | No | Subscription start date. Defaults to now. | | `payment_provider_config` | No | Collect a recurring mandate instead of a one-time link. Omit for the default payment-link flow. See [Autopay & Mandates](/docs/checkout/autopay-mandates). | | `payment_provider_config.collection_method` | No | Set to `charge_automatically` to register a mandate. | | `payment_provider_config.preferred_method` | No | `UPI` or `CARD`. Selects the mandate type. | | `payment_provider_config.max_mandate_limit` | No | Ceiling for any single future auto-charge, in the major currency unit. | | `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. | ## Other Checkout Actions Four more actions exist, but they are not created through this endpoint. Each is triggered by adding a `checkout` object to the endpoint that owns the change, so that Flexprice can compute the amount and defer the change itself: | Action | Create it with | | --------------------- | ----------------------------------------- | | `modify_subscription` | `POST /subscriptions/{id}/modify/execute` | | `add_addon` | `POST /subscriptions/addon` | | `wallet_topup` | `POST /wallets/{id}/top-up` | | `pay_invoice` | `POST /invoices` | Passing any of these actions here returns a validation error naming the endpoint to use instead. Once created, all of them are read, cancelled, deleted, and monitored through the same endpoints and webhook events described on this page. See [Pay-First Checkout](/docs/checkout/pay-first-checkout). ## Get a Checkout Session ```bash theme={null} curl https://us.api.flexprice.io/v1/checkout/sessions/ \ -H "x-api-key: " \ -H "X-Environment-ID: " ``` Use this to confirm `checkout_status` on your redirect pages before showing the customer a result. **Response (200):** ```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" }, "terminal": false, "next_poll_after_ms": 2000, "stale": false, "payment": { "id": "pay_01HDEF", "status": "PENDING", "gateway": "razorpay" } } ``` ### Polling for the result Reading a session that is still `initiated` or `pending` checks it against the payment provider and completes it if the customer has already paid, so a late or dropped webhook does not leave a paying customer waiting. Gateway calls are debounced, so reading in a tight loop does not add load. Poll until `terminal` is `true`, waiting `next_poll_after_ms` between reads. | Field | Description | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `terminal` | `true` once the session has finished (`completed`, `failed`, or `expired`). Poll on this rather than on the status set. | | `next_poll_after_ms` | How long to wait before reading again. Starts at 2 seconds for a fresh session and backs off to 5 and then 10 seconds as the session ages. `0` means stop: the session is terminal. | | `stale` | `true` when this response is stored state that was not checked against the provider on this read: the check was debounced, the gateway did not answer, or no payment exists yet. Show "still checking" rather than a final result. | | `payment` | The payment the session is waiting on, with its `id`, `status`, and `gateway`. Omitted when no payment has been created yet. | Reconciliation on read applies to Razorpay sessions. Chargebee sessions reconcile through webhooks, and reads return stored state. ## Cancel a Checkout Session Cancel a session the customer has abandoned so that everything it was holding is released immediately instead of waiting for `expires_at`. ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/checkout/sessions//cancel \ -H "x-api-key: " \ -H "X-Environment-ID: " ``` The response has the same shape as `GET /checkout/sessions/{id}`, with `terminal` set to `true`. An active session comes back as `expired`. A session that had already failed or expired is returned unchanged, so `checkout_status` can also be `failed`. Cancelling releases whatever the session was holding: * **Draft subscription**: A subscription created behind the session is archived along with its addons, coupons, credit grants, and tax associations. A change deferred behind the session is never applied. * **Pending addon attachments**: Addons attached behind the session are archived. * **Pending wallet top-up**: The pending wallet transaction is marked failed, along with any pending bonus credit earned from the purchase. * **Payment and invoice**: The payment and invoice created for the session are archived. If the invoice already holds customer value, such as applied prepaid credits, it is voided first so that value is returned. Cleanup runs in a single transaction. If any step fails, nothing is changed and the session stays active, so the call is safe to retry. A cancel that races the expiry job cleans up once. | Session status | Result | | ---------------------- | ----------------------------------------------------------------- | | `initiated`, `pending` | Cleaned up and marked `expired`; `checkout.session.expired` fires | | `failed`, `expired` | Returned unchanged | | `completed` | `400`: a completed session cannot be cancelled | Cancelling frees the session's `idempotency_key` for reuse. ## Delete a Checkout Session ```bash theme={null} curl -X DELETE https://us.api.flexprice.io/v1/checkout/sessions/ \ -H "x-api-key: " \ -H "X-Environment-ID: " ``` Deleting an active session first cancels it, releasing everything it holds exactly as `POST /checkout/sessions/{id}/cancel` does, and then archives it. Sessions already in a terminal state (`completed`, `failed`, `expired`) are archived as is. Use cancel instead of delete when you still want to read the session's final state afterwards. ## 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, or was cancelled | 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.** An `idempotency_key` is reserved while its session is active, so retrying with the same key returns 409 rather than creating a duplicate. Read the open session with `GET /checkout/sessions/{id}` to recover its payment URL. Once the session reaches a terminal state the key can be reused. 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) * [Cancel a Checkout Session](/api-reference/checkout/cancel-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://us.api.flexprice.io/v1/customers \ -H "x-api-key: " \ -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://us.api.flexprice.io/v1/checkout/sessions \ -H "x-api-key: " \ -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://us.api.flexprice.io/v1/checkout/sessions/ \ -H "x-api-key: " \ -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 | ## Recurring payments This guide uses the default one-time payment link: each invoice generates its own link. To charge future invoices automatically, collect a mandate at checkout instead. The customer authorizes once, and renewals — plus one-off and wallet top-up invoices — auto-charge with no further action. See [Autopay & Mandates](/docs/checkout/autopay-mandates). ## 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. ## Collection methods A checkout session collects payment one of three ways, depending on whether you need a recurring mandate: * **One-time payment link** (default) — the customer pays a single hosted link. Every future invoice needs a new link. * **UPI Autopay mandate** — the customer authorizes a recurring UPI mandate once, and future invoices auto-charge. * **Card mandate** — the customer saves a card and authorizes recurring charges once, and future invoices auto-charge. The default is the payment link. To collect a mandate, pass `payment_provider_config` on the session — see [Autopay & Mandates](/docs/checkout/autopay-mandates). ## 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. Collect a recurring mandate once, then auto-charge future invoices. Subscribe to `checkout.session.*` events to trigger fulfillment in your system. Create subscriptions directly without a payment step. Pricing, usage, and credits components — feed SDK, API, or your own data. # Pay-First Checkout Source: https://docs.flexprice.io/docs/checkout/pay-first-checkout Collect payment before a subscription or wallet change takes effect, using an opt-in checkout object on the endpoint you already call Some billing changes should not take effect until the customer has paid for them. A seat increase that generates a proration charge, or a wallet top-up that adds purchased credits, can both be gated behind a hosted payment. You opt in by adding a `checkout` object to the request you already send. Flexprice then defers the change, returns a payment link, and applies the change only after the payment succeeds. Omit `checkout` and the endpoint behaves exactly as before. ## Supported flows | Flow | Endpoint | Checkout action | | ----------------------- | ----------------------------------------- | --------------------- | | Seat or quantity change | `POST /subscriptions/{id}/modify/execute` | `modify_subscription` | | Purchased credit top-up | `POST /wallets/{id}/top-up` | `wallet_topup` | Both actions are created only through the endpoint that owns them. Passing either to `POST /checkout/sessions` returns a validation error pointing you at the right endpoint: ```json theme={null} { "code": "validation_error", "message": "Use POST /subscriptions/{id}/modify/execute with a checkout object instead", "http_status_code": 400 } ``` ## The checkout object ```json theme={null} { "checkout": { "payment_provider": "razorpay", "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": "seat-upgrade-2026-07-29-001", "payment_provider_config": { "collection_method": "send_invoice" }, "metadata": {} } } ``` | Field | Required | Description | | ------------------------- | -------- | -------------------------------------------------------------------------------------------------- | | `payment_provider` | Yes | Payment gateway to use. `razorpay` is the only accepted value | | `success_url` | No | Where to redirect after a 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 a duplicate session on retry. Unique only while a session is active | | `payment_provider_config` | No | Per-checkout payment behavior. See [Charge a saved payment method](#charge-a-saved-payment-method) | | `metadata` | No | Key-value pairs stored on the session | ## How it works 1. Flexprice validates the request and computes the amount to collect. 2. A `ONE_OFF` invoice is created in `DRAFT` status. This invoice locks the amount — it is never recomputed later. 3. A checkout session is created in `pending` status. The change you asked for is stored on the session's `configuration`, and the invoice and payment IDs are recorded as `checkout_invoice_id` and `checkout_payment_id`. 4. A payment is created in `INITIATED` status and the provider is called for a payment link. 5. The response includes a `checkout_session` object. Redirect the customer to `checkout_session.payment_action.url`. 6. When the provider reports success, Flexprice completes the session: it applies the deferred change, finalizes the draft invoice, and marks the payment `SUCCEEDED`. Nothing about the subscription or wallet balance changes between steps 1 and 6. ## Status at each phase | Phase | Checkout session | Invoice | Payment | Change applied | | ----------------------------- | --------------------- | ----------------- | ----------- | --------------------- | | After the pay-first call | `pending` | `DRAFT` | `INITIATED` | No | | After payment succeeds | `completed` | `FINALIZED`, paid | `SUCCEEDED` | Yes | | Failed, expired, or cancelled | `failed` or `expired` | Archived | Archived | No, and never will be | Poll `GET /v1/checkout/sessions/{id}` from your redirect pages to read `checkout_status` before showing the customer a result. ## Charge a saved payment method Set `collection_method` to `charge_automatically` to attempt an off-session charge against a payment method the customer has already authorized, instead of sending them to a payment page. ```json theme={null} { "payment_provider_config": { "collection_method": "charge_automatically", "payment_method": "UPI", "max_mandate_limit": "5000" } } ``` | Field | Description | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `collection_method` | `send_invoice` (default) returns a payment link. `charge_automatically` attempts a saved-method charge first | | `payment_method` | Restricts the attempt to one method: `CARD`, `UPI`, `ACH`, `OFFLINE`, `CREDITS`, or `PAYMENT_LINK`. Omit to let the provider choose | | `max_mandate_limit` | Ceiling for the mandate being registered, as a decimal string. Must be greater than zero | With `charge_automatically`, Flexprice first looks for a confirmed saved token and tries to charge it off-session. Two outcomes are possible: * **The charge is submitted.** No payment URL is returned, so there is nothing to redirect to. The session completes when the provider's payment webhook arrives. * **No usable saved method exists, or the amount exceeds the mandate ceiling.** Flexprice falls back to a Razorpay authorization link, which registers a mandate and collects this payment in one step. Redirect the customer to it as usual. Because the first outcome returns no URL, treat `payment_action.url` as optional in your integration and drive completion from the webhook rather than the redirect. `max_mandate_limit` is capped against your tenant-level mandate ceiling for UPI only. If you request a higher limit than the tenant allows, the tenant ceiling applies. Card mandates have no ceiling. ## Guards and retries **One active session per target.** A second pay-first request for the same subscription, or the same wallet, is rejected while a session is `initiated` or `pending`. The error names the session that is already open: ```json theme={null} { "code": "already_exists", "message": "a pending checkout session already exists for this subscription", "details": { "subscription_id": "subs_01KXZW2XJJPJ5GP919AD31HY7K", "checkout_session_id": "cs_01KY9HRZ25PPJ7W1THZNM43QW8" } } ``` Complete or cancel the open session before starting another. Deleting a session is allowed only while it is non-terminal. **Sessions expire.** Razorpay sessions expire 15 minutes after creation. If the provider's own link expires sooner, Flexprice shortens the session to match. On expiry the draft invoice and payment are archived and the change is discarded. **Completion is idempotent.** If your provider delivers the same success event twice, only the first delivery applies the change; the duplicate is rejected. Make your own webhook handler idempotent for the same reason. **`idempotency_key` scope.** A key is reserved only while its session is active. Once the session reaches a terminal state the same key can be reused. Reusing a key that belongs to an active session returns a conflict rather than the existing session. ## Webhook events | Event | Fired when | | ---------------------------- | ---------------------------------------------------------- | | `checkout.session.initiated` | Session created and payment link returned | | `checkout.session.completed` | Payment confirmed, change applied, invoice finalized | | `checkout.session.failed` | Payment failed or the session was cleaned up with an error | | `checkout.session.expired` | Session timed out without payment | Completing a `modify_subscription` session also publishes `subscription.updated`. ## Related Topics Require payment before a seat increase takes effect. Require payment before purchased credits land. * [Checkout Sessions API](/docs/checkout/checkout-sessions) * [Razorpay Setup](/docs/checkout/razorpay-checkout) * [Understanding Proration](/docs/subscriptions/understanding-proration) # 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 these 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 | | `token.confirmed` | Confirming a mandate registration (autopay only) | If `payment_link.paid` is not enabled, Flexprice will never receive payment confirmation and the subscription will stay inactive. Verify these events are enabled before testing. `token.confirmed` is only needed if you use [Autopay & Mandates](/docs/checkout/autopay-mandates). Without it, mandate registrations never complete and the session stays `pending`. It is harmless to enable for payment-link-only setups. ### 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. # Overview Source: https://docs.flexprice.io/docs/cli-reference/overview Every resource the Flexprice CLI can act on, and the actions available on each Commands follow a consistent shape: ```sh theme={null} flexprice [flags] ``` Run `flexprice resources` to see this list from your terminal, or `flexprice --help` for a single resource. For flags accepted by every command, see [Global flags](/docs/cli/global-flags). | Resource | Actions | | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Addons](/docs/cli-reference/resources/addons) | `create`, `delete`, `list`, `lookup`, `retrieve`, `update` | | [Alert Settings](/docs/cli-reference/resources/alert-settings) | `create`, `delete`, `list`, `retrieve`, `update` | | [Alerts](/docs/cli-reference/resources/alerts) | `list` | | [Checkout](/docs/cli-reference/resources/checkout) | `create`, `delete`, `retrieve` | | [Costs](/docs/cli-reference/resources/costs) | `active`, `analytics`, `analytics-v2`, `create`, `delete`, `list`, `retrieve`, `update` | | [Coupon Associations](/docs/cli-reference/resources/coupon-associations) | `list`, `retrieve` | | [Coupons](/docs/cli-reference/resources/coupons) | `create`, `delete`, `list`, `lookup`, `retrieve`, `update` | | [Credit Grants](/docs/cli-reference/resources/credit-grants) | `create`, `delete`, `for-addon`, `for-plan`, `retrieve`, `update` | | [Credit Notes](/docs/cli-reference/resources/credit-notes) | `create`, `finalize`, `retrieve`, `void` | | [Customers](/docs/cli-reference/resources/customers) | `by-external-id`, `create`, `delete`, `entitlements`, `entitlements-by-external-id`, `list`, `retrieve`, `subscriptions`, `upcoming-grants`, `update`, `usage` | | [Entitlements](/docs/cli-reference/resources/entitlements) | `create`, `create-bulk`, `delete`, `for-addon`, `for-plan`, `list`, `retrieve`, `update` | | [Environments](/docs/cli-reference/resources/environments) | `clone` | | [Events](/docs/cli-reference/resources/events) | `analytics`, `huggingface-inference`, `ingest`, `ingest-bulk`, `list`, `retrieve`, `usage`, `usage-by-meter` | | [Features](/docs/cli-reference/resources/features) | `clone`, `create`, `delete`, `list`, `update` | | [Groups](/docs/cli-reference/resources/groups) | `create`, `delete`, `list`, `retrieve` | | [Integrations](/docs/cli-reference/resources/integrations) | `config`, `link`, `mappings`, `unlink` | | [Invoices](/docs/cli-reference/resources/invoices) | `create`, `customer-summary`, `finalize`, `list`, `payment-attempt`, `payment-status`, `pdf`, `preview`, `recalculate`, `retrieve`, `trigger-comms-webhook`, `update`, `void` | | [Payments](/docs/cli-reference/resources/payments) | `create`, `delete`, `list`, `process`, `retrieve`, `update` | | [Plans](/docs/cli-reference/resources/plans) | `clone`, `create`, `delete`, `list`, `retrieve`, `sync-prices`, `update` | | [Price Units](/docs/cli-reference/resources/price-units) | `create`, `delete`, `list`, `lookup`, `retrieve`, `search`, `update` | | [Prices](/docs/cli-reference/resources/prices) | `create`, `create-bulk`, `delete`, `list`, `lookup`, `retrieve`, `update` | | [Rbac](/docs/cli-reference/resources/rbac) | `list`, `retrieve` | | [Scheduled Tasks](/docs/cli-reference/resources/scheduled-tasks) | `create`, `delete`, `list`, `retrieve`, `run`, `schedule-billing-period-update`, `schedule-draft-finalization`, `update` | | [Secrets](/docs/cli-reference/resources/secrets) | `create`, `delete`, `list` | | [Subscription Line Items](/docs/cli-reference/resources/subscription-line-items) | `create`, `delete`, `list`, `update` | | [Subscription Schedules](/docs/cli-reference/resources/subscription-schedules) | `cancel`, `list`, `list-for-subscription`, `retrieve` | | [Subscriptions](/docs/cli-reference/resources/subscriptions) | `activate`, `add-addon`, `addon-associations`, `cancel`, `change-execute`, `change-preview`, `create`, `entitlements`, `execute-subscription-plan-change-v2`, `list`, `modify-execute`, `modify-preview`, `preview-subscription-plan-change-v2`, `remove-addon`, `retrieve`, `retrieve-v2`, `upcoming-grants`, `update`, `usage` | | [Tasks](/docs/cli-reference/resources/tasks) | `download`, `list`, `result`, `retrieve`, `update-status` | | [Tax Associations](/docs/cli-reference/resources/tax-associations) | `create`, `delete`, `list`, `retrieve`, `update` | | [Tax Rates](/docs/cli-reference/resources/tax-rates) | `create`, `delete`, `list`, `retrieve`, `update` | | [Tenants](/docs/cli-reference/resources/tenants) | `billing-usage`, `retrieve`, `update` | | [Users](/docs/cli-reference/resources/users) | `create`, `delete`, `list`, `me`, `update`, `update-me`, `update-user-roles` | | [Wallets](/docs/cli-reference/resources/wallets) | `balance`, `by-customer`, `create`, `list`, `list-for-customer`, `retrieve`, `search-transactions`, `terminate`, `top-up`, `transactions`, `update` | | [Workflows](/docs/cli-reference/resources/workflows) | `list` | # Addons Source: https://docs.flexprice.io/docs/cli-reference/resources/addons Every addons command in the Flexprice CLI, with flags, required fields and examples. ## create Create addon | Flag | Type | Required | | --------------- | ------ | -------- | | `--description` | string | | | `--lookup_key` | string | yes | | `--name` | string | yes | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice addons create --lookup_key= --name= ``` ## delete Delete addon ```sh theme={null} flexprice addons delete ``` ## list Query addons | Flag | Type | Required | | -------------- | ------- | -------- | | `--end_time` | string | | | `--expand` | string | | | `--limit` | integer | | | `--offset` | integer | | | `--order` | string | | | `--start_time` | string | | | `--status` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `addon_ids` (array) * `filters` (array) * `lookup_keys` (array) * `sort` (array) ```sh theme={null} flexprice addons list ``` ## lookup Get addon by lookup key ```sh theme={null} flexprice addons lookup ``` ## retrieve Get addon ```sh theme={null} flexprice addons retrieve ``` ## update Update addon | Flag | Type | Required | | --------------- | ------ | -------- | | `--description` | string | | | `--name` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice addons update ``` # Alert Settings Source: https://docs.flexprice.io/docs/cli-reference/resources/alert-settings Every alert-settings command in the Flexprice CLI, with flags, required fields and examples. ## create Create alert settings | Flag | Type | Required | Description | | ---------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--entity_id` | string | yes | entity\_id is the id of the monitored entity: subscription id, subscription\_line\_item id, or group id, matching entity\_type. | | `--entity_type` | string | yes | | | `--parent_entity_id` | string | | parent\_entity\_id is the subscription id that owns a line-item or group alert. Required when entity\_type is "subscription\_line\_item" or "group"; omitted for subscription-level alerts. | | `--parent_entity_type` | string | | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `config` (object) ```sh theme={null} flexprice alert-settings create --entity_id= --entity_type= ``` ## delete Delete alert settings ```sh theme={null} flexprice alert-settings delete ``` ## list Query alert settings | Flag | Type | Required | | ---------------------- | ------- | -------- | | `--enabled` | boolean | | | `--end_time` | string | | | `--entity_id` | string | | | `--entity_type` | string | | | `--expand` | string | | | `--limit` | integer | | | `--offset` | integer | | | `--order` | string | | | `--parent_entity_id` | string | | | `--parent_entity_type` | string | | | `--start_time` | string | | | `--status` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `entity_ids` (array) * `filters` (array) * `parent_entity_ids` (array) * `sort` (array) ```sh theme={null} flexprice alert-settings list ``` ## retrieve Get alert settings ```sh theme={null} flexprice alert-settings retrieve ``` ## update Update alert settings These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `config` (object) ```sh theme={null} flexprice alert-settings update ``` # Alerts Source: https://docs.flexprice.io/docs/cli-reference/resources/alerts Every alerts command in the Flexprice CLI, with flags, required fields and examples. ## list Query alert logs | Flag | Type | Required | | ---------------- | ------- | -------- | | `--alert_status` | string | | | `--alert_type` | string | | | `--customer_id` | string | | | `--end_time` | string | | | `--entity_id` | string | | | `--entity_type` | string | | | `--expand` | string | | | `--limit` | integer | | | `--offset` | integer | | | `--order` | string | | | `--start_time` | string | | | `--status` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `filters` (array) * `sort` (array) ```sh theme={null} flexprice alerts list ``` # Checkout Source: https://docs.flexprice.io/docs/cli-reference/resources/checkout Every checkout command in the Flexprice CLI, with flags, required fields and examples. ## create Create checkout session | Flag | Type | Required | | ------------------------ | ------ | -------- | | `--action` | string | yes | | `--cancel_url` | string | | | `--customer_external_id` | string | yes | | `--failure_url` | string | | | `--idempotency_key` | string | | | `--payment_provider` | string | yes | | `--success_url` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `configuration` (object) * `metadata` (object) * `payment_provider_config` (object) ```sh theme={null} flexprice checkout create --action= --customer_external_id= --payment_provider= ``` ## delete Delete checkout session ```sh theme={null} flexprice checkout delete ``` ## retrieve Get checkout session ```sh theme={null} flexprice checkout retrieve ``` # Costs Source: https://docs.flexprice.io/docs/cli-reference/resources/costs Every costs command in the Flexprice CLI, with flags, required fields and examples. ## active Get active costsheet ```sh theme={null} flexprice costs active ``` ## analytics Get combined revenue and cost analytics | Flag | Type | Required | Description | | ------------------------ | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--end_time` | string | | | | `--external_customer_id` | string | | Optional - for specific customer | | `--include_children` | boolean | | IncludeChildren, when true and ExternalCustomerID belongs to a parent customer, aggregates every inherited-child customer's usage into the revenue and cost totals. Default (false) restricts the query to the customer's own usage — mirrors the meter-usage analytics contract. | | `--limit` | integer | | Pagination | | `--offset` | integer | | | | `--start_time` | string | | Time range fields (optional - defaults to last 7 days if not provided) | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `expand` (array) * `feature_ids` (array) * `property_filters` (object) ```sh theme={null} flexprice costs analytics ``` ## analytics-v2 Get combined revenue and cost analytics (V2) | Flag | Type | Required | Description | | ------------------------ | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--end_time` | string | | | | `--external_customer_id` | string | | Optional - for specific customer | | `--include_children` | boolean | | IncludeChildren, when true and ExternalCustomerID belongs to a parent customer, aggregates every inherited-child customer's usage into the revenue and cost totals. Default (false) restricts the query to the customer's own usage — mirrors the meter-usage analytics contract. | | `--limit` | integer | | Pagination | | `--offset` | integer | | | | `--start_time` | string | | Time range fields (optional - defaults to last 7 days if not provided) | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `expand` (array) * `feature_ids` (array) * `property_filters` (object) ```sh theme={null} flexprice costs analytics-v2 ``` ## create Create costsheet | Flag | Type | Required | | --------------- | ------ | -------- | | `--description` | string | | | `--lookup_key` | string | | | `--name` | string | yes | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice costs create --name= ``` ## delete Delete costsheet ```sh theme={null} flexprice costs delete ``` ## list Query costsheets | Flag | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------ | | `--environmentID` | string | | EnvironmentID filters by specific environment ID | | `--lookupKey` | string | | LookupKey filters by lookup key | | `--name` | string | | Name filters by costsheet name | | `--status` | string | | | | `--tenantID` | string | | TenantID filters by specific tenant ID | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `costsheetIDs` (array) * `filters` (array) * `queryFilter` (object) * `sort` (array) * `timeRangeFilter` (object) ```sh theme={null} flexprice costs list ``` ## retrieve Get costsheet ```sh theme={null} flexprice costs retrieve ``` ## update Update costsheet | Flag | Type | Required | | --------------- | ------ | -------- | | `--description` | string | | | `--lookup_key` | string | | | `--name` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice costs update ``` # Coupon Associations Source: https://docs.flexprice.io/docs/cli-reference/resources/coupon-associations Every coupon-associations command in the Flexprice CLI, with flags, required fields and examples. ## list List coupon associations ```sh theme={null} flexprice coupon-associations list ``` ## retrieve Get coupon association ```sh theme={null} flexprice coupon-associations retrieve ``` # Coupons Source: https://docs.flexprice.io/docs/cli-reference/resources/coupons Every coupons command in the Flexprice CLI, with flags, required fields and examples. ## create Create coupon | Flag | Type | Required | | ----------------------- | ------- | -------- | | `--amount_off` | string | | | `--cadence` | string | yes | | `--coupon_code` | string | | | `--currency` | string | | | `--duration_in_periods` | integer | | | `--max_redemptions` | integer | | | `--name` | string | yes | | `--percentage_off` | string | | | `--redeem_after` | string | | | `--redeem_before` | string | | | `--type` | string | yes | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) * `rules` (object) ```sh theme={null} flexprice coupons create --cadence= --name= --type= ``` ## delete Delete coupon ```sh theme={null} flexprice coupons delete ``` ## list Query coupons | Flag | Type | Required | | ---------- | ------- | -------- | | `--expand` | string | | | `--limit` | integer | | | `--offset` | integer | | | `--order` | string | | | `--status` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `coupon_codes` (array) * `coupon_ids` (array) * `filters` (array) * `sort` (array) ```sh theme={null} flexprice coupons list ``` ## lookup Get coupon by code ```sh theme={null} flexprice coupons lookup ``` ## retrieve Get coupon ```sh theme={null} flexprice coupons retrieve ``` ## update Update coupon | Flag | Type | Required | | -------- | ------ | -------- | | `--name` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice coupons update ``` # Credit Grants Source: https://docs.flexprice.io/docs/cli-reference/resources/credit-grants Every credit-grants command in the Flexprice CLI, with flags, required fields and examples. ## create Create credit grant | Flag | Type | Required | Description | | ---------------------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--addon_id` | string | | | | `--cadence` | string | yes | | | `--conversion_rate` | string | | amount in the currency = number of credits \* conversion\_rate ex if conversion\_rate is 1, then 1 USD = 1 credit ex if conversion\_rate is 2, then 1 USD = 0.5 credits ex if conversion\_rate is 0.5, then 1 USD = 2 credits | | `--credits` | string | yes | | | `--end_date` | string | | | | `--expiration_duration` | integer | | | | `--expiration_duration_unit` | string | | | | `--expiration_type` | string | | | | `--name` | string | yes | | | `--period` | string | | | | `--period_count` | integer | | | | `--plan_id` | string | | | | `--priority` | integer | | | | `--scope` | string | yes | | | `--start_date` | string | | | | `--subscription_id` | string | | | | `--topup_conversion_rate` | string | | topup\_conversion\_rate is the conversion rate for the topup to the currency ex if topup\_conversion\_rate is 1, then 1 USD = 1 credit ex if topup\_conversion\_rate is 2, then 1 USD = 0.5 credits ex if topup\_conversion\_rate is 0.5, then 1 USD = 2 credits | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice credit-grants create --cadence= --credits= --name= --scope= ``` ## delete Delete credit grant | Flag | Type | Required | Description | | ------------------ | ------ | -------- | ------------------------------------------------------------------------------------------------ | | `--effective_date` | string | | EffectiveDate is optional; when set (subscription scope) the grant end date is set to this time. | ```sh theme={null} flexprice credit-grants delete ``` ## for-addon Get addon credit grants ```sh theme={null} flexprice credit-grants for-addon ``` ## for-plan Get plan credit grants ```sh theme={null} flexprice credit-grants for-plan ``` ## retrieve Get credit grant ```sh theme={null} flexprice credit-grants retrieve ``` ## update Update credit grant | Flag | Type | Required | | ------------ | ------ | -------- | | `--end_date` | string | | | `--name` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice credit-grants update ``` # Credit Notes Source: https://docs.flexprice.io/docs/cli-reference/resources/credit-notes Every credit-notes command in the Flexprice CLI, with flags, required fields and examples. ## create Create credit note | Flag | Type | Required | Description | | ----------------------- | ------- | -------- | ---------------------------------------------------------------------------------- | | `--credit_note_number` | string | | credit\_note\_number is an optional human-readable identifier for the credit note | | `--idempotency_key` | string | | idempotency\_key is an optional key used to prevent duplicate credit note creation | | `--invoice_id` | string | yes | invoice\_id is the unique identifier of the invoice this credit note is applied to | | `--memo` | string | | memo is an optional free-text field for additional notes about the credit note | | `--process_credit_note` | boolean | | process\_credit\_note is a flag to process the credit note after creation | | `--reason` | string | yes | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `line_items` (array) * `metadata` (object) ```sh theme={null} flexprice credit-notes create --invoice_id= --reason= ``` ## finalize Finalize credit note ```sh theme={null} flexprice credit-notes finalize ``` ## retrieve Get credit note ```sh theme={null} flexprice credit-notes retrieve ``` ## void Void credit note ```sh theme={null} flexprice credit-notes void ``` # Customers Source: https://docs.flexprice.io/docs/cli-reference/resources/customers Every customers command in the Flexprice CLI, with flags, required fields and examples. ## by-external-id Get customer by external ID ```sh theme={null} flexprice customers by-external-id ``` ## create Create customer | Flag | Type | Required | Description | | ---------------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--address_city` | string | | address\_city is the city name with maximum 100 characters | | `--address_country` | string | | address\_country is the two-letter ISO 3166-1 alpha-2 country code | | `--address_line1` | string | | address\_line1 is the primary address line with maximum 255 characters | | `--address_line2` | string | | address\_line2 is the secondary address line with maximum 255 characters | | `--address_postal_code` | string | | address\_postal\_code is the ZIP code or postal code with maximum 20 characters | | `--address_state` | string | | address\_state is the state, province, or region name with maximum 100 characters | | `--contact` | string | | contact is an optional contact number for the customer (e.g. phone) | | `--email` | string | | email is the customer's email address and must be a valid email format if provided | | `--external_id` | string | yes | external\_id is the unique identifier from your system to reference this customer (required) | | `--name` | string | yes | name is the full name or company name of the customer | | `--onboarding_workflow_name` | string | | onboarding\_workflow\_name is given if a custom onboarding workflow is to be triggered for this customer | | `--skip_onboarding_workflow` | boolean | | skip\_onboarding\_workflow when true, prevents the customer onboarding workflow from being triggered This is used internally when a customer is created via a workflow to prevent infinite loops Default: false | | `--timezone` | string | | timezone is the customer's IANA timezone name (e.g. "Asia/Kolkata", "America/New\_York") Defaults to "UTC" if not provided | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `integration_entity_mapping` (array) * `metadata` (object) * `tax_rate_overrides` (array) ```sh theme={null} flexprice customers create --external_id= --name= ``` ## delete Delete customer ```sh theme={null} flexprice customers delete ``` ## entitlements Get customer entitlements ```sh theme={null} flexprice customers entitlements ``` ## entitlements-by-external-id Get customer entitlements by external ID ```sh theme={null} flexprice customers entitlements-by-external-id ``` ## list Query customers | Flag | Type | Required | | --------------- | ------- | -------- | | `--email` | string | | | `--end_time` | string | | | `--expand` | string | | | `--external_id` | string | | | `--limit` | integer | | | `--offset` | integer | | | `--order` | string | | | `--start_time` | string | | | `--status` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `customer_ids` (array) * `external_ids` (array) * `filters` (array) * `metadata` (object) * `sort` (array) ```sh theme={null} flexprice customers list ``` ## retrieve Get customer ```sh theme={null} flexprice customers retrieve ``` ## subscriptions Get subscriptions for customer by external ID ```sh theme={null} flexprice customers subscriptions ``` ## upcoming-grants Get upcoming credit grant applications ```sh theme={null} flexprice customers upcoming-grants ``` ## update Update customer | Flag | Type | Required | Description | | ----------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------ | | `--address_city` | string | | address\_city is the updated city name with maximum 100 characters | | `--address_country` | string | | address\_country is the updated two-letter ISO 3166-1 alpha-2 country code | | `--address_line1` | string | | address\_line1 is the updated primary address line with maximum 255 characters | | `--address_line2` | string | | address\_line2 is the updated secondary address line with maximum 255 characters | | `--address_postal_code` | string | | address\_postal\_code is the updated postal code with maximum 20 characters | | `--address_state` | string | | address\_state is the updated state, province, or region name with maximum 100 characters | | `--contact` | string | | contact is the updated contact number for the customer (e.g. phone) | | `--email` | string | | email is the updated email address and must be a valid email format if provided | | `--external_id` | string | | external\_id is the updated external identifier for the customer | | `--name` | string | | name is the updated name or company name for the customer | | `--timezone` | string | | timezone is the updated IANA timezone name for the customer (e.g. "Asia/Kolkata", "America/New\_York") | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `integration_entity_mapping` (array) * `metadata` (object) ```sh theme={null} flexprice customers update ``` ## usage Get customer usage summary ```sh theme={null} flexprice customers usage ``` # Entitlements Source: https://docs.flexprice.io/docs/cli-reference/resources/entitlements Every entitlements command in the Flexprice CLI, with flags, required fields and examples. ## create Create entitlement | Flag | Type | Required | | ----------------------------- | ------- | -------- | | `--aggregation_mode` | string | | | `--end_date` | string | | | `--entity_id` | string | | | `--entity_type` | string | | | `--feature_id` | string | yes | | `--feature_type` | string | yes | | `--grant_allocation_behavior` | string | | | `--grant_duration_unit` | string | | | `--grant_duration_value` | integer | | | `--grant_measure` | string | | | `--grant_quota` | string | | | `--is_enabled` | boolean | | | `--is_soft_limit` | boolean | | | `--parent_entitlement_id` | string | | | `--plan_id` | string | | | `--start_date` | string | | | `--static_value` | string | | | `--usage_limit` | integer | | | `--usage_reset_period` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `config_value` (object) ```sh theme={null} flexprice entitlements create --feature_id= --feature_type= ``` ## create-bulk Create entitlements in bulk These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `items` (array) ```sh theme={null} flexprice entitlements create-bulk ``` ## delete Delete entitlement ```sh theme={null} flexprice entitlements delete ``` ## for-addon Get addon entitlements ```sh theme={null} flexprice entitlements for-addon ``` ## for-plan Get plan entitlements ```sh theme={null} flexprice entitlements for-plan ``` ## list Query entitlements | Flag | Type | Required | Description | | -------------------- | ------- | -------- | -------------------------------------------------------------------------- | | `--end_time` | string | | | | `--entity_type` | string | | | | `--expand` | string | | | | `--feature_type` | string | | | | `--has_grant_config` | boolean | | HasGrantConfig filters on grant-config presence (grant\_quota set or not). | | `--is_enabled` | boolean | | | | `--limit` | integer | | | | `--offset` | integer | | | | `--order` | string | | | | `--start_time` | string | | | | `--status` | string | | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `entity_ids` (array) * `feature_ids` (array) * `filters` (array) * `plan_ids` (array) * `sort` (array) ```sh theme={null} flexprice entitlements list ``` ## retrieve Get entitlement ```sh theme={null} flexprice entitlements retrieve ``` ## update Update entitlement | Flag | Type | Required | Description | | ----------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `--aggregation_mode` | string | | | | `--clear_grant_config` | boolean | | Grant config — nil fields leave the current value alone. ClearGrantConfig=true wipes the whole grant config (back to a legacy entitlement). | | `--grant_allocation_behavior` | string | | | | `--grant_duration_unit` | string | | | | `--grant_duration_value` | integer | | | | `--grant_measure` | string | | | | `--grant_quota` | string | | | | `--is_enabled` | boolean | | | | `--is_soft_limit` | boolean | | | | `--static_value` | string | | | | `--usage_limit` | integer | | | | `--usage_reset_period` | string | | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `config_value` (object) ```sh theme={null} flexprice entitlements update ``` # Environments Source: https://docs.flexprice.io/docs/cli-reference/resources/environments Every environments command in the Flexprice CLI, with flags, required fields and examples. ## clone Clone an environment | Flag | Type | Required | Description | | ------------------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--name` | string | | Name of the new environment (required when target\_environment\_id is not provided) | | `--target_environment_id` | string | | TargetEnvironmentID is the ID of an existing environment to clone into (optional). When provided, Name and Type are ignored. When omitted, Name and Type are required. | | `--type` | string | | | ```sh theme={null} flexprice environments clone ``` # Events Source: https://docs.flexprice.io/docs/cli-reference/resources/events Every events command in the Flexprice CLI, with flags, required fields and examples. ## analytics Get usage analytics | Flag | Type | Required | Description | | ------------------------ | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--breakdown_bucket` | boolean | | BreakdownBucket when true augments each time-series point with BucketID/PriceID and appends a BucketSummaries rollup to each item. Requires WindowSize to be set and the item to be linked to a subscription line item that has CommitmentTimeBuckets. Default: false (opt-in, backward compatible). | | `--end_time` | string | | | | `--external_customer_id` | string | | ExternalCustomerID is the single external customer ID. Optional when ExternalCustomerIDs is provided; required otherwise. | | `--include_children` | boolean | | IncludeChildren when true folds child customers' usage into the single aggregated total. Default: false. | | `--start_time` | string | | | | `--window_size` | string | | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `expand` (array) * `feature_ids` (array) * `group_by` (array) * `property_filters` (object) * `sources` (array) ```sh theme={null} flexprice events analytics ``` ## huggingface-inference Get Hugging Face inference data These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `requestIds` (array) ```sh theme={null} flexprice events huggingface-inference ``` ## ingest Ingest event | Flag | Type | Required | Description | | ------------------------ | ------ | -------- | --------------------------------- | | `--customer_id` | string | | | | `--event_id` | string | | | | `--event_name` | string | yes | | | `--external_customer_id` | string | yes | | | `--source` | string | | | | `--timestamp` | string | | Handled separately due to parsing | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `properties` (object) ```sh theme={null} flexprice events ingest --event_name= --external_customer_id= ``` ## ingest-bulk Bulk ingest events These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `events` (array) ```sh theme={null} flexprice events ingest-bulk ``` ## list List raw events | Flag | Type | Required | Description | | ------------------------ | ------- | -------- | ---------------------------------------------------------------------------------------------------------- | | `--end_time` | string | | End time of the events to be fetched in ISO 8601 format Defaults to now if not provided | | `--event_id` | string | | Event ID is the idempotency key for the event | | `--event_name` | string | | Event name / Unique identifier for the event in your system | | `--external_customer_id` | string | | Customer ID in your system that was sent with the event | | `--iter_first_key` | string | | First key to iterate over the events | | `--iter_last_key` | string | | Last key to iterate over the events | | `--offset` | integer | | Offset to fetch the events and is set to 0 by default | | `--order` | string | | Order by condition. Allowed values (case sensitive): asc, desc (default: desc) | | `--page_size` | integer | | Page size to fetch the events and is set to 50 by default | | `--sort` | string | | Sort by the field. Allowed values (case sensitive): timestamp, event\_name (default: timestamp) | | `--source` | string | | Source to filter the events by the source | | `--start_time` | string | | Start time of the events to be fetched in ISO 8601 format Defaults to last 7 days from now if not provided | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `property_filters` (object) ```sh theme={null} flexprice events list ``` ## retrieve Get event ```sh theme={null} flexprice events retrieve ``` ## usage Get usage statistics | Flag | Type | Required | Description | | ------------------------ | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--aggregation_type` | string | yes | | | `--billing_anchor` | string | | BillingAnchor enables custom monthly billing periods for usage aggregation. When to use: - WindowSize = "MONTH" AND you need custom monthly periods (not calendar months) - Subscription billing that doesn't align with calendar months - Example: Customer signed up on 15th, so billing periods are 15th to 15th When NOT to use: - WindowSize != "MONTH" (ignored for DAY, HOUR, WEEK, etc.) - Standard calendar-based billing (1st to 1st of each month) Example values: - "2024-03-05T14:30:45.123456789Z" (5th of each month at 2:30:45 PM) - "2024-01-15T00:00:00Z" (15th of each month at midnight) - "2024-02-29T12:00:00Z" (29th of each month at noon - handles leap years) | | `--bucket_size` | string | | | | `--customer_id` | string | | | | `--end_time` | string | | | | `--event_name` | string | yes | | | `--external_customer_id` | string | | | | `--group_by_property` | string | | GroupByProperty is the property name in event.properties to group by before aggregating. When set, aggregation is applied per unique value of this property within each bucket, then the per-group results are summed to produce the bucket total. Deprecated: prefer GroupBy \[]string for parity with other analytics endpoints. ToUsageParams translates this field into GroupBy when GroupBy is otherwise empty. | | `--multiplier` | string | | | | `--property_name` | string | | will be empty/ignored in case of COUNT | | `--start_time` | string | | | | `--timezone` | string | | | | `--window_size` | string | | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `filters` (object) * `group_by` (array) ```sh theme={null} flexprice events usage --aggregation_type= --event_name= ``` ## usage-by-meter Get usage by meter | Flag | Type | Required | Description | | ------------------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--billing_anchor` | string | | BillingAnchor enables custom monthly billing periods for meter usage aggregation. Usage guidelines: - Only effective when WindowSize = "MONTH" - For other window sizes (DAY, HOUR, WEEK), this field is ignored - When nil, uses standard calendar months (1st to 1st) - When provided, creates custom monthly periods (e.g., 5th to 5th) Common use cases: - Subscription billing periods that don't align with calendar months - Customer-specific billing cycles (e.g., signed up on 15th) - Multi-tenant systems with different billing anchor dates Example: If BillingAnchor = "2024-03-05T14:30:45Z" and WindowSize = "MONTH": - March period: 2024-03-05 14:30:45 to 2024-04-05 14:30:45 - April period: 2024-04-05 14:30:45 to 2024-05-05 14:30:45 | | `--bucket_size` | string | | | | `--customer_id` | string | | | | `--end_time` | string | | | | `--external_customer_id` | string | | | | `--meter_id` | string | yes | | | `--start_time` | string | | | | `--timezone` | string | | | | `--window_size` | string | | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `filters` (object) ```sh theme={null} flexprice events usage-by-meter --meter_id= ``` # Features Source: https://docs.flexprice.io/docs/cli-reference/resources/features Every features command in the Flexprice CLI, with flags, required fields and examples. ## clone Clone a feature | Flag | Type | Required | Description | | --------------- | ------ | -------- | --------------------------------------------------------------------- | | `--description` | string | | Description overrides the source feature's description when provided | | `--lookup_key` | string | | LookupKey is required and must be unique across published features | | `--name` | string | | Name is required and must be different from the source feature's name | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice features clone ``` ## create Create feature | Flag | Type | Required | Description | | ----------------- | ------ | -------- | ---------------------------------------------------- | | `--description` | string | | | | `--group_id` | string | | GroupID is the id of the group to add the feature to | | `--lookup_key` | string | | | | `--meter_id` | string | | | | `--name` | string | yes | | | `--type` | string | yes | | | `--unit_plural` | string | | | | `--unit_singular` | string | | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `alert_settings` (object) * `metadata` (object) * `meter` (object) * `reporting_unit` (object) ```sh theme={null} flexprice features create --name= --type= ``` ## delete Delete feature ```sh theme={null} flexprice features delete ``` ## list Query features | Flag | Type | Required | | ----------------- | ------- | -------- | | `--end_time` | string | | | `--expand` | string | | | `--limit` | integer | | | `--lookup_key` | string | | | `--name_contains` | string | | | `--offset` | integer | | | `--order` | string | | | `--start_time` | string | | | `--status` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `feature_ids` (array) * `filters` (array) * `lookup_keys` (array) * `meter_ids` (array) * `sort` (array) ```sh theme={null} flexprice features list ``` ## update Update feature | Flag | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------ | | `--description` | string | | | | `--group_id` | string | | GroupID is the id of the group to assign the feature to. Pass empty string to clear. | | `--name` | string | | | | `--unit_plural` | string | | | | `--unit_singular` | string | | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `alert_settings` (object) * `filters` (array) * `metadata` (object) * `reporting_unit` (object) ```sh theme={null} flexprice features update ``` # Groups Source: https://docs.flexprice.io/docs/cli-reference/resources/groups Every groups command in the Flexprice CLI, with flags, required fields and examples. ## create Create group | Flag | Type | Required | | --------------- | ------ | -------- | | `--entity_type` | string | yes | | `--lookup_key` | string | yes | | `--name` | string | yes | ```sh theme={null} flexprice groups create --entity_type= --lookup_key= --name= ``` ## delete Delete group ```sh theme={null} flexprice groups delete ``` ## list Query groups | Flag | Type | Required | | --------------- | ------- | -------- | | `--end_time` | string | | | `--entity_type` | string | | | `--expand` | string | | | `--limit` | integer | | | `--lookup_key` | string | | | `--name` | string | | | `--offset` | integer | | | `--order` | string | | | `--start_time` | string | | | `--status` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `filters` (array) * `group_ids` (array) * `sort` (array) ```sh theme={null} flexprice groups list ``` ## retrieve Get group ```sh theme={null} flexprice groups retrieve ``` # Integrations Source: https://docs.flexprice.io/docs/cli-reference/resources/integrations Every integrations command in the Flexprice CLI, with flags, required fields and examples. ## config Get integration configurations ```sh theme={null} flexprice integrations config ``` ## link Link integration mapping | Flag | Type | Required | | ---------------------- | ------ | -------- | | `--entity_id` | string | yes | | `--entity_type` | string | yes | | `--provider_entity_id` | string | yes | | `--provider_type` | string | yes | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice integrations link --entity_id= --entity_type= --provider_entity_id= --provider_type= ``` ## mappings Get entity integration mappings ```sh theme={null} flexprice integrations mappings ``` ## unlink Delink integration mapping | Flag | Type | Required | | ----------------- | ------ | -------- | | `--entity_id` | string | yes | | `--entity_type` | string | yes | | `--provider_type` | string | yes | ```sh theme={null} flexprice integrations unlink --entity_id= --entity_type= --provider_type= ``` # Invoices Source: https://docs.flexprice.io/docs/cli-reference/resources/invoices Every invoices command in the Flexprice CLI, with flags, required fields and examples. ## create Create one-off invoice | Flag | Type | Required | Description | | ------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--amount_due` | string | yes | amount\_due is the total amount that needs to be paid for this invoice | | `--amount_paid` | string | | amount\_paid is the amount that has been paid towards this invoice | | `--billing_period` | string | | billing\_period is the period this invoice covers (e.g., "monthly", "yearly") | | `--billing_reason` | string | | | | `--currency` | string | yes | currency is the three-letter ISO currency code (e.g., USD, EUR) for the invoice | | `--customer_id` | string | yes | customer\_id is the unique identifier of the customer this invoice belongs to | | `--description` | string | | description is an optional text description of the invoice | | `--due_date` | string | | due\_date is the date by which payment is expected | | `--force_sync_invoice` | boolean | | force\_sync\_invoice, when true, attempts to synchronously sync this invoice to Moyasar (if enabled) before returning, instead of relying solely on the async Kafka + Temporal vendor-sync pipeline. Only honored by CreateOneOffInvoice. Best-effort: sync failures do not fail invoice creation. | | `--idempotency_key` | string | | idempotency\_key is an optional key used to prevent duplicate invoice creation | | `--invoice_number` | string | | invoice\_number is an optional human-readable identifier for the invoice | | `--invoice_pdf_url` | string | | invoice\_pdf\_url is the URL where customers can download the PDF version of this invoice | | `--invoice_status` | string | | | | `--invoice_type` | string | | | | `--issue_date` | string | | issue\_date overrides the user-facing date of the invoice. Defaults to created\_at if not provided. | | `--payment_status` | string | | | | `--period_end` | string | | period\_end is the end date of the billing period | | `--period_start` | string | | period\_start is the start date of the billing period | | `--subscription_id` | string | | subscription\_id is the optional unique identifier of the subscription associated with this invoice | | `--subtotal` | string | yes | subtotal is the amount before taxes and discounts are applied | | `--total` | string | yes | total is the total amount of the invoice including taxes and discounts | | `--total_prepaid_applied` | string | | total\_prepaid\_applied is the total amount of prepaid applied to this invoice. | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `coupons` (array) * `invoice_coupons` (array) * `line_item_coupons` (array) * `line_items` (array) * `metadata` (object) * `prepared_tax_rates` (array) * `tax_rate_overrides` (array) * `tax_rates` (array) ```sh theme={null} flexprice invoices create --amount_due= --currency= --customer_id= --subtotal= --total= ``` ## customer-summary Get customer invoice summary ```sh theme={null} flexprice invoices customer-summary ``` ## finalize Finalize invoice ```sh theme={null} flexprice invoices finalize ``` ## list Query invoices | Flag | Type | Required | Description | | ------------------------ | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--amount_due_gt` | number | | amount\_due\_gt filters invoices with a total amount due greater than the specified value Useful for finding invoices above a certain threshold or identifying high-value invoices | | `--amount_remaining_gt` | number | | amount\_remaining\_gt filters invoices with an outstanding balance greater than the specified value Useful for finding invoices that still have significant unpaid amounts | | `--billing_reason` | string | | | | `--currency` | string | | currency filters invoices by their currency (ISO 4217 code, e.g. "usd", "eur"). Matches on the invoices.currency column exactly. | | `--customer_id` | string | | customer\_id filters invoices for a specific customer using FlexPrice's internal customer ID This is the ID returned by FlexPrice when creating or retrieving customers | | `--end_time` | string | | | | `--expand` | string | | | | `--external_customer_id` | string | | external\_customer\_id filters invoices for a customer using your system's customer identifier This is the ID you provided when creating the customer in FlexPrice | | `--invoice_type` | string | | | | `--limit` | integer | | | | `--offset` | integer | | | | `--order` | string | | | | `--period_end_gte` | string | | period\_end\_gte filters invoices with period\_end >= value | | `--period_end_lte` | string | | period\_end\_lte filters invoices with period\_end \<= value | | `--period_start_gte` | string | | period\_start\_gte filters invoices with period\_start >= value | | `--period_start_lte` | string | | period\_start\_lte filters invoices with period\_start \<= value | | `--skip_line_items` | boolean | | SkipLineItems if true, will not include line items in the response | | `--start_time` | string | | | | `--status` | string | | | | `--subscription_id` | string | | subscription\_id filters invoices generated for a specific subscription Only returns invoices that were created as part of the specified subscription's billing | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `filters` (array) * `invoice_ids` (array) * `invoice_status` (array) * `payment_status` (array) * `sort` (array) * `subscription_customer_id` (array) ```sh theme={null} flexprice invoices list ``` ## payment-attempt Attempt invoice payment ```sh theme={null} flexprice invoices payment-attempt ``` ## payment-status Update invoice payment status | Flag | Type | Required | Description | | ------------------ | ------ | -------- | ----------------------------------------------- | | `--amount` | string | | amount is the optional payment amount to record | | `--payment_status` | string | yes | | ```sh theme={null} flexprice invoices payment-status --payment_status= ``` ## pdf Get invoice PDF ```sh theme={null} flexprice invoices pdf ``` ## preview Get invoice preview | Flag | Type | Required | Description | | -------------------------------- | ------- | -------- | ------------------------------------------------------------------------------------ | | `--hide_zero_charges_line_items` | boolean | | hide\_zero\_charges\_line\_items indicates whether to hide line items with zero cost | | `--period_end` | string | | period\_end is the optional end date of the period to preview | | `--period_start` | string | | period\_start is the optional start date of the period to preview | | `--subscription_id` | string | yes | subscription\_id is the unique identifier of the subscription to preview invoice for | ```sh theme={null} flexprice invoices preview --subscription_id= ``` ## recalculate Recalculate draft invoice (v2) ```sh theme={null} flexprice invoices recalculate ``` ## retrieve Get invoice ```sh theme={null} flexprice invoices retrieve ``` ## trigger-comms-webhook Trigger invoice communication webhook ```sh theme={null} flexprice invoices trigger-comms-webhook ``` ## update Update invoice | Flag | Type | Required | Description | | ------------------- | ------- | -------- | ----------------------------------------------------------------------------------------- | | `--apply_discount` | boolean | | When true, recalculates discount from existing coupon associations (draft invoices only). | | `--due_date` | string | | | | `--invoice_pdf_url` | string | | invoice\_pdf\_url is the URL where customers can download the PDF version of this invoice | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice invoices update ``` ## void Void invoice ```sh theme={null} flexprice invoices void ``` # Payments Source: https://docs.flexprice.io/docs/cli-reference/resources/payments Every payments command in the Flexprice CLI, with flags, required fields and examples. ## create Create payment | Flag | Type | Required | | ------------------------------ | ------- | -------- | | `--amount` | string | yes | | `--cancel_url` | string | | | `--currency` | string | yes | | `--destination_id` | string | yes | | `--destination_type` | string | yes | | `--idempotency_key` | string | | | `--payment_gateway` | string | | | `--payment_method_id` | string | | | `--payment_method_type` | string | yes | | `--process_payment` | boolean | | | `--save_card_and_make_default` | boolean | | | `--success_url` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `gateway_options` (object) * `metadata` (object) ```sh theme={null} flexprice payments create --amount= --currency= --destination_id= --destination_type= --payment_method_type= ``` ## delete Delete payment ```sh theme={null} flexprice payments delete ``` ## list List payments ```sh theme={null} flexprice payments list ``` ## process Process payment ```sh theme={null} flexprice payments process ``` ## retrieve Get payment ```sh theme={null} flexprice payments retrieve ``` ## update Update payment | Flag | Type | Required | | ---------------------- | ------ | -------- | | `--error_message` | string | | | `--failed_at` | string | | | `--gateway_payment_id` | string | | | `--payment_gateway` | string | | | `--payment_method_id` | string | | | `--payment_status` | string | | | `--refunded_at` | string | | | `--succeeded_at` | string | | | `--voided_at` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice payments update ``` # Plans Source: https://docs.flexprice.io/docs/cli-reference/resources/plans Every plans command in the Flexprice CLI, with flags, required fields and examples. ## clone Clone a plan | Flag | Type | Required | Description | | ----------------- | ------- | -------- | -------------------------------------------------------------------- | | `--description` | string | | Description overrides the source plan's description when provided | | `--display_order` | integer | | DisplayOrder overrides the source plan's display order when provided | | `--lookup_key` | string | | LookupKey is required and must be unique across published plans | | `--name` | string | | Name is required and must be different from the source plan's name | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice plans clone ``` ## create Create plan | Flag | Type | Required | | ----------------- | ------- | -------- | | `--description` | string | | | `--display_order` | integer | | | `--lookup_key` | string | | | `--name` | string | yes | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice plans create --name= ``` ## delete Delete plan ```sh theme={null} flexprice plans delete ``` ## list Query plans | Flag | Type | Required | | -------------- | ------- | -------- | | `--end_time` | string | | | `--expand` | string | | | `--limit` | integer | | | `--lookup_key` | string | | | `--offset` | integer | | | `--order` | string | | | `--start_time` | string | | | `--status` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `filters` (array) * `metadata` (object) * `plan_ids` (array) * `sort` (array) ```sh theme={null} flexprice plans list ``` ## retrieve Get plan ```sh theme={null} flexprice plans retrieve ``` ## sync-prices Synchronize plan prices ```sh theme={null} flexprice plans sync-prices ``` ## update Update plan | Flag | Type | Required | | ----------------- | ------- | -------- | | `--description` | string | | | `--display_order` | integer | | | `--lookup_key` | string | | | `--name` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice plans update ``` # Price Units Source: https://docs.flexprice.io/docs/cli-reference/resources/price-units Every price-units command in the Flexprice CLI, with flags, required fields and examples. ## create Create price unit | Flag | Type | Required | Description | | ------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--base_currency` | string | yes | base\_currency is the currency that the price unit is based on | | `--code` | string | yes | | | `--conversion_rate` | string | yes | ConversionRate defines the exchange rate from this price unit to the base currency. This rate is used to convert amounts in the custom price unit to the base currency for storage and billing. Conversion formula: price\_unit\_amount \* conversion\_rate = base\_currency\_amount Example: If conversion\_rate = "0.01" and base\_currency = "usd": 100 price\_unit tokens \* 0.01 = 1.00 USD Note: Rounding precision is determined by the base currency (e.g., USD uses 2 decimal places, JPY uses 0). | | `--name` | string | yes | | | `--symbol` | string | yes | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice price-units create --base_currency= --code= --conversion_rate= --name= --symbol= ``` ## delete Delete price unit ```sh theme={null} flexprice price-units delete ``` ## list List price units ```sh theme={null} flexprice price-units list ``` ## lookup Get price unit by code ```sh theme={null} flexprice price-units lookup ``` ## retrieve Get price unit ```sh theme={null} flexprice price-units retrieve ``` ## search Query price units | Flag | Type | Required | | -------------- | ------- | -------- | | `--end_time` | string | | | `--expand` | string | | | `--limit` | integer | | | `--offset` | integer | | | `--order` | string | | | `--start_time` | string | | | `--status` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `filters` (array) * `price_unit_ids` (array) * `sort` (array) ```sh theme={null} flexprice price-units search ``` ## update Update price unit | Flag | Type | Required | | -------- | ------ | -------- | | `--name` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice price-units update ``` # Prices Source: https://docs.flexprice.io/docs/cli-reference/resources/prices Every prices command in the Flexprice CLI, with flags, required fields and examples. ## create Create price | Flag | Type | Required | Description | | ------------------------ | ------- | -------- | -------------------------------------------------- | | `--amount` | string | | | | `--billing_model` | string | yes | | | `--billing_period` | string | yes | | | `--billing_period_count` | integer | | | | `--currency` | string | yes | | | `--description` | string | | | | `--display_name` | string | | | | `--end_date` | string | | | | `--entity_id` | string | yes | | | `--entity_type` | string | yes | | | `--group_id` | string | | GroupID is the id of the group to add the price to | | `--invoice_cadence` | string | yes | | | `--lookup_key` | string | | | | `--meter_id` | string | | | | `--min_quantity` | integer | | MinQuantity is the minimum quantity of the price | | `--price_unit_type` | string | yes | | | `--start_date` | string | | | | `--tier_mode` | string | | | | `--trial_period_days` | integer | | | | `--type` | string | yes | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `filter_values` (object) * `metadata` (object) * `price_unit_config` (object) * `tiers` (array) * `transform_quantity` (object) ```sh theme={null} flexprice prices create --billing_model= --billing_period= --currency= --entity_id= --entity_type= --invoice_cadence= --price_unit_type= --type= ``` ## create-bulk Create prices in bulk These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `items` (array) ```sh theme={null} flexprice prices create-bulk ``` ## delete Delete price | Flag | Type | Required | | ------------ | ------ | -------- | | `--end_date` | string | | ```sh theme={null} flexprice prices delete ``` ## list Query prices | Flag | Type | Required | | ------------------------ | ------- | -------- | | `--allow_expired_prices` | boolean | | | `--end_time` | string | | | `--entity_type` | string | | | `--expand` | string | | | `--limit` | integer | | | `--offset` | integer | | | `--order` | string | | | `--parent_price_id` | string | | | `--sort` | string | | | `--start_date_lt` | string | | | `--start_time` | string | | | `--status` | string | | | `--subscription_id` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `billing_periods` (array) * `entity_ids` (array) * `filters` (array) * `meter_ids` (array) * `plan_ids` (array) * `price_ids` (array) ```sh theme={null} flexprice prices list ``` ## lookup Get price by lookup key ```sh theme={null} flexprice prices lookup ``` ## retrieve Get price ```sh theme={null} flexprice prices retrieve ``` ## update Update price | Flag | Type | Required | Description | | --------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--amount` | string | | Amount is the new price amount that overrides the original price (optional) | | `--billing_model` | string | | | | `--description` | string | | | | `--display_name` | string | | | | `--effective_from` | string | | | | `--group_id` | string | | GroupID is the id of the group to update the price in. If not provided (nil), the group will not be changed If provided as empty string (""), the group will be removed (price will be ungrouped) If provided as a group ID, the price will be assigned to that group (must exist and be published) | | `--lookup_key` | string | | All price fields that can be updated Non-critical fields (can be updated directly) | | `--price_unit_amount` | string | | PriceUnitAmount is the price unit amount (for CUSTOM price unit type, FLAT\_FEE/PACKAGE billing models) | | `--tier_mode` | string | | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) * `price_unit_tiers` (array) * `tiers` (array) * `transform_quantity` (object) ```sh theme={null} flexprice prices update ``` # Rbac Source: https://docs.flexprice.io/docs/cli-reference/resources/rbac Every rbac command in the Flexprice CLI, with flags, required fields and examples. ## list List all RBAC roles ```sh theme={null} flexprice rbac list ``` ## retrieve Get a specific RBAC role ```sh theme={null} flexprice rbac retrieve ``` # Scheduled Tasks Source: https://docs.flexprice.io/docs/cli-reference/resources/scheduled-tasks Every scheduled-tasks command in the Flexprice CLI, with flags, required fields and examples. ## create Create scheduled task | Flag | Type | Required | | ----------------- | ------- | -------- | | `--connection_id` | string | yes | | `--enabled` | boolean | | | `--entity_type` | string | yes | | `--interval` | string | yes | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `job_config` (object) ```sh theme={null} flexprice scheduled-tasks create --connection_id= --entity_type= --interval= ``` ## delete Delete a scheduled task ```sh theme={null} flexprice scheduled-tasks delete ``` ## list List scheduled tasks ```sh theme={null} flexprice scheduled-tasks list ``` ## retrieve Get scheduled task ```sh theme={null} flexprice scheduled-tasks retrieve ``` ## run Trigger force run | Flag | Type | Required | | -------------- | ------ | -------- | | `--end_time` | string | | | `--start_time` | string | | ```sh theme={null} flexprice scheduled-tasks run ``` ## schedule-billing-period-update Schedule update billing period ```sh theme={null} flexprice scheduled-tasks schedule-billing-period-update ``` ## schedule-draft-finalization Schedule draft finalization ```sh theme={null} flexprice scheduled-tasks schedule-draft-finalization ``` ## update Update a scheduled task | Flag | Type | Required | | ----------- | ------- | -------- | | `--enabled` | boolean | yes | ```sh theme={null} flexprice scheduled-tasks update --enabled= ``` # Secrets Source: https://docs.flexprice.io/docs/cli-reference/resources/secrets Every secrets command in the Flexprice CLI, with flags, required fields and examples. ## create Create a new API key | Flag | Type | Required | | ---------------------- | ------ | -------- | | `--expires_at` | string | | | `--name` | string | yes | | `--service_account_id` | string | | | `--type` | string | yes | ```sh theme={null} flexprice secrets create --name= --type= ``` ## delete Delete an API key ```sh theme={null} flexprice secrets delete ``` ## list List API keys ```sh theme={null} flexprice secrets list ``` # Subscription Line Items Source: https://docs.flexprice.io/docs/cli-reference/resources/subscription-line-items Every subscription-line-items command in the Flexprice CLI, with flags, required fields and examples. ## create Create subscription line item | Flag | Type | Required | Description | | ------------------------------ | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | | `--commitment_amount` | number | | Commitment fields | | `--commitment_duration` | string | | | | `--commitment_overage_factor` | number | | | | `--commitment_quantity` | number | | | | `--commitment_true_up_enabled` | boolean | | | | `--commitment_type` | string | | | | `--commitment_windowed` | boolean | | | | `--display_name` | string | | | | `--end_date` | string | | | | `--price_id` | string | | PriceID references an existing price (plan, addon, or subscription-scoped). Exactly one of price\_id or price must be set. | | `--proration_behavior` | string | | | | `--quantity` | number | | | | `--start_date` | string | | | | `--subscription_phase_id` | string | | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `commitment_time_buckets` (array) * `metadata` (object) * `price` (object) ```sh theme={null} flexprice subscription-line-items create ``` ## delete Delete subscription line item | Flag | Type | Required | | ---------------------- | ------ | -------- | | `--effective_from` | string | | | `--proration_behavior` | string | | ```sh theme={null} flexprice subscription-line-items delete ``` ## list Search subscription line items | Flag | Type | Required | | ------------------------ | ------- | -------- | | `--active_filter` | boolean | | | `--current_period_start` | string | | | `--end_time` | string | | | `--entity_type` | string | | | `--expand` | string | | | `--limit` | integer | | | `--offset` | integer | | | `--order` | string | | | `--start_time` | string | | | `--status` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `addon_association_ids` (array) * `billing_periods` (array) * `currencies` (array) * `customer_ids` (array) * `entity_ids` (array) * `filters` (array) * `meter_ids` (array) * `price_ids` (array) * `sort` (array) * `subscription_ids` (array) * `subscription_line_item_ids` (array) ```sh theme={null} flexprice subscription-line-items list ``` ## update Update subscription line item | Flag | Type | Required | Description | | ------------------------------ | ------- | -------- | --------------------------------------------------------------------------- | | `--amount` | string | | Amount is the new price amount that overrides the original price | | `--billing_model` | string | | | | `--commitment_amount` | number | | Commitment fields | | `--commitment_duration` | string | | | | `--commitment_overage_factor` | number | | | | `--commitment_quantity` | number | | | | `--commitment_true_up_enabled` | boolean | | | | `--commitment_type` | string | | | | `--commitment_windowed` | boolean | | | | `--effective_from` | string | | EffectiveFrom for the existing line item (if not provided, defaults to now) | | `--tier_mode` | string | | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `commitment_time_buckets` (array) * `metadata` (object) * `tiers` (array) * `transform_quantity` (object) ```sh theme={null} flexprice subscription-line-items update ``` # Subscription Schedules Source: https://docs.flexprice.io/docs/cli-reference/resources/subscription-schedules Every subscription-schedules command in the Flexprice CLI, with flags, required fields and examples. ## cancel Cancel subscription schedule | Flag | Type | Required | Description | | ------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------- | | `--schedule_id` | string | | schedule\_id is the ID of the schedule to cancel (optional if subscription\_id and schedule\_type are provided) | | `--schedule_type` | string | | | | `--subscription_id` | string | | subscription\_id is the ID of the subscription (required if schedule\_id is not provided) | ```sh theme={null} flexprice subscription-schedules cancel ``` ## list List all subscription schedules ```sh theme={null} flexprice subscription-schedules list ``` ## list-for-subscription List subscription schedules ```sh theme={null} flexprice subscription-schedules list-for-subscription ``` ## retrieve Get subscription schedule ```sh theme={null} flexprice subscription-schedules retrieve ``` # Subscriptions Source: https://docs.flexprice.io/docs/cli-reference/resources/subscriptions Every subscriptions command in the Flexprice CLI, with flags, required fields and examples. ## activate Activate draft subscription | Flag | Type | Required | Description | | -------------- | ------ | -------- | ---------------------------------------------------------------------- | | `--start_date` | string | yes | start\_date is the new start date for the subscription when activating | ```sh theme={null} flexprice subscriptions activate --start_date= ``` ## add-addon Add addon to subscription | Flag | Type | Required | | ---------------------- | ------ | -------- | | `--addon_id` | string | yes | | `--cadence` | string | | | `--proration_behavior` | string | | | `--start_date` | string | | | `--subscription_id` | string | yes | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `checkout` (object) * `line_item_commitments` (object) * `metadata` (object) * `override_line_items` (array) ```sh theme={null} flexprice subscriptions add-addon --addon_id= --subscription_id= ``` ## addon-associations Get active addon associations ```sh theme={null} flexprice subscriptions addon-associations ``` ## cancel Cancel subscription | Flag | Type | Required | Description | | ------------------------------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--cancel_at` | string | | CancelAt is the exact date/time when the subscription should be cancelled. Required for cancellation\_type "scheduled\_date"; optional for "immediate" (past dates only — backdated cancellation). For "scheduled\_date", accepts both future dates (deferred cancellation) and past dates (backdated cancellation). For "immediate", accepts past/current dates only; use "scheduled\_date" for future dates. | | `--cancel_immediately_inovice_policy` | string | | | | `--cancellation_type` | string | yes | | | `--proration_behavior` | string | | | | `--reason` | string | | Reason for cancellation (for audit and business intelligence) | ```sh theme={null} flexprice subscriptions cancel --cancellation_type= ``` ## change-execute Execute subscription plan change | Flag | Type | Required | Description | | ------------------------ | ------- | -------- | --------------------------------------------------------------------------- | | `--billing_cadence` | string | yes | | | `--billing_cycle` | string | yes | | | `--billing_period` | string | yes | | | `--billing_period_count` | integer | | billing\_period\_count is the billing period count for the new subscription | | `--change_at` | string | | | | `--proration_behavior` | string | yes | | | `--target_plan_id` | string | yes | target\_plan\_id is the ID of the new plan to change to (required) | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice subscriptions change-execute --billing_cadence= --billing_cycle= --billing_period= --proration_behavior= --target_plan_id= ``` ## change-preview Preview subscription plan change | Flag | Type | Required | Description | | ------------------------ | ------- | -------- | --------------------------------------------------------------------------- | | `--billing_cadence` | string | yes | | | `--billing_cycle` | string | yes | | | `--billing_period` | string | yes | | | `--billing_period_count` | integer | | billing\_period\_count is the billing period count for the new subscription | | `--change_at` | string | | | | `--proration_behavior` | string | yes | | | `--target_plan_id` | string | yes | target\_plan\_id is the ID of the new plan to change to (required) | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice subscriptions change-preview --billing_cadence= --billing_cycle= --billing_period= --proration_behavior= --target_plan_id= ``` ## create Create subscription | Flag | Type | Required | Description | | ----------------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--auto_invoice_threshold` | string | | AutoInvoiceThreshold triggers a mid-period invoice when usage (in subscription currency) exceeds this amount. Standalone subscriptions only; all plan prices must be usage-based. Immutable after creation. | | `--billing_anchor` | string | | BillingAnchor overrides the derived anchor for anniversary billing. For monthly billing, the day-of-month defines cycle boundaries (shorter first period if start is before that day). | | `--billing_cycle` | string | | | | `--billing_period` | string | yes | | | `--billing_period_count` | integer | | | | `--collection_method` | string | | | | `--commitment_amount` | string | | | | `--commitment_duration` | string | | | | `--currency` | string | yes | | | `--customer_id` | string | | CustomerID takes priority over ExternalCustomerID when both are provided. | | `--enable_true_up` | boolean | | | | `--end_date` | string | | | | `--external_customer_id` | string | | | | `--gateway_payment_method_id` | string | | | | `--lookup_key` | string | | | | `--overage_factor` | string | | | | `--payment_behavior` | string | | | | `--payment_terms` | string | | | | `--plan_id` | string | yes | | | `--proration_behavior` | string | | | | `--start_date` | string | | | | `--subscription_status` | string | | | | `--timezone` | string | | | | `--trial_period_days` | integer | | TrialPeriodDays: nil = inherit from plan prices, 0 = no trial, >0 = override in days. | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `addons` (array) * `checkout` (object) * `coupons` (array) * `credit_grants` (array) * `inheritance` (object) * `line_item_commitments` (object) * `line_item_coupons` (object) * `line_items` (array) * `metadata` (object) * `override_entitlements` (array) * `override_line_items` (array) * `phases` (array) * `subscription_coupons` (array) * `tax_rate_overrides` (array) ```sh theme={null} flexprice subscriptions create --billing_period= --currency= --plan_id= ``` ## entitlements Get subscription entitlements ```sh theme={null} flexprice subscriptions entitlements ``` ## execute-subscription-plan-change-v2 Execute a plan change (v2, swap in place) | Flag | Type | Required | | ---------------------- | ------ | -------- | | `--idempotency_key` | string | | | `--proration_behavior` | string | yes | | `--target_plan_id` | string | yes | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `entity_policies` (object) * `metadata` (object) ```sh theme={null} flexprice subscriptions execute-subscription-plan-change-v2 --proration_behavior= --target_plan_id= ``` ## list Query subscriptions | Flag | Type | Required | Description | | ----------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--active_at` | string | | ActiveAt filters subscriptions that are active at the given time | | `--customer_id` | string | | CustomerID filters by customer ID | | `--effective_date_for_update` | string | | EffectiveDateForUpdate selects subscriptions that need a billing-period pass on or before this time: current\_period\_end \<= date OR (cancel\_at IS NOT NULL AND cancel\_at \<= date). When nil, period/cancel cutoff logic is not applied by this field (see TimeRangeFilter for legacy period-end filtering). | | `--end_time` | string | | | | `--expand` | string | | | | `--external_customer_id` | string | | ExternalCustomerID filters by external customer ID | | `--limit` | integer | | | | `--offset` | integer | | | | `--order` | string | | | | `--plan_id` | string | | PlanID filters by plan ID | | `--start_time` | string | | | | `--status` | string | | | | `--trial_end_due_lte` | string | | TrialEndDueLTE, when set, restricts to subscriptions with trial\_end not nil and trial\_end \<= trial\_end\_due\_lte. Use with subscription\_status trialing for trial-end cron processing. | | `--with_coupon_associations` | boolean | | WithCouponAssociations eager-loads coupon associations and their coupons. Kept separate from WithLineItems because the coupon\_associations table has no index leading with subscription\_id, so Ent's edge load degrades to a full table scan. Only set it when the response actually surfaces the associations; the service layer back-fills it from expand="coupon\_associations". | | `--with_line_items` | boolean | | WithLineItems includes line items in the response. Deprecated: use expand="subscription\_line\_items" instead. Retained for backwards compatibility and for internal callers that need to force-disable line item loading (set to false). The service layer ORs this with the expand check before invoking the repository. | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `billing_cadence` (array) * `billing_period` (array) * `customer_ids` (array) * `filters` (array) * `invoicing_customer_ids` (array) * `parent_subscription_ids` (array) * `sort` (array) * `subscription_ids` (array) * `subscription_status` (array) * `subscription_type` (array) ```sh theme={null} flexprice subscriptions list ``` ## modify-execute Execute subscription modification | Flag | Type | Required | | -------- | ------ | -------- | | `--type` | string | yes | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `checkout` (object) * `coupon_params` (object) * `grouped_invoicing_params` (object) * `inheritance_params` (object) * `quantity_change_params` (object) * `tax_params` (object) * `trial_end_params` (object) ```sh theme={null} flexprice subscriptions modify-execute --type= ``` ## modify-preview Preview subscription modification | Flag | Type | Required | | -------- | ------ | -------- | | `--type` | string | yes | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `checkout` (object) * `coupon_params` (object) * `grouped_invoicing_params` (object) * `inheritance_params` (object) * `quantity_change_params` (object) * `tax_params` (object) * `trial_end_params` (object) ```sh theme={null} flexprice subscriptions modify-preview --type= ``` ## preview-subscription-plan-change-v2 Preview a plan change (v2, swap in place) | Flag | Type | Required | | ---------------------- | ------ | -------- | | `--idempotency_key` | string | | | `--proration_behavior` | string | yes | | `--target_plan_id` | string | yes | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `entity_policies` (object) * `metadata` (object) ```sh theme={null} flexprice subscriptions preview-subscription-plan-change-v2 --proration_behavior= --target_plan_id= ``` ## remove-addon Remove addon from subscription | Flag | Type | Required | Description | | ------------------------ | ------ | -------- | --------------------------------------------------------------------------------------------------------- | | `--addon_association_id` | string | yes | | | `--effective_date` | string | | EffectiveDate defaults to period end when nil; mid-period with create\_prorations issues a wallet credit. | | `--proration_behavior` | string | | | | `--reason` | string | | | ```sh theme={null} flexprice subscriptions remove-addon --addon_association_id= ``` ## retrieve Get subscription ```sh theme={null} flexprice subscriptions retrieve ``` ## retrieve-v2 Get subscription (V2) ```sh theme={null} flexprice subscriptions retrieve-v2 ``` ## upcoming-grants Get upcoming credit grant applications ```sh theme={null} flexprice subscriptions upcoming-grants ``` ## update Update subscription | Flag | Type | Required | Description | | -------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------- | | `--cancel_at` | string | | | | `--cancel_at_period_end` | boolean | | | | `--parent_subscription_id` | string | | ParentSubscriptionID sets or clears the parent subscription. Omit to leave unchanged; send "" to clear. | | `--status` | string | | | ```sh theme={null} flexprice subscriptions update ``` ## usage Get usage by subscription | Flag | Type | Required | | ------------------- | ------- | -------- | | `--end_time` | string | | | `--lifetime_usage` | boolean | | | `--start_time` | string | | | `--subscription_id` | string | yes | ```sh theme={null} flexprice subscriptions usage --subscription_id= ``` # Tasks Source: https://docs.flexprice.io/docs/cli-reference/resources/tasks Every tasks command in the Flexprice CLI, with flags, required fields and examples. ## download Download task export file ```sh theme={null} flexprice tasks download ``` ## list List tasks ```sh theme={null} flexprice tasks list ``` ## result Get task processing result ```sh theme={null} flexprice tasks result ``` ## retrieve Get a task ```sh theme={null} flexprice tasks retrieve ``` ## update-status Update task status | Flag | Type | Required | | --------------- | ------ | -------- | | `--task_status` | string | yes | ```sh theme={null} flexprice tasks update-status --task_status= ``` # Tax Associations Source: https://docs.flexprice.io/docs/cli-reference/resources/tax-associations Every tax-associations command in the Flexprice CLI, with flags, required fields and examples. ## create Create Tax Association | Flag | Type | Required | Description | | ------------------------ | ------- | -------- | ------------------------------------------------------------------------------------------- | | `--auto_apply` | boolean | | | | `--currency` | string | | | | `--end_date` | string | | EndDate sets when this association expires. Must be after StartDate when both are provided. | | `--entity_id` | string | | | | `--entity_type` | string | | | | `--external_customer_id` | string | | | | `--priority` | integer | | | | `--start_date` | string | | StartDate sets when this association becomes active. Defaults to now if omitted. | | `--tax_rate_code` | string | yes | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice tax-associations create --tax_rate_code= ``` ## delete Delete tax association ```sh theme={null} flexprice tax-associations delete ``` ## list List tax associations ```sh theme={null} flexprice tax-associations list ``` ## retrieve Get Tax Association ```sh theme={null} flexprice tax-associations retrieve ``` ## update Update tax association | Flag | Type | Required | | -------------- | ------- | -------- | | `--auto_apply` | boolean | | | `--priority` | integer | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice tax-associations update ``` # Tax Rates Source: https://docs.flexprice.io/docs/cli-reference/resources/tax-rates Every tax-rates command in the Flexprice CLI, with flags, required fields and examples. ## create Create a tax rate | Flag | Type | Required | Description | | -------------------- | ------ | -------- | -------------------------------------------------------------------------------------- | | `--code` | string | yes | code is the unique alphanumeric case sensitive identifier for the tax rate (required) | | `--description` | string | | description is an optional text description providing details about the tax rate | | `--fixed_value` | string | | fixed\_value is the fixed monetary amount when tax\_rate\_type is "fixed" | | `--name` | string | yes | name is the human-readable name for the tax rate (required) | | `--percentage_value` | string | | percentage\_value is the percentage value (0-100) when tax\_rate\_type is "percentage" | | `--scope` | string | | | | `--tax_rate_type` | string | | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice tax-rates create --code= --name= ``` ## delete Delete a tax rate ```sh theme={null} flexprice tax-rates delete ``` ## list Get tax rates ```sh theme={null} flexprice tax-rates list ``` ## retrieve Get a tax rate ```sh theme={null} flexprice tax-rates retrieve ``` ## update Update a tax rate | Flag | Type | Required | Description | | ------------------- | ------ | -------- | ------------------------------------------------------------------- | | `--code` | string | | code is the updated unique alphanumeric identifier for the tax rate | | `--description` | string | | description is the updated text description for the tax rate | | `--name` | string | | name is the updated human-readable name for the tax rate | | `--tax_rate_status` | string | | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice tax-rates update ``` # Tenants Source: https://docs.flexprice.io/docs/cli-reference/resources/tenants Every tenants command in the Flexprice CLI, with flags, required fields and examples. ## billing-usage Get billing usage for the current tenant ```sh theme={null} flexprice tenants billing-usage ``` ## retrieve Get tenant by ID ```sh theme={null} flexprice tenants retrieve ``` ## update Update a tenant | Flag | Type | Required | | -------- | ------ | -------- | | `--name` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `billing_details` (object) * `metadata` (object) ```sh theme={null} flexprice tenants update ``` # Users Source: https://docs.flexprice.io/docs/cli-reference/resources/users Every users command in the Flexprice CLI, with flags, required fields and examples. ## create Create user or service account | Flag | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------- | | `--email` | string | | Required when type is "user" | | `--name` | string | | Display name; optional for service accounts | | `--type` | string | yes | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `roles` (array) ```sh theme={null} flexprice users create --type= ``` ## delete Delete service account ```sh theme={null} flexprice users delete ``` ## list Query users | Flag | Type | Required | | -------------- | ------- | -------- | | `--end_time` | string | | | `--expand` | string | | | `--limit` | integer | | | `--offset` | integer | | | `--order` | string | | | `--start_time` | string | | | `--status` | string | | | `--type` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `filters` (array) * `roles` (array) * `sort` (array) * `user_ids` (array) ```sh theme={null} flexprice users list ``` ## me Get current user ```sh theme={null} flexprice users me ``` ## update Update service account | Flag | Type | Required | | -------- | ------ | -------- | | `--name` | string | yes | ```sh theme={null} flexprice users update --name= ``` ## update-me Update current user | Flag | Type | Required | | -------- | ------ | -------- | | `--name` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `metadata` (object) ```sh theme={null} flexprice users update-me ``` ## update-user-roles Update user roles These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `roles` (array) ```sh theme={null} flexprice users update-user-roles ``` # Wallets Source: https://docs.flexprice.io/docs/cli-reference/resources/wallets Every wallets command in the Flexprice CLI, with flags, required fields and examples. ## balance Get wallet balance ```sh theme={null} flexprice wallets balance ``` ## by-customer Get wallets by customer ID ```sh theme={null} flexprice wallets by-customer ``` ## create Create a new wallet | Flag | Type | Required | Description | | --------------------------------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--conversion_rate` | string | | amount in the currency = number of credits \* conversion\_rate ex if conversion\_rate is 1, then 1 USD = 1 credit ex if conversion\_rate is 2, then 1 USD = 0.5 credits ex if conversion\_rate is 0.5, then 1 USD = 2 credits | | `--currency` | string | yes | | | `--customer_id` | string | | | | `--description` | string | | | | `--external_customer_id` | string | | external\_customer\_id is the customer id in the external system | | `--initial_credits_expiry_date_utc` | string | | initial\_credits\_expiry\_date\_utc is the expiry date in UTC timezone (optional to set nil means no expiry) ex 2025-01-01 00:00:00 UTC | | `--initial_credits_to_load` | string | | initial\_credits\_to\_load is the number of credits to load to the wallet if not provided, the wallet will be created with 0 balance NOTE: this is not the amount in the currency, but the number of credits | | `--initial_credits_to_load_expiry_date` | integer | | initial\_credits\_to\_load\_expiry\_date YYYYMMDD format in UTC timezone (optional to set nil means no expiry) for ex 20250101 means the credits will expire on 2025-01-01 00:00:00 UTC hence they will be available for use until 2024-12-31 23:59:59 UTC | | `--price_unit` | string | | price\_unit is the code of the price unit to use for wallet creation If provided, the price unit will be used to set the currency and conversion rate of the wallet: - currency: set to price unit's base\_currency - conversion\_rate: set to price unit's conversion\_rate | | `--topup_conversion_rate` | string | | topup\_conversion\_rate is the conversion rate for the topup to the currency ex if topup\_conversion\_rate is 1, then 1 USD = 1 credit ex if topup\_conversion\_rate is 2, then 1 USD = 0.5 credits ex if topup\_conversion\_rate is 0.5, then 1 USD = 2 credits | | `--wallet_type` | string | | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `alert_settings` (object) * `auto_topup` (object) * `metadata` (object) ```sh theme={null} flexprice wallets create --currency= ``` ## list Query wallets | Flag | Type | Required | | ----------------- | ------- | -------- | | `--alert_enabled` | boolean | | | `--expand` | string | | | `--limit` | integer | | | `--offset` | integer | | | `--order` | string | | | `--sort` | string | | | `--status` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `wallet_ids` (array) ```sh theme={null} flexprice wallets list ``` ## list-for-customer Get Customer Wallets ```sh theme={null} flexprice wallets list-for-customer ``` ## retrieve Get wallet ```sh theme={null} flexprice wallets retrieve ``` ## search-transactions Query wallet transactions | Flag | Type | Required | | ------------------------ | ------- | -------- | | `--created_by` | string | | | `--credits_available_gt` | number | | | `--end_time` | string | | | `--expand` | string | | | `--expiry_date_after` | string | | | `--expiry_date_before` | string | | | `--id` | string | | | `--limit` | integer | | | `--offset` | integer | | | `--order` | string | | | `--priority` | integer | | | `--reference_id` | string | | | `--reference_type` | string | | | `--start_time` | string | | | `--status` | string | | | `--transaction_reason` | string | | | `--transaction_status` | string | | | `--type` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `filters` (array) * `sort` (array) ```sh theme={null} flexprice wallets search-transactions ``` ## terminate Terminate a wallet ```sh theme={null} flexprice wallets terminate ``` ## top-up Top up wallet | Flag | Type | Required | Description | | --------------------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--amount` | string | | amount is the amount in the currency of the wallet to be added NOTE: this is not the number of credits to add, but the amount in the currency amount = credits\_to\_add \* conversion\_rate if both amount and credits\_to\_add are provided, amount will be ignored ex if the wallet has a conversion\_rate of 2 then adding an amount of 10 USD in the wallet wil add 5 credits in the wallet | | `--bonus_credits_expiry_date_utc` | string | | bonus\_credits\_expiry\_date\_utc is the expiry (UTC, full precision) applied to the bonus credits transaction. Independent of expiry\_date\_utc, which governs the purchase credits. Only honored when bonus credits are actually granted (explicit BonusCreditsToAdd or slab). | | `--bonus_credits_to_add` | string | | bonus\_credits\_to\_add is an explicit override for the bonus credits granted alongside this purchase. When nil/omitted, the bonus is resolved from the tenant's bonus\_credits\_topup\_config slabs (if enabled). When set, it must be greater than 0 and is used as-is, skipping slab resolution. To grant no bonus, omit this field entirely. | | `--credits_to_add` | string | | credits\_to\_add is the number of credits to add to the wallet | | `--description` | string | | description to add any specific details about the transaction | | `--expiry_date_utc` | string | | expiry\_date\_utc is the expiry date in UTC timezone ex 2025-01-01 00:00:00 UTC | | `--force_sync_invoice` | boolean | | | | `--idempotency_key` | string | | idempotency\_key is a unique key for the transaction | | `--priority` | integer | | priority is the priority of the transaction lower number means higher priority default is nil which means no priority at all | | `--transaction_reason` | string | yes | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `checkout` (object) * `metadata` (object) ```sh theme={null} flexprice wallets top-up --transaction_reason= ``` ## transactions Get wallet transactions ```sh theme={null} flexprice wallets transactions ``` ## update Update a wallet | Flag | Type | Required | | --------------- | ------ | -------- | | `--description` | string | | | `--name` | string | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `alert_settings` (object) * `auto_topup` (object) * `config` (object) * `metadata` (object) ```sh theme={null} flexprice wallets update ``` # Workflows Source: https://docs.flexprice.io/docs/cli-reference/resources/workflows Every workflows command in the Flexprice CLI, with flags, required fields and examples. ## list Query workflows | Flag | Type | Required | Description | | ------------------- | ------- | -------- | -------------------------------- | | `--end_time` | string | | | | `--entity` | string | | e.g. plan, invoice, subscription | | `--entity_id` | string | | e.g. plan\_01ABC123 | | `--expand` | string | | | | `--limit` | integer | | | | `--offset` | integer | | | | `--order` | string | | | | `--start_time` | string | | | | `--status` | string | | | | `--task_queue` | string | | | | `--workflow_id` | string | | Workflow-specific filters | | `--workflow_status` | string | | e.g. Running, Completed, Failed | | `--workflow_type` | string | | | These fields are nested and cannot be set with flags. Use `--edit` to fill in a pre-built request body, or `--data @file.json`. * `filters` (array) * `sort` (array) ```sh theme={null} flexprice workflows list ``` # Authentication Source: https://docs.flexprice.io/docs/cli/authentication How API keys, environments, and profiles fit together in the Flexprice CLI ## One key, one environment An API key belongs to exactly one environment. There is no `--environment` flag, because the key itself already determines it. Switching environments means switching **profiles**. A profile bundles three things: a stored API key, the region it belongs to, and an optional label you choose. The API does not report which environment a key belongs to, so the CLI cannot tell you either. Label your profiles yourself with `--label`, and check with `flexprice whoami` before doing anything destructive. ## Getting set up `flexprice init` is the guided path: it prompts for a region and key, verifies the key against the API, and stores it as the `default` profile. ```sh theme={null} flexprice init ``` To add more environments later, use `login`: ```sh theme={null} flexprice login --label "production" ``` ## Working with multiple profiles ```sh theme={null} flexprice config list # every stored profile flexprice -p production customers list # use one for a single command flexprice config use production # change the default flexprice logout -p production # remove a profile and its key ``` `flexprice env list` shows every environment in your tenant, which is useful for orientation — but remember it cannot tell you which one your active key maps to. ## Credential precedence When more than one source could supply a key, the CLI resolves in this order and stops at the first match: Highest precedence. Intended for CI and one-off commands. Overrides any stored profile. The usual choice for CI, since it keeps the key out of your shell history and process list. Either the one named by `-p/--profile`, or your default profile. The API base URL resolves separately, on the same first-match basis: `--base-url`, then `--region`, then the region stored on the active profile. A key on its own does not identify a region. If you pass `--api-key` without a stored profile, you must also pass `--region` or `--base-url` — otherwise the CLI stops and tells you so rather than guessing. ## Where keys are stored Keys go in your operating system's keychain when one is available. Where it is not — inside a container, or a CI runner — the CLI falls back to an encrypted file at `~/.flexprice/keys`. `flexprice whoami` reports which backend is in use: ```sh theme={null} flexprice whoami ``` ``` Profile: default Label: production Region: us Base URL: https://us.api.flexprice.io/v1 Key backend: encrypted file (~/.flexprice/keys) Key: sk_test_…4b ``` Non-secret settings live in `~/.flexprice/config.toml`. **Keys are never written there.** ## In CI Set the key through the environment and pin the region explicitly: ```sh theme={null} export FLEXPRICE_API_KEY=sk_... flexprice --region us customers list --output json ``` No `init` or `login` step is needed — the environment variable takes precedence over stored profiles, so nothing has to be configured on the runner. ## Next steps Every flag the CLI accepts, grouped by purpose. Exit codes and non-interactive behaviour. # Global flags Source: https://docs.flexprice.io/docs/cli/global-flags Flags accepted by every Flexprice CLI command, grouped by what they control Every command accepts the flags below. They can appear anywhere in the invocation. ```sh theme={null} flexprice [global flags] [command flags] ``` ## Targeting Which account, environment, and endpoint the command talks to. | Flag | Description | | ---------------------- | ----------------------------------------------------------------------------------------------- | | `-p, --profile ` | Use a specific stored profile for this command, instead of the default. | | `--region ` | Region to target, e.g. `us` or `in`. | | `--base-url ` | Override the API base URL outright. Takes precedence over `--region`. | | `--api-key ` | Use this key instead of a stored one. Intended for CI — prefer `flexprice login` interactively. | See [Authentication](/docs/cli/authentication) for how these resolve against each other and against stored profiles. A key passed with `--api-key` does not identify a region on its own. Pair it with `--region` or `--base-url`, or the command stops and tells you rather than guessing. ## Output How results are formatted and how much commentary you get. | Flag | Description | | ------------------- | --------------------------------------------------------------------------- | | `--output ` | `table` (default), `json`, or `yaml`. Use `json` for anything machine-read. | | `--columns ` | Restrict table output to specific columns. | | `--no-color` | Disable colour. Status icons (`✓ ✗ ⚠`) remain. | | `--quiet` | Suppress progress and commentary. Results and errors still print. | Colour is also disabled automatically when `NO_COLOR` is set, when `TERM=dumb`, and whenever stderr is not a terminal — so CI logs stay clean without passing anything. ## Pagination | Flag | Description | | ------------- | ------------------------------------------------ | | `--limit ` | Maximum records to return. Defaults to `20`. | | `--all` | Page through every record. Prints the last page. | For bulk export, combine `--output json` with `--limit` rather than reaching for `--all` — `--all` prints only the final page, which is rarely what an export wants. ## Automation | Flag | Description | | ------------ | ------------------------------------------------------------------- | | `--no-input` | Never prompt. Fails with a message naming the flag to pass instead. | | `--debug` | Dump requests and responses. Secrets are redacted. | `--no-input` is what you want in a non-interactive environment where a hung prompt would stall the job: instead of waiting for an answer nobody can give, the command fails immediately and tells you which flag would have supplied it. ## Informational | Flag | Description | | --------------- | -------------------------------------------------------------------------------- | | `-h, --help` | Help for the command. Works at every level: `flexprice customers create --help`. | | `-v, --version` | Print the CLI version and the embedded spec build. | ## Not global: `--force` `--force` skips the confirmation prompt on destructive actions, but it is declared **per command**, not on the root. It has to come after the resource and action: ```sh theme={null} flexprice customers delete cust_123 --force # correct flexprice --force customers delete cust_123 # wrong ``` Passing it earlier makes the CLI read the resource name as the flag's value and report a confusing error about the action instead. ## Next steps Exit codes and the stdout/stderr contract. Profiles, regions, and credential precedence. # Output & scripting Source: https://docs.flexprice.io/docs/cli/output-and-scripting The machine-facing contract: stdout/stderr separation, stable exit codes, and non-interactive behaviour The CLI is designed to be scripted against. Three guarantees make that safe: data and commentary never mix, exit codes are stable, and nothing destructive happens without either a confirmation or an explicit `--force`. ## Data goes to stdout, everything else to stderr Results go to **stdout**. Spinners, footers, receipts, warnings, and empty-state hints go to **stderr**. Redirecting stdout therefore gives you clean data with no commentary in it. ```sh theme={null} flexprice customers list --output json > customers.json ``` That file contains only JSON. The progress spinner and the `profile: … · region: …` footer went to your terminal, not into the file. This is why the CLI can print a friendly footer on every table without breaking a single pipeline. If you have ever had to `grep -v` a tool's own status output back out of your data, this is the problem being avoided. Piping works as you would expect: ```sh theme={null} flexprice customers list --output json | jq -r '.items[].email' ``` ## Exit codes Stable and safe to branch on: | Code | Meaning | | ----- | ---------------------- | | `0` | Success | | `1` | Generic failure | | `2` | Usage error | | `3` | Authentication failure | | `4` | Not found | | `5` | Rate limited | | `130` | Interrupted (Ctrl-C) | ```sh theme={null} if ! flexprice customers retrieve "$ID" --output json > customer.json; then case $? in 3) echo "check FLEXPRICE_API_KEY" ;; 4) echo "no such customer: $ID" ;; 5) echo "rate limited, backing off" ;; esac fi ``` ## Running non-interactively The CLI notices whether a human is watching and adapts, without being told: Animation is suppressed when stderr is not a terminal, under `TERM=dumb`, and under `--quiet`. Disabled when `NO_COLOR` is set, under `TERM=dumb`, and with `--no-color`. `--no-input` makes any prompt a hard failure that names the flag to pass instead. In practice CI logs stay clean without passing any flags at all. ## Destructive commands `delete`, `void`, `cancel`, `terminate`, `archive`, and `finalize` confirm before acting. In a script, pass `--force`: ```sh theme={null} flexprice customers delete cust_123 --force ``` Without `--force`, a non-interactive run **fails rather than proceeding**. The CLI will not destroy something on the grounds that nobody was available to be asked. Note that `--force` is a per-command flag, so it goes after the resource and action — see [Global flags](/docs/cli/global-flags). ## A worked CI example ```sh theme={null} #!/usr/bin/env bash set -euo pipefail export FLEXPRICE_API_KEY="$CI_FLEXPRICE_KEY" flexprice --region us --no-input --quiet \ customers create --external_id="$TENANT" --email="$EMAIL" \ --output json > created.json CUSTOMER_ID=$(jq -r '.id' created.json) echo "created $CUSTOMER_ID" ``` `--no-input` guarantees the job can never hang on a prompt, `--quiet` keeps the log readable, `set -e` plus stable exit codes means a failure stops the build, and the key comes from the environment so it never appears in the process list. ## Next steps Every flag, grouped by what it controls. Credential precedence and CI setup. # Overview Source: https://docs.flexprice.io/docs/cli/overview Drive usage-based billing from your terminal: send events, inspect metering, and reach every Flexprice API endpoint The **Flexprice CLI** puts your billing account behind a command prompt. Send usage events and see how they metered, create and inspect customers, subscriptions and invoices, and reach any API endpoint — without leaving the terminal or writing a script against the REST API first. It is built for two audiences at once: a human exploring the API interactively, and a CI job that needs stable, parseable output. Every command adapts to which one is running it. Ingest events and inspect exactly how they metered. Customers, subscriptions, invoices, plans, wallets, and more. Stable JSON output and documented exit codes. Raw `get`/`post`/`delete` for anything without a named command. ## Install On macOS or Linux, install the latest release: ```sh theme={null} curl -fsSL https://cli.flexprice.io | sh ``` The binary is installed to `/usr/local/bin` if writable, or `~/.local/bin` otherwise. Make sure the install directory is on your `PATH`. Grab the archive for your platform from the [latest release](https://github.com/flexprice/cli/releases/latest), extract it, and put the `flexprice` binary somewhere on your `PATH`. Builds are published for macOS, Linux, and Windows on both `amd64` and `arm64`. ```sh theme={null} tar -xzf flexprice_*_darwin_arm64.tar.gz sudo mv flexprice /usr/local/bin/ ``` If you have a Go toolchain: ```sh theme={null} go install github.com/flexprice/cli/cmd/flexprice@latest ``` `go install` places the binary in `$(go env GOPATH)/bin` — commonly `~/go/bin`. If that directory is not on your `PATH`, the install succeeds silently but `flexprice` comes back as **command not found**. Check and fix it: ```sh theme={null} echo $PATH | tr ':' '\n' | grep "$(go env GOPATH)/bin" # prints nothing if missing echo 'export PATH="$(go env GOPATH)/bin:$PATH"' >> ~/.zshrc # or ~/.bashrc source ~/.zshrc ``` Confirm the install: ```sh theme={null} flexprice version ``` ## Upgrading On macOS or Linux, update to the latest release: ```sh theme={null} flexprice update ``` On Windows, download the [latest release](https://github.com/flexprice/cli/releases/latest). If you installed with Go, run `go install github.com/flexprice/cli/cmd/flexprice@latest` to upgrade. Updating preserves your configuration and stored keys. `go install ...@latest` resolves through Go's module proxy, which can take a few minutes to index a brand-new release. To install a specific version immediately, pin it: `go install github.com/flexprice/cli/cmd/flexprice@v1.0.1`. ## Next steps Authenticate and run your first command. How keys, environments, and profiles fit together. Flags every command accepts. Exit codes and the machine-facing contract. # Quickstart Source: https://docs.flexprice.io/docs/cli/quickstart Authenticate the Flexprice CLI and run your first commands in under five minutes This walks through installing the CLI, connecting it to your account, and confirming it points where you think it does. On macOS or Linux: ```sh theme={null} curl -fsSL https://cli.flexprice.io | sh ``` For Windows, Go installation, or prebuilt binaries, see [installation options](/docs/cli/overview#install). `flexprice init` walks you through picking a data region and pasting an API key, verifies the key against the API, and stores it in your OS keychain. ```sh theme={null} flexprice init ``` ``` ███████╗██╗ ███████╗██╗ ██╗██████╗ ██████╗ ██╗ ██████╗███████╗ ██╔════╝██║ ██╔════╝╚██╗██╔╝██╔══██╗██╔══██╗██║██╔════╝██╔════╝ █████╗ ██║ █████╗ ╚███╔╝ ██████╔╝██████╔╝██║██║ █████╗ ██╔══╝ ██║ ██╔══╝ ██╔██╗ ██╔═══╝ ██╔══██╗██║██║ ██╔══╝ ██║ ███████╗███████╗██╔╝ ██╗██║ ██║ ██║██║╚██████╗███████╗ ╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═════╝╚══════╝ Usage-based billing from your terminal Welcome to Flexprice — let's get you set up. ? Data region > us https://us.api.flexprice.io/v1 in https://api.cloud.flexprice.io/v1 API key: •••••••••••••••••••• ⠹ Verifying your key… ✓ Verified — stored as profile "default" ``` The region menu is arrow-key driven. To skip both prompts entirely — in CI, for example — pass `--region` and `--api-key`. ```sh theme={null} flexprice whoami ``` ``` Profile: default Region: us Base URL: https://us.api.flexprice.io/v1 Key backend: encrypted file (~/.flexprice/keys) Key: sk_test_…4b ``` Worth doing before anything destructive: an API key belongs to exactly one environment, and this is the fastest way to catch pointing at the wrong one. ```sh theme={null} flexprice resources ``` Lists every resource and the actions available on it. Then try a read: ```sh theme={null} flexprice customers list ``` ``` ID NAME STATUS CREATED_AT cust_01J8XABCDEF Ada Lovelace active 2026-01-02 cust_02 Grace Hopper archived 2026-03-14 profile: default · region: us · v1.0.1 ``` That footer names which profile and region actually served the request. The banner, spinners, footers, and confirmation messages all go to **stderr**, never stdout. Piping or redirecting stdout gives you clean data with none of the commentary mixed in. ## Creating something Most resources take flags directly: ```sh theme={null} flexprice customers create --external_id=acme --email=billing@acme.com ``` ``` ✓ Created customer cust_01J8XGHIJKL ``` For a request body too deep to express as flags, open it in your editor with the required fields pre-filled: ```sh theme={null} flexprice subscriptions create --edit ``` Or supply it directly: ```sh theme={null} flexprice subscriptions create --data @subscription.json ``` Destructive actions (`delete`, `void`, `cancel`, `terminate`, `archive`, `finalize`) ask for confirmation first. Off a terminal they **refuse** rather than proceeding — pass `--force` in scripts. See [Output & scripting](/docs/cli/output-and-scripting). ## Next steps Manage multiple environments with profiles. JSON output, exit codes, and CI usage. # 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://us.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://us.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://us.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 Archive Customer Option * Select Archive from the dropdown options * A confirmation dialog will appear asking if you want to archive the customer. Confirm Archive * 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 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:** Billing Details * **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. Save Customer 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 A self-service billing portal where your customers view their account and settle what they owe The Customer Portal is a hosted, self-service interface that gives your customers access to their own billing data and payment actions. Customers view subscriptions, invoices, wallet balances and usage, pay outstanding invoices, top up credits, save a card and turn on automatic top-ups, without going through your support or sales team. The portal authenticates with a short-lived session token that you generate from your backend. You control which sections a customer sees, and the portal renders under your brand colours and logo. **Benefits:** * **Fewer billing tickets** - Customers answer their own questions about balances, invoices and usage * **Faster collection** - An unpaid invoice carries a Pay now button instead of a support thread * **Uninterrupted service** - Automatic top-ups refill a wallet before the balance runs out * **No card data on your side** - Cards are collected and stored by the payment provider's hosted page, never by Flexprice or by you * **Tenant-controlled surface** - Each section is switched on or off per environment Customer Portal Overview showing the wallet balance, saved payment methods and active subscriptions ## What customers can do The portal is organised into sections shown as tabs across the top. Which sections appear is [configurable](/docs/customers/customer-portal/configuration); the defaults are below. | Section | Contents | | ------------ | -------------------------------------------------------------------------------- | | **Overview** | Wallet balance with top-up actions, saved payment methods, active subscriptions | | **Usage** | Account summary, metric cards, usage trend chart, usage breakdown, current usage | | **Credits** | Wallet balance, auto top-up settings, full transaction history | | **Invoices** | Invoice list with status, detail drawer, PDF download, Pay now | Actions available to a customer: * **View and download invoices** with their status and line-item breakdown * **Pay an outstanding invoice** through a hosted checkout page * **Top up a wallet** by card, or by raising an invoice to settle later * **Configure auto top-up** with a threshold, an amount and an optional cooloff * **Add, remove and set a default card** through the provider's hosted card form * **Track usage and cost** per feature over a chosen date range Payment actions depend on a payment provider being connected for the environment. See [Portal payments](/docs/customers/customer-portal/payments) for the provider capability matrix and the full flow of each action. **Session tokens expire after 1 hour** The portal reads a session token from the URL. Generate a fresh session each time a customer needs access rather than storing the token. ## Generating a portal session To give a customer access, generate a session token from your backend using your API key. The token names one customer and carries the tenant and environment, so it grants access to that customer's data only. **Endpoint:** `GET /v1/customers/portal/{external_id}` **Authentication:** your Flexprice API key in the `x-api-key` header **Path parameters:** | Parameter | Type | Required | Description | | ------------- | ------ | -------- | -------------------------- | | `external_id` | string | Yes | The 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 os 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" } ``` | Field | Type | Description | | ------------ | ------ | --------------------------------------------------- | | `token` | string | JWT used to authenticate customer portal requests | | `url` | string | Portal URL with the token already attached | | `expires_at` | string | ISO 8601 timestamp at which the token stops working | ### Redirecting a customer Send the customer to the `url` field. The portal reads the token from the query string and authenticates on load. ```javascript theme={null} const session = await response.json(); window.location.href = session.url; ``` **Do not put the portal URL in a shared or logged location** The URL carries the session token. Anyone holding it can act as that customer until it expires. Generate the link at the moment the customer clicks through to it, and do not email a raw portal URL to a shared inbox. ### Handling expiry Read `expires_at` and generate a new session when the token has passed it: ```javascript theme={null} const session = await response.json(); if (new Date(session.expires_at) > new Date()) { window.location.href = session.url; } else { // Request a new session and redirect again } ``` ### Embedding the portal The portal renders inside an iframe: ```html theme={null} ``` Hosted checkout pages open in a new browser tab, not inside the iframe. Payment providers block being framed. The portal handles the hand-off and the return automatically, described in [Portal payments](/docs/customers/customer-portal/payments#returning-from-a-hosted-checkout). ## Opening the portal from the dashboard You can open a customer's portal from the Flexprice dashboard without writing any code. ### From the Customers page 1. Go to **Customers** 2. Find the customer in the table 3. Click the three-dot menu at the end of the row 4. Select **Open Customer Portal** A session is generated and the portal opens in a new tab. Customers Page with Portal Option ### From the customer details page 1. Go to **Customers** and click a customer 2. Open the **Information** tab 3. In the Customer Details section, click the share icon to copy the portal link Customer Details Page The copied link contains a session token that expires after 1 hour. Share it through a channel the customer controls, and generate a new one if they come back later. ## Section reference ### Overview The default landing section. It answers three questions in one screen: what the balance is, how it gets paid, and what the customer is signed up to. * **Wallet balance** with credits, monetary value, and Add credits and Pay now actions * **Payment methods** listing saved cards, with Add card, Set as default and Remove * **Subscriptions** showing plan name, status, current period and next billing date Saved payment methods on Overview, with the default card marked and the row actions open ### Usage Feature-level consumption and cost over a date range. When features are assigned to [groups](/docs/product-catalogue/groups), the usage breakdown is grouped accordingly. * **Account summary** with balance, amount due and next billing date * **Metric cards** for revenue and any custom analytics metrics * **Usage trend** chart, with a date preset selector and optional custom range * **Usage breakdown** table of feature, total usage, events and total cost * **Current usage** for the in-progress billing period Customer Portal Usage Cost and margin metric cards are switched off by default. They show your cost to serve the customer and your margin on them, which are not the customer's numbers to see. Turn them on only for an internal-facing portal. ### Credits The prepaid wallet in full. * **Wallet balance** in credits and monetary value, with the wallet status * **Auto top-up** summary and settings * **Transaction history** listing each credit and debit with date, expiry, priority and amount Customer Portal Credits ### Invoices Every invoice raised against the customer. * **Invoice table** with date, invoice number, status, amount and row actions * **Detail drawer** showing amount due, billing period, issue and due dates, line items, subtotal, discount, tax and total * **Download** as PDF, available once an invoice is finalized * **Pay now**, shown when the invoice is awaiting payment and a provider is connected Invoice rows show each invoice in its own currency, so a customer billed in more than one currency reads the correct symbol on every row. Customer Portal Invoices with per-row currency and the row action menu showing Pay now Invoice detail drawer showing amount due, billing period, line items, totals, and the Pay and Download actions ## Security model | Control | Behaviour | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Session scope** | The token names one customer. Every portal request is scoped to that customer, and to the tenant and environment the token was issued in | | **Token lifetime** | 1 hour from issue | | **Tenant suspension** | A suspended tenant's portal stops serving requests, the same as the rest of the API | | **Card data** | Collected and stored by the payment provider's hosted page. Card details never reach Flexprice or your servers | | **Gateway identity** | Provider names are shown only where the customer must choose between two connected providers | | **Return URLs** | The session token is stripped from the URL handed to a payment provider, so it never enters the provider's logs, referrer chain or the customer's history at the provider | | **Amount authority** | Invoice payments take their amount from the invoice, so a customer cannot part-pay. Top-up credit reason, expiry and priority are pinned server-side | ## Related resources Invoice payment, top-ups, auto top-up and saved cards Control which sections appear and how the portal looks Every endpoint under /v1/customer/portal How wallet top-ups work outside the portal Threshold-based automatic top-ups The checkout session model the portal builds on # Portal API Reference Source: https://docs.flexprice.io/docs/customers/customer-portal/api-reference Every endpoint available to an authenticated Customer Portal session under /v1/customer/portal The Customer Portal APIs are the customer-facing half of Flexprice. They authenticate with a portal session token rather than an API key, and every request is scoped to the one customer that token names. Use them if you build your own portal front end, or embed portal actions in your product. For how to obtain a token, see [Customer Portal](/docs/customers/customer-portal#generating-a-portal-session). ## Authentication Send the session token in the `X-Session-Token` header: ```bash theme={null} curl --request GET \ --url https://api.cloud.flexprice.io/v1/customer/portal/info \ --header 'X-Session-Token: ' ``` | Property | Value | | -------------- | ------------------------------------------------------------------- | | Base path | `/v1/customer/portal` | | Header | `X-Session-Token` | | Token lifetime | 1 hour | | Scope | One customer, in the tenant and environment the token was issued in | The session token is a credential. Do not log it, put it in a URL you send to a third party, or hand it to a payment provider as a return URL. See how the portal builds return URLs in [Portal payments](/docs/customers/customer-portal/payments#returning-from-a-hosted-checkout). A suspended tenant's portal endpoints stop serving requests, the same as the rest of the API. ## Customer | Method | Path | Description | | ------ | -------- | ---------------------------------------- | | `GET` | `/info` | The authenticated customer's profile | | `PUT` | `/info` | Update the customer's own profile fields | | `GET` | `/usage` | Usage summary for the customer | ## Subscriptions | Method | Path | Description | | ------ | --------------------- | -------------------------------------------- | | `POST` | `/subscriptions` | List the customer's subscriptions, paginated | | `GET` | `/subscriptions/{id}` | One subscription | ## Invoices | Method | Path | Description | | ------ | -------------------- | --------------------------------------------------- | | `POST` | `/invoices` | List the customer's invoices, paginated | | `GET` | `/invoices/{id}` | One invoice with its line items | | `GET` | `/invoices/{id}/pdf` | Presigned URL for the invoice PDF. Pass `?url=true` | | `POST` | `/invoices/{id}/pay` | Start payment for an invoice | ### Pay an invoice ```bash theme={null} curl --request POST \ --url https://api.cloud.flexprice.io/v1/customer/portal/invoices/inv_01H.../pay \ --header 'X-Session-Token: ' \ --header 'Content-Type: application/json' \ --data '{ "idempotency_key": "portal-pay-inv_01H-1736180000", "success_url": "https://portal.yourapp.com?checkout_return=1" }' ``` ```json theme={null} { "payment_id": "pay_01H...", "invoice_id": "inv_01H...", "status": "pending", "amount": "149.00", "currency": "USD", "payment_action": { "type": "payment_link", "url": "https://provider.example.com/pay/xyz" } } ``` There is no amount field. The charge comes from the invoice, so a customer cannot part-pay. ## Wallets | Method | Path | Description | | ------ | ---------------------------- | ------------------------------------------------- | | `POST` | `/wallets` | List the customer's wallets | | `GET` | `/wallets/{id}` | One wallet with its real-time balance | | `GET` | `/wallets/{id}/transactions` | Transaction history. Accepts `limit` and `offset` | | `POST` | `/wallets/{id}/top-up` | Top up a wallet | | `PUT` | `/wallets/{id}/auto-topup` | Configure automatic top-ups | ### Top up a wallet ```bash theme={null} curl --request POST \ --url https://api.cloud.flexprice.io/v1/customer/portal/wallets/wal_01H.../top-up \ --header 'X-Session-Token: ' \ --header 'Content-Type: application/json' \ --data '{ "credits_to_add": "100", "idempotency_key": "portal-topup-wal_01H-1736180000", "checkout": { "use_saved_method": false } }' ``` ```json theme={null} { "wallet_transaction": { "id": "wtx_01H...", "amount": "100.00", "credits": "100" }, "checkout_session": { "id": "cs_01H...", "checkout_status": "initiated", "payment_provider": "chargebee", "payment_action": { "type": "checkout_url", "url": "https://provider.example.com/checkout/xyz" }, "expires_at": "2026-01-06T18:00:00Z" } } ``` `idempotency_key` is required. Omit `checkout` entirely to raise an invoice for the top-up instead of charging now. Credit reason, expiry and priority are pinned server-side and cannot be set from the portal. ### Configure auto top-up ```bash theme={null} curl --request PUT \ --url https://api.cloud.flexprice.io/v1/customer/portal/wallets/wal_01H.../auto-topup \ --header 'X-Session-Token: ' \ --header 'Content-Type: application/json' \ --data '{ "enabled": true, "threshold": "20", "amount": "100", "cooldown": { "value": 6, "unit": "hour" } }' ``` Send `cooldown` as `{ "value": 0, "unit": "hour" }` to clear a stored cooldown. `null` leaves the existing one in place. ## Checkout sessions | Method | Path | Description | | ------ | -------------------------------- | ------------------------------- | | `GET` | `/checkout-sessions/{id}` | Read a session's current status | | `POST` | `/checkout-sessions/{id}/cancel` | Abandon a session | ```json theme={null} { "id": "cs_01H...", "checkout_status": "completed", "payment_provider": "chargebee", "checkout_invoice_id": "inv_01H...", "checkout_payment_id": "pay_01H...", "expires_at": "2026-01-06T18:00:00Z", "completed_at": "2026-01-06T17:31:04Z" } ``` | `checkout_status` | Meaning | | ----------------- | ------------------------------------------------------------------------ | | `initiated` | Created, nothing attempted yet | | `pending` | In flight at the provider | | `completed` | The gateway is done. Credits and payment records follow from the webhook | | `failed` | Declined. Read `failure_reason` | | `expired` | Timed out before completion | `completed` means the gateway finished, not that the wallet transaction exists yet. It is written by the provider's webhook, which can land a moment later. Recheck the wallet rather than assuming the balance from the status. ## Payment methods | Method | Path | Description | | ------ | -------------------------- | ---------------------------------------------------------------- | | `GET` | `/payment-methods` | Saved methods, grouped by provider. Accepts a `providers` filter | | `POST` | `/payment-methods` | Start a hosted card-capture session | | `POST` | `/payment-methods/delete` | Remove a saved method | | `POST` | `/payment-methods/default` | Promote a method to default at its provider | Delete and default both take `payment_provider` and `payment_method_id`. Adding returns a `SetupAction`, not a payment method, because nothing is stored until the customer completes the provider's form. ```json theme={null} { "provider": "chargebee", "action": { "type": "redirect", "url": "https://provider.example.com/setup/xyz" } } ``` `action.url` is an unconstrained API string. Refuse any scheme that is not `http` or `https` before opening it. ## Integrations and configuration | Method | Path | Description | | ------ | --------------- | -------------------------------------------------------------------- | | `GET` | `/integrations` | Payment providers available to this session, with their capabilities | | `GET` | `/config` | The tenant's portal configuration, merged with the defaults | `/integrations` is derived from the tenant's published connections. Use it to decide which actions to render rather than hard-coding a provider. See the [capability matrix](/docs/customers/customer-portal/payments#provider-capabilities). ## Analytics | Method | Path | Description | | ------ | -------------------- | -------------------------------------------- | | `POST` | `/analytics/revenue` | Usage and revenue analytics for the customer | | `POST` | `/analytics/cost` | Cost analytics for the customer | Cost analytics returns your cost to serve the customer and your margin on them. Do not render those figures in a customer-facing surface. ## Errors Portal errors carry a hint written for the customer. Common ones: | Message | Cause | | ----------------------------------------------- | --------------------------------------------------------------- | | `This invoice cannot be paid yet` | The invoice is still a draft | | `This invoice is already settled` | The invoice payment status is already succeeded | | `Contact support to activate this subscription` | The invoice's subscription is not in a collectable state | | `credits must be greater than zero` | A top-up of zero or a negative amount | | `top-up amount is below the minimum` | Under the tenant's per-currency floor. Details name the minimum | | `requested payment provider is not connected` | `payment_provider` names a gateway with no published connection | | `capability is required` | A provider resolution request without a named capability | ## Related resources Sessions, sections and the security model The flow behind each payment endpoint The schema returned by /config The full Flexprice API # Portal Configuration Source: https://docs.flexprice.io/docs/customers/customer-portal/configuration Control which sections your customers see in the Customer Portal, and style it with your own brand The Customer Portal is configured per tenant and environment. You choose which sections appear, which widgets sit inside them, and what colours, logo and font the portal renders with. The configuration is stored under the `customer_portal_config` setting key, and the portal fetches it on load. Anything you leave unset falls back to a default, so a tenant that has never touched the configuration still gets a complete portal. ## Settings toggles The dashboard exposes the five section-level switches most tenants need. Go to **Settings** and open the **Customer Portal** tab. | Toggle | Controls | | ------------------------ | ------------------------------------------------------------------ | | **Show invoices** | The Invoices section, including PDF download and Pay now | | **Show wallet balance** | The Credits section, including transaction history and auto top-up | | **Show usage** | The Usage section, including charts, breakdown and metric cards | | **Show subscriptions** | The subscriptions widget on Overview | | **Show payment methods** | Saved card management | Changes apply to every portal session opened in that environment from the next page load. Settings, Customer Portal tab, showing the five portal visibility toggles **Saving the configuration requires Super Admin** The portal configuration is stored through the settings API, whose writes are restricted to Super Admin regardless of the `portal:write` permission. A user without it sees the toggles but cannot save. ## Configuration schema For anything beyond the five toggles, write the configuration directly through the settings API under the key `customer_portal_config`. ```json theme={null} { "version": "1.0", "theme": { "primary_color": "#15547c", "background_color": "#ffffff", "surface_color": "#f9fafb", "border_color": "#e5e7eb", "logo_url": "https://yourapp.com/logo.svg", "font_family": "Inter, sans-serif" }, "sections": [ { "id": "overview", "label": "Overview", "enabled": true, "order": 1, "tabs": [ { "id": "16", "type": "wallet_balance", "enabled": true, "order": 1 }, { "id": "17", "type": "payment_methods", "enabled": true, "order": 2 }, { "id": "9", "type": "subscriptions", "enabled": true, "order": 3 } ] } ] } ``` ### Section fields | Field | Type | Description | | --------- | ------- | ----------------------------------------------------------------------------------- | | `id` | string | Section identifier. The known ids are `overview`, `usage`, `credits` and `invoices` | | `label` | string | Name shown in the portal's tab bar | | `enabled` | boolean | Whether the section appears at all | | `order` | number | Position in the tab bar. Standard sections use the product's own ordering | | `tabs` | array | Widgets rendered inside the section, sorted by their `order` | ### Widget fields | Field | Type | Description | | -------------- | ------- | ------------------------------------------------------------- | | `id` | string | Unique instance key within the section | | `type` | string | Which widget to render. See the widget types below | | `label` | string | Optional display label, overriding the widget's default title | | `enabled` | boolean | Whether the widget renders | | `order` | number | Position within the section | | `usage_graph` | object | Only read when `type` is `usage_graph` | | `metric_cards` | object | Only read when `type` is `metric_cards` | ## Widget types | Type | Renders | | --------------------- | --------------------------------------------------------------- | | `account_summary` | Compact strip of balance, amount due and next billing date | | `subscriptions` | Active subscriptions with plan, status, period and next billing | | `invoices` | Invoice table with search, detail drawer, download and Pay now | | `wallet_balance` | Wallet balance in credits and currency, with top-up actions | | `wallet_transactions` | Full credit and debit history | | `wallet_topup` | Standalone top-up card | | `auto_topup` | Auto top-up summary and settings | | `payment_methods` | Saved cards, with add, remove and set default | | `usage_graph` | Usage trend chart with a date range selector | | `usage_breakdown` | Per-feature table of usage, events and cost | | `current_usage` | Consumption in the in-progress billing period | | `metric_cards` | Revenue, custom analytics and optional cost metrics | ### Usage graph configuration ```json theme={null} { "id": "2", "type": "usage_graph", "enabled": true, "order": 2, "usage_graph": { "date_presets": ["today", "last_7_days", "last_30_days", "current_month", "last_month"], "default_preset": "last_7_days", "allow_custom_date_range": true, "feature_filter_mode": "all", "feature_ids": [] } } ``` | Field | Description | | ------------------------- | ------------------------------------------------------------------ | | `date_presets` | Which range presets the customer can pick from | | `default_preset` | The range selected when the section opens | | `allow_custom_date_range` | Whether a custom start and end date picker is offered | | `feature_filter_mode` | `all`, `include_list` or `exclude_list` | | `feature_ids` | Feature ids used when the mode is `include_list` or `exclude_list` | Valid presets are `today`, `last_7_days`, `last_30_days`, `current_month` and `last_month`. ### Metric cards configuration ```json theme={null} { "id": "1", "type": "metric_cards", "enabled": true, "order": 1, "metric_cards": { "show_custom_metrics": true, "show_revenue_metric": true, "show_cost_metrics": false } } ``` | Field | Default | Shows | | --------------------- | ------- | ------------------------------------------------------ | | `show_custom_metrics` | `true` | Cards defined in your revenue analytics custom metrics | | `show_revenue_metric` | `true` | The customer's total spend | | `show_cost_metrics` | `false` | Cost, Margin and Margin % | **Cost metrics are your numbers, not the customer's** `show_cost_metrics` exposes your cost to serve this customer and your margin on them. It is off by default. Turn it on only for a portal your own team uses. ## How your configuration merges with the defaults Understanding the merge matters when a Flexprice release adds a section or changes one. * **Your sections win on content.** Labels, `enabled` and the widgets inside a section are yours. * **Standard sections take the product's ordering.** A stored configuration carries the `order` values current when you saved it, so a section later promoted in the defaults would otherwise stay where it was. Sections you add yourself keep their own order and are placed after the standard ones. * **New default sections are appended, not dropped.** A wholesale replace would hide any section shipped after you saved, so a section you have never seen is added rather than lost. Express removal as `"enabled": false`, not by deleting the entry. * **Overview's composition is fixed.** Overview is balance, payment methods and subscriptions. It is not customisable per tenant, because any analytics widget placed there also renders the section's date filter, which duplicated Usage. * **A theme with no values is ignored.** An empty theme object does not override the default light appearance. **The Payments section is retired** Saved cards used to live in a section of their own. They now sit on Overview, beside the balance they top up and the subscriptions they pay for. A stored configuration naming a `payment_methods` section is honoured as hidden. Use the **Show payment methods** toggle to control the widget. ## Theming Supply four brand colours and the portal derives the rest, including text colours, from the brightness of your background. | Field | Applies to | | ------------------ | -------------------------------- | | `primary_color` | Buttons, active tabs and accents | | `background_color` | The page background | | `surface_color` | Card and panel backgrounds | | `border_color` | Card and table borders | | `logo_url` | The logo in the portal header | | `font_family` | A CSS font stack for portal text | Leave the whole `theme` object out to use the portal's default light appearance. Give `font_family` a real fallback stack, for example `"Inter, system-ui, sans-serif"`. The portal does not load web fonts on your behalf. ## Reading the configuration from the portal A portal session reads its own resolved configuration, already merged with the defaults: ``` GET /v1/customer/portal/config X-Session-Token: ``` Use this if you build your own portal front end against the Flexprice APIs and want to honour the same tenant configuration. ## Related resources Sessions, sections and the security model Invoice payment, top-ups, auto top-up and saved cards All tenant and environment setting keys Embed the same widgets directly in your own product # Portal Payments Source: https://docs.flexprice.io/docs/customers/customer-portal/payments Let customers pay invoices, top up wallets, save cards and enable automatic top-ups from the Customer Portal The Customer Portal turns billing from a support conversation into a self-service action. A customer with an outstanding invoice pays it from the invoice row. A customer running low on credits tops up, or turns on auto top-up so it never happens again. Cards are collected on the payment provider's own hosted page, so no card data reaches Flexprice or your servers. Every payment action follows the same shape: the portal asks the API to start the action, the API returns a `payment_action` describing what to do next, and the portal opens it. Credits and payment records are written by the provider's webhook once the money actually moves, never optimistically on the customer's screen. Portal Overview with the wallet balance and Add credits action above the saved payment methods list ## Prerequisites Portal payment actions require a payment provider connection published in the environment the session token was issued in. Without one, the portal renders its read-only surface and hides the payment actions. Set up a [Chargebee](/integrations/chargebee/connection-setup), [Razorpay](/integrations/razorpay/connection-setup) or [Stripe](/integrations/stripe/connection-setup) connection and publish it. Not every provider supports every action. See the capability matrix below. In **Settings**, turn on the sections that carry the actions. See [Portal configuration](/docs/customers/customer-portal/configuration). ## Provider capabilities Flexprice names each payment operation as a capability and resolves the provider per capability from the connections published in the environment. A capability is what Flexprice implements for that gateway, not everything the gateway's own API can do. | Capability | Chargebee | Razorpay | Stripe | Nomod | | ----------------------------------------------------- | :-------: | :------: | :----: | :---: | | `checkout` (hosted payment page) | Yes | Yes | No | No | | `payment_link` (invoice payment link) | Yes | Yes | Yes | Yes | | `auto_charge` (charge a saved card off-session) | Yes | Yes | No | No | | `payment_method_management` (list, add, remove cards) | Yes | No | No | No | | `set_default_method` (promote a card to default) | Yes | No | No | No | | `invoice_sync` (push invoices to the provider) | Yes | No | No | No | Read the capabilities available to a session from `GET /v1/customer/portal/integrations`: ```json theme={null} { "payment_integrations": [ { "provider": "chargebee", "capabilities": [ { "type": "checkout", "is_default": true }, { "type": "payment_link", "is_default": true }, { "type": "auto_charge", "is_default": true }, { "type": "payment_method_management", "is_default": true }, { "type": "set_default_method", "is_default": true } ] } ] } ``` `is_default` marks the provider Flexprice picks for that capability when the caller names none. When exactly one provider serves a capability, the portal never shows its name to the customer. A provider picker appears only when two or more connected providers can serve the same action. **A requested provider must be both connected and capable** Passing `payment_provider` for a gateway that is not connected in the environment returns a validation error. Passing one that is connected but does not implement the capability returns a validation error naming the candidates that do. Omit the field to let Flexprice resolve it. ## Paying an invoice An invoice row in the Invoices section carries a **Pay now** action when the invoice is awaiting payment. The customer clicks it, a hosted checkout opens in a new tab, and the invoice is marked paid by the provider's webhook when the payment settles. **Endpoint:** `POST /v1/customer/portal/invoices/{id}/pay` ```json theme={null} { "payment_provider": "chargebee", "idempotency_key": "portal-pay-inv_01H...-1736180000", "success_url": "https://portal.yourapp.com?checkout_return=1", "cancel_url": "https://portal.yourapp.com?checkout_return=1", "failure_url": "https://portal.yourapp.com?checkout_return=1" } ``` | Field | Required | Description | | ------------------ | -------- | ------------------------------------------------------------------- | | `payment_provider` | No | Gateway to use. Omit to let Flexprice resolve the capable provider | | `idempotency_key` | No | Prevents a retry from raising a second payment | | `success_url` | No | Where the provider returns the customer after a successful payment | | `cancel_url` | No | Where the provider returns the customer after they abandon the page | | `failure_url` | No | Where the provider returns the customer after a declined payment | **Response:** ```json theme={null} { "payment_id": "pay_01H...", "invoice_id": "inv_01H...", "status": "pending", "amount": "149.00", "currency": "USD", "payment_action": { "type": "payment_link", "url": "https://provider.example.com/pay/xyz" } } ``` Read `payment_action` rather than reaching for a URL field of your own. Whether a provider hands back a hosted checkout page or a payment link is a property of the provider, and the `type` says which. **The amount comes from the invoice** There is no amount field in the request. A customer cannot part-pay an invoice from the portal. ### When Pay now is not available The API rejects a payment and the portal disables the action when: * The invoice is still a draft, or is voided * The invoice payment status is already `succeeded`, or a payment is already in flight * The invoice belongs to a subscription that is not in a state that allows collection. The portal shows a message asking the customer to contact support Invoices section with a pending invoice row and its action menu open on Pay now ## Topping up a wallet **Add credits** on the wallet card opens a top-up dialog. The customer enters a credit amount, sees the exact charge stated above the confirm button, and continues. **Endpoint:** `POST /v1/customer/portal/wallets/{id}/top-up` ```json theme={null} { "credits_to_add": "100", "idempotency_key": "portal-topup-wal_01H...-1736180000", "description": "Monthly credit refill", "checkout": { "use_saved_method": false, "success_url": "https://portal.yourapp.com?checkout_return=1", "cancel_url": "https://portal.yourapp.com?checkout_return=1" } } ``` | Field | Required | Description | | ----------------------------------------------------- | -------- | ------------------------------------------------------------------------------- | | `credits_to_add` | Yes | Credits to add. Converted to a charge using the wallet's top-up conversion rate | | `idempotency_key` | Yes | Without it a retry grants credits twice | | `description` | No | Free-text note stored on the transaction | | `checkout` | No | Present to charge now. Omit to raise an invoice the customer settles later | | `checkout.payment_provider` | No | Gateway to use. Omit to let Flexprice resolve it | | `checkout.use_saved_method` | No | Charge the saved card directly instead of opening a hosted page | | `checkout.success_url` / `cancel_url` / `failure_url` | No | Where the provider returns the customer | **`idempotency_key` is required on portal top-ups** The server's fallback key is timestamp-derived, so a retried request without an explicit key reads as a fresh top-up and grants the credits a second time. ### Charge now or invoice later The `checkout` object decides which of the two flows runs. | `checkout` | Behaviour | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Present** | Pay-first. The customer is charged before any credit lands. The response carries a `checkout_session`, and credits are applied by the payment webhook once the charge settles | | **Omitted** | Invoiced. An invoice is raised for the top-up and the customer settles it later, through the Invoices section or any other channel you offer | The portal always sends `checkout`. The invoiced path is available to your own integrations calling the portal API directly. **Credits land after the payment, not on the click** The portal reports "Top-up requested, your balance will update once payment completes" rather than claiming success. The wallet transaction is written by the provider's webhook. After a return, the portal rechecks the balance on a short schedule (immediately, then at 1.5s and 4s) and stops as soon as the wallet actually changes, so a webhook arriving a moment after the redirect still shows up without a manual refresh. ### Paying with a saved card When `use_saved_method` is `true`, the charge runs against the card on file with the customer present, and no redirect is involved. The response comes back with `checkout_status: "completed"` and no `payment_action`. Read `checkout_status`, not the presence of a URL: | `checkout_status` | Meaning | | ----------------------- | -------------------------------------------------------------- | | `completed` | Paid. Credits follow from the webhook | | `failed` | The gateway declined. `failure_reason` carries its own message | | `expired` | The session timed out before it was completed | | `initiated` / `pending` | Still settling. Neither outcome can be claimed yet | If no usable saved method exists, or the charge declines, the response falls back to a `payment_action` with a link. The portal shows the saved-card option only when the resolved provider supports `auto_charge` and a chargeable card is on file. ### Minimum top-up amount Top-ups below the tenant's per-currency floor are rejected before the customer is redirected, with a message naming the minimum for that currency. Configure the floor under the `wallet_topup_config` setting key. See [Settings](/docs/settings/settings). ### A top-up already in flight If the customer already has a pending top-up session on the wallet, the API hands back the session already in flight rather than a conflict. The customer can complete the payment they started instead of being blocked by an abandoned tab. Add credits dialog showing the credit amount, the charge summary, the payment provider choice and the saved-card toggle ## Auto top-up Auto top-up refills the wallet without the customer present. It reads as a plain-language summary in the Credits section: "Automatically add $100.00 when your balance falls below $20.00", with a **Manage** action, or "Auto top-up is off" with **Enable**. **Endpoint:** `PUT /v1/customer/portal/wallets/{id}/auto-topup` ```json theme={null} { "enabled": true, "threshold": "20", "amount": "100", "cooldown": { "value": 6, "unit": "hour" } } ``` | Field | Required | Description | | ----------- | -------- | ----------------------------------------------------------------------------------------- | | `enabled` | Yes | Turns automatic top-ups on or off | | `threshold` | No | Balance at or below which a top-up runs | | `amount` | No | Credits added each time | | `cooldown` | No | Minimum wait between two automatic top-ups. `unit` is `second`, `minute`, `hour` or `day` | **Clearing a cooldown** Send `{ "value": 0, "unit": "hour" }` to remove a stored cooldown. Sending `null` reads as an absent field on the server and leaves the existing cooldown in place. Enabling auto top-up is itself the customer's consent to be charged unattended, so there is no separate auto-charge flag in the portal request. Whether the resulting credits are invoiced is pinned server-side, because it selects the transaction reason and is your decision, not the customer's. ### Requirements Auto top-up needs a saved card that can be charged off-session. The portal shows "Add a payment method to enable automatic top-ups" and keeps the toggle unavailable until one exists, which means: * The resolved provider supports the `auto_charge` capability (Chargebee or Razorpay) * The customer has at least one saved card with `can_auto_charge: true` For the mandate rules behind unattended charging, see [Autopay mandates](/docs/checkout/autopay-mandates). Auto top-up dialog showing the enable toggle, threshold, top-up amount and the cooloff duration and unit ## Saved payment methods The payment methods list sits on Overview, beside the balance it tops up and the subscriptions it pays for. It shows each saved card as brand and last four digits with its expiry, marks the default, and offers **Add card**, **Set as default** and **Remove**. **List:** `GET /v1/customer/portal/payment-methods` ```json theme={null} { "providers": [ { "provider": "chargebee", "items": [ { "id": "pm_01H...", "provider": "chargebee", "type": "card", "status": "ACTIVE", "card": { "brand": "visa", "last4": "4242", "exp_month": 4, "exp_year": 2028 }, "is_default": true, "can_auto_charge": true } ] } ] } ``` | Field | Meaning | | ----------------- | ------------------------------------------------------------------------------------------------------- | | `status` | `ACTIVE`, `INACTIVE` or `EXPIRED`. An expired card cannot be made default | | `is_default` | Which card is used at this provider. Scoped per provider, so two providers means two defaults | | `can_auto_charge` | Whether this card can be charged with nobody present. A capability, not a permission | | `error` | Set on a provider entry that could not be read. Keeps "no saved cards" distinct from "we could not ask" | ### Adding a card **Endpoint:** `POST /v1/customer/portal/payment-methods` ```json theme={null} { "payment_provider": "chargebee", "success_url": "https://portal.yourapp.com?checkout_return=1", "cancel_url": "https://portal.yourapp.com?checkout_return=1" } ``` The response returns an action, not a payment method. Nothing is stored until the customer completes the provider's form. ```json theme={null} { "provider": "chargebee", "action": { "type": "redirect", "url": "https://provider.example.com/setup/xyz", "expires_at": "2026-01-06T18:00:00Z" } } ``` The portal opens the URL in a new tab and shows the same link on screen, so a customer whose popup blocker stopped the open still has a way through. Card details are entered on the provider's page and never touch Flexprice. The setup URL is an API string. The portal refuses to open anything that is not `http` or `https`, so a `javascript:` URL cannot be executed. Apply the same check if you build your own portal against these endpoints. ### Setting a default and removing **Set default:** `POST /v1/customer/portal/payment-methods/default` with `payment_provider` and `payment_method_id`. **Remove:** `POST /v1/customer/portal/payment-methods/delete` with the same two fields. The portal confirms first, warning that the card will no longer be available for payments or automatic top-ups. Setting a default requires the `set_default_method` capability. On a provider without it, the portal explains that a default card is not supported rather than offering an action that would fail. Payment methods listing two saved cards, one marked Default, with Set as default and Remove in the row menu ## Returning from a hosted checkout Hosted checkout pages cannot be framed, so the portal opens them in a new tab. That raises a problem worth knowing about if you embed the portal or build your own: the provider redirects the tab it is in, which is the new one, leaving the customer looking at a second copy of their account while the tab they started from still shows pre-payment numbers. The portal resolves this itself: 1. The return URL is rebuilt from the portal path with the session token removed, so the token never reaches the provider, its redirect logs, or the customer's browser history at the provider. A `checkout_return` marker is added in its place, and only recognised portal parameters such as `section` are carried across. 2. The tab the provider returns to reads the marker, announces itself over a `BroadcastChannel`, and closes. The announcement carries no outcome. 3. The original tab holds the checkout session id and asks the API what happened, so a message from any same-origin page cannot make the portal claim a payment succeeded. 4. A tab that cannot close, which browsers only permit for script-opened windows, strips the marker and renders as a normal portal instead. 5. The session token is held in `localStorage` for 30 minutes so the return trip can restore it. `sessionStorage` would not survive, because the provider tab is opened with `noopener` and inherits none. **Checking a session directly:** `GET /v1/customer/portal/checkout-sessions/{id}` returns the session with its `checkout_status`, `failure_reason` and linked `checkout_invoice_id` and `checkout_payment_id`. `POST /v1/customer/portal/checkout-sessions/{id}/cancel` abandons one. Polling gives up after roughly 40 seconds without discarding the session id, so a customer who lingers on the provider's page still has their result resolved when they come back. ## Chargebee as a portal provider Chargebee is the only connected provider that currently serves every portal capability, including saved-card management and default-card selection. Alongside the portal work, Chargebee invoice sync now pushes Flexprice invoices to Chargebee for collection and reconciles payments back through the Chargebee webhook, so an invoice paid on either side settles on both. ## Troubleshooting | Symptom | Cause | Resolution | | --------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | Payment actions do not appear | No payment provider connection published in the environment | Publish a connection, then reload the portal | | Add card is missing | The connected provider lacks `payment_method_management` | Connect Chargebee, or collect cards outside the portal | | Auto top-up cannot be enabled | No saved card with `can_auto_charge: true` | Add a card at a provider supporting `auto_charge` | | The checkout page did not open | A popup blocker stopped the new tab | Use the link shown in the dialog. The portal keeps it on screen for this reason | | The balance is unchanged after paying | The provider's webhook has not landed yet | Wait a moment. The portal rechecks automatically. If it persists, check webhook delivery for the provider | | "Top-up amount is below the minimum" | The charge is under the tenant's per-currency floor | Raise the credit amount, or lower the floor in `wallet_topup_config` | | "requested payment provider is not connected" | `payment_provider` names a gateway with no published connection | Omit the field, or name a connected gateway | ## Related resources Sessions, sections and the security model Control which sections and actions appear Every endpoint under /v1/customer/portal Mandates behind unattended charging # 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. A customer's `tax_treatment` controls whether they are charged tax at all. See [Tax exempt customers](/docs/product-catalogue/taxes/overview#tax-exempt-customers) for how to mark a customer exempt before subscribing them. # 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://us.api.flexprice.io/v1/customers \ -H "x-api-key: " \ -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://us.api.flexprice.io/v1/customers/{id} \ -H "x-api-key: " \ -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 S3 Data Exports
Page ### 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. Create S3
Connection 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. Create Export
Task #### 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 Export Tasks
List ### 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 Export Details ## 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. Manual Export
Modal #### 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. Runs History 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 x-api-key: {api_key} { "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 x-api-key: {api_key} { "enabled": false } ``` ### Trigger Manual Export ```bash theme={null} POST /api/v1/tasks/scheduled/{task_id}/run Content-Type: application/json x-api-key: {api_key} { "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 x-api-key: {api_key} ``` ### Get Task Execution History ```bash theme={null} GET /api/v1/tasks?scheduled_task_id={task_id}&task_type=EXPORT x-api-key: {api_key} ``` ## 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 Plan Configuration ### 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"** 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 Upcoming Invoices **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 Create Feature Form ### 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"`. Event Name Configuration ### 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**. Aggregation Function Selection ### 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 } } ``` Aggregation Field Configuration ### 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 Usage Reset Configuration ### 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"`. Event Filters Configuration ### 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. Event Debugger ## 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 } }' ``` cURL Event Ingestion Example ### Response ```json theme={null} { "event_id": "event_01K389J4M1F1NZG6XP0AMD6J52", "message": "Event accepted for processing" } ``` Postman Event Ingestion Example ## 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 Events Dashboard ### 2. View Event Details Click on any event in the list to view the complete payload that was transmitted: Event Details Modal 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. Query Tab Event Preview ### Validate Billing Periods For **Periodic** usage reset: * Usage should reset at the start of each billing cycle * Check that usage accumulates correctly within the period For **Cumulative** usage reset: * Usage should keep growing across billing cycles * Verify no unexpected resets ## Troubleshooting Common Scenarios ### Scenario 1: Events Transmitted but Not Counted **Symptoms**: Events appear in dashboard but don't affect billing **Check**: 1. Feature configuration (Event Name, Aggregation Field) 2. Event properties (correct field name and data type) 3. Customer subscription status 4. Plan configuration ### Scenario 2: Wrong Aggregation Values **Symptoms**: Billing shows incorrect quantities **Check**: 1. Aggregation Field name matches exactly 2. Property values are correct data type 3. No duplicate or conflicting events 4. Usage Reset configuration ### Scenario 3: Events Missing from Dashboard **Symptoms**: Events not visible in Events list **Check**: 1. API response status (should be 202 Accepted) 2. Event Name validity 3. Customer existence 4. API key permissions ### Scenario 4: Unexpected Usage Resets **Symptoms**: Usage resets when it shouldn't **Check**: 1. Usage Reset setting (Periodic vs Cumulative) 2. Billing cycle configuration 3. Subscription changes 4. Feature configuration changes ## Best Practices for Validation ### 1. Test Before Production Always test your event ingestion setup with a small number of events before going live. ### 2. Monitor Regularly Check the Events dashboard regularly to ensure events are being processed correctly. ### 3. Use Consistent Identifiers Use consistent Event Names and External Customer IDs across your system. ### 4. Include Debugging Information Add useful properties to your events for debugging: ```json theme={null} { "event_name": "model.usage", "external_customer_id": "cust_123", "properties": { "credits": 2, "request_id": "req_abc123", "version": "1.0" }, "source": "api" } ``` ### 5. Set Up Alerts Consider setting up alerts for: * Failed event ingestion * Unusual usage patterns * Missing events ## Next Steps After validating your events: **[Connect to Billing](/docs/event-ingestion/connecting-to-billing)** - Set up pricing and subscriptions ## Getting Help If you're still experiencing issues after following this validation guide: 1. Check the **[Event Debugger](/docs/event-ingestion/event-debugger)** for detailed error messages 2. Review your feature configuration carefully 3. Test with a simple event payload 4. Contact support with specific error messages and event examples # Credit Balance Source: https://docs.flexprice.io/docs/exportable-ui/credits-widgets/credit-balance Wallet balance card showing current credits and monetary value Use **Credit Balance** to show a customer's current wallet balance: credits, monetary value, and wallet status. Part of [Credits Widgets](/docs/exportable-ui/credits-widgets/credits-widgets). ```tsx theme={null} ``` ## Supports * Credit balance and monetary value * Wallet status (active, frozen, closed) * Empty state when there is no wallet * Loading skeleton state ## Best used for * Customer portal overview * Billing page * Account settings ## Import ```tsx theme={null} import '@flexprice/ui/style.css'; import { CreditBalance, type CreditBalanceData } from '@flexprice/ui'; ``` ## Usage ```tsx theme={null} const wallet: CreditBalanceData = { id: 'wallet_1', name: 'Main Wallet', status: 'active', creditBalance: 4200, balance: 42, currency: 'USD', }; ``` Pass `wallet={null}` to render the empty state (no wallet set up yet). See [Ways to provide data](/docs/exportable-ui/overview#ways-to-provide-data) for SDK, API, backend, and static JSON options. ## Map Flexprice API data ```ts theme={null} import { adaptCreditBalance } from '@flexprice/ui'; const wallet = adaptCreditBalance(walletFromList, realtimeWalletBalance); ``` `realtimeWalletBalance` is optional. When provided, its real-time balance fields take precedence over the wallet list snapshot. ## See also Overview of both credits components. Transaction history table with pagination and multi-wallet selection. # Credit History Source: https://docs.flexprice.io/docs/exportable-ui/credits-widgets/credit-history Wallet transaction history table with pagination and multi-wallet selection Use **Credit History** to show a customer's wallet transactions: credits added, credits spent, and the reason for each. Part of [Credits Widgets](/docs/exportable-ui/credits-widgets/credits-widgets). ```tsx theme={null} const [page, setPage] = useState(1); ``` ## Supports * Credit and debit transactions, with reason labels * Pending-transaction styling * Multiple wallets, with a selector shown when there is more than one * Pagination * Loading skeleton state * Empty state when there are no transactions ## Best used for * Customer portal billing tab * Account settings * Wallet detail pages ## Import ```tsx theme={null} import '@flexprice/ui/style.css'; import { CreditHistory, type CreditTransaction } from '@flexprice/ui'; ``` ## Usage Pagination is fully controlled: the component has no dependency on a router or URL, so you own the current page in your own state and pass it back in. ```tsx theme={null} const [page, setPage] = useState(1); const transactions: CreditTransaction[] = [ { id: 'txn_1', type: 'credit', amount: 100, creditAmount: 100, reason: 'FREE_CREDIT_GRANT', createdAt: '2026-01-01T00:00:00Z' }, { id: 'txn_2', type: 'debit', amount: 20, creditAmount: 20, reason: 'INVOICE_PAYMENT', createdAt: '2026-01-05T00:00:00Z' }, ]; ``` For accounts with more than one wallet, also pass `wallets`, `selectedWalletId`, and `onSelectWallet`: ```tsx theme={null} ``` See [Ways to provide data](/docs/exportable-ui/overview#ways-to-provide-data) for SDK, API, backend, and static JSON options. ## Map Flexprice API data ```ts theme={null} import { adaptCreditTransactions, adaptWalletOptions } from '@flexprice/ui'; const transactions = adaptCreditTransactions(walletTransactions.items); const wallets = adaptWalletOptions(walletList); ``` ## See also Overview of both credits components. Wallet balance card showing current credits and monetary value. # Credits Widgets Source: https://docs.flexprice.io/docs/exportable-ui/credits-widgets/credits-widgets Wallet balance and transaction history components **Credits Widgets** are two independent components for showing a customer's prepaid credit wallet: [Credit Balance](/docs/exportable-ui/credits-widgets/credit-balance) and [Credit History](/docs/exportable-ui/credits-widgets/credit-history). Use one on its own, or place both together on a billing or account page. Each component is presentational and takes its own prop shape. There is no shared parent component. ## Components Wallet balance card showing current credits and monetary value. Transaction history table with pagination and multi-wallet selection. ## Import ```tsx theme={null} import '@flexprice/ui/style.css'; import { CreditBalance, CreditHistory } from '@flexprice/ui'; ``` Each component page documents its own prop shape, an example, and the adapters for mapping Flexprice wallet API responses. ## See also Install Flexprice UI, choose a data source, theme, and integrate components. Metric cards, trend chart, breakdown table, and quota bars. # Overview Source: https://docs.flexprice.io/docs/exportable-ui/overview Production-ready billing UI, pulled straight from the Flexprice dashboard, that renders from any data source Building billing UI from scratch means re-solving problems Flexprice already solved: currency formatting, entitlement states, empty states, dark mode, responsive layout. **Flexprice UI** (`@flexprice/ui`) skips that work. It's the actual React components from the Flexprice dashboard, published as an installable library, so your pricing page, usage dashboard, and billing screens match a production system on day one instead of a first draft. Every component is presentational: no fetching, no auth, no routing baked in. You own the data. Feed a component from the Flexprice SDK, the REST API, your own backend, or a plain JSON file, and it renders identically either way. One CSS variable away from your brand. Every component ships a light and dark treatment. Typed props and adapters, no `any`. Works in Next.js, Remix, and Astro. ## Components | Component | Description | | ----------------------------------------------------------------- | ------------------------------------------------------------------------------- | | [Pricing Table](/docs/exportable-ui/pricing-widget/pricing-table) | Comparison grid of multiple plans, with billing period and currency controls. | | [Pricing Card](/docs/exportable-ui/pricing-widget/pricing-card) | A single plan: price, entitlements, and a CTA. Compose your own layout with it. | | Component | Description | | ------------------------------------------------------------------------ | ------------------------------------------------------------ | | [Metric Cards](/docs/exportable-ui/usage-widgets/metric-cards) | Headline numbers: revenue, cost, margin, and custom metrics. | | [Usage Trend Chart](/docs/exportable-ui/usage-widgets/usage-trend-chart) | Line chart of usage over time, one series per feature. | | [Usage Breakdown](/docs/exportable-ui/usage-widgets/usage-breakdown) | Grouped, sortable table of usage and cost by feature. | | [Usage Quota](/docs/exportable-ui/usage-widgets/usage-quota) | Progress bars showing usage against each plan limit. | | Component | Description | | -------------------------------------------------------------------- | ---------------------------------------------------------------- | | [Credit Balance](/docs/exportable-ui/credits-widgets/credit-balance) | Wallet balance card: credits, monetary value, and status. | | [Credit History](/docs/exportable-ui/credits-widgets/credit-history) | Paginated wallet transaction history, with multi-wallet support. | Every component page has a live, interactive **Preview** you can toggle between light and dark, right next to the **Code** that produces it. The full Flexprice documentation, including every component on this page, as one plain-text file at `/llms-full.txt`. Open it to copy the raw text, or paste the link into an AI tool that can fetch URLs. As more Flexprice surfaces become exportable, they ship here under the same pattern: install, bring your own data, render. ## Install ```bash npm theme={null} npm install @flexprice/ui ``` ```bash pnpm theme={null} pnpm add @flexprice/ui ``` ```bash yarn theme={null} yarn add @flexprice/ui ``` ### Peer dependencies `react` and `react-dom` (v18). ### Stylesheet Import the stylesheet once in your app: ```tsx theme={null} import '@flexprice/ui/style.css'; ``` You can also override appearance with CSS variables, Tailwind, CSS Modules, or other styling approaches — see [Theming](#theming). ## Ways to provide data Every component follows the same pattern: fetch data from a source you choose, map it into the presentational props the component expects, and render. ### Option 1: Flexprice SDK (recommended) ``` Frontend → Flexprice SDK → Flexprice APIs → Components ``` Best for existing Flexprice customers, live data, and automatic catalog updates. ### Option 2: Flexprice REST APIs ``` Frontend → Flexprice REST API → Components ``` Best for Server Components, a backend proxy, or custom authentication. ### Option 3: Your own backend ``` Frontend → Your backend → Flexprice → Components ``` Your backend can cache responses, add auth, transform data, or merge internal fields. ### Option 4: Your own APIs ``` Frontend → Your API → Components ``` If your APIs already expose the data you need, map them into each component's prop shape. No Flexprice API dependency is required. ### Option 5: Static JSON Best for documentation sites, marketing pages, and demos. ### Which approach to use | Approach | Best for | | ----------------------- | ---------------------------------------- | | **Flexprice SDK** | Existing Flexprice users, live data | | **Flexprice REST APIs** | Direct API control with live data | | **Backend proxy** | Auth, caching, or server-side transforms | | **Custom APIs** | Systems outside Flexprice | | **Static JSON** | Marketing sites, demos, documentation | Component pages document the exact prop shapes and any adapters for mapping Flexprice API responses. ## Theming Every component renders inside an element carrying the `flexprice-ui` class, and that element is where the theme tokens are declared. Override them with a selector that **matches that element** — `--primary` is the accent hook: ```css theme={null} .my-app .flexprice-ui { --primary: 243 75% 59%; } ``` Setting these variables on a wrapping *ancestor* has no effect. The package declares them on `.flexprice-ui` itself, so an ancestor rule loses the cascade and the components render unchanged. Target `.flexprice-ui` (or an element that also carries the class). Values are HSL channels — `243 75% 59%`, not `hsl(243 75% 59%)` or `#4f46e5`. Add the `dark` class to any ancestor to toggle dark mode: ```tsx theme={null}
{/* Flexprice UI components */}
``` Individual components may also accept theme-related props — see each component page. ## Event callbacks Components expose callbacks for user actions (selection, checkout, contact sales, and similar). Wire them to analytics, [Checkout](/docs/checkout/overview), a payment provider, or your own flow. See each component page for the callbacks it supports. ## Server-side rendering Components work with SSR in Next.js (App Router and Pages Router), Remix, and Astro. Fetch data on the server when you can, then pass it as props to avoid client-only waterfalls. ## Typical integration flow Add `@flexprice/ui` and ensure `react` / `react-dom` are available as peer dependencies. Import `@flexprice/ui/style.css` once, then import the components you need. Pick Flexprice SDK, REST APIs, your backend, your own APIs, or static JSON — see [Ways to provide data](#ways-to-provide-data). Load records from your source and map them into each component's presentational props (adapters are documented on the component pages). Pass the mapped data (and any callbacks) into the component and render it in your page or layout. ## Future components The package is designed to expand beyond the current exports. Planned surfaces include invoices, subscription details, and the full customer portal. New components follow the same pattern: install from `@flexprice/ui`, load data from any source, map into props, and render. # Pricing Card Source: https://docs.flexprice.io/docs/exportable-ui/pricing-widget/pricing-card Individual plan card for dashboards, billing pages, upgrade modals, and custom layouts Use **Pricing Card** to render a single plan anywhere in your product. Compose multiple cards yourself, or use [Pricing Table](/docs/exportable-ui/pricing-widget/pricing-table) for a ready-made grid. ```tsx theme={null} ``` ## Supports * Feature list * Custom badges * Billing cadence * Usage pricing * Trial messaging * CTA buttons ## Best used for * Dashboard * Billing page * Subscription selector * Upgrade modal * Checkout ## Import ```tsx theme={null} import '@flexprice/ui/style.css'; import { PricingCard } from '@flexprice/ui'; ``` ## Usage ```tsx theme={null} ``` Pass a presentational plan from the Flexprice SDK, REST, your backend, or static JSON. See [Ways to provide data](/docs/exportable-ui/overview#ways-to-provide-data). ## Map Flexprice API data ```ts theme={null} import { adaptPlanToCard, filterAndSortPlans } from '@flexprice/ui'; const [plan] = filterAndSortPlans(plansWithData, 'USD', 'MONTHLY').map((p) => adaptPlanToCard(p, grants) ); ``` ## See also Install Flexprice UI, choose a data source, theme, and integrate components. Comparison table for multiple plans, features, and entitlements. # Pricing Table Source: https://docs.flexprice.io/docs/exportable-ui/pricing-widget/pricing-table Comparison table for showcasing multiple plans, features, and entitlements Use **Pricing Table** to show a comparison table of multiple plans, with billing period and currency controls built in. ```tsx theme={null} const plans: Plan[] = /* fetch + map */; router.push(`/checkout/${planId}`)} /> ``` ## Supports * Unlimited plans * Feature comparison * Usage limits and included entitlements * Custom pricing units * Custom CTA * Recommended plan highlighting * Billing period and currency controls ## Best used for * Pricing pages * Documentation * Sales pages ## Import ```tsx theme={null} import '@flexprice/ui/style.css'; import { PricingTable, type Plan } from '@flexprice/ui'; ``` ## Usage ```tsx theme={null} const plans: Plan[] = /* fetch + map */; router.push(`/checkout/${planId}`)} /> ``` Wire `onSelectPlan` to [Checkout](/docs/checkout/checkout-sessions) or your own flow. See [Ways to provide data](/docs/exportable-ui/overview#ways-to-provide-data) for SDK, API, backend, and static JSON options. ## Map Flexprice API data ```ts theme={null} import { adaptPlanToCard, filterAndSortPlans } from '@flexprice/ui'; const plans = filterAndSortPlans(plansWithData, 'USD', 'MONTHLY').map((p) => adaptPlanToCard(p, grants) ); ``` ## See also Install Flexprice UI, choose a data source, theme, and integrate components. Single plan card for dashboards, modals, and custom layouts. # Metric Cards Source: https://docs.flexprice.io/docs/exportable-ui/usage-widgets/metric-cards Grid of revenue, cost, margin, and custom usage metrics Use **Metric Cards** to show a row of headline numbers: revenue, cost, margin, margin percent, and any custom metrics you define. Part of [Usage Widgets](/docs/exportable-ui/usage-widgets/usage-widgets). ```tsx theme={null} ``` ## Supports * Revenue, cost, margin, and margin percent cards * Custom metrics (any name and value you pass in) * Currency formatting per card * Percent formatting * Change indicators (up / down) * Loading skeleton state ## Best used for * Usage dashboards * Customer portal overview * Account summary pages ## Import ```tsx theme={null} import '@flexprice/ui/style.css'; import { MetricCards, type MetricCardItem } from '@flexprice/ui'; ``` ## Usage ```tsx theme={null} const metrics: MetricCardItem[] = [ { id: 'revenue', titleKey: 'revenue', value: 12500, currency: 'USD' }, { id: 'cost', titleKey: 'cost', value: 4200, currency: 'USD' }, { id: 'margin', titleKey: 'margin', value: 8300, currency: 'USD', showChangeIndicator: true }, { id: 'margin-percent', titleKey: 'marginPercent', value: 66.4, isPercent: true, showChangeIndicator: true }, ]; ``` `titleKey` is one of `revenue`, `cost`, `margin`, `marginPercent`, `cpm`, or `custom`. For `custom`, also pass `customLabel` with the display name. See [Ways to provide data](/docs/exportable-ui/overview#ways-to-provide-data) for SDK, API, backend, and static JSON options. ## Map Flexprice API data ```ts theme={null} import { adaptMetricCards } from '@flexprice/ui'; const metrics = adaptMetricCards(costAnalytics, customAnalytics, { show_revenue_metric: true, show_cost_metrics: true, show_custom_metrics: true, }); ``` ## See also Overview of all four usage components. Line chart of usage over time, one series per feature. Grouped, sortable table of usage and cost by feature. # Usage Breakdown Source: https://docs.flexprice.io/docs/exportable-ui/usage-widgets/usage-breakdown Grouped, sortable table of usage and cost by feature Use **Usage Breakdown** to show a detailed, sortable table of usage and cost per feature, with optional grouping. Part of [Usage Widgets](/docs/exportable-ui/usage-widgets/usage-widgets). ```tsx theme={null} ``` ## Supports * Sort by total usage or total cost * Grouping (features roll up under a group, with an ungrouped bucket for the rest) * Expand / collapse all groups * Custom units per row * Loading skeleton state * Empty state when there is no data ## Best used for * Usage dashboards * Customer portal usage tab * Cost analysis pages ## Import ```tsx theme={null} import '@flexprice/ui/style.css'; import { UsageBreakdown, type UsageBreakdownRow } from '@flexprice/ui'; ``` ## Usage ```tsx theme={null} const rows: UsageBreakdownRow[] = [ { id: 'feat_api_calls', name: 'API Calls', groupId: 'grp_core', groupName: 'Core', totalUsage: 8200, unit: 'calls', totalCost: 41, currency: 'USD' }, { id: 'feat_storage', name: 'Storage', totalUsage: 42, unit: 'GB', totalCost: 12.5, currency: 'USD' }, ]; ``` Rows without a `groupId` render in an "ungrouped" bucket. See [Ways to provide data](/docs/exportable-ui/overview#ways-to-provide-data) for SDK, API, backend, and static JSON options. ## Map Flexprice API data ```ts theme={null} import { adaptUsageBreakdownRows } from '@flexprice/ui'; const rows = adaptUsageBreakdownRows(analytics.items); ``` ## See also Overview of all four usage components. Grid of revenue, cost, margin, and custom usage metrics. Line chart of usage over time, one series per feature. # Usage Quota Source: https://docs.flexprice.io/docs/exportable-ui/usage-widgets/usage-quota Progress bars showing current usage against plan limits for each metered feature Use **Usage Quota** to show a customer how much of each metered feature they've used against its plan limit, as a set of progress bars. Part of [Usage Widgets](/docs/exportable-ui/usage-widgets/usage-widgets). ```tsx theme={null} ``` ## Supports * One progress bar per metered feature * Unlimited features (rendered without a limit) * Over-limit styling * Empty state when there is nothing to show ## Best used for * Customer portal overview * Account settings / usage tab * Upgrade prompts near a limit ## Import ```tsx theme={null} import '@flexprice/ui/style.css'; import { UsageQuota, type UsageQuotaItem } from '@flexprice/ui'; ``` ## Usage ```tsx theme={null} const items: UsageQuotaItem[] = [ { id: 'feat_api_calls', name: 'API Calls', currentUsage: 8200, limit: 10000, isUnlimited: false }, { id: 'feat_storage', name: 'Storage', currentUsage: 42, limit: null, isUnlimited: true }, ]; ``` Only metered features have a quota to show: filter your data to metered entitlements before mapping into `items`. See [Ways to provide data](/docs/exportable-ui/overview#ways-to-provide-data) for SDK, API, backend, and static JSON options. ## Map Flexprice API data ```ts theme={null} import { adaptUsageQuotaItems } from '@flexprice/ui'; const items = adaptUsageQuotaItems(usageSummary.features); ``` ## See also Overview of all four usage components. Grouped, sortable table of usage and cost by feature. Grid of revenue, cost, margin, and custom usage metrics. # Usage Trend Chart Source: https://docs.flexprice.io/docs/exportable-ui/usage-widgets/usage-trend-chart Line chart of usage over time, one series per feature or event source Use **Usage Trend Chart** to plot usage over time. Each series draws its own line, so you can compare multiple features or event sources on the same chart. Part of [Usage Widgets](/docs/exportable-ui/usage-widgets/usage-widgets). ```tsx theme={null} ``` ## Supports * Multiple series (one line per feature or source) * Custom time ranges (pass whatever window of points you want) * Zoom and reset * Loading skeleton state * Empty state when there is no data ## Best used for * Usage dashboards * Customer portal usage tab * Account health pages ## Import ```tsx theme={null} import '@flexprice/ui/style.css'; import { UsageTrendChart, type UsageTrendSeries } from '@flexprice/ui'; ``` ## Usage ```tsx theme={null} const series: UsageTrendSeries[] = [ { id: 'feat_api_calls', name: 'API Calls', points: [ { timestamp: '2026-01-01T00:00:00Z', usage: 320 }, { timestamp: '2026-01-02T00:00:00Z', usage: 410 }, { timestamp: '2026-01-03T00:00:00Z', usage: 380 }, ], }, ]; ``` Filtering by feature (include or exclude a list of feature IDs) is handled before the data reaches the component: filter your `series` array, or filter at the adapter step below. See [Ways to provide data](/docs/exportable-ui/overview#ways-to-provide-data) for SDK, API, backend, and static JSON options. ## Map Flexprice API data ```ts theme={null} import { adaptUsageTrendSeries } from '@flexprice/ui'; const series = adaptUsageTrendSeries(analytics.items, { feature_filter_mode: 'all' }); ``` Pass `feature_filter_mode: 'include_list'` or `'exclude_list'` with `feature_ids` to filter which features appear. ## See also Overview of all four usage components. Grid of revenue, cost, margin, and custom usage metrics. Grouped, sortable table of usage and cost by feature. # Usage Widgets Source: https://docs.flexprice.io/docs/exportable-ui/usage-widgets/usage-widgets Metric cards, trend chart, breakdown table, and quota bars for showing customer usage **Usage Widgets** are four independent components for showing a customer's usage: [Metric Cards](/docs/exportable-ui/usage-widgets/metric-cards), [Usage Trend Chart](/docs/exportable-ui/usage-widgets/usage-trend-chart), [Usage Breakdown](/docs/exportable-ui/usage-widgets/usage-breakdown), and [Usage Quota](/docs/exportable-ui/usage-widgets/usage-quota). Use one on its own, or combine them into a usage dashboard. Each component is presentational and takes its own prop shape. There is no shared parent component: render the ones you need, in whatever layout fits your page. ## Components Grid of revenue, cost, margin, and custom usage metrics. Line chart of usage over time, one series per feature. Grouped, sortable table of usage and cost by feature. Progress bars for each metered feature against its plan limit. ## Import ```tsx theme={null} import '@flexprice/ui/style.css'; import { MetricCards, UsageTrendChart, UsageBreakdown, UsageQuota } from '@flexprice/ui'; ``` Each component page documents its own prop shape, an example, and the adapter for mapping Flexprice usage analytics API responses. ## See also Install Flexprice UI, choose a data source, theme, and integrate components. Wallet balance and transaction history components. # Architecture Source: https://docs.flexprice.io/docs/getting-started/architecture How Flexprice is engineered for scale, reliability, and data integrity — the infrastructure, data flows, failure modes, and recovery guarantees behind the platform. Flexprice is a usage metering, pricing, and billing engine that sits between your application and your payment providers. You stream usage events in, configure how each event, feature, or model is priced, and Flexprice handles everything downstream — metering, credit balances, entitlements, invoicing, and settlement — then plugs into your gateway of choice. This document describes how the platform is built: its components, how data moves through them, what happens when something fails, and how the system recovers. It is written for the engineers and architects who need to satisfy themselves that Flexprice can carry production billing traffic. ## Design principles The architecture is shaped by a small set of decisions that hold across every layer. Every billing outcome is derived from raw usage events. Those events are persisted redundantly and retained for replay, so any downstream state can be rebuilt from first principles. No single component failure causes data loss. Each stage of the pipeline has an independent durable store that absorbs the failure of the stage after it. Every component is region-restricted. Data for a region never leaves it, and each region runs an independent stack. The same artifacts that run Flexprice Cloud ship as Helm charts. Every dependency is open source and swappable, so a dedicated deployment is the cloud architecture, not a fork. ## System architecture The platform is fully containerized. A single codebase runs in three modes — **API**, **Consumer**, and **Background Worker** — behind a load balancer, with a private data tier (multi-AZ) and a set of external components for analytics, orchestration, and webhook delivery. Only the API service is exposed to the internet. It sits behind an Application Load Balancer and a WAF in public subnets; every database, broker, and cache lives in private subnets, across multiple availability zones, with no inbound internet route. Flexprice system architecture: customer infrastructure sends events via SDK or the Flexprice collector through an Application Load Balancer and WAF into multi-AZ private subnets running the API, Consumer, and Background Worker services alongside MSK, RDS, ElastiCache, DynamoDB, and S3, with ClickHouse, Temporal, and Svix as external components. ### 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. Flexprice event ingestion pipeline: the SDK or collector posts to /events async with retries; on success the request passes the WAF to the API service, which dual-writes each event to DynamoDB and to MSK before the Consumer enriches and writes to ClickHouse. When retries are exhausted, events are written to an S3 bucket and replayed by a retry job. 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 Environments ## 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 API Keys Page 2. Click **Add** to create a new API key Create 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** Created API Key 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: AWS architecture for Flexprice **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**. Invoices Section * 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**. Invoice List * Choose from: Invoice Details * **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: Download PDF * **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. Invoice Actions # 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. Invoice Overview Invoices are essential for: * Tracking revenue and outstanding payments. * Maintaining financial records for compliance. * Enabling clear and transparent billing for customers. Flexprice allows you to create, manage, and update invoices directly from the dashboard or through the API. **Invoice Lifecycle** Invoices progress through the following states: * **Draft** – Created but not yet finalized; can be edited. * **Finalized** – Locked and ready for payment processing. * **Paid** – The customer has successfully made the payment. * **Failed** – Payment attempt failed (e.g., insufficient funds, expired card). * **Void** – The invoice is canceled and will not be processed further. # Partial payments Source: https://docs.flexprice.io/docs/invoices/partial-payments Flexprice supports partial payments for invoices, allowing businesses to configure whether invoices can be partially settled using available credits. This ensures a flexible payment process for customers who may not have enough credits to cover the entire invoice. By enabling partial payments, businesses can: * Automatically deduct available credits from an invoice before charging the remaining balance. * Allow invoices to be settled partially if the customer doesn’t have enough credits. Important point to note is: * Partial payments only apply to invoices where credits are eligible for that payment. * If partial payments are disabled, invoices remain unpaid (pending) until the full amount is covered **How to enable patial payments using Flexprice** * **When partial payments are allowed** * If a customer **doesn’t have enough credits** to cover the full invoice, the available credits will be **partially applied**. * The remaining balance can be settled via the customer’s **default payment method** or remain as **outstanding**. * The invoice will reflect **partial payment status** until fully paid. * **When partial payments are not allowed** * The invoice remains in **Finalized** state with a **Pending payment status** until the **full amount is paid**. * No credits are applied unless the available credits cover the **entire invoice amount**. # Apply Coupon to a Subscription Source: https://docs.flexprice.io/docs/product-catalogue/coupons/apply-discount-on-subscription Link coupons to subscriptions at creation or post-creation. Use subscription phases to schedule discount windows. You can attach coupons at subscription creation time. For post-creation changes (add, remove, or schedule a future removal), use the subscription modification API. ## Before you start The coupon must be: * Status: `active` * Within its `redeem_after` / `redeem_before` window (if set) * Below its `max_redemptions` limit (if set) Validation runs at association time. If any condition fails, you get a validation error before the association is created. ## At subscription creation Pass coupons in the `subscription_coupons` array. Each entry takes a `coupon_code` (the human-readable code from the coupon object), optional `start_date`/`end_date`, and an optional `price_id` to target a specific line item. ### Subscription-level coupon Discounts the full invoice subtotal. Omit `price_id`: ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/subscriptions \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "customer_id": "", "plan_id": "", "start_date": "2025-01-01T00:00:00Z", "subscription_coupons": [ { "coupon_code": "SUMMER15" } ] }' ``` `start_date` defaults to the subscription start date. `end_date` is optional: omit it to let the coupon run until its own `cadence` ends or the subscription cancels. ### Line-item coupon Discounts one specific price in the plan only. Set `price_id` to the target price: ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/subscriptions \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "customer_id": "", "plan_id": "", "start_date": "2025-01-01T00:00:00Z", "subscription_coupons": [ { "coupon_code": "USAGE10", "price_id": "" } ] }' ``` ## Scheduled discounts via phases Subscription phases are the recommended way to apply time-bounded coupons. Each phase has its own `start_date` and optional `end_date`. Coupons in a phase inherit those dates as their association window. ### Example: introductory discount for Q1 only ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/subscriptions \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "customer_id": "", "plan_id": "", "start_date": "2025-01-01T00:00:00Z", "phases": [ { "start_date": "2025-01-01T00:00:00Z", "end_date": "2025-03-31T23:59:59Z", "subscription_coupons": [ { "coupon_code": "Q1PROMO" } ] }, { "start_date": "2025-04-01T00:00:00Z" } ] }' ``` The coupon is active during Phase 1 only. Phase 2 has no coupon, so invoices from April onwards are at full price. ### Example: line-item discount scoped to a phase ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/subscriptions \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "customer_id": "", "plan_id": "", "start_date": "2025-01-01T00:00:00Z", "phases": [ { "start_date": "2025-01-01T00:00:00Z", "end_date": "2025-06-30T23:59:59Z", "subscription_coupons": [ { "coupon_code": "USAGE10", "price_id": "" } ] }, { "start_date": "2025-07-01T00:00:00Z" } ] }' ``` ## Common scheduling patterns | Goal | Phase configuration | | ---------------------------------------- | ---------------------------------------------------------------------------------- | | Discount for first 3 months | Phase 1: `end_date` = 3 months out, `coupons: [id]`. Phase 2: no coupon. | | Discount starting 6 months in | Phase 1: no coupon. Phase 2: `start_date` = 6 months in, `coupons: [id]`. | | Remove a discount on a known future date | Phase 1: today to removal date with coupon. Phase 2: from removal date, no coupon. | | Discount on one charge only | In `subscription_coupons`, set `price_id` to the target price ID. | ## Add or remove a coupon after subscription creation Use the subscription modification API with `type: "coupon"`. ### Add a coupon ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/subscriptions//modify/execute \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "type": "coupon", "coupon_params": { "action": "add", "coupon_code": "", "start_date": "2025-06-01T00:00:00Z", "end_date": "2025-08-31T23:59:59Z" } }' ``` `start_date` defaults to now if omitted. `end_date` is optional: omit it to apply the coupon indefinitely. To target a specific line item instead of the full invoice, add `subscription_line_item_id` (mutually exclusive with `subscription_id`): ```bash theme={null} { "type": "coupon", "coupon_params": { "action": "add", "coupon_code": "", "start_date": "2025-06-01T00:00:00Z", "subscription_line_item_id": "" } } ``` ### Remove a coupon ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/subscriptions//modify/execute \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "type": "coupon", "coupon_params": { "action": "remove", "coupon_association_id": "" } }' ``` Get the `coupon_association_id` from the subscription response with `expand=coupon_associations`. Removal affects only invoices generated after the request. Previously issued invoices are not changed. ### Preview before executing ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/subscriptions//modify/preview \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "type": "coupon", "coupon_params": { "action": "add", "coupon_code": "", "start_date": "2025-06-01T00:00:00Z" } }' ``` ## subscription\_coupons fields (at creation) | Field | Type | Required | Description | | ------------- | --------- | -------- | ------------------------------------------------------------------------------ | | `coupon_code` | string | Yes | The coupon's `code` value (case-insensitive) | | `start_date` | timestamp | No | When the association starts. Defaults to the subscription or phase start date. | | `end_date` | timestamp | No | When the association ends. Omit for indefinite. | | `price_id` | string | No | Target a specific line item. Omit for subscription-level. | ## coupon\_params fields (post-creation modification) | Field | Type | Required | Description | | --------------------------- | --------- | -------------------- | ----------------------------------------------------------------------- | | `action` | string | Yes | `add` or `remove` | | `coupon_code` | string | When `action=add` | Code of the coupon to attach | | `coupon_association_id` | string | When `action=remove` | ID of the coupon association to detach | | `start_date` | timestamp | No | When the association starts. Defaults to now. | | `end_date` | timestamp | No | When the association ends. Omit for indefinite. | | `subscription_line_item_id` | string | No | Target a specific line item. Mutually exclusive with `subscription_id`. | ## How discounts appear on invoices Discount amounts are tracked at two levels on each line item: | Field | Description | | ------------------------ | ------------------------------------------------------------------------------------------ | | `line_item_discount` | Discount applied directly to this line item (from a line-item coupon targeting this price) | | `invoice_level_discount` | Invoice-wide coupon discount prorated to this line item | At the invoice level, `total_discount` is the sum of all coupon discounts across all line items. The discount terms are snapshotted when the coupon is applied, so they are preserved even if the coupon definition is later updated. ## Check active coupons on a subscription ```bash theme={null} curl "https://us.api.flexprice.io/v1/subscriptions/?expand=coupon_associations" \ -H "x-api-key: " ``` ## Validation errors | Error | Cause | | ------------------------- | -------------------------------------- | | `coupon_not_active` | Coupon is not in `active` status | | `coupon_expired` | Current time is after `redeem_before` | | `coupon_not_yet_valid` | Current time is before `redeem_after` | | `max_redemptions_reached` | `total_redemptions >= max_redemptions` | # Create a Coupon Source: https://docs.flexprice.io/docs/product-catalogue/coupons/create Create percentage or fixed-amount discount coupons via API or dashboard. ## Via API ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/coupons \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "name": "Summer Sale", "type": "percentage", "percentage_off": "15.00", "cadence": "once", "coupon_code": "SUMMER15", "redeem_after": "2025-07-01T00:00:00Z", "redeem_before": "2025-08-31T23:59:59Z", "max_redemptions": 1000 }' ``` **Response** ```json theme={null} { "id": "coup_01abc123", "name": "Summer Sale", "type": "percentage", "percentage_off": "15.00", "cadence": "once", "coupon_code": "SUMMER15", "redeem_after": "2025-07-01T00:00:00Z", "redeem_before": "2025-08-31T23:59:59Z", "max_redemptions": 1000, "total_redemptions": 0, "status": "active" } ``` ## Request fields | Field | Type | Required | Description | | --------------------- | -------------- | ----------------------- | ------------------------------------------------------------------------------------------------- | | `name` | string | Yes | Display name shown in the dashboard and invoice details | | `type` | string | Yes | `percentage` or `fixed` | | `cadence` | string | Yes | `once`, `repeated`, or `forever` | | `percentage_off` | decimal string | When `type=percentage` | Discount as a percent, e.g. `"15.00"` for 15% | | `amount_off` | decimal string | When `type=fixed` | Flat discount amount, e.g. `"50.00"` | | `currency` | string | When `type=fixed` | ISO currency code, e.g. `"USD"` | | `duration_in_periods` | int | When `cadence=repeated` | Number of billing cycles the discount applies. Must not be set for other cadences. | | `coupon_code` | string | No | Human-readable code customers can enter to redeem (e.g. `"SUMMER15"`). Auto-generated if omitted. | | `redeem_after` | timestamp | No | Earliest time a new association can be created | | `redeem_before` | timestamp | No | Expiry. New associations cannot be created after this time. | | `max_redemptions` | int | No | Cap on total associations across all subscriptions | | `metadata` | object | No | Key-value pairs for your own tracking | ## Cadence behaviour | Cadence | What happens | | ---------- | --------------------------------------------------------------------- | | `once` | Discount applies to the first invoice in the association window only | | `repeated` | Discount applies for `duration_in_periods` consecutive billing cycles | | `forever` | Discount applies to every invoice for the life of the subscription | ## Fixed-amount coupon For a fixed dollar amount off, set `type: fixed` and provide both `amount_off` and `currency`: ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/coupons \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "name": "New Customer Credit", "type": "fixed", "amount_off": "50.00", "currency": "USD", "cadence": "once" }' ``` ## Recurring coupon To discount multiple billing cycles, use `cadence: repeated` with `duration_in_periods`: ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/coupons \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "name": "Quarterly Promo", "type": "percentage", "percentage_off": "20.00", "cadence": "repeated", "duration_in_periods": 3 }' ``` This applies a 20% discount for the first 3 billing cycles of each association. ## Manage existing coupons ```bash theme={null} # List coupons curl https://us.api.flexprice.io/v1/coupons \ -H "x-api-key: " # Get a coupon curl https://us.api.flexprice.io/v1/coupons/ \ -H "x-api-key: " # Search coupons by status curl -X POST https://us.api.flexprice.io/v1/coupons/search \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "status": "active" }' # Update a coupon (only name and metadata are updatable) curl -X PUT https://us.api.flexprice.io/v1/coupons/ \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "name": "Summer Sale 2025" }' # Delete a coupon curl -X DELETE https://us.api.flexprice.io/v1/coupons/ \ -H "x-api-key: " ``` ## Via dashboard Go to **Product Catalog** > **Coupons** > **Create Coupon** and fill in the same fields described above. ## Next step [Apply the coupon to a subscription](/docs/product-catalogue/coupons/apply-discount-on-subscription) at creation time or post-creation via the subscription modification API. # Coupons Overview Source: https://docs.flexprice.io/docs/product-catalogue/coupons/overview Create percentage or fixed-amount discounts and apply them to subscriptions at the invoice or line-item level. A coupon is a reusable discount definition. You create it once, then link it to one or more subscriptions via an **association**. Each association can be scoped to a time window and optionally targeted at a specific line item rather than the whole invoice. ## Coupon types | Type | Discount field | Currency required | Example | | ------------ | ---------------- | ----------------- | ------------------- | | `percentage` | `percentage_off` | No | 15% off the invoice | | `fixed` | `amount_off` | Yes | \$50 off | ## Cadence Cadence controls how many billing cycles the discount repeats within an association's active window. | Cadence | Behavior | | ---------- | ------------------------------------------------------------ | | `once` | Applied to the first invoice of the association period only | | `repeated` | Applied for `duration_in_periods` consecutive billing cycles | | `forever` | Applied to every invoice for the life of the subscription | ## Validity controls These fields live on the coupon itself and gate whether a new association can be created. | Field | Purpose | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `redeem_after` | Earliest timestamp a coupon can be associated to a subscription | | `redeem_before` | Expiry timestamp. Associations cannot be created after this date. Existing associations created before expiry remain active. | | `max_redemptions` | Maximum total associations allowed across all subscriptions | The `total_redemptions` counter increments when the association is **created**, not when an invoice is generated. ## Coupon scope **Subscription level**: discount applies to the invoice subtotal (or the running subtotal after earlier coupons have applied). **Line-item level**: discount applies to a specific price within the invoice before any subscription-level coupons run. ## How multiple coupons apply When an invoice has both line-item and subscription-level coupons: 1. Line-item coupons apply first, reducing each targeted line item's amount individually. 2. Subscription-level coupons apply next, in association order, each seeing the subtotal left after the previous coupon. 3. Wallet credits are deducted after all coupon discounts. 4. Tax is calculated on `MAX(subtotal - all_discounts, 0)`. Tax is never charged on discounted-away amounts. Two 20% subscription-level coupons do not produce a 40% total discount. The second coupon applies to the amount remaining after the first. See [Billing Calculation Order](/docs/invoices/billing-calculation-order) for the full worked example. ## Coupon statuses | Status | Meaning | | ---------- | ------------------------------------- | | `draft` | Created but not yet usable | | `active` | Can be associated to subscriptions | | `archived` | No longer usable for new associations | Only `active` coupons pass association validation. ## Quick start 1. [Create a coupon](/docs/product-catalogue/coupons/create) with a type, value, and optional validity window. 2. [Apply it to a subscription](/docs/product-catalogue/coupons/apply-discount-on-subscription) at creation time or via subscription phases for scheduled windows. # Custom currency Source: https://docs.flexprice.io/docs/product-catalogue/custom-pricing/custom-currency Define your own currencies, price your catalogue in them, and let Flexprice settle invoices in fiat Custom currency lets you price your catalogue in a unit of your own, such as credits or tokens. You configure prices and send usage in your currency, and Flexprice issues the invoice in a fiat settlement currency. **Benefits:** * **No conversion logic on your side**: price, meter, and report in your own unit. Flexprice converts once, at invoicing. * **Unchanged settlement**: invoices are denominated in fiat, so payment providers, accounting exports, and ledger integrations are unaffected. * **Defined once, used everywhere**: a currency is configured in one place and available to **prices**, **subscriptions**, **wallets**, **addons**, and **coupons**. Custom currency and [pricing units](/docs/product-catalogue/custom-pricing/pricing-unit) address different problems. A pricing unit applies to a single price and is converted when that price is created. A custom currency is defined once and applies to every entity created in it, including subscriptions and wallets. ## Configure Define the currencies you need for your environment in the `custom_currency_config` setting. See [Settings](/docs/settings/settings#custom-currency-configuration) for the full schema and validation rules. ```json theme={null} { "custom_currencies": { "fpc": { "name": "FinePrint Credits", "symbol": "FPC", "fiat_conversion_factors": { "usd": "0.10", "inr": "8.50" } } }, "default_fiat_currency": "usd" } ``` ## Conversion factors A conversion factor states how much fiat **one unit** of your currency is worth. ```text theme={null} fiat amount = custom amount × conversion factor ``` With `"usd": "0.10"`, one credit is worth \$0.10, so 150 credits invoice as \$15.00. ## What to make sure Give every custom currency a factor for the same fiat currencies. If one currency defines `inr` and another does not, the configuration is rejected. * **Currency codes are immutable.** Prices, subscriptions, and wallets store the code as their currency. Supplying a different code adds a currency rather than renaming the existing one. * **A subscription bills in one currency.** A plan can carry charges in both a custom currency and fiat. A subscription bills only the charges matching its own currency. * **Names and symbols are safe to change.** They are read at display time and are not stored on any entity. ## Invoicing Invoices are denominated in the `default_fiat_currency` you configure. The amounts in your currency are retained on the invoice and on each line item, so both are available on the API response and in the dashboard. ```json theme={null} { "currency": "usd", "subtotal": "15", "total": "15", "amount_due": "15", "custom_currency": { "code": "fpc", "rate": "0.1", "subtotal": "150", "total_discount": "0", "total_tax": "0", "total_prepaid_credits_applied": "0", "total": "150", "amount_due": "150" } } ``` The conversion factor is recorded when the invoice is finalized. Changing a factor afterwards does not affect invoices that are already finalized. ## Use cases If you already track consumption internally in credits, configure that as your currency and send usage in it directly, no exchange logic on your side, no conversion before sending it to Flexprice. Pricing, metering, entitlements, and reporting all stay in the unit you already use. Flexprice handles conversion and settlement. You can also use it to sell credits to customers: publish prices in credits so customers see one unit across your catalogue, while invoices and payments settle in the currency you collect and report in. # Pricing unit Source: https://docs.flexprice.io/docs/product-catalogue/custom-pricing/pricing-unit This feature allows you to create and use your own currency in Flexprice. With create Price Units, add them to plans and wallets in custom currency. **Custom pricing** allows you to price features using your own unit (such as credits) instead of only standard currencies like USD or EUR. ### What is custom currency? **Custom currency** is pricing in your own unit (such as credits) instead of a standard money currency like USD or EUR. You set and show prices in that unit-for example, "10 credits per month"-and **Flexprice converts those amounts to your base currency (e.g. USD) for billing**, This ensures a consistent currency for accounting, even though the feature is shown in credits. *** ## How it works internally All billing calculations are performed using the pricing unit’s base currency (for example, USD). Amounts are defined and displayed in the **pricing unit** (e.g. credits), Conversion happens only at price or wallet creation; Flexprice uses the base currency for all calculations. In other words: input and display use the pricing unit; computation, persistence, and invoicing use base currency. Conversion uses the formula **amount in your unit × conversion\_rate = amount in base currency**. Rounding and precision (e.g. two decimals for USD, zero for JPY) are applied only when **invoice charges are computed**, not at conversion or when creating prices or price units. Wallet balance is always shown in **base currency and credits**, whether the wallet was created with fiat or a custom price unit. Flexprice **stores and bills** everything in the base currency (e.g. USD). *** ## Using custom currency 1. **Create** a Price Unit (dashboard or API, below). 2. **Use in plans** — add charges or create prices in your unit; see [Use cases](/docs/product-catalogue/custom-pricing/use-cases#use-custom-currency-in-plans-and-prices). 3. **Use in wallets** — create a wallet in your unit; see [Use cases](/docs/product-catalogue/custom-pricing/use-cases). *** ## Use cases * **Credits or tokens** - Sell in credits or tokens; show balance and charges in that unit while billing and storing in base currency (e.g. USD). * **Multi-currency display, single-currency billing** - Present prices in a custom unit per region or product while retaining one base currency for accounting and payouts. *** ## Concepts and terminology **Custom currency / Price Unit** - Your own pricing unit (e.g. credits) used instead of fiat currency. Defined once as a Price Unit and reused across plans, prices, and wallets, with automatic conversion to the base currency for billing. | Field | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | **code** | **3-character** identifier (e.g. `CRD`, `TOK`, `fpc`) used when referencing the unit in a price or wallet. Must be unique per environment when Active. | | **symbol** | Display symbol shown in the UI and on invoices (e.g. ¢, ==). | | **base\_currency** | The standard currency the unit is pegged to (e.g. `usd`). All billing and storage use this currency. | | **conversion\_rate** | Multiplier from the custom unit to base currency. | **Conversion formula** ```text theme={null} amount in your unit × conversion_rate = amount in base currency ``` **Example:** If 1 credit = 0.01 USD, set `conversion_rate = "0.01"`. Then 100 credits = 1.00 USD. **Price unit types: FIAT vs CUSTOM** * **FIAT** - Amounts are in a standard currency (USD, EUR, etc.). No conversion is applied. * **CUSTOM** - Amounts are in a Price Unit. Flexprice converts to base currency for billing; you configure and display values in the custom unit. *** ## Create a Price Unit (dashboard) Go to **Product Catalog** → **Price Units** → **Add** (or **Create Price Unit**). Enter **Name**, **Code**, **Symbol**, **Base currency**, and **Conversion rate**.\ Example: 100 units = 1 USD → use conversion rate **0.01**. Create Price Unit *** ## 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. Currency dropdown - Custom and Standard Add Recurring and Usage Based charges *** ## 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. Wallet in custom currency **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) MAX feature with Bucket Size and Group By configured *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). Boolean Feature Creation ## How to create a Static feature Select **Static** as feature type which represents fixed attributes or entitlements (e.g., priority support) Static Feature Creation ## 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. Add Entitlement with Config feature JSON editor 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. Entitlements tab showing Config feature value 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". Metered Feature Creation ### Define Event Details 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. Aggregation Settings #### 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. Aggregation section with the Custom expression toggle highlighted Enter a CEL formula in the **Custom Expression** field. Variables are the top-level property names on your events. Custom Expression field with ceil(duration_ms / 1000) and inline help text listing available functions On save, the expression is compiled and validated. If it has a syntax error, references no variables, or evaluates to a non-numeric type, saving fails with the error. ## Supported Functions The following math helpers are available inside expressions. All take and return numbers. | Function | Signature | Description | | ----------- | --------------------------- | ------------------------------------ | | `max(a, b)` | `(number, number) → number` | Returns the larger of `a` and `b` | | `min(a, b)` | `(number, number) → number` | Returns the smaller of `a` and `b` | | `pow(x, y)` | `(number, number) → number` | Raises `x` to the power `y` | | `abs(x)` | `(number) → number` | Absolute value | | `ceil(x)` | `(number) → number` | Rounds up to the nearest integer | | `floor(x)` | `(number) → number` | Rounds down to the nearest integer | | `round(x)` | `(number) → number` | Rounds half away from zero | | `sqrt(x)` | `(number) → number` | Square root; errors if `x < 0` | | `log(x)` | `(number) → number` | Natural logarithm; errors if `x ≤ 0` | Standard CEL operators are also available: `+`, `-`, `*`, `/`, `<`, `<=`, `>`, `>=`, `==`, `!=`, `&&`, `||`, `!`, and the ternary `cond ? a : b`. Modulo (`%`) is not available because all values are typed as doubles. Use `x - floor(x / y) * y` if you need a remainder. ## Calculation Example ### Configuration * **Event Name**: `phone_call` * **Aggregation Function**: Sum * **Custom Expression**: `ceil(duration_ms / 1000)` * **Unit Name**: `second / seconds` ### Event Data ```json theme={null} [ { "event_id": "evt_001", "event_name": "phone_call", "external_customer_id": "customer_123", "timestamp": "2026-07-09T10:00:00Z", "properties": { "duration_ms": 4200 } }, { "event_id": "evt_002", "event_name": "phone_call", "external_customer_id": "customer_123", "timestamp": "2026-07-09T10:05:00Z", "properties": { "duration_ms": 8100 } }, { "event_id": "evt_003", "event_name": "phone_call", "external_customer_id": "customer_123", "timestamp": "2026-07-09T10:10:00Z", "properties": { "duration_ms": 500 } } ] ``` ### Calculation Process 1. **Per-event quantity** — Evaluate `ceil(duration_ms / 1000)` on each event * `evt_001` → `ceil(4200 / 1000)` = `5` * `evt_002` → `ceil(8100 / 1000)` = `9` * `evt_003` → `ceil(500 / 1000)` = `1` 2. **Aggregation** — SUM across events: `5 + 9 + 1 = 15` **Result**: `15 seconds` Without the expression, a plain `SUM(duration_ms)` would have produced `12,800` — the expression bills whole seconds with per-call rounding, which is what the pricing model requires. ## Use Cases ### Token-based AI billing **Perfect for**: LLM APIs that charge different rates for input vs. output tokens. **Expression**: `input_tokens + output_tokens * 3` ```json theme={null} { "event_name": "llm.completion", "external_customer_id": "acme_corp", "properties": { "input_tokens": 500, "output_tokens": 120, "model": "gpt-4" } } ``` *Aggregation: SUM. Each output token is billed as 3 units to reflect the higher output cost.* ### Rounded call duration (telephony) **Perfect for**: Voice APIs billed by the whole second or whole minute, with per-call rounding. **Expression**: `ceil(duration_ms / 1000)` ```json theme={null} { "event_name": "phone_call", "external_customer_id": "customer_123", "properties": { "duration_ms": 8100 } } ``` *Aggregation: SUM. A 8.1s call is billed as 9 seconds; the sum reflects total billable seconds.* ### Compute cost per event **Perfect for**: Infrastructure billing where quantity depends on multiple dimensions per job. **Expression**: `cores * hours * rate` ```json theme={null} { "event_name": "compute.job", "external_customer_id": "user_456", "properties": { "cores": 8, "hours": 2.5, "rate": 0.05 } } ``` *Aggregation: SUM. Each job contributes its own cost; the feature value is total spend.* ### Quota-clamped usage **Perfect for**: Fair-use plans that cap what a single event can contribute. **Expression**: `min(api_calls, 1000)` ```json theme={null} { "event_name": "api.batch", "external_customer_id": "merchant_789", "properties": { "api_calls": 4200 } } ``` *Aggregation: SUM. A batch of 4,200 calls contributes at most 1,000 to the metered total.* ### Pulse pricing (minimum billable unit) **Perfect for**: Charging in fixed pulses (e.g. 15-second SMS pulses, 6-second call pulses). **Expression**: `duration <= 10 ? 0 : ceil(duration / 15) * 15` ```json theme={null} { "event_name": "sms.send", "external_customer_id": "customer_101", "properties": { "duration": 18 } } ``` *Aggregation: SUM. Short pulses under 10 units are free; longer pulses round up to the next 15-unit block.* ### Data transfer with unit conversion **Perfect for**: Events emitted in bytes but billed in GB. **Expression**: `bytes / 1073741824` ```json theme={null} { "event_name": "data.transfer", "external_customer_id": "acme_corp", "properties": { "bytes": 5368709120 } } ``` *Aggregation: SUM. Each event converts bytes to GB; the feature value is total GB transferred.* ## Error Handling Expressions can fail either at **save time** (compile) or **event time** (evaluate). Compile errors block feature creation; evaluate errors cause the specific event's quantity to fail. ### Compile-time errors Surfaced when you save the feature. Fix the expression before saving. | Error | Cause | Example | | ------------------ | ----------------------------------------- | ------------- | | Syntax error | Malformed CEL | `a + b +` | | No variables | Expression has only literals | `1 + 2` | | Non-numeric result | Result is not a number | `a + "hello"` | | Unknown function | Function is not in the supported list | `tan(theta)` | | Wrong arity | Function called with wrong number of args | `max(tokens)` | ### Event-time errors Surfaced per event during ingestion. | Error | Cause | | ----------------------- | ---------------------------------------------------------------------------------- | | Property is not numeric | An identifier resolves to a non-numeric string (e.g. `"abc"`) | | Non-finite property | Property value is `NaN`, `+Inf`, or `-Inf` | | Non-finite result | Expression produced `NaN` or `±Inf` (division by zero, `sqrt(-1)`, `pow` overflow) | Test your expression against a few real payloads in a scratch event before switching production traffic. A quick way is to enable the feature on a test plan and confirm the aggregated value matches what you expect. ## Critical Notes * **Only SUM, AVG, MAX, and LATEST** support custom expressions today. * **Top-level properties only** — nested fields (e.g. `payload.tokens`) are not addressable; flatten them at ingestion. * **Missing properties default to `0`**, not an error. If a required property is missing on the event, the expression will still evaluate but the quantity will reflect that zero. * **CEL reserved keywords** cannot be property names inside expressions: `as`, `break`, `const`, `continue`, `else`, `false`, `for`, `function`, `if`, `import`, `in`, `let`, `loop`, `package`, `namespace`, `null`, `return`, `true`, `var`, `void`, `while`. Rename the property on your event if it clashes. * **Real division** — Integer literals are promoted to doubles, so `total / 1000` returns a real number, not a truncated integer. * **Expression changes apply to new events only** — Historical events are not re-evaluated when you edit the expression. ## Next Steps * **[SUM Aggregation](/docs/product-catalogue/features/aggregation/sum)** — The most common aggregation used with custom expressions * **[Creating a Metered Feature](/docs/event-ingestion/creating-a-metered-feature)** — Full feature setup walkthrough * **[Sending Events](/docs/event-ingestion/sending-events)** — How to structure `event.properties` for expression variables # Entitlement Grants Source: https://docs.flexprice.io/docs/product-catalogue/features/entitlement-grants Configure time-boxed usage quotas on entitlements — rolling windows, parallel limits, and amount-based caps with exhaustion alerts and overage billing Entitlement grants add time-boxed quotas on top of [entitlements](/docs/product-catalogue/features/linking-to-plans). A regular entitlement gives a customer a usage limit per billing period; a grant config turns it into a quota per window — "1M tokens per 5 hours" or "\$50 of compute per day" — independent of the billing cycle. Flexprice opens a concrete grant window when usage starts, tracks consumption against the quota, fires a webhook when the quota is exhausted, and bills usage beyond the quota as overage on the invoice. **Benefits:** * **Rolling limits** — Quotas measured in hours, days, or weeks instead of the billing period * **Parallel limits** — Stack independent windows on one feature, such as a 5-hour limit and a weekly limit * **Amount-based quotas** — Cap spend in currency, priced through your existing billing pipeline * **Exhaustion webhooks** — `entitlement.grant.exhausted` fires when a window's quota is used up * **Overage billing** — Usage beyond the quota lands on the arrears invoice automatically ## Concepts | Term | Meaning | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Entitlement config** | An entitlement that carries a grant config (`grant_measure`, `grant_duration_value`, `grant_duration_unit`, `grant_quota`, `aggregation_mode`). Setting these fields is the opt-in — there is no separate type flag. | | **Entitlement grant** | One concrete window instantiated from the config: a `valid_from`/`valid_to` range with an immutable quota and a usage snapshot. | | **Measure** | What the quota counts: `quantity` (raw meter units) or `amount` (currency, priced through the billing pipeline). | | **Aggregation mode** | How multiple grant configs on the same feature combine: `additive` merges quotas into one bucket, `parallel` keeps each config as its own independent bucket. | ## Configuring a Grant Add the grant fields to a [create entitlement](/api-reference/entitlements/create-entitlement) or [update entitlement](/api-reference/entitlements/update-entitlement) request. The feature must be metered. ```bash cURL theme={null} curl -X POST "https://us.api.flexprice.io/v1/entitlements" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "feature_id": "feat_tokens", "feature_type": "metered", "entity_type": "plan", "entity_id": "plan_pro", "is_enabled": true, "grant_measure": "quantity", "grant_quota": "1000000", "grant_duration_value": 5, "grant_duration_unit": "hour", "aggregation_mode": "parallel" }' ``` ### Grant Config Fields | Field | Description | | ---------------------- | ----------------------------------------------- | | `grant_measure` | `quantity` (meter units) or `amount` (currency) | | `grant_quota` | Quota per window, as a decimal string | | `grant_duration_value` | Window length, a positive integer | | `grant_duration_unit` | `hour`, `day`, or `week` | | `aggregation_mode` | `additive` (default) or `parallel` | The grant config is all-or-nothing: set every field or none. On update, `clear_grant_config: true` removes the config and returns the entitlement to legacy behavior. Grant configs are validated at write time: * The feature must be metered. Meters with `MAX` aggregation or bucketed aggregation are not supported. * All grant configs on one feature must share the same `grant_measure` and the same `aggregation_mode`. Additive configs must also share the same duration. * `amount` grants require flat per-unit pricing on the meter — tiered prices are rejected. A grant config whose duration is the billing period or longer is ignored at runtime — a window spanning the whole period is just the period quota. Use `usage_limit` with `usage_reset_period` for that case. ## How Grant Windows Work Windows are usage-anchored. Flexprice does not open a window on a fixed schedule; it opens one when usage arrives: 1. The first usage event past the covered range opens a window at that event's timestamp. An event at 2:00 opens a window starting at 2:00, even if evaluation runs at 2:07. 2. The window runs for the configured duration: `valid_to = valid_from + duration`. 3. When the window ends, nothing happens until the next usage event — idle periods open no windows, so an inactive customer accumulates no grant rows. 4. Windows never straddle billing cycles. A window that would end within an hour of the cycle boundary stretches to the boundary instead of leaving a short stub. Each grant tracks two states in `grant_status`: * `active` — usage is below the quota * `exhausted` — usage reached the quota (`usage >= quota`) Expiry is not a status. A grant is closed when `valid_to` has passed, regardless of how much quota was used. ### Grant Object | Field | Description | | -------------------------------------- | -------------------------------------------------------------------------------------------------- | | `id` | Grant identifier (`eg_` prefix) | | `entitlement_config_id` | The entitlement this window was instantiated from | | `customer_id`, `subscription_id` | Who the window belongs to | | `scope_entity_type`, `scope_entity_id` | What the grant meters — `feature` and the feature ID | | `measure` | `quantity` or `amount` | | `quota` | The window's quota (immutable; the summed quota for additive groups) | | `usage` | Usage consumed in this window, refreshed on each evaluation | | `valid_from`, `valid_to` | The window boundaries | | `grant_status` | `active` or `exhausted` | | `quota_crossed_at` | Set once, when evaluation first sees `usage >= quota`. Overage billing starts from this timestamp. | ## Aggregation Modes When a customer holds multiple grant configs on the same feature — a plan entitlement plus an addon entitlement, for example — `aggregation_mode` decides how they combine. ### Additive All configs merge into **one** window with `quota = sum of quotas`. Use this when an addon should top up the base allowance. **Example**: The Pro plan grants 100K tokens per day. A booster addon grants another 50K per day. The customer gets one daily window with a 150K quota. ### Parallel Each config opens its **own** independent window with its own duration. All windows meter the same usage stream. A request is within limits only while every applicable window has quota left; exhausting any one window puts the feature into overage for the remainder of that window. **Example**: 1M tokens per 5 hours *and* 5M tokens per week, as two parallel configs. Heavy use exhausts the 5-hour window first; sustained use over days exhausts the weekly window even though no single 5-hour burst did. ## Use Cases ### Rolling Rate Limits for an AI Product Cap token consumption in a rolling window that has nothing to do with the monthly invoice — the pattern used by AI assistants that advertise "resets every 5 hours". ```json theme={null} { "grant_measure": "quantity", "grant_quota": "1000000", "grant_duration_value": 5, "grant_duration_unit": "hour", "aggregation_mode": "parallel" } ``` Add a second parallel config with `"grant_duration_unit": "week"` to layer a weekly ceiling on top of the burst limit. ### Daily Spend Cap Cap what a customer's usage *costs* rather than how many units they consume. The `amount` measure prices usage through your existing meter prices, so a mid-window price change is respected. ```json theme={null} { "grant_measure": "amount", "grant_quota": "50", "grant_duration_value": 1, "grant_duration_unit": "day", "aggregation_mode": "additive" } ``` The customer gets \$50 of compute per day. Alert on exhaustion, or let usage continue and bill the excess as overage. ### Plan Plus Booster Addon Sell top-ups without new plumbing. The plan carries a grant config; the addon carries another on the same feature with `additive` mode. Buying the addon raises the shared window's quota by the addon's amount. With `parallel` mode instead, the addon grants a separate window — useful when the addon's allowance runs on its own clock. ### Soft Limits With Billed Overage Grants implement soft limits: usage is never blocked. When a window's quota is crossed, usage recorded after the crossing is billed as overage on the subscription's arrears invoice — quantity overage re-enters the pricer as billable units, amount overage lands directly as a charge. With parallel windows, overlapping overage periods are merged so a unit of usage is never billed twice. Overage folding skips line items with tiered prices, commitments, or true-up — those price against the full cycle, so grant overage on them falls back to the standard billing calculation. ## Exhaustion Alerts Evaluation runs after usage events arrive (debounced, \~5m30s by default). When a window's usage reaches its quota, the grant flips to `exhausted`, one alert log is written, and a webhook fires. Recovery is not a state transition — a fresh window with a fresh quota simply opens when the next one starts. ### Webhook Payload **Event type:** `entitlement.grant.exhausted` ```json theme={null} { "event_type": "entitlement.grant.exhausted", "alert_type": "entitlement_grant_exhausted", "alert_status": "in_alarm", "usage_ratio": "1.15", "triggered_at": "2026-07-25T20:20:23Z", "subscription": { "id": "subs_01KYARKSC3GVX64QEF4T4ABS9X", "customer_id": "cust_01KYARB3TVZ0RZPDGC5P40NHHC", "plan_id": "plan_pro", "subscription_status": "active", "currency": "usd", "billing_period": "MONTHLY", "current_period_start": "2026-07-24T19:11:06Z", "current_period_end": "2026-08-24T19:11:06Z" }, "customer": { "id": "cust_01KYARB3TVZ0RZPDGC5P40NHHC", "external_id": "cust-new", "name": "Acme Corp", "email": "billing@acme.com" }, "entitlement": { "id": "ent_01KYB5ESPNNB8DWFVQAFJN6KKZ", "feature_id": "feat_tokens", "feature_type": "metered", "grant_measure": "quantity", "grant_quota": "1000000", "grant_duration_value": 5, "grant_duration_unit": "hour", "aggregation_mode": "parallel" }, "entitlement_grant": { "id": "eg_01KYDEZ86JA78AM7F7Y2SMTRWE", "entitlement_config_id": "ent_01KYB5ESPNNB8DWFVQAFJN6KKZ", "customer_id": "cust_01KYARB3TVZ0RZPDGC5P40NHHC", "subscription_id": "subs_01KYARKSC3GVX64QEF4T4ABS9X", "scope_entity_type": "feature", "scope_entity_id": "feat_tokens", "measure": "quantity", "quota": "1000000", "usage": "1150000", "valid_from": "2026-07-25T15:20:00Z", "valid_to": "2026-07-25T20:20:00Z", "grant_status": "exhausted", "quota_crossed_at": "2026-07-25T20:20:23Z" } } ``` ### Webhook Fields | Field | Description | | ------------------- | --------------------------------------------------------------------------- | | `alert_type` | Always `entitlement_grant_exhausted` | | `alert_status` | `in_alarm` when the quota is exhausted | | `usage_ratio` | `usage / quota` at evaluation time — `1` or more when exhausted | | `triggered_at` | When the exhaustion was detected | | `subscription` | The owning subscription, without line items or plan expansions | | `customer` | The customer the grant belongs to | | `entitlement` | The entitlement config the window was instantiated from | | `entitlement_grant` | The exhausted window, including `quota`, `usage`, and the window boundaries | # Link features to plans Source: https://docs.flexprice.io/docs/product-catalogue/features/linking-to-plans In **Flexprice**, managing customer access to features is done through **entitlements**. An **entitlement** defines which features a customer can access based on their subscription plan. This ensures that each customer receives the correct level of access, allowing precise control over feature availability and usage limits. **Linking Features to Plans through Entitlements** * Choose the plan to which you want to link features. * Navigate to the **Entitlements** section within the selected plan. * Click **Add** to add the desired features to the plan. Add Entitlement * Configure access parameters, including usage limits, permissions, and other relevant settings. Configure Entitlement * 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**. Boolean Entitlement * **Metered features** * **Setting the Value** Metered Entitlement Value * Enter the maximum usage limit a customer is allowed before restrictions apply. * If there is no cap, select Set to Infinite. * **Usage reset** Metered 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** Static Entitlement * 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** Feature Comparison Table **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 Alert Settings Dropdown ### Step 3: Configure Alert Thresholds A dialog box titled "Feature Alert Settings" will appear with the following configuration options: Configure Feature Alert Settings #### 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 Assign group to charges * Open the plan → **Charges** → **Edit Details** * In **Edit Price Details**, set **Group** to the desired group or **None** to ungroup * Save Update group in price ## 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 Groups list * 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** Create Group The list shows **Type** (Price or Feature) for each group. Groups list — feature groups ## 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 Create feature with group * Open the feature → **Edit** * In **Group**, set the desired group or **None** to ungroup * Save Edit feature — Group ## 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. Usage breakdown 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 Archive Plan Option * 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. Confirm Archive Plan * 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. Flat Fee Charge # 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. Package Charge # 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. Volume Tiered Charge # 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. Recurring Charge Billing Timing * 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. Usage Charge Billing Timing * 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 Clone plan option * Enter **Plan Name** (required) * Enter **Lookup Key** (required, must be unique). It is auto-generated by default. * Optionally enter **Description** and **Metadata** * Click **Duplicate** Duplicate plan dialog 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. Cloned plan detail page showing Plan Details, Metadata with source_plan_id, and Overview tab 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. Add Plan Button 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. Plan Details **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. Plan Charges * **Setting-up a recurring charge:** Recurring charges are fixed charges that your customers are billed at regular intervals (e.g., \$10/month subscription fee). Recurring Charge * **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. Usage Based Charge * **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`. Currency Dropdown **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. Pricing Plans Illustration **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 Price Override Dropdown ### 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 Override Price Dialog * 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 Overridden Prices Summary ## 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. Add Pricing Plan 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. Plan Details Form Plan Configuration ## **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. Billing Preferences ## **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 Multiple Charges ### 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. Recurring Charge * **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. Flat Fee Pricing * **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. Package Pricing * **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 Volume Tiered Pricing * 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. Hybrid Charges ### **Review & Save** * Double-check all entered details. * Click **Save** to finalize the pricing plan. *** ## **Managing Plan Prices After Creation** ### **Terminating Plan Prices** Once a pricing plan is created and has active subscriptions, you may need to terminate specific prices for various business reasons such as: * **Price increases or decreases** * **Feature deprecation** * **Plan restructuring** * **Regulatory compliance** * **Business model changes** #### **Immediate Price Termination** To immediately terminate a price: 1. **Navigate to the specific price** in your plan 2. **Use the DELETE API endpoint**: `DELETE /prices/{price_id}` 3. **The price will be terminated immediately** and marked with the current timestamp as the end date **API Example:** ```bash theme={null} curl -X DELETE "https://us.api.flexprice.io/v1/prices/{price_id}" \ -H "x-api-key: YOUR_API_KEY" ``` #### **Scheduled Price Termination** For better business continuity, you can schedule price termination in the future: 1. **Use the DELETE API endpoint** with an `end_date` parameter 2. **Set the end\_date to a future date** when you want the price to terminate 3. **Existing subscriptions will continue to use the price** until the specified end date **API Example with Future End Date:** ```bash theme={null} curl -X DELETE "https://us.api.flexprice.io/v1/prices/{price_id}" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "end_date": "2025-12-31T23:59:59Z" }' ``` **Benefits of Scheduled Termination:** * **Customer notification time** - Gives customers advance notice of price changes * **Billing continuity** - Prevents disruption to existing subscriptions * **Compliance** - Meets regulatory requirements for price change notifications * **Customer experience** - Allows customers to plan for changes #### **Price Termination Behavior** When a price is terminated: * **Active subscriptions** continue to use the terminated price until the end date * **New subscriptions** cannot be created with the terminated price * **Billing continues normally** until the termination date * **Line items are automatically ended** when the price expires ### **Synchronizing Plan Prices with Existing Subscriptions** **⚠️ Important: After terminating prices, you must run the Plan Price Sync API to ensure existing subscriptions reflect the changes.** #### **Why Plan Price Sync is Required** When you terminate a price in a plan: 1. **Existing subscriptions still reference the old price** 2. **Line items need to be updated** to reflect the termination 3. **Billing calculations must be adjusted** for the new pricing structure 4. **Override prices and customizations** need to be preserved #### **Running Plan Price Sync** Use the Plan Price Sync API to synchronize terminated prices across all existing subscriptions: **API Endpoint:** `POST /plans/{plan_id}/sync/subscriptions` **Example:** ```bash theme={null} curl -X POST "https://us.api.flexprice.io/v1/plans/{plan_id}/sync/subscriptions" \ -H "x-api-key: YOUR_API_KEY" ``` #### **What Plan Price Sync Does** The sync process: 1. **Identifies all active subscriptions** using the plan 2. **Processes each subscription** to update line items 3. **Ends line items for terminated prices** (unless overridden) 4. **Creates line items for new active prices** (if applicable) 5. **Preserves subscription-specific overrides** and customizations 6. **Maintains billing continuity** throughout the transition #### **Best Practices for Price Termination** 1. **Plan Ahead** * Schedule terminations well in advance * Consider customer notification periods * Plan for business continuity 2. **Use Scheduled Termination** * Avoid immediate termination when possible * Give customers time to adjust * Maintain billing stability 3. **Always Run Plan Price Sync** * Run sync after any price changes * Ensure existing subscriptions are updated * Verify billing accuracy 4. **Monitor the Process** * Check sync results and statistics * Verify line item updates * Test billing calculations 5. **Preserve Customer Relationships** * Communicate changes clearly * Honor existing commitments * Provide migration paths #### **Monitoring Sync Results** The Plan Price Sync API returns detailed statistics: ```json theme={null} { "message": "Plan prices synchronized successfully", "plan_id": "plan_123", "plan_name": "Premium Plan", "synchronization_summary": { "subscriptions_processed": 150, "prices_added": 25, "prices_removed": 10, "prices_skipped": 115 } } ``` **Key Metrics:** * **Subscriptions Processed**: Total number of subscriptions updated * **Prices Added**: New line items created for active prices * **Prices Removed**: Line items ended for terminated prices * **Prices Skipped**: Line items that didn't require changes #### **Common Scenarios** **Scenario 1: Price Increase** 1. Terminate old price with future end date 2. Create new price with higher amount 3. Run plan price sync to update subscriptions 4. Customers continue with old price until end date 5. New billing period uses new price **Scenario 2: Feature Deprecation** 1. Terminate price for deprecated feature 2. Run plan price sync to end related line items 3. Existing subscriptions lose access to feature 4. New subscriptions cannot include deprecated feature **Scenario 3: Plan Restructuring** 1. Terminate multiple prices simultaneously 2. Create new pricing structure 3. Run plan price sync to update all subscriptions 4. Ensure billing continuity during transition ### **Troubleshooting Price Termination** #### **Common Issues** 1. **Sync Failures** * Check plan ID validity * Verify API permissions * Review error logs for specific issues 2. **Incomplete Updates** * Verify all prices are properly terminated * Check for override prices that may interfere * Ensure sync completed successfully 3. **Billing Discrepancies** * Review sync statistics * Check individual subscription line items * Verify price end dates are set correctly #### **Validation Rules** * **End date must be in the future** for scheduled termination * **Price must exist** and be accessible * **Plan must have active subscriptions** for sync to be meaningful * **API key must have appropriate permissions** ### **API Reference** **Delete Price:** * **Endpoint:** `DELETE /prices/{price_id}` * **Body:** `{"end_date": "2025-12-31T23:59:59Z"}` (optional) * **Response:** `{"message": "price deleted successfully"}` **Plan Price Sync:** * **Endpoint:** `POST /plans/{plan_id}/sync/subscriptions` * **Response:** Detailed synchronization summary with statistics **Get Plan:** * **Endpoint:** `GET /plans/{plan_id}` * **Use:** To verify plan structure before and after changes *** *For technical implementation details and advanced use cases, refer to the [API Reference](/api-reference//plans/create-plan).* # Updating a Plan Price Source: https://docs.flexprice.io/docs/product-catalogue/plans/update-price Update a plan price's amount, tiers, or metadata — and safely propagate the change to all active subscribers using the price sync workflow. Updating a plan price changes how it is charged going forward. Depending on what you change, the system either mutates the price in place or creates a new price version. After updating pricing fields, you need to run the price sync workflow to propagate the change to existing subscribers. ## The Update Price API ``` PUT /prices/{id} ``` All fields are optional. Only include the fields you want to change. ### Available Fields | Field | Type | Notes | | -------------------- | --------- | ------------------------------------------------------- | | `amount` | decimal | New unit price (FIAT prices) | | `billing_model` | string | `FLAT_FEE`, `TIERED`, or `PACKAGE` | | `tier_mode` | string | `VOLUME` or `SLAB` — for TIERED model | | `tiers` | array | New tier breakpoints (TIERED/FIAT) | | `transform_quantity` | object | Package billing: `divide_by` and `round` | | `price_unit_amount` | decimal | New unit price (CUSTOM price unit) | | `price_unit_tiers` | array | New tiers (CUSTOM price unit + TIERED) | | `display_name` | string | Display label shown on invoices | | `description` | string | Internal description | | `lookup_key` | string | Unique lookup key | | `metadata` | object | Key-value metadata | | `group_id` | string | Assign/reassign to a price group; `""` clears the group | | `effective_from` | timestamp | When the new price version takes effect (default: now) | ### Fields You Cannot Change These are immutable after a price is created: | Field | Why | | ----------------------------------------- | ---------------------------------- | | `type` | Cannot switch FIXED ↔ USAGE | | `currency` | Currency is locked at creation | | `billing_period` / `billing_period_count` | Billing cycle is fixed | | `billing_cadence` | Cannot switch recurring ↔ one-time | | `invoice_cadence` | ARREAR/ADVANCE is fixed | | `meter_id` | The usage metric cannot be swapped | | `price_unit_type` | Cannot switch FIAT ↔ CUSTOM | | `entity_type` / `entity_id` | Price ownership is fixed | ## Two Update Paths ### Path A — Metadata-only update (in-place) If you only update `display_name`, `description`, `lookup_key`, `metadata`, or `group_id`, the price is updated **in place**. No new price version is created. No sync is needed. ```json theme={null} PUT /prices/price_abc { "display_name": "API Calls (v2)", "metadata": { "tier": "enterprise" } } ``` Response: the updated price object. Done. ### Path B — Pricing field update (new version created) If you update any pricing field (`amount`, `billing_model`, `tiers`, `tier_mode`, `transform_quantity`, `price_unit_amount`, `price_unit_tiers`), the system: 1. **Terminates the current price** — sets its `end_date` to `effective_from` (or now if not provided) 2. **Creates a new price** — starts exactly at `effective_from`, with the updated values 3. Returns the **new price** in the response (with a new `id`) ```json theme={null} PUT /prices/price_abc { "amount": "49.99", "effective_from": "2026-04-01T00:00:00Z" } ``` Response: the new price object (`id` will be different from the original). Save the new price `id` from the response. Existing subscription line items still reference the old price ID. The sync workflow creates new line items referencing the new price. ## Syncing the Change to Existing Subscribers After a pricing field update, existing subscriptions are **not automatically updated**. You must run the price sync workflow to propagate the new price to all active subscribers on the plan. ### Why Manual Sync? The sync workflow can affect large numbers of subscriptions. Triggering it automatically on every price update would be unpredictable. Doing it explicitly gives you control over timing. ### Safe Sync Sequence **Step 1 — Update the price** ```json theme={null} PUT /prices/{price_id} { "amount": "49.99", "effective_from": "2026-04-01T00:00:00Z" } ``` Note the plan ID associated with this price (needed for sync). **Step 2 — Check for a running sync** ```json theme={null} POST /workflows/search { "workflow_type": "PriceSyncWorkflow", "entity_id": "", "workflow_status": "Running" } ``` * If `pagination.total > 0` — a sync is already running. Do not trigger another. Poll `GET /workflows/{workflow_id}/{run_id}` every 30 seconds until `status` is `Completed` or `Failed`, then continue to Step 3. * If `pagination.total == 0` — proceed to Step 3. **Step 3 — Trigger sync** ``` POST /plans/{plan_id}/sync/subscriptions ``` Response: ```json theme={null} { "workflow_id": "PriceSyncWorkflow-plan_xxx", "run_id": "run_abc123", "message": "price sync workflow started successfully" } ``` **Step 4 — Poll until complete** ``` GET /workflows/{workflow_id}/{run_id} ``` Poll every 30 seconds. Terminal states: | Status | Action | | -------------------------------------- | ----------------------------------------------- | | `Completed` | Done — all subscribers updated | | `Failed` | Check error details, fix root cause, re-trigger | | `Canceled` / `Terminated` / `TimedOut` | Re-trigger sync | **Step 5 — Verify (optional)** Check the sync summary in the completed workflow response: ```json theme={null} { "summary": { "line_items_found_for_creation": 120, "line_items_created": 120, "line_items_terminated": 120 } } ``` `line_items_terminated` = old price line items closed. `line_items_created` = new price line items added. ## What Sync Does After a Price Update When you update a price, the old price gets an `end_date` and a new price is created. The sync workflow: 1. **Terminates** existing line items that reference the old price (their `end_date` is set to match the old price's `end_date`) 2. **Creates** new line items for the new price on all subscriptions that don't have one yet Subscriptions with **overridden** line items (custom pricing) are **not affected** — their line items point to subscription-scoped prices, not the plan price you just updated. ## Using `effective_from` `effective_from` controls when the old price expires and the new one starts. | Scenario | Recommendation | | --------------------------------------------------- | ---------------------------------------------------------------- | | Change takes effect immediately | Omit `effective_from` (defaults to now) | | Change takes effect at start of next billing period | Set `effective_from` to the next period start | | Scheduled future price change | Set `effective_from` to the future date, trigger sync in advance | The sync workflow creates new line items with `start_date = price.start_date`. If you set `effective_from: "2026-04-01"`, the new line items will start on April 1 and the old ones will end on April 1. ## Examples ### Update a flat fee amount ```json theme={null} PUT /prices/price_monthly_base { "amount": "79.00", "effective_from": "2026-04-01T00:00:00Z" } ``` Then sync: `POST /plans/plan_growth/sync/subscriptions` ### Update usage tiers ```json theme={null} PUT /prices/price_api_calls { "billing_model": "TIERED", "tier_mode": "VOLUME", "tiers": [ { "up_to": 50000, "unit_amount": "0.002" }, { "up_to": 200000, "unit_amount": "0.001" }, { "up_to": null, "unit_amount": "0.0005" } ] } ``` ### Update metadata only (no sync needed) ```json theme={null} PUT /prices/price_api_calls { "display_name": "API Calls — Updated Tiers", "metadata": { "updated_by": "billing-team" } } ``` No sync required. ## Related Topics * [Subscription Line Item Overrides](/docs/subscriptions/subscription-line-item-overrides) * [Plan Price Overrides](/docs/subscriptions/plan-price-overrides) * [Plan Pricing](/docs/product-catalogue/plans/pricing) * [Billing Models](/docs/product-catalogue/plans/billing-models/flat-fee) # Clone Cursor pricing Source: https://docs.flexprice.io/docs/product-catalogue/plans/use-cases/clone-cursor-pricing Cursor’s pricing model combines subscription tiers with usage-based metering, allowing developers and teams to scale AI-powered assistance effectively. Cursor charges users based on their selected plan (Free, Pro, or Team) and additional metered usage for AI completions, codebase indexing, and customization. By setting this up in Flexprice, you can provide users with transparent billing while managing limits dynamically. This guide walks you through setting up Cursor’s pricing model using a structured approach, ensuring clarity in billing and monetization. **Use Cases** * AI-powered development tools * Code completion and AI assistance platforms * Team-based coding environments with AI-driven automation Whether you're a solo developer or a growing team, replicating Cursor’s pricing in Flexprice ensures seamless billing automation and monetization. This guide walks you through the setup process step by step. **Cursor’s pricing model** Cursor provides three pricing tiers: | Plan Name | Price per Month | Features Included | | --------- | --------------- | --------------------------------------------------------------------------------- | | Hobby | \$0 | 2,000 completions, 50 slow premium requests | | Pro | \$20 | Unlimited completions, 500 fast premium requests, unlimited slow premium requests | | Business | \$40 per user | Pro features + privacy mode, centralized billing, admin dashboard, SAML/OIDC SSO | In addition, Cursor provides usage-based metering for: * AI completions * Premium requests (fast and slow) **Configuring Cursor’s Pricing in Flexprice** 1. **Define **[**Features**](/docs/product-catalogue/features/create)** in Flexprice**\ Flexprice allows you to define different types of features to enable precise billing and usage tracking for Cursor's pricing model.\ \ **Metered Features** These features track usage over time and are billed based on consumption. | Feature Name | Feature Type | Aggregation Method | Key Filters | | --------------------- | ------------ | ------------------ | --------------------- | | AI Completions | Metered | SUM | Feature: AI Queries | | Fast Premium Requests | Metered | SUM | Feature: Premium Fast | | Slow Premium Requests | Metered | SUM | Feature: Premium Slow | **Boolean Features (On/Off Toggle)**\ These features enable or disable specific functionality without tracking usage. | Feature Name | Feature Type | Description | | --------------- | ------------ | -------------------------------------- | | Privacy Mode | Boolean | Enables privacy mode for organizations | | Admin Dashboard | Boolean | Grants access to admin-level analytics | | SAML/OIDC SSO | Boolean | Enables single sign-on authentication | **Static Features**\ These features are always included in a specific plan and do not change based on usage. | Feature Name | Feature Type | Description | | ------------- | ------------ | ------------------------------------- | | SAML/OIDC SSO | Static | Enables single sign-on authentication | 2. **Create \*\* [**Plans**](/docs/product-catalogue/plans/create)** for Subscription Tiers\*\* 1. Each of Cursor’s plans can be created as a **recurring charges** in Flexprice: | Plan Name | Billing Type | Base Price | | --------- | ------------ | --------------- | | Hobby | Subscription | \$0 | | Pro | Subscription | \$20/month | | Business | Subscription | \$40/user/month | 2. Define usage-based charges along with recurring charges\\ Cursor Plan Charges # Clone OpenAI pricing Source: https://docs.flexprice.io/docs/product-catalogue/plans/use-cases/clone-openai-pricing Manually managing per-token pricing can be complex, but with Flexprice, you can automate and scale your billing effortlessly. This guide walks you through setting up OpenAI's O1 pricing model using a **package-based pricing approach**. This method ensures clarity in billing, making it ideal for AI APIs, generative models, and machine learning services. **Use Cases** * AI APIs (LLMs like OpenAI, Anthropic, Mistral) * Machine learning inference services * Text-to-Speech or Speech-to-Text APIs **OpenAI** charges users based on the number of input, output and cached tokens processed by their models. You can view the official pricing details here: [OpenAI Pricing](https://openai.com/api/pricing/). The O1 model has the following pricing structure: | **Token Type** | **Price per Million Tokens** | | ------------------- | ---------------------------- | | Input Tokens | \$15.00 per million tokens | | Output Tokens | \$60.00 per million tokens | | Cached Input Tokens | \$7.50 per million tokens | For example, if a user processes: * 5 million input tokens → $75.00 ($15.00 x 5) * 2 million output tokens → $120.00 ($60.00 x 2) * 1 million cached input tokens → $7.50 ($7.50 x 1) * Total Cost = \$202.50 Now, let’s configure the pricing for the O1 model using **Flexprice**. **Configuring Pricing of o1 model in Flexprice** 1. **Create** [Metered Features](/docs/product-catalogue/features/create) **for Token Usage** Since token usage is metered, we first define three separate Metered Features in Flexprice for input tokens, output tokens, and cached input tokens. | **Feature Name** | **Feature Type** | **Aggregation Method** | **Key** | **Filters** | | ------------------- | ---------------- | ---------------------- | ----------- | --------------------------------------------- | | Input Tokens | Metered | SUM | model\_name | model: OpenAI O1, prompt\_type: input | | Output Tokens | Metered | SUM | model\_name | model: OpenAI O1, prompt\_type: output | | Cached Input Tokens | Metered | SUM | model\_name | model: OpenAI O1, prompt\_type: cached\_input | 2. **Create a Plan with** [Package-Based Pricing](/docs/product-catalogue/plans/billing-models/package) Once the metered features are created, we define a **Plan** that charges users per million tokens rather than per individual token. | **Metered Feature** | **Billing Model** | Charges | | ------------------- | ----------------- | -------------------------- | | Input Tokens | Package Charge | \$15.00 per million tokens | | Output Tokens | Package Charge | \$60.00 per million tokens | | Cached Input Tokens | Package Charge | \$7.50 per million tokens | Now, whenever a customer **purchases this plan and starts using it**, they will: * See **real-time usage events** for token consumption. * Get a **dynamically generated proposed invoice** based on their usage. * Have full transparency in billing, ensuring clarity on costs. This process ensures that **AI companies can charge users fairly based on actual usage** while providing predictable and scalable billing. # Apply Tax to Customers and Subscriptions Source: https://docs.flexprice.io/docs/product-catalogue/taxes/apply Link tax rates to tenants, customers and subscriptions, and choose whether each is added to the price or included in it A **tax association** is what makes a tax rate collect: it associates the rate with an entity, and carries the two decisions that matter, which rate applies and whether that rate is added on top of your price (`exclusive`) or already inside it (`inclusive`). Flexprice supports associations at three scopes: tenant (default for all customers), customer (overrides tenant for that customer), and subscription (overrides customer for that subscription's invoices). | Level | Use it when | | ------------ | ------------------------------------------------------------------------------ | | Tenant | Every customer you bill pays this tax by default | | Customer | This customer pays a tax the rest do not, or is in a different jurisdiction | | Subscription | One subscription needs different tax treatment from the rest of the customer's | ## Association fields | Field | Type | Required | Description | | ---------------------- | --------- | -------- | ----------------------------------------------------------------------------------- | | `tax_rate_code` | string | Yes | The `code` of the tax rate to link | | `entity_type` | string | No | `tenant`, `customer`, `subscription`, or `invoice` | | `entity_id` | string | No | ID of the entity to link | | `external_customer_id` | string | No | Your own customer ID, usable instead of `entity_id` for customer-level associations | | `auto_apply` | bool | No | Set to `true` to apply this tax automatically on every new invoice | | `tax_behavior` | string | No | `inclusive` or `exclusive`. Resolved from the subscription's currency if omitted. | | `priority` | int | No | Lower number fires first. Default: `0` | | `currency` | string | No | Scopes this association to a specific currency, e.g. `"USD"` | | `start_date` | timestamp | No | When the association becomes active. Defaults to now. | | `end_date` | timestamp | No | When the association expires. Omit for indefinite. | ## Apply to a tenant (default for all customers) A tenant-level association acts as the default tax for every customer that has no association of its own. ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/taxes/associations \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "tax_rate_code": "TAX_US_CA", "entity_type": "tenant", "entity_id": "", "auto_apply": true, "currency": "USD", "tax_behavior": "inclusive", "priority": 0 }' ``` **Response** ```json theme={null} { "id": "txa_01abc123", "tax_rate_id": "txr_01abc456", "entity_type": "tenant", "entity_id": "", "auto_apply": true, "currency": "USD", "tax_behavior": "inclusive", "priority": 0 } ``` ## Apply to a specific customer Customer-level associations override tenant-level ones for that customer only. ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/taxes/associations \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "tax_rate_code": "TAX_EU_VAT", "entity_type": "customer", "external_customer_id": "your-customer-id", "auto_apply": true, "currency": "EUR", "tax_behavior": "exclusive", "priority": 0 }' ``` ## Apply at subscription creation Pass `tax_rate_overrides` in the create subscription request to link rates directly to that subscription. These override any customer or tenant associations for that subscription's invoices. ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/subscriptions \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "customer_id": "", "plan_id": "", "start_date": "2025-01-01T00:00:00Z", "tax_rate_overrides": [ { "tax_rate_code": "TAX_US_CA", "currency": "USD", "tax_behavior": "exclusive" } ] }' ``` When `tax_rate_overrides` is set, flexprice uses only those rates for the subscription's invoices. Customer and tenant associations are not inherited. Omit `tax_rate_overrides` entirely to inherit from the customer. **`tax_rate_overrides` fields** | Field | Type | Required | Description | | --------------- | ------ | -------- | --------------------------------------------------------------------------------- | | `tax_rate_code` | string | Yes | The `code` of the tax rate to link | | `currency` | string | Yes | ISO currency code, e.g. `"USD"` | | `tax_behavior` | string | No | `inclusive` or `exclusive`. Resolved from the subscription's currency if omitted. | | `auto_apply` | bool | No | Defaults to `true` | | `priority` | int | No | Lower number fires first. Default: `0` | | `metadata` | object | No | Key-value pairs for your own tracking | ## Add or remove tax after subscription creation Use the subscription modification API with `type: "tax"`. ### Add a tax rate ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/subscriptions//modify/execute \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "type": "tax", "tax_params": { "action": "add", "tax_rate_id": "", "effective_date": "2025-06-01T00:00:00Z" } }' ``` `effective_date` is optional. Omit it to apply the change immediately. ### Remove a tax rate ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/subscriptions//modify/execute \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "type": "tax", "tax_params": { "action": "remove", "tax_association_id": "" } }' ``` Get the `tax_association_id` from `GET /v1/taxes/associations?entity_type=subscription&entity_id=`. Removing a tax association affects only invoices generated after the change. Previously issued invoices are not changed. ### Preview before executing Swap `/modify/execute` for `/modify/preview` to see the effect without committing: ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/subscriptions//modify/preview \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "type": "tax", "tax_params": { "action": "add", "tax_rate_id": "" } }' ``` ## tax\_params fields | Field | Type | Required | Description | | -------------------- | --------- | -------------------- | ---------------------------------------------- | | `action` | string | Yes | `add` or `remove` | | `tax_rate_id` | string | When `action=add` | ID of the tax rate to attach | | `tax_association_id` | string | When `action=remove` | ID of the tax association to detach | | `effective_date` | timestamp | No | When the change takes effect. Defaults to now. | ## Multiple tax rates on the same entity Create one association per rate. Each rate is applied independently to the same taxable base and does not compound. ```bash theme={null} # State tax (higher priority) curl -X POST https://us.api.flexprice.io/v1/taxes/associations \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "tax_rate_code": "TAX_STATE", "entity_type": "customer", "entity_id": "", "auto_apply": true, "currency": "USD", "priority": 0 }' # Federal tax (lower priority) curl -X POST https://us.api.flexprice.io/v1/taxes/associations \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "tax_rate_code": "TAX_FEDERAL", "entity_type": "customer", "entity_id": "", "auto_apply": true, "currency": "USD", "priority": 1 }' ``` If the taxable amount is \$100, state at 6% adds \$6 and federal at 2% adds \$2 for a total tax of \$8. They do not compound. ## List associations ```bash theme={null} curl "https://us.api.flexprice.io/v1/taxes/associations?entity_type=customer&entity_id=" \ -H "x-api-key: " ``` ## Update an association You can update `priority`, `auto_apply`, `tax_behavior`, and `metadata` without deleting and recreating. ```bash theme={null} curl -X PUT https://us.api.flexprice.io/v1/taxes/associations/ \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "auto_apply": false }' ``` Setting `auto_apply: false` pauses the tax without removing the record. Set it back to `true` to re-enable. # Configure Tax Rates Source: https://docs.flexprice.io/docs/product-catalogue/taxes/configure Create and manage reusable tax rate definitions in flexprice. A tax rate is a reusable definition of a percentage you collect. You create it once and reference it by `code` wherever you apply it. ## Create a tax rate ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/taxes/rates \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "name": "US Sales Tax", "code": "TAX_US_CA", "tax_rate_type": "percentage", "percentage_value": "8.25", "description": "California state sales tax" }' ``` **Response** ```json theme={null} { "id": "txr_01abc123", "name": "US Sales Tax", "code": "TAX_US_CA", "tax_rate_type": "percentage", "percentage_value": "8.25", "tax_rate_status": "active", "description": "California state sales tax", "created_at": "2025-01-15T10:00:00Z" } ``` Required properties: * The `name` appears on your customer's invoice. Use a short label for the kind of tax, such as `Sales Tax`, `VAT`, or `GST`. * The `code` is your unique identifier for the rate. You pass this `code`, not the ID, whenever you link the rate to a customer or subscription. * The `percentage_value` is the percentage to collect, between `0` and `100`. Pass it as a string, for example `"8.25"` for 8.25%. * The `tax_rate_type` is always `percentage`. Optional properties: * The `description` stores notes for your own reference. Your customers do not see it. * The `metadata` object stores your own key-value pairs on the rate. The `code` field is immutable. Choose it carefully since it is the key used in all association requests. To rename a code, create a new tax rate and migrate your associations to it. A rate on its own collects nothing. It starts applying once you [associate it with a customer or subscription](/docs/product-catalogue/taxes/apply), which is also where you decide whether it is inclusive or exclusive. ## List tax rates ```bash theme={null} curl https://us.api.flexprice.io/v1/taxes/rates \ -H "x-api-key: " ``` ## Get a tax rate ```bash theme={null} curl https://us.api.flexprice.io/v1/taxes/rates/ \ -H "x-api-key: " ``` ## Update a tax rate You can update `name`, `description`, and `metadata`. The `code` and `tax_rate_type` fields are immutable. ```bash theme={null} curl -X PUT https://us.api.flexprice.io/v1/taxes/rates/ \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "description": "Updated for 2025 rate change" }' ``` ## Delete a tax rate A tax rate can only be deleted if it has no active associations and no applied records on existing invoices. To stop applying a tax without losing audit history, delete the association instead of the rate. ```bash theme={null} curl -X DELETE https://us.api.flexprice.io/v1/taxes/rates/ \ -H "x-api-key: " ``` **Error: rate has active associations** ```json theme={null} { "error": "tax_rate_in_use", "message": "Cannot delete a tax rate with active associations. Delete associations first." } ``` ## Next step # Taxes Overview Source: https://docs.flexprice.io/docs/product-catalogue/taxes/overview Collect tax on invoices with percentage tax rates applied to customers and subscriptions Flexprice charges tax by applying a tax rate to your tenant, a customer, or a subscription. Every invoice then picks up that rate automatically, either adding it to the price or treating it as already included, depending on how you set it up. ## Tax associations You can create a tax association at three levels. Each level is copied down to the next when the child is created, so setting rates on a customer is usually enough. | Create an association on | It is copied to | Unless | | ------------------------ | ----------------------------------- | ----------------------------------------------------- | | Your tenant | Every new customer | The customer is created with `tax_rate_overrides` | | A customer | Every new subscription for them | The subscription is created with `tax_rate_overrides` | | A subscription | Nothing, this is what invoices read | | Copying happens once, at creation time. A tax association added to your tenant today does not reach customers who already exist; create it on those customers directly. ## Inclusive versus exclusive tax A tax association is either exclusive or inclusive. An *exclusive* tax is added to the price. An *inclusive* tax is already part of the price. | Tax | Subtotal | Tax due | Total | | ------------- | -------- | ------------------------ | ------------ | | 10% exclusive | \$100.00 | \$10.00 | **\$110.00** | | 10% inclusive | \$100.00 | \$9.09, already included | **\$100.00** | Inclusive tax never changes what the customer pays, only how much of that payment counts as tax. Pick exclusive when you list pre-tax prices and add tax at checkout, the norm in the US and Canada. Pick inclusive when your listed price is the final price, the norm for VAT and GST. Set this with `tax_behavior` on the association. Leave it out and Flexprice decides from the subscription's currency, once, at creation time: | Currency | Default `tax_behavior` | | ------------- | ---------------------- | | `USD`, `CAD` | `exclusive` | | Anything else | `inclusive` | ## Tax exempt customers Set a customer's `tax_treatment` to `exempt` and no tax is collected from them, regardless of what tax associations exist. Mark a customer exempt when you create them, before subscribing them to a plan. Flexprice never creates a tax association for an exempt customer's subscriptions, so there is nothing to charge and back out later. This is the recommended way to onboard a tax-exempt customer. If a customer becomes exempt after they already have a taxed subscription, their existing associations stay in place but stop collecting. For an inclusive tax, the customer then pays the price minus the tax that would have been included: | Tax | Amount | Tax due | Total | | ------------- | -------- | ------- | -------------------------------------------- | | 10% inclusive | \$100.00 | \$0.00 | **\$90.91**, the \$9.09 of tax is subtracted | | 10% exclusive | \$100.00 | \$0.00 | **\$100.00** | A `tax_treatment` change applies from the next invoice onward. It never rewrites an invoice already issued. If no tax was charged, the invoice records why: `tax_summary.exemption.reason_code` reads `customer_exempt` for an exempt customer, or `no_tax_configured` when no tax association was set up at all. ## Multiple tax rates You can create more than one tax association on the same entity. Rates never compound: each is calculated on the same base, not on top of the others. | Invoice | Tax rate | Tax | | ----------------- | -------- | ------------ | | \$100.00 subtotal | | | | State tax | 6% excl. | \$6.00 | | City tax | 2% excl. | \$2.00 | | **Total** | | **\$108.00** | The city tax is calculated on \$100.00, not \$106.00. Combining several inclusive rates works differently since they cannot each be pulled out of the price independently. See [Multiple tax rates](/docs/product-catalogue/taxes/apply#apply-several-rates-at-once) for that math. ## Quick start 1. [Create a tax rate](/docs/product-catalogue/taxes/configure) with a unique `code` and a percentage. 2. [Create a tax association](/docs/product-catalogue/taxes/apply) linking that rate to a customer or subscription, choosing `inclusive` or `exclusive`. 3. Generate an invoice. Flexprice applies the subscription's tax associations and returns the breakdown in `tax_summary`. # Manage API Keys Source: https://docs.flexprice.io/docs/rbac/manage-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 ones. **A user's active keys block their role changes** ## Creating an API Key ### Via Dashboard Navigate to **Developers > API Keys** and click **Create** to open the API key creation dialog. Create API Key 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} { "secret": { "id": "sec_xyz789", "name": "Event Service Production Key", "type": "private_key", "provider": "flexprice", "display_id": "sk_01***BN", "roles": ["event_ingestor"], "user_type": "service_account", "user_id": "user_0123ABCD", "status": "published", "created_at": "2026-09-18T09:33:55Z", "updated_at": "2026-09-18T09:33:55Z" }, "api_key": "sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } ``` **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`) ### Example 3: Read-Only API Key **Service Account has**: `all_reader` role **API Key inherits**: `all_reader` role **What happens**: * ✅ Can read every resource (customers, plans, subscriptions, invoices, wallets, events) * ❌ Cannot create, update, or delete anything (403 Forbidden) Use this for reporting tools, data warehouse syncs, and monitoring jobs. See [Creating a Service Account](/docs/rbac/service-accounts/create#read-only-service-account) for the setup. ## 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 → # Manage Roles Source: https://docs.flexprice.io/docs/rbac/manage-roles Manage the roles assigned to a user account in your organization Roles decide what a user can read and write in Flexprice. A `super_admin` manages the roles of any other user in the same organization, which is how you promote a member to write access or downgrade them to read-only. Role changes apply to user accounts only. Service account roles are fixed when the account is created. ## Roles for User Accounts | Role | Grants | | ------------- | -------------------------------- | | `super_admin` | Every action on every resource | | `all_writer` | Read and write on every resource | | `all_reader` | Read on every resource | The `event_ingestor` and `event_reader` roles are reserved for service accounts and cannot be assigned to a user account. ## Who Can Change Roles Two rules govern every role change: * Only a `super_admin` can change another user's roles. * Nobody can change their own roles, including a `super_admin`. ## Expire API Keys First An API key copies its owner's roles at the moment it is created and never re-reads them. If a user's roles changed while their keys stayed active, those keys would keep operating under the old roles. Flexprice rejects the role change instead of silently rewriting the keys. Expire the user's active API keys, then change the role. **Active API keys block role changes** The request returns `400` while the target user holds any active API key in any environment. The response lists the keys you need to expire. ## Updating Roles Send the complete role set you want the user to end up with. The list replaces their existing roles rather than adding to them. ```bash cURL theme={null} curl --request PUT \ --url https://api.cloud.flexprice.io/v1/users/{id}/roles \ --header 'Content-Type: application/json' \ --header 'x-api-key: ' \ --data '{ "roles": ["all_writer"] }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.cloud.flexprice.io/v1/users/usr_abc123/roles', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'x-api-key': '' }, body: JSON.stringify({ roles: ['all_writer'] }) }); ``` ```python Python theme={null} import requests response = requests.put( 'https://api.cloud.flexprice.io/v1/users/usr_abc123/roles', headers={ 'Content-Type': 'application/json', 'x-api-key': '' }, json={ 'roles': ['all_writer'] } ) ``` ### Request Fields The complete set of roles the user should hold. Must contain at least one role. Available roles for user accounts: * `super_admin` - Every action on every resource * `all_writer` - Read and write on every resource * `all_reader` - Read on every resource ### Response ```json theme={null} { "id": "usr_abc123", "email": "teammate@example.com", "type": "user", "roles": ["all_writer"], "tenant": { "id": "tenant_xyz789", "name": "Acme Inc", "status": "published", "created_at": "2025-01-14T10:00:00Z", "updated_at": "2025-01-14T10:00:00Z" } } ``` The user's new roles take effect on their next request. They do not need to sign out and back in. ## Error Responses ### Target Has Active API Keys Status `400`, code `validation_error`. Delete the listed keys and retry. ```json theme={null} { "code": "validation_error", "message": "Expire this user's existing API keys before changing their role", "http_status_code": 400, "details": { "id": "usr_abc123", "active_api_key_count": 2, "active_api_keys": { "env_prod123": { "env_name": "Production", "api_keys": [ { "id": "sec_aaa111", "key_name": "billing-service-key" } ] }, "env_stg456": { "env_name": "Staging", "api_keys": [ { "id": "sec_bbb222", "key_name": "ci-key" } ] } } } } ``` The `active_api_keys` object is keyed by environment ID. Each entry carries the environment's display name and the keys held in it. ### Caller Is Not a Super Admin Status `403`, code `permission_denied`. ```json theme={null} { "code": "permission_denied", "message": "Ask a tenant super_admin to update this user's roles", "http_status_code": 403 } ``` ### Changing Your Own Roles Status `403`, code `permission_denied`. Ask another `super_admin` to make the change. ```json theme={null} { "code": "permission_denied", "message": "Ask another super_admin to change your roles", "http_status_code": 403 } ``` ### Target Is a Service Account Status `400`, code `validation_error`. ```json theme={null} { "code": "validation_error", "message": "Service account roles are fixed at creation and cannot be changed", "http_status_code": 400 } ``` To change what a service account can do, create a replacement service account with the role set you want and move your integration to a key from the new account. See [Creating a Service Account](/docs/rbac/service-accounts/create). ### Role Not Valid for a User Account Status `400`, code `validation_error`. `event_ingestor` and `event_reader` belong to service accounts. ```json theme={null} { "code": "validation_error", "message": "Role 'event_ingestor' cannot be assigned to a 'user' account", "http_status_code": 400 } ``` ## Best Practices **Run automation on service accounts, not user accounts** Give scripts, integrations, and scheduled jobs their own service account and use its API key. A workflow tied to user's account stops working when their roles change or their account no longer exists. **Plan the key rotation before changing a role** Changing a role expires nothing on its own, but you must delete the user's active keys to proceed. Know which services use those keys before you start. **Grant `all_reader` for review-only access** Members who only need to inspect billing data do not need write access. ## Related Resources See all roles and what they permit → Create and rotate API keys → # 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 * Hold one of the organization-wide roles: `super_admin`, `all_writer`, or `all_reader` * API keys from user accounts inherit the roles the user held when the key was created A `super_admin` can change another user's roles at any time. See [Managing Roles](/docs/rbac/manage-roles). ### 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 * Have fixed roles: a service account's roles cannot be changed after creation **Run automation on service accounts** Give scripts, integrations, and scheduled jobs their own service account and use its API key. A workflow tied to user's account stops working when their roles change or their account no longer exists. ## Available Roles | Role | Account Type | Grants | | ---------------- | ------------------------ | -------------------------------- | | `super_admin` | User and service account | Every action on every resource | | `all_writer` | User account | Read and write on every resource | | `all_reader` | User and service account | Read on every resource | | `event_ingestor` | Service account | Send events | | `event_reader` | Service account | Read events | ### super\_admin For user accounts and service accounts that need unrestricted access to every 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. ### all\_writer For people who need to read and write every resource. **Can do:** * Read every resource * Write to every resource **Cannot do:** * Change any user's roles, which requires `super_admin` **Use for:** People who create plans, manage customers, and issue invoices. ### all\_reader For people and service accounts that need read-only access to every resource. **Can do:** * Read every resource **Cannot do:** * Create, update, or delete anything **Use for:** Finance reviewers, support staff checking a customer's usage, reporting tools, data warehouse syncs, and monitoring jobs that must never write to Flexprice. Assign `all_reader` to a service account to issue read-only API keys. See [Use Case 3](#use-case-3-read-only-integration). ### 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 events from Flexprice. **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 ### Use Case 3: Read-Only Integration **Scenario**: A reporting tool or data warehouse sync needs to read customers, subscriptions, invoices, and usage, but must never change billing data. **Solution**: Create a service account with the `all_reader` role and generate an API key. The key is read-only across every resource. **What happens**: * ✅ The integration can read every resource * ❌ Any create, update, or delete request gets 403 Forbidden ## 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 account behind the key has 2. Verify that role includes the permission you need 3. For a service account, create a replacement account with the role set you need, because service account roles are fixed at creation. For a user account, ask a `super_admin` to [update the user's roles](/docs/rbac/manage-roles) 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 '{ "type": "service_account", "name": "", "roles": [ "" ] }' ``` ### 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 → Change the roles of a user in your organization → 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) | | `all_reader` | Your service needs to read every resource but never write anywhere (read-only API keys) | | `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. Create Service Account In the dialog: 1. Enter a name for the service account 2. Select the roles you want to assign, for example `all_reader` for a read-only integration or `event_ingestor` for an event pipeline 3. 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": "user_0123ABCD", "name": "Event Ingestion Worker", "type": "service_account", "roles": ["event_ingestor"], "tenant": { "id": "tenant_xyz789", "name": "Acme Inc", "status": "published", "created_at": "2025-01-20T14:13:31Z", "updated_at": "2026-09-08T08:37:06Z" } } ``` Save the `id` from the response. You'll need it when creating API keys. ### Read-Only Service Account To issue read-only API keys, create a service account with the `all_reader` role. Keys created for this account can read every resource in the environment and get `403 Forbidden` on any write. ```bash 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": "Reporting Sync", "roles": ["all_reader"] }' ``` ```json theme={null} { "id": "user_0123ABCD", "name": "Reporting Sync", "type": "service_account", "roles": ["all_reader"], "tenant": { "id": "tenant_xyz789", "name": "Acme Inc", "status": "published", "created_at": "2025-01-20T14:13:31Z", "updated_at": "2026-09-08T08:37:06Z" } } ``` Then [create an API key](/docs/rbac/manage-api#for-a-service-account) with `service_account_id` set to the returned `id`. The key inherits `all_reader`. ## 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 * `all_reader` - Can read every resource, cannot write * `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" } ``` **Role Not Allowed for Service Accounts** ```json theme={null} { "code": "validation_error", "message": "Role 'all_writer' cannot be assigned to a 'service_account' account", "http_status_code": 400 } ``` ## 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) ### Example 3: Reporting or Data Warehouse Sync **Scenario**: A nightly job pulls customers, subscriptions, invoices, and usage into your data warehouse. It never writes to Flexprice. **Create service account with**: * Role: `all_reader` **What happens**: * ✅ Job can read every resource (customers, plans, subscriptions, invoices, wallets, events) * ❌ Job cannot create, update, or delete anything (403 Forbidden) This is the role to use whenever you need a read-only API key. ## Assigning Multiple Roles You can assign more than one role 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)" }' ``` ## 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 → # 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 the following 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 * **Bonus Credits Topup Configuration** (`bonus_credits_topup_config`) — Slab-based bonus credit rules applied automatically to purchased wallet top-ups * **Custom Currency Configuration** (`custom_currency_config`): currencies of your own that the catalogue can be priced in, and the fiat currency invoices are denominated in *** ## 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, "alert_threshold_type": "absolute" | "percentage", "critical": { "threshold": string, "condition": "below" }, "warning": { "threshold": string, "condition": "below" }, "info": { "threshold": string, "condition": "below" } } ``` * **`alert_enabled`**: Required. Boolean. Turn alerts on or off at tenant and environment level. * **`alert_threshold_type`**: Optional. Defaults to `"absolute"` when omitted. The threshold type applies to all three levels, so Critical, Warning and Info are always read the same way. 1. **`"absolute"`**: Thresholds are fixed amounts denominated in the customer's wallet currency. On a USD denominated wallet, a threshold of `10` triggers when the balance falls below \$10. Negative values are allowed, so `-5` triggers when a post-paid wallet has gone \$5 below zero. 2. **`"percentage"`**: Thresholds are specified as a percentage of the credits granted during the customer's current billing period. For example, a threshold of `20` triggers when the ongoing balance falls below 20% of the credits granted in that billing period. If the customer has no active subscription, the wallet's total lifetime credits granted is used as the reference amount. Values must be between 0 and 100. See [Threshold Types](/docs/wallet/low-balance-alert#threshold-types) for a full comparison of the available threshold types. * **`critical`**, **`warning`**, **`info`**: Optional. Each has: * **`threshold`**: String. Read as an amount or a percentage according to `alert_threshold_type`. * **`condition`**: `"below"`. Wallet balance alerts trigger on a falling balance. ### 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. * A critical threshold is required when a warning threshold is set. * Thresholds must ascend by severity: critical \< warning \< info. ### 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. *** ## Bonus Credits Topup Configuration **Key:** `bonus_credits_topup_config` Defines slab-based rules for granting bonus credits automatically on purchased wallet top-ups. When enabled, a top-up's credit amount is matched against a table of thresholds, and the matching slab's bonus is granted alongside the purchase. ### Schema ```json theme={null} { "enabled": boolean, "slabs": [ { "threshold": string, "operator": "gte", "bonus": { "type": "flat" | "percentage", "value": string }, "expiration_duration": number, "expiration_duration_unit": "DAY" | "WEEK" | "MONTH" | "YEAR" } ] } ``` * **`enabled`** — Required. Boolean. Turn automatic slab-based bonus resolution on or off for this tenant and environment. * **`slabs`** — Required and non-empty if `enabled` is `true`; optional otherwise. Array of slab objects, evaluated in list order, so they must be sorted **descending** by `threshold`. The first slab a top-up's credit amount clears is the one that applies. * **`threshold`** — Required. Decimal string ≥ 0. The credit amount being purchased is compared against this value. * **`operator`** — Required. Only `gte` is accepted; the top-up's credit amount must be greater than or equal to `threshold` to match. * **`bonus.type`** — Required. `flat` or `percentage`. * **`bonus.value`** — Required. Decimal string ≥ 0. For `flat`, this is the bonus credit amount added as-is. For `percentage`, this is a percentage of the credits being purchased. * **`expiration_duration`** / **`expiration_duration_unit`** — Optional; must be set together or not at all. Sets an expiry for bonus credits granted from this slab, unless the top-up request already sets `bonus_credits_expiry_date_utc` explicitly. Unit is one of `DAY`, `WEEK`, `MONTH`, `YEAR`. ### Default Values When creating a new setting via API, defaults are: ```json theme={null} { "enabled": false, "slabs": [] } ``` ### What Happens When Enabled * **Applies only to purchased credits** — Slab resolution runs only for top-ups with `transaction_reason` set to `PURCHASED_CREDIT_DIRECT` or `PURCHASED_CREDIT_INVOICED` (including pay-first checkout). Free credit top-ups never receive a bonus. * **Manual override skips resolution** — If the top-up request sets `bonus_credits_to_add` directly, slab resolution does not run for that top-up. * **Bonus is a separate transaction** — The bonus is recorded as its own wallet transaction with `transaction_reason: PURCHASED_CREDIT_BONUS`, linked to the purchase transaction via `parent_transaction_id`. It shares the purchase transaction's status and completes with it. See [Top-Up Wallet](/docs/wallet/top-up#bonus-credits-on-top-up) for how slabs resolve against a purchase, the manual override, and how the bonus transaction is recorded. See the [Top-Up Wallet API Reference](/api-reference/wallets/top-up-wallet) for the `bonus_credits_to_add` and `bonus_credits_expiry_date_utc` request fields. ### Validation * **New settings:** `enabled` required, boolean. If `enabled` is `true`, `slabs` required, non-empty. * **Slab constraints:** `operator` must be `gte`; `threshold` ≥ 0; `bonus.type` one of `flat`, `percentage`; `bonus.value` ≥ 0; `expiration_duration` and `expiration_duration_unit` must both be set or both omitted; slabs must be sorted strictly descending by `threshold`. * **Updates:** Partial updates allowed; only provided fields validated; omitted fields unchanged. ## Custom Currency Configuration **Key:** `custom_currency_config` Defines currencies of your own, such as credits, and the fiat currency invoices are denominated in. Prices, subscriptions, wallets, addons, and coupons can then be created in a configured currency, or in any of the fiat currencies listed in its conversion factors. ### Schema ```json theme={null} { "custom_currencies": { "": { "name": string, "symbol": string, "fiat_conversion_factors": { "": string } } }, "default_fiat_currency": string } ``` * **`custom_currencies`**: Optional. Map keyed by a 3-character currency code. When empty, only Flexprice's supported fiat currencies are accepted, therefore a custom code must be configured here before anything can be created in it. * **`name`**: Required. Display name, for example `FinePrint Credits`. * **`symbol`**: Required. Shown wherever an amount in this currency is displayed. * **`fiat_conversion_factors`**: Required, at least one entry. Decimal string > 0 per fiat currency. The factor is how much fiat one unit of this currency is worth, so `"usd": "0.10"` means one unit is \$0.10. * **`default_fiat_currency`**: Required when `custom_currencies` is non-empty. The currency every invoice is issued in. ### Example ```json theme={null} { "custom_currencies": { "fpc": { "name": "FinePrint Credits", "symbol": "FPC", "fiat_conversion_factors": { "usd": "0.10", "inr": "8.50" } } }, "default_fiat_currency": "usd" } ``` ### Default Values When creating a new setting via API, defaults are: ```json theme={null} { "custom_currencies": {}, "default_fiat_currency": "" } ``` ### What Happens When Configured * **Currencies are restricted**: Creating a price, subscription, wallet, addon, or coupon in a currency that is neither a configured code nor one of the mapped fiat currencies is rejected. The error names both sets. * **Invoices are issued in fiat**: Every invoice uses `default_fiat_currency`, with the pre-conversion amounts kept on the invoice and its line items in a `custom_currency` object. * **The rate is frozen at finalization**: Editing a factor afterwards does not change an invoice that is already finalized. ### Validation * **Codes:** Exactly 3 characters. Case-insensitive and stored lowercase, so two codes differing only in case collide. * **Factors:** Every factor must be a positive decimal. * **Default currency:** Required when `custom_currencies` is non-empty, and cannot itself be a custom currency code. Every custom currency must define a factor for it. * **Matching fiat currencies:** Every custom currency must define factors for the same fiat currencies. If one defines `inr` and another does not, the update is rejected. *** ## 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 ### Bonus Credits on Top-Up * **Volume incentives** — Grant a larger bonus on bigger top-ups (e.g. 10% bonus at \$200+) to encourage customers to purchase more credits at once * **Flat promotional bonus** — Grant a fixed bonus amount for top-ups that clear a lower threshold (e.g. 50 bonus credits at \$100+) * **Automatic, no per-request work** — Configure the slab table once; the bonus is resolved on every purchased-credit top-up without callers passing bonus fields *** ## 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. 💡 **Sort slabs descending, and use round thresholds** — Since the first matching slab wins, keep `slabs` sorted from highest to lowest `threshold` and pick thresholds that make the applicable bracket predictable at a glance. *** ## Related Documentation * [Wallet Sentinel Alerts](/docs/wallet/low-balance-alert) — Configure per-wallet balance alerts * [Top-Up Wallet](/docs/wallet/top-up#bonus-credits-on-top-up) — Slab-based and manual bonus credits on purchased wallet top-ups * [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** Subscription create form showing Customer Hierarchy section ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/subscriptions \ -H "x-api-key: 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://us.api.flexprice.io/v1/subscriptions/{parent_subscription_id}/modify/execute \ -H "x-api-key: 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://us.api.flexprice.io/v1/events/analytics \ -H "x-api-key: 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://us.api.flexprice.io/v1/events/analytics \ -H "x-api-key: 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://us.api.flexprice.io/v1/subscriptions \ -H "x-api-key: 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://us.api.flexprice.io/v1/subscriptions \ -H "x-api-key: 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://us.api.flexprice.io/v1/subscriptions/{parent_subscription_id}/modify/preview \ -H "x-api-key: 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://us.api.flexprice.io/v1/subscriptions/{parent_subscription_id}/modify/execute \ -H "x-api-key: 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://us.api.flexprice.io/v1/subscriptions/{parent_subscription_id}/modify/execute \ -H "x-api-key: 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://us.api.flexprice.io/v1/subscriptions \ -H "x-api-key: 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. Defining Commitment ## 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. Enabling True-Up ## 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. Peak hours bucket layered on top of a full-day reserved commitment, with separate overage prices inside and outside the bucket 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). Add customers to inherit
modal ### 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. Inherited subscriptions table on a child
customer 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** Subscription create form showing Customer Hierarchy section with child customer IDs ```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. Select Customer * 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. Select Plan * 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. Subscription Preview * 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. Subscription Created ### **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). # Group Spend Alerts Source: https://docs.flexprice.io/docs/subscriptions/how-to-configure-spend-alerts/how-to-configure-group-spend-alerts Monitor spend summed across every subscription line item whose feature belongs to a group, configured via API Group spend alerts monitor the **usage-based spend summed across every line item on a subscription whose feature belongs to a given group**, for example every metered feature grouped under "Compute" or "Storage". This scope is useful when several distinct meters should count toward one shared budget, and is configured through the API. ## Prerequisite: Assign Features to the Group A group's spend total only includes line items whose meter is backed by a **feature that has this group set as its `group_id`**. A feature with no group, or a different group, never contributes to this alert, even if it's on the same subscription. If none of a subscription's line items resolve to a feature in the configured group, the group total is `0` and the alert never fires. Confirm every feature you expect to count is assigned to the group before troubleshooting a group alert that isn't triggering. Create a group with **Entity type** set to **Feature**. See [Creating a group](/docs/product-catalogue/groups/create). For every feature whose usage should count toward this alert, set its `group_id` to the group created above. See [Feature grouping](/docs/product-catalogue/groups/feature-grouping). ## Configuring via API A group alert is always scoped to one subscription. The same group can have a different configuration (or no configuration) on a different subscription. ```json theme={null} { "entity_type": "group", "entity_id": "group_01K7YQ0000GROUPID000001", "parent_entity_type": "subscription", "parent_entity_id": "subs_01K7YQ8ZR4M3V2N1XA9C6D0EFG", "config": { "alert_enabled": true, "warning": { "threshold": "400", "condition": "above" }, "critical": { "threshold": "800", "condition": "above" } } } ``` ```bash cURL theme={null} curl -X POST 'https://api.cloud.flexprice.io/v1/alerts/setting' \ -H 'x-api-key: ' \ -H 'Content-Type: application/json' \ -d '{ "entity_type": "group", "entity_id": "group_01K7YQ0000GROUPID000001", "parent_entity_type": "subscription", "parent_entity_id": "subs_01K7YQ8ZR4M3V2N1XA9C6D0EFG", "config": { "alert_enabled": true, "warning": { "threshold": "400", "condition": "above" }, "critical": { "threshold": "800", "condition": "above" } } }' ``` `parent_entity_type` and `parent_entity_id` are mandatory for group alerts. Unlike subscription-level alerts, a group alert cannot be unscoped. ## Managing Alert Settings ### Viewing current configuration Query by `entity_type`, `entity_id`, and `parent_entity_id` to find the setting for this group and subscription. ```bash theme={null} curl -X POST 'https://api.cloud.flexprice.io/v1/alerts/setting/search' \ -H 'x-api-key: ' \ -H 'Content-Type: application/json' \ -d '{ "entity_type": "group", "entity_id": "group_01K7YQ0000GROUPID000001", "parent_entity_id": "subs_01K7YQ8ZR4M3V2N1XA9C6D0EFG" }' ``` ### Changing thresholds Include every threshold you want to keep, not just the one you're changing. The update replaces `config` entirely. It does not merge with what's stored. Send every threshold you want to keep on each update; a threshold left out is cleared. `PUT /v1/alerts/setting/:id` with the complete desired `config`. ### Removing a threshold Build the update payload with that threshold omitted and the others included as-is. `PUT /v1/alerts/setting/:id` with the trimmed `config`. ### Disabling alerts Build the update payload with `config.alert_enabled` set to `false`. `PUT /v1/alerts/setting/:id` with the updated `config`. ## Webhooks | Event | Fires when | | ---------------------------------------------- | ------------------------- | | `subscription.group_spend.threshold_reached` | Spend crosses a threshold | | `subscription.group_spend.threshold_recovered` | Spend recovers to `ok` | Only groups actually touched by an event's usage are re-evaluated for that event. ## Use Cases * **Shared budgets across meters**: Alert once for combined spend across several related metered features (e.g. all compute-related meters) instead of tracking each individually * **Product-line budgets**: Group features by product line and alert per line, independent of how many individual meters make it up * **Consolidating noisy line items**: Roll up several low-value, high-frequency line items into one group threshold instead of alerting on each separately ## Related * [What Are Spend Alerts?](/docs/subscriptions/how-to-configure-spend-alerts/what-are-spend-alerts): all three alert scopes, shared validation, and the full API reference * [Subscription spend alerts](/docs/subscriptions/how-to-configure-spend-alerts/how-to-configure-subscription-spend-alerts): monitor the whole subscription * [Line item spend alerts](/docs/subscriptions/how-to-configure-spend-alerts/how-to-configure-subscription-line-item-spend-alerts): monitor a single line item * [Groups: Feature grouping](/docs/product-catalogue/groups/feature-grouping): assign features to a group * [Webhooks](/docs/webhook/webhooks): endpoint setup, payload signatures, and retry behavior # Line Item Spend Alerts Source: https://docs.flexprice.io/docs/subscriptions/how-to-configure-spend-alerts/how-to-configure-subscription-line-item-spend-alerts Monitor spend on a single subscription line item and get notified by webhook when it crosses a configured threshold Line item spend alerts monitor the **usage-based spend of a single line item** on a subscription, independently of the subscription's total. Use this when one specific charge (for example, a particular metered API) needs its own budget, separate from the rest of the subscription. ## Configuring via the Dashboard Go to **Billing** → **Subscriptions**, open the subscription, and go to **Edit Subscription**. Find the line item to monitor, click its **three-dot menu (⋮)**, and select **Alert Settings**. It appears alongside **Edit** and **Terminate** for that line item. Open Alert Settings Turn **Enable Alerts** on. Enable Alerts Set **Threshold Value** for the levels you want (Critical, Warning, Info). Condition is fixed to **Above**. Use **Remove** to drop a level you don't need, then click **Save Changes**. Configure thresholds ## Configuring via API ```json theme={null} { "entity_type": "subscription_line_item", "entity_id": "subs_line_01K7YQ0000LINEITEM00001", "parent_entity_type": "subscription", "parent_entity_id": "subs_01K7YQ8ZR4M3V2N1XA9C6D0EFG", "config": { "alert_enabled": true, "warning": { "threshold": "150", "condition": "above" }, "critical": { "threshold": "300", "condition": "above" } } } ``` ```bash cURL theme={null} curl -X POST 'https://api.cloud.flexprice.io/v1/alerts/setting' \ -H 'x-api-key: ' \ -H 'Content-Type: application/json' \ -d '{ "entity_type": "subscription_line_item", "entity_id": "subs_line_01K7YQ0000LINEITEM00001", "parent_entity_type": "subscription", "parent_entity_id": "subs_01K7YQ8ZR4M3V2N1XA9C6D0EFG", "config": { "alert_enabled": true, "warning": { "threshold": "150", "condition": "above" }, "critical": { "threshold": "300", "condition": "above" } } }' ``` `entity_id` must be a line item that belongs to the subscription identified by `parent_entity_id`. The API rejects a line item ID from a different subscription. ## Alert Behavior Flexprice recalculates this line item's usage-based spend on every usage event affecting the subscription and sends a webhook only when the alert state changes. Usage on a different line item in the same subscription does not affect this alert. It only tracks the one line item it's configured on. ## Managing Alert Settings ### Viewing current configuration Open **Edit Subscription**, find the line item, and click its **three-dot menu (⋮)**. Select **Alert Settings**. The dialog shows **Enable Alerts** and all configured threshold levels with their values. ### Changing thresholds Open **Edit Subscription**, find the line item, click its **three-dot menu (⋮)** → **Alert Settings**. Change **Enable Alerts** or any threshold value in the dialog. Click **Save Changes**. ### Removing a threshold Open **Edit Subscription**, find the line item, click its **three-dot menu (⋮)** → **Alert Settings**. Click **Remove** on the threshold level you want to drop. Click **Save Changes**. Removed thresholds are cleared, not just hidden, so send the complete remaining configuration. ### Disabling alerts Open **Edit Subscription**, find the line item, click its **three-dot menu (⋮)** → **Alert Settings**. Set **Enable Alerts** to off (or set `alert_enabled` to `false` via API). Click **Save Changes**. ## Webhooks | Event | Fires when | | -------------------------------------------------- | ------------------------- | | `subscription.line_item_spend.threshold_reached` | Spend crosses a threshold | | `subscription.line_item_spend.threshold_recovered` | Spend recovers to `ok` | ## Use Cases * **Per-charge budgets**: Alert on a specific metered charge (e.g. a particular API or compute meter) without noise from the rest of the subscription * **High-cost line items**: Set tighter thresholds on line items with the highest per-unit cost or the most usage variance * **Customer-specific overrides**: Combine with per-subscription price overrides to alert on a customer's negotiated rate independently ## Related * [What Are Spend Alerts?](/docs/subscriptions/how-to-configure-spend-alerts/what-are-spend-alerts): all three alert scopes, shared validation, and the full API reference * [Subscription spend alerts](/docs/subscriptions/how-to-configure-spend-alerts/how-to-configure-subscription-spend-alerts): monitor the whole subscription instead of one line item * [Group spend alerts](/docs/subscriptions/how-to-configure-spend-alerts/how-to-configure-group-spend-alerts): monitor spend across a feature group * [Webhooks](/docs/webhook/webhooks): endpoint setup, payload signatures, and retry behavior # Subscription Spend Alerts Source: https://docs.flexprice.io/docs/subscriptions/how-to-configure-spend-alerts/how-to-configure-subscription-spend-alerts Monitor total usage-based spend across a subscription and get notified by webhook when it crosses a configured threshold Subscription spend alerts monitor the **total usage-based spend across an entire subscription** for the current billing period. This is the broadest of the three spend alert scopes: it catches overall budget overruns regardless of which line item drove the spend. ## Configuring via the Dashboard Go to **Billing** → **Subscriptions**, or open a customer and select their subscription, from either the **Subscription Details** page or the customer's **Overview** tab subscriptions list. Click the **three-dot menu (⋮)** on the subscription and select **Alert Settings**. Open Alert Settings Turn **Enable Alerts** on. Enable Alerts Set **Threshold Value** for the levels you want (Critical, Warning, Info). Condition is fixed to **Above**. Use **Remove** to drop a level you don't need, then click **Save Changes**. Configure thresholds ## Configuring via API ```json theme={null} { "entity_type": "subscription", "entity_id": "subs_01K7YQ8ZR4M3V2N1XA9C6D0EFG", "config": { "alert_enabled": true, "info": { "threshold": "250", "condition": "above" }, "warning": { "threshold": "500", "condition": "above" }, "critical": { "threshold": "1000", "condition": "above" } } } ``` ```bash cURL theme={null} curl -X POST 'https://api.cloud.flexprice.io/v1/alerts/setting' \ -H 'x-api-key: ' \ -H 'Content-Type: application/json' \ -d '{ "entity_type": "subscription", "entity_id": "subs_01K7YQ8ZR4M3V2N1XA9C6D0EFG", "config": { "alert_enabled": true, "info": { "threshold": "250", "condition": "above" }, "warning": { "threshold": "500", "condition": "above" }, "critical": { "threshold": "1000", "condition": "above" } } }' ``` No `parent_entity_type` or `parent_entity_id` is sent for subscription-level alerts. `entity_id` is the subscription itself. ## Alert Behavior Once configured and enabled, Flexprice: * **Recalculates spend**: On every usage event for the subscription, recomputes total usage-based spend using the same invoicing-grade calculation (commitment- and overage-aware) used to generate invoices * **Compares against thresholds**: Evaluates the recalculated total against Critical, Warning, and Info * **Updates state**: Reflects the current state (`ok`, `info`, `warning`, `in_alarm`) in alert history * **Notifies**: Sends a webhook only when the state changes, not on every event ## Managing Alert Settings ### Viewing current configuration Open the subscription and click the **three-dot menu (⋮)**. Select **Alert Settings**. The dialog shows **Enable Alerts** and all configured threshold levels with their values. ### Changing thresholds Click the **three-dot menu (⋮)** on the subscription and select **Alert Settings**. Change **Enable Alerts** or any threshold value in the dialog. Click **Save Changes**. ### Removing a threshold Click the **three-dot menu (⋮)** on the subscription and select **Alert Settings**. Click **Remove** on the threshold level you want to drop. Click **Save Changes**. Removed thresholds are cleared, not just hidden, so send the complete remaining configuration. ### Disabling alerts Click the **three-dot menu (⋮)** on the subscription and select **Alert Settings**. Set **Enable Alerts** to off (or set `alert_enabled` to `false` via API). Click **Save Changes**. ## Webhooks | Event | Fires when | | ---------------------------------------- | ------------------------- | | `subscription.spend.threshold_reached` | Spend crosses a threshold | | `subscription.spend.threshold_recovered` | Spend recovers to `ok` | ## Use Cases * **Overall budget cap**: Alert when a customer's total subscription spend approaches or exceeds a contracted limit * **Early warning for usage spikes**: Set Info well below Critical to catch unexpected spend trends before they become a problem * **Account health monitoring**: Route alerts to a support or account-management queue ## Related * [What Are Spend Alerts?](/docs/subscriptions/how-to-configure-spend-alerts/what-are-spend-alerts): all three alert scopes, shared validation, and the full API reference * [Line item spend alerts](/docs/subscriptions/how-to-configure-spend-alerts/how-to-configure-subscription-line-item-spend-alerts): monitor one line item instead of the whole subscription * [Group spend alerts](/docs/subscriptions/how-to-configure-spend-alerts/how-to-configure-group-spend-alerts): monitor spend across a feature group * [Webhooks](/docs/webhook/webhooks): endpoint setup, payload signatures, and retry behavior # What Are Spend Alerts? Source: https://docs.flexprice.io/docs/subscriptions/how-to-configure-spend-alerts/what-are-spend-alerts Threshold-based alerts that track billed spend at the subscription, line item, or group level, with webhook notifications on every state change Spend alerts monitor usage-based spend against configured thresholds and notify you by webhook when a threshold is crossed. Unlike wallet and feature-usage alerts, which track balance or raw usage, spend alerts track **billed spend**: the same invoicing-grade calculation used to generate invoices, including commitments and overage. Spend alerts have three independent scopes: * **Subscription**: total usage-based spend across the whole subscription * **Subscription line item**: spend on a single line item * **Group**: spend summed across every line item whose feature belongs to a given group All three scopes share the same configuration shape, the same three-level alert model (`info` → `warning` → `critical`), and the same webhook delivery mechanism. Pick a scope based on how granular the budget you're protecting actually is: a whole subscription, one metered charge, or a set of related meters. **Benefits:** * **Budget enforcement**: Catch spend overruns before the invoice is generated * **Granular scope**: Monitor an entire subscription, a single line item, or a feature group independently * **Invoicing-grade accuracy**: Thresholds compare against the same commitment- and overage-aware spend calculation used for billing * **Real-time evaluation**: Spend is recalculated and compared against thresholds as usage events arrive, not on a delay ## Alert Scopes at a Glance | Scope | What is monitored | Webhook event | | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------- | | [Subscription](/docs/subscriptions/how-to-configure-spend-alerts/how-to-configure-subscription-spend-alerts) | Total usage-based spend across the subscription | `subscription.spend.threshold_reached` / `.threshold_recovered` | | [Subscription line item](/docs/subscriptions/how-to-configure-spend-alerts/how-to-configure-subscription-line-item-spend-alerts) | Spend on one usage-priced line item | `subscription.line_item_spend.threshold_reached` / `.threshold_recovered` | | [Group](/docs/subscriptions/how-to-configure-spend-alerts/how-to-configure-group-spend-alerts) | Spend summed across every line item in a group | `subscription.group_spend.threshold_reached` / `.threshold_recovered` | ## Alert Levels and Conditions Every alert configuration supports up to three independent threshold levels: | Level | API state | Purpose | | --------------- | ---------- | --------------------------------------------- | | **Info** | `info` | Early milestone: spend is approaching a limit | | **Warning** | `warning` | Action needed: spend is significantly high | | **Critical** | `in_alarm` | Immediate action required | | (none breached) | `ok` | Healthy, no thresholds breached | Spend alerts only support the **above** condition: an alert fires when spend rises above the threshold. The `below` condition (used by wallet balance alerts) is rejected for subscription, line item, and group scopes. Because every threshold uses `above`, ordering runs from smallest to largest: ``` Info < Warning < Critical ``` A **Warning** threshold requires a **Critical** threshold to also be set. You cannot configure Warning without Critical. **Info** can be set independently of the other two. ## Configuration API All three scopes are configured through the same `alert_settings` resource: | Action | Method | Endpoint | | -------------------- | ------ | --------------------------- | | Create alert setting | POST | `/v1/alerts/setting` | | Get alert setting | GET | `/v1/alerts/setting/:id` | | Query alert settings | POST | `/v1/alerts/setting/search` | | Update alert setting | PUT | `/v1/alerts/setting/:id` | | Delete alert setting | DELETE | `/v1/alerts/setting/:id` | **Create request body:** ```json theme={null} { "entity_type": "subscription", "entity_id": "subs_01K7YQ8ZR4M3V2N1XA9C6D0EFG", "config": { "alert_enabled": true, "info": { "threshold": "100", "condition": "above" }, "warning": { "threshold": "500", "condition": "above" }, "critical": { "threshold": "1000", "condition": "above" } } } ``` | Field | Description | | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `entity_type` | `subscription`, `subscription_line_item`, or `group` | | `entity_id` | ID of the monitored entity, matching `entity_type`: subscription ID, subscription line item ID, or group ID | | `parent_entity_type` | `subscription` for line item and group alerts; omitted for subscription-level alerts | | `parent_entity_id` | The subscription ID that owns a line item or group alert; omitted for subscription-level alerts | | `config.alert_enabled` | Turns evaluation on or off for this alert setting | | `config.critical` / `config.warning` / `config.info` | Threshold objects: `{ "threshold": "", "condition": "above" }` | `PUT /v1/alerts/setting/:id` replaces the stored `config` wholesale. It does not merge with the existing configuration. Send the complete set of thresholds you want on every update; a threshold left out of the request is cleared, not left unchanged. ## Validation | Rule | Detail | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | **Condition** | Every threshold's `condition` must be `above` | | **Warning requires Critical** | A `warning` threshold cannot exist without a `critical` threshold | | **Info stands alone** | `info` can be configured with or without `warning`/`critical` | | **Ordering** | `info` \< `warning` \< `critical`, when each is set | | **One setting per entity** | At most one alert setting per `(entity_type, entity_id, parent_entity_type, parent_entity_id)`; creating a duplicate is rejected | | **Line item scope** | `entity_id` must be a line item that belongs to the subscription identified by `parent_entity_id` | | **Group scope** | `parent_entity_id` is mandatory; a group alert always belongs to exactly one subscription | ## What's Next * [Subscription spend alerts](/docs/subscriptions/how-to-configure-spend-alerts/how-to-configure-subscription-spend-alerts): monitor total subscription spend, via UI or API * [Line item spend alerts](/docs/subscriptions/how-to-configure-spend-alerts/how-to-configure-subscription-line-item-spend-alerts): monitor one line item's spend, via UI or API * [Group spend alerts](/docs/subscriptions/how-to-configure-spend-alerts/how-to-configure-group-spend-alerts): monitor spend across a feature group, via API # 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. Progressive billing configuration: Auto invoice threshold Pass `auto_invoice_threshold` when creating a subscription: ```bash theme={null} curl -X POST https://us.api.flexprice.io/v1/subscriptions \ -H "x-api-key: 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. Pricing configured in the 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**. Select 'Configure Commitment' from the line item menu 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. Configure commitment type, quantity, overage factor, and true-up ``` 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. Pricing configured in the 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**. Select 'Configure Commitment' from the line item menu 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. Configure commitment for windowed setup ``` 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": "subs_01JVKM6S2BTUV8JKVWX3PFAD7", "status": "ACTIVE", "customer_id": "cust_01JVKM2N8XPQR4FGHT9LBWZ3S", "plan_id": "plan_01JVKM3P9YQRS5GHTU0MCXA4T", "line_items": [ { "id": "subs_line_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. Creating a subscription with seat count ## 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/subs_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/subs_01JVKM6S2BTUV8JKVWX3PFAD7/modify/preview { "type": "quantity_change", "quantity_change_params": { "line_items": [ { "id": "subs_line_01JVKM5R1ASTU7IJUVW2OEZC6", "quantity": "40" } ] } } ``` Review changes preview 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": "subs_01JVKM6S2BTUV8JKVWX3PFAD7", "..." : "..." }, "changed_resources": { "line_items": [ { "id": "(preview-ended)", "price_id": "price_01JVKM4Q0ZRST6HITUV1NDYB5", "quantity": "25", "start_date": "2026-06-01T00:00:00Z", "end_date": "2026-06-10T14:32:00Z", "change_action": "ended" }, { "id": "(preview-created)", "price_id": "price_01JVKM4Q0ZRST6HITUV1NDYB5", "quantity": "40", "start_date": "2026-06-10T14:32:00Z", "end_date": "2026-07-01T00:00:00Z", "change_action": "created" } ], "invoices": [ { "id": "(preview-invoice)", "action": "created", "status": "preview", "invoice": { "invoice_status": "DRAFT", "invoice_type": "ONE_OFF", "billing_reason": "SUBSCRIPTION_UPDATE", "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. Preview never writes anything, so the IDs are the placeholders `(preview-ended)`, `(preview-created)`, and `(preview-invoice)` rather than real IDs. ### 3c. Execute an Immediate Seat Change Omit `effective_date` to apply the change right now: ```json theme={null} POST /subscriptions/subs_01JVKM6S2BTUV8JKVWX3PFAD7/modify/execute { "type": "quantity_change", "quantity_change_params": { "line_items": [ { "id": "subs_line_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 real IDs and the invoice's payment status in place of `preview`: ```json theme={null} { "subscription": { "id": "subs_01JVKM6S2BTUV8JKVWX3PFAD7", "..." : "..." }, "changed_resources": { "line_items": [ { "id": "subs_line_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": "subs_line_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": "PENDING", "invoice": { "invoice_status": "FINALIZED", "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/subs_01JVKM6S2BTUV8JKVWX3PFAD7/modify/execute { "type": "quantity_change", "quantity_change_params": { "line_items": [ { "id": "subs_line_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. Adjusting seat count in the UI ### 3e. Collect Payment Before Seats Change By default the seat change applies immediately and the proration charge is invoiced afterwards, which can leave an unpaid invoice. To require payment first, add a `checkout` object to the execute call: ```json theme={null} POST /subscriptions/subs_01KXZW2XJJPJ5GP919AD31HY7K/modify/execute { "type": "quantity_change", "quantity_change_params": { "line_items": [ { "id": "subs_line_01KXZW2XJP3S3D5NE7TE84YP0V", "quantity": "5" } ] }, "checkout": { "payment_provider": "razorpay", "success_url": "https://app.example.com/seats/success", "cancel_url": "https://app.example.com/seats/cancel" } } ``` Seats are left untouched. Flexprice creates a `DRAFT` proration invoice for the amount, opens a checkout session, and returns a payment link in `checkout_session.payment_action.url`: ```json theme={null} { "subscription": { "id": "subs_01KXZW2XJJPJ5GP919AD31HY7K", "..." : "..." }, "changed_resources": { "line_items": [ { "id": "(preview-ended)", "quantity": "1", "change_action": "ended" }, { "id": "(preview-created)", "quantity": "5", "change_action": "created" } ], "invoices": [ { "id": "inv_01KY9HQG28KJ4PVVFH5A7M4P77", "action": "created", "status": "PENDING", "invoice": { "invoice_status": "DRAFT", "amount_due": "56.89", "currency": "usd" } } ] }, "checkout_session": { "id": "cs_01KY9HRZ25PPJ7W1THZNM43QW8", "checkout_status": "pending", "payment_action": { "type": "payment_link", "url": "https://rzp.io/l/XXXXXXX" } } } ``` Redirect the customer to that URL. When the payment succeeds, Flexprice ends the old line item, creates the replacement, and finalizes the same invoice without recalculating the proration. | Behavior | Detail | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Allowed types | `checkout` is only accepted with `type: "quantity_change"`. Any other type returns a validation error | | When it engages | Only when the batch nets to a charge. If charges minus credits is zero or negative, the change applies immediately and `checkout` is ignored | | Mixed batches | An upgrade and a downgrade in one request are netted into a single amount, not rejected | | Line item IDs | The response carries the `(preview-ended)` and `(preview-created)` placeholders because the real replacement ID is only generated after payment. Refetch the subscription once the session completes | The seat count does not change while the session is pending, and a second pay-first request for the same subscription is rejected until the open session is completed or cancelled. If the customer never pays, the draft invoice is archived and seats stay as they were. For the session lifecycle, expiry, webhook events, and charging a saved payment method instead of sending a link, see [Pay-First Checkout](/docs/checkout/pay-first-checkout). ## 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. Seat change history on subscription **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. Invoice line items for a seat change ## 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` | | Require payment before the seat increase applies | `POST /subscriptions/{id}/modify/execute` with a `checkout` object - see [Pay-First Checkout](/docs/checkout/pay-first-checkout) | | 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: `subs_line_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. **`effective_date` must fall inside the line item's active window.** Targeting a line item before it starts or after it ends returns a validation error naming the line item and its start date. This usually means you picked the future-dated record from a scheduled change instead of the currently active one. ## 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) * [Pay-First Checkout](/docs/checkout/pay-first-checkout) * [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. Subscriptions List * 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** Upcoming Invoice * 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. Subscription Options Menu * Choose Pause Subscription from the dropdown. * Select the Pause Start Date and enter the number of days to pause the subscription. Pause Subscription Form * 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” Resume Subscription Option * A confirmation message appears, indicating that resuming will **restart the billing cycle**. * Click **Yes, Resume** to reactivate the subscription. Resume Confirmation 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://us.api.flexprice.io/v1/prices \ -H "x-api-key: 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://us.api.flexprice.io/v1/subscriptions \ -H "x-api-key: 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://us.api.flexprice.io/v1/subscriptions \ -H "x-api-key: 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://us.api.flexprice.io/v1/subscriptions \ -H "x-api-key: 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 Access Wallet Settings Toggle the **Enable Auto Top-Up** switch to activate automatic wallet recharging 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 * **Cooloff** (optional): Turn on **Wait before next auto top-up**, then set a duration and unit so the wallet is not auto-recharged again until that window ends * **Invoicing Mode**: Toggle the switch to choose between: * **Direct Credit**: Credits added immediately * **Invoiced**: Invoice created, credits added after payment Configure Auto Top-Up Cooloff duration and unit 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 * **Cooloff** (optional): Duration and unit to wait after a successful auto top-up before another may run * **Invoicing Mode**: Toggle the switch to choose between: * **Direct Credit**: Credits added immediately * **Invoiced**: Invoice created, credits added after payment Configure Auto Top-Up 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 ### Cooloff Optional wait after a **successful** auto top-up before another auto top-up may run. Evaluations during the window are skipped (not queued). Manual credit, debit, and top-up are never blocked by cooloff. Without cooloff, **direct** mode can top up repeatedly in the same evaluation until ongoing balance is above the threshold. With cooloff set, the wallet gets at most one successful auto top-up per window. There is no timer when the window ends. The next top-up waits for a normal re-evaluation (usage, debit, or payment complete). Set `cooldown` on the wallet's `auto_topup` object: ```json theme={null} { "enabled": true, "threshold": "50", "amount": "100", "invoicing": false, "cooldown": { "value": 1, "unit": "day" } } ``` | Field | Description | | ---------------- | ------------------------------------ | | `cooldown.value` | Positive integer | | `cooldown.unit` | `second`, `minute`, `hour`, or `day` | Omit `cooldown` to leave it unset. On update, omit or send `null` to keep the current value; send `{ "value": 0, "unit": "second" }` to clear cooloff. A pending auto top-up transaction also blocks another auto top-up until it completes or is voided. In invoiced mode, an unpaid `WALLET_AUTO_TOPUP` invoice does the same. ## 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. 💡 **Use Cooloff for Direct Mode**: If the top-up amount is smaller than the gap to threshold, turn cooloff on so direct mode does not recharge in a tight loop. 💡 **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 * Cooloff is still active after the last successful auto top-up * A pending auto top-up transaction (or unpaid `WALLET_AUTO_TOPUP` invoice) is blocking the wallet **Solutions:** * Verify auto top-up is enabled in wallet settings * Check real-time balance vs. threshold value * Wait for cooloff to end, then trigger a new evaluation (usage, debit, or payment) * Complete, pay, or void the in-flight auto top-up before expecting another ### 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 # Wallet balance types Source: https://docs.flexprice.io/docs/wallet/balance-types How Current Balance and Ongoing Balance differ, when each is used, and which webhook events reference them. A Flexprice wallet exposes two distinct balance values: **Current Balance** and **Ongoing Balance**. They answer different questions and are used by different parts of the platform. Knowing which one you are reading prevents alert misconfiguration and surprises when reconciling usage against credits. ## At a glance | | **Current Balance** | **Ongoing Balance** | | --------------------------------- | ----------------------------------- | ------------------------------------------------------ | | What it reflects | Settled credits in the wallet | Real-time balance after pending and in-period activity | | Includes wallet credits | Yes | Yes | | Includes pending invoice amounts | No | Yes | | Includes current period usage | No | Yes | | Updates on top-up or manual debit | Yes | Yes | | Updates on metered usage events | No (until the period closes) | Yes (continuously) | | Best for | Accounting, audit, settlement views | Entitlement checks, low-balance alerts, auto top-up | ## Current Balance Current Balance is the settled credit balance of the wallet. It changes when a transaction completes — for example, a top-up is paid, a manual debit is applied, or an invoice that draws from the wallet is finalized. It does not move while usage is accumulating during a billing period. Use Current Balance when you need a stable, audit-friendly number — for example in finance exports, reconciliation reports, or the wallet ledger. ## Ongoing Balance Ongoing Balance is the real-time view of what the wallet has left after accounting for activity that has not yet settled. It starts from the Current Balance and subtracts: * **Pending invoice amounts** — invoices that are finalized but not yet paid and will draw from the wallet. * **Current period usage** — usage events ingested in the active billing period that will be billed against the wallet. Ongoing Balance is what powers entitlement decisions, [low balance alerts](/docs/wallet/low-balance-alert), and [auto top-up](/docs/wallet/auto-top-up). It is the number to read when you want to answer "does this customer have enough balance to perform this action right now?". Ongoing Balance is computed from real-time usage in ClickHouse. See [Real-time balances and alerting](/docs/getting-started/architecture#real-time-balances-and-alerting) for how freshness is guaranteed. ## Which balance triggers alerts Different alert and webhook events read different balances. Pick the event that matches the behavior you want. | Event | Balance evaluated | Triggers when | | ---------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `wallet.ongoing_balance.dropped` | Ongoing Balance | Ongoing Balance drops below a configured alert threshold | | `wallet.ongoing_balance.recovered` | Ongoing Balance | Ongoing Balance rises back above the threshold that previously triggered a `dropped` event | | `feature.wallet_balance.alert` | Feature-scoped wallet balance | A feature-scoped wallet crosses a configured threshold (above or below), per [feature alert settings](/docs/product-catalogue/features/wallet-balance-alert) | For a generic "wallet is running low" notification, use `wallet.ongoing_balance.dropped` and pair it with `wallet.ongoing_balance.recovered` to close out the alert state. Use `feature.wallet_balance.alert` when you need per-feature thresholds inside a wallet. ## Reading balances Both values are returned by the wallet read APIs and surfaced in the customer dashboard. When you build internal dashboards, gate actions on Ongoing Balance; when you build accounting views, prefer Current Balance. Do not configure low-balance alerts against Current Balance — it will not move during a billing period, so the alert will fire late or not at all. Configure thresholds against Ongoing Balance using [Wallet Sentinel Alerts](/docs/wallet/low-balance-alert). # 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. Create Price Unit 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**. Create Price Unit ### 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.”* No Wallet Linked * Click the **Add Wallet** button to open the wallet creation form. * Fill out the following details: Wallet Form * **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. Wallet Created # 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 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 * **Absolute or percentage**: Set thresholds as a fixed amount or as a share of granted credits * **Per-wallet settings**: Enable or disable alerts and configure thresholds per wallet ## Threshold Types Every threshold is read either as an **Absolute** amount or as a **Percentage**. The threshold type applies to all three levels together. | | Absolute | Percentage | | ----------------------------- | ------------------------------------ | --------------------------------------------- | | **A threshold of `10` means** | 10 in the customer's wallet currency | 10% of granted credits | | **Measured against** | The wallet balance itself | Credits granted in the current billing period | | **Accepted values** | Any amount, including negative | 0 to 100 | ### Absolute Thresholds Thresholds are fixed amounts denominated in the customer's wallet currency. On a USD denominated wallet, a Critical threshold of `10` triggers when the balance falls below \$10. Use **Absolute** when the threshold you care about is a specific fixed value, such as a minimum balance before service is interrupted or the level at which a top-up should be triggered. Negative values are allowed, so a post-paid wallet can alert on how far below zero it has gone. Absolute is the default. A wallet or tenant configuration saved without a threshold type is read as Absolute. ### Percentage Thresholds Thresholds are specified as a percentage of the credits granted during the customer's current billing period. A Critical threshold of `10` triggers when the ongoing balance falls below 10% of the credits granted in that billing period. If the customer has no active subscription, the wallet's total lifetime credits granted is used as the reference amount. Use Percentage when the meaningful floor scales with the customer. 20% left means the same thing to a customer granted 500 credits and one granted 50,000, where a fixed amount would not. Values must be between 0 and 100. Percentage threshold type selected, with each level shown as a percentage Percentage thresholds are calculated by expressing the [ongoing balance](/docs/wallet/balance-types#ongoing-balance) as a percentage of the credits granted for the current billing period. The ongoing balance reflects the real-time balance after accounting for current usage. ## 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 takes a single **threshold value**, read according to the configured [threshold type](#threshold-types). An alert triggers when the balance falls below that value. Configure only the levels you need, subject to two rules: a critical threshold is required when a warning threshold is set, and thresholds must ascend by severity, so critical is lower than warning and warning is lower than info. Alerts are turned on or off for a wallet using **Enable wallet 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 Access Alert Settings In the **Wallet Alert Settings** dialog, turn **Enable wallet alerts** on to activate monitoring against your configured thresholds. Enable Alert Settings * Choose **Absolute** or **Percentage** under **Threshold type**. * Set a value for the levels you want. Each level triggers when its threshold is breached. * Click **Save Changes** to apply. Configure Alert Settings ## 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 wallet alerts**, the selected **Threshold type**, and the threshold levels with their values. ### Changing Thresholds In the **Alert Settings** dialog box, adjust **Enable wallet alerts**, the **Threshold type**, or any threshold value, then click **Save Changes** to apply. ### Disabling Alerts Set **Enable wallet 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**: Critical at 0, Warning at 5, Info at 10 so you get early notice and 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 💡 **Pick the right threshold type**: use Absolute for a fixed floor in the wallet's currency, and Percentage when the meaningful floor scales with how many credits the customer was granted 💡 **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 Manual Debit Menu ### Step 3: Enter Debit Details A "Manual Debit" dialog will appear with the following fields: Manual Debit Form * **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. Create Wallet Credit Types **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 Top Wallet In the top-up dialog, select your credit type and enter the amount to add to the wallet Top Up Credit 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 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 Transaction Before 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. Record Payment 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. Top Up Payment Link * **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 Transaction After 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 *** ## Collect Payment Before Credits Apply The flow above finalizes an invoice and waits for you to record payment. If you would rather hand the customer a payment page and let a successful payment release the credits, add a `checkout` object to the top-up request: ```json theme={null} POST /wallets/wallet_01KY1TRVGSC4YVB3XAMFV66EC5/top-up { "credits_to_add": "500", "transaction_reason": "PURCHASED_CREDIT_INVOICED", "checkout": { "payment_provider": "razorpay", "success_url": "https://app.example.com/wallet/success", "cancel_url": "https://app.example.com/wallet/cancel" } } ``` The wallet balance does not move. Flexprice creates a pending wallet transaction and a `DRAFT` invoice, then returns a payment link: ```json theme={null} { "wallet": { "id": "wallet_01KY1TRVGSC4YVB3XAMFV66EC5", "balance": "100" }, "wallet_transaction": { "id": "wtxn_01KY9HQ95BGBN77GZXZ45W6B6W", "transaction_status": "pending", "transaction_reason": "PURCHASED_CREDIT_INVOICED", "credits": "500" }, "invoice_id": "inv_01KY9HBFHBX2JBD01QZTS109D2", "checkout_session": { "id": "cs_01KY9HRYQBMHN2NM718CX16Z1R", "checkout_status": "pending", "payment_action": { "type": "payment_link", "url": "https://rzp.io/l/XXXXXXX" } } } ``` Redirect the customer to `checkout_session.payment_action.url`. When the payment succeeds, the invoice is finalized, the transaction moves to `completed`, and the 500 credits land in the wallet. | Behavior | Detail | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Supported reason | `checkout` requires `transaction_reason: "PURCHASED_CREDIT_INVOICED"`. Any other reason returns a validation error | | Auto-complete override | If your environment has auto-complete enabled for purchased credit transactions, `checkout` overrides it. The transaction always stays pending until the payment clears | | One session per wallet | A second pay-first top-up on the same wallet is rejected while a session is open | | If payment never happens | On failure or expiry the draft invoice and the pending transaction are archived, and the balance is unchanged | `checkout` and the manual **Record Payment** flow are alternatives, not steps in sequence. Use `checkout` when you want the customer to pay through a hosted page; use the invoice flow when you settle payment yourself or out of band. To attempt a charge against a payment method the customer has already authorized instead of sending them to a payment page, see [Charge a saved payment method](/docs/checkout/pay-first-checkout#charge-a-saved-payment-method). *** ## Bonus Credits on Top-Up Purchased credit top-ups (`PURCHASED_CREDIT_DIRECT` and `PURCHASED_CREDIT_INVOICED`, including pay-first checkout) can grant an extra bonus credit alongside the purchase. Free credit top-ups never receive a bonus. The bonus is resolved automatically from a slab table, or set directly on the request. Each slab grants its bonus as one of two types: * **`flat`** : a fixed number of bonus credits, regardless of purchase size * **`percentage`** : a percentage of the credits being purchased ### Automatic Slab-Based Bonus Configure the [Bonus Credits Topup Configuration](/docs/settings/settings#bonus-credits-topup-configuration) setting with a table of slabs, each mapping a purchase threshold to a bonus: ```json theme={null} PUT /v1/settings/bonus_credits_topup_config { "value": { "enabled": true, "slabs": [ { "threshold": "200", "operator": "gte", "bonus": { "type": "percentage", "value": "10" } }, { "threshold": "100", "operator": "gte", "bonus": { "type": "flat", "value": "50" } } ] } } ``` When a purchased credit top-up runs, Flexprice matches `credits_to_add` against the slabs and grants the bonus from the first one it clears: * A top-up of `250` credits clears the `200` threshold first and gets a **10% bonus** (`25` bonus credits) * A top-up of `150` credits does not clear `200` but clears `100`, and gets a **flat 50 bonus credits** * A top-up of `50` credits does not clear any threshold and gets no bonus Slabs are evaluated in list order and the first match wins, so `slabs` must be sorted **descending** by `threshold`. Only the `gte` operator is supported. ### Manual Override To grant a specific bonus instead of resolving it from slabs, set `bonus_credits_to_add` on the top-up request. Setting `bonus_credits_to_add` grants that amount as-is and skips slab resolution entirely for that top-up. When the field isn't set, Flexprice resolves the bonus from the slab table instead, and if no slab matches, no bonus is granted. See the [Top-Up Wallet API reference](/api-reference/wallets/top-up-wallet) for the full request and response schema. ### Bonus Credit Expiry Bonus credits can expire independently of the purchased credits they came with. Set `bonus_credits_expiry_date_utc` (an RFC3339 UTC timestamp) on the request to expire the bonus at a specific time; it's rejected if it's in the past, and only takes effect when a bonus is actually granted. If the request doesn't set `bonus_credits_expiry_date_utc`, and the matched slab defines `expiration_duration` and `expiration_duration_unit` (`DAY`, `WEEK`, `MONTH`, or `YEAR`, both required together), the bonus expires that long after it's granted instead. If neither is set, the bonus credits don't expire. ### How the Bonus Is Recorded The bonus is not merged into the purchase. It is created as its own wallet transaction: * **`transaction_reason`**: `PURCHASED_CREDIT_BONUS` * **`parent_transaction_id`**: the ID of the purchase transaction it's linked to * **`transaction_status`**: mirrors the purchase transaction's status. For a direct top-up both complete immediately; for an invoiced or pay-first top-up, the bonus stays `pending` until the purchase transaction completes, then both complete together The [top-up wallet](/api-reference/wallets/top-up-wallet) response only returns the purchase transaction in `wallet_transaction`. Bonus credits do not appear there. Look up wallet transactions filtered by `parent_transaction_id` or `transaction_reason: "PURCHASED_CREDIT_BONUS"` to see the bonus. Once both transactions complete, `wallet.balance` reflects the purchase and the bonus together. ## 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 * [Pay-First Checkout](/docs/checkout/pay-first-checkout) - Gate a top-up behind a successful payment * [Settings](/docs/settings/settings#bonus-credits-topup-configuration) - Configure automatic bonus credit slabs * [Top-Up Wallet API Reference](/api-reference/wallets/top-up-wallet) - Full request and response schema *** # 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. Wallet Transactions ## Transaction Types Wallet transactions fall into three main categories based on how they affect the wallet balance: ### Free Credits Added Free Credits Transaction 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) Free Credits Transaction 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 Free Credits Transaction 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 * `entitlement.grant.exhausted` - Fired when an [entitlement grant](/docs/product-catalogue/features/entitlement-grants) window's quota is exhausted ### 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: Flexprice Product 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. Connect to Chargebee ### API Request **Endpoint:** `POST /api/v1/connections` **Headers:** ```http theme={null} Content-Type: application/json x-api-key: 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. Edit Chargebee Connection **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/introduction Find all Flexprice integrations with third-party tools: payment providers, accounting systems, and cloud marketplaces Flexprice connects to the tools you already use to collect payments, keep your books, and sell through cloud marketplaces. Each integration is built and maintained by Flexprice. Pick a category below to see what each connection syncs and how to set it up. ## Payments & Invoicing integrations Collect card payments, generate payment links, and sync customers and invoices with Stripe. Generate Razorpay payment links for invoices and sync customers between Flexprice and Razorpay. Sync customers and invoices to Paddle and collect payments through the Paddle-hosted Flexprice Checkout. Push invoices to Nomod, generate payment links, and receive payment events back through Zapier. Sync invoices to Moyasar, generate payment links, and enable autopay through the Flexprice checkout page. Push finalized invoices to Whop, get a Whop-hosted payment URL for each, and reconcile payments automatically. Sync items, customers, and invoices to Chargebee, and let Chargebee collect payment for each invoice. ## ERPs & Accounting integrations Sync items, customers, and invoices to QuickBooks Online and reconcile payments. Connect over OAuth, sync invoices to Zoho Books, receive paid and voided webhooks, and optionally import customers from contacts. ## Marketplace integrations Sell through a cloud marketplace and report each buyer's Flexprice usage back to it, so the marketplace bills your customers and pays you out. Start with the [marketplace overview](/integrations/marketplace-integration/overview) for the shared workflow. Register buyer agreements and report subscription usage to AWS through BatchMeterUsage. Register buyer agreements and report subscription usage to Google Cloud Marketplace. Register buyer agreements and report subscription usage to Azure Marketplace. # AWS Marketplace Agreement Registration Source: https://docs.flexprice.io/integrations/marketplace-integration/aws/agreement-registration Link each buyer AWS Marketplace agreement to a Flexprice subscription so their usage is reported to AWS ## Overview When a buyer subscribes to your product on AWS Marketplace, AWS creates an agreement for them and redirects them to your application. Registering that agreement tells Flexprice which subscription's usage belongs to which buyer on AWS. Registration links three Flexprice entities to their AWS counterparts: | Flexprice entity | Links to | AWS identifier | | ---------------- | ------------------------- | ---------------------- | | **Customer** | The buyer's AWS account | `CustomerAWSAccountId` | | **Subscription** | The buyer's AWS agreement | `LicenseArn` | | **Plan** | Your AWS product | `ProductCode` | Register the agreement once per buyer, at the point they subscribe. After that, usage reporting runs on its own. ## Prerequisites Before registering an agreement, ensure you have: 1. **A published AWS Marketplace connection** - See [How to Set Up AWS Marketplace](/integrations/marketplace-integration/aws/connection-setup) 2. **Your dimension API identifier** - The single usage dimension you created on your AWS product 3. **A Flexprice customer, plan, and active subscription** - For the buyer, see [Step 3](#step-3-make-sure-the-buyer-exists-in-flexprice) ## Step 1: Handle the AWS Redirect When a buyer subscribes, AWS redirects their browser to the **registration URL** configured on your product. This is an HTTP `POST` to your URL carrying a single form field: | Field | Description | | -------------------------- | ------------------------------------------------------------------------------ | | `x-amzn-marketplace-token` | A short-lived registration token identifying this buyer's subscription attempt | This token expires quickly. Exchange it immediately. Do not queue it, store it for later, or wait for a user action first. AWS returns `ExpiredTokenException` if you wait too long. For details on configuring your registration URL, see [Determine how buyers will access your product](https://docs.aws.amazon.com/marketplace/latest/userguide/saas-product-settings.html#configure-product-access) in the AWS documentation. ## Step 2: Call ResolveCustomer Call the AWS Marketplace Metering Service `ResolveCustomer` API from your backend, passing the token from Step 1. **Request to AWS:** ```json theme={null} { "RegistrationToken": "the x-amzn-marketplace-token value" } ``` This is a call your own backend makes directly to AWS, using credentials from the AWS account that published the product. Flexprice is not involved in this exchange. **AWS responds with:** | Field | What it is | Needed by Flexprice | | ---------------------- | --------------------------------------------------------------------------- | ------------------- | | `CustomerAWSAccountId` | The buyer's AWS account ID | ✅ | | `LicenseArn` | The unique identifier for this buyer's granted license, their AWS agreement | ✅ | | `ProductCode` | Your AWS product code | ✅ | | `CustomerIdentifier` | An identifier for this buyer, scoped to your product | ❌ | Persist all four values on your own side. AWS does not return them again outside this exchange, and the registration token cannot be replayed. ## Step 3: Make Sure the Buyer Exists in Flexprice Three things must already exist in Flexprice before you can register the agreement. Flexprice does not create any of them for you. Create one with `POST /v1/customers` if it does not already exist. Usage and billing are attributed to this customer. The plan whose usage-based charge corresponds to the AWS product the buyer subscribed to. See [Creating a Plan](/docs/product-catalogue/plans/create). Create it with `POST /v1/subscriptions` on that plan if it does not already exist. The subscription must be **active**: registration rejects a subscription in any other state. ## Step 4: Register the Agreement ### Using Flexprice Dashboard You can link a buyer's AWS agreement to a Flexprice subscription directly from the Flexprice dashboard. ### API Request **Endpoint:** `POST /v1/marketplace/agreements` **Headers:** ```http theme={null} Content-Type: application/json x-api-key: your_api_key X-Environment-ID: your_environment_id ``` **Request Body:** ```json theme={null} { "provider": "aws_marketplace", "subscription_id": "subs_01KX6ABC3T731GFVZDYH3Z19CP", "customer_id": "cust_01KX6ABC211T628N0NZHVMK2PZ", "plan_id": "plan_01KX5YWBTX2SZ76R7NPRAEBXZH", "customer_aws_account_id": "222222222222", "license_arn": "arn:aws:license-manager:us-east-1:222222222222:license:l-abc123xyz", "product_code": "4qwerty789", "dimension": "usage_fee", "concurrent_agreements": true } ``` | Field | Where it comes from | Required | | ------------------------- | ---------------------------------------------------------------------------------- | -------- | | `provider` | Must be `aws_marketplace` | ✅ | | `subscription_id` | The Flexprice subscription for this buyer. Must exist and be active | ✅ | | `customer_id` | The Flexprice customer for this buyer. Must match the subscription's customer | ✅ | | `plan_id` | The Flexprice plan the subscription is on. Must match the subscription's plan | ✅ | | `customer_aws_account_id` | `CustomerAWSAccountId` from the `ResolveCustomer` response | ✅ | | `license_arn` | `LicenseArn` from the `ResolveCustomer` response | ✅ | | `product_code` | `ProductCode` from the `ResolveCustomer` response | ✅ | | `dimension` | The API identifier of your product's single usage dimension | ✅ | | `concurrent_agreements` | Whether your product is enrolled in AWS Concurrent Agreements. Defaults to `false` | ❌ | Set `concurrent_agreements` to `true` for products enrolled in AWS Concurrent Agreements. For these products, AWS identifies the product by the `LicenseArn` and does not accept a `ProductCode`. Set it to `false` for legacy products. ### Response ```json theme={null} { "plan_mapping_id": "eim_01KX5YWBTX2SZ76R7NPRAEBXZH", "subscription_mapping_id": "eim_01KX6ABC3T731GFVZDYH3Z19CP", "customer_mapping_id": "eim_01KX6ABC211T628N0NZHVMK2PZ", "status": "active" } ``` A successful response means the mapping is recorded. Nothing else happens synchronously: usage reporting begins on its own schedule, described in [Usage Reporting](/integrations/marketplace-integration/aws/usage-reporting). ### Validation Rules Registration rejects the request if any of these do not hold: | Rule | Error | | ----------------------------------------------------------------------- | ----------------------------------------------------------------- | | The subscription exists and is **active** | `subscription is not active` | | `customer_id` matches the subscription's customer | `customer_id does not match subscription` | | `plan_id` matches the subscription's plan | `plan_id does not match subscription` | | The `license_arn` is not already registered to a different subscription | `agreement identifier already registered` | | The subscription is not already registered to a different `license_arn` | `subscription already mapped to a different agreement identifier` | Each agreement maps to exactly one subscription, and a subscription cannot be re-pointed to a different agreement. If a buyer re-subscribes and AWS issues a new `LicenseArn`, create a new Flexprice subscription for them rather than reusing the old one. ## Registering Multiple Buyers on One Plan The plan mapping stores your product-level AWS configuration, so it is created once and reused. Registering a second buyer on the same plan reuses the existing plan mapping rather than creating another one. This means `product_code`, `dimension`, and `concurrent_agreements` are set by the **first** agreement registered against a plan. Later registrations for that plan do not change them. Register every buyer for the same AWS product against the same Flexprice plan, with the same `product_code` and `dimension`. ## Next Steps Set up your AWS listing, IAM role, and Flexprice connection. Understand what gets metered to AWS and when. **API References** * [Create a Customer](/api-reference/customers/create-customer) * [Create a Subscription](/api-reference/subscriptions/create-subscription) # How to Set Up AWS Marketplace Source: https://docs.flexprice.io/integrations/marketplace-integration/aws/connection-setup Complete guide to listing your product on AWS Marketplace and connecting your AWS seller account to Flexprice ## Overview The AWS Marketplace integration reports a subscription's Flexprice-computed usage to your AWS Marketplace product, going forward from when it is set up. You sell and manage your product on AWS Marketplace yourself; Flexprice's role is to compute usage and report it, so AWS can bill the buyer and pay you out. Setting this up is a one-time job with three parts: * **Your AWS listing**: a product with a single usage dimension, priced so AWS bills exactly what Flexprice computes * **An IAM role**: cross-account permission for Flexprice to report usage into your AWS account * **A Flexprice connection**: stores the role details so reporting can run Once this is done, you link each buyer's AWS agreement to a Flexprice subscription. That is covered in [AWS Marketplace Agreement Registration](/integrations/marketplace-integration/aws/agreement-registration). ## Prerequisites Before setting up your AWS Marketplace connection, ensure you have: 1. **Approved AWS Marketplace Seller Account** - With permission to create and publish products 2. **AWS IAM Access** - To create a policy and a cross-account role in your seller account ## Step 1: Create Your AWS Marketplace Listing Create your product in the AWS Marketplace Management Portal. The integration supports products priced with **exactly one usage dimension**. Select a usage-based pricing model for your product. Add **one** usage dimension. Note its **API identifier**: this is the dimension name Flexprice reports against, and AWS makes it immutable once created. | Field | Value | | ------------------- | -------------------------------------------------------------------------------------- | | **Dimension count** | Exactly one | | **API identifier** | Your choice, for example `usage_fee`. Copy it: you need it when registering agreements | | **Price per unit** | `$0.01` | Price your single usage dimension at **`$0.01` per unit**. AWS only accepts whole numbers as a usage quantity, so Flexprice reports your billable amount in cents. Pricing at this rate means AWS multiplies those cents back into the exact amount Flexprice computed, down to the cent. The price must be exactly `$0.01` per unit. Any other value scales what AWS bills your buyers by that factor. ## Step 2: Set Up IAM Credentials for Flexprice to Meter on Your Behalf Flexprice reports usage by assuming a cross-account IAM role in your AWS account. You create a policy allowing the metering action, then attach it to a role that trusts Flexprice and requires an external ID. ### Create the IAM Policy In the AWS IAM console, go to **Policies → Create policy**, switch to the **JSON** editor, and paste the following. It grants the single permission Flexprice needs, and nothing else. ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "aws-marketplace:BatchMeterUsage", "Resource": "*" } ] } ``` Specify permissions for the IAM policy Name the policy, for example `flexprice-marketplace-policy`, then choose **Create policy**. Review and create the IAM policy ### Get Your Account ID and External ID Go to your AWS Marketplace connection setup in Flexprice to find your **AWS account ID** and **external ID**. You need both in the next step. ### Create the Cross-Account Role In the AWS IAM console, go to **Roles → Create role**, and under **Trusted entity type** choose **Custom trust policy**. Paste the following, replacing `` with the external ID from the previous step and keeping the account ID exactly as written. ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::851725232919:root" }, "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "sts:ExternalId": "" } } } ] } ``` Select a custom trust policy for the cross-account role Search for and select the policy you created above. Attach the policy to the role Give the role a name, for example `flexprice-marketplace-role`, confirm the trust policy matches what you pasted, and choose **Create role**. Review and create the role Open the role and copy its ARN. It has the format `arn:aws:iam:::role/`. ## Step 3: Create the AWS Marketplace Connection ### Using Flexprice Dashboard You can create an AWS Marketplace connection directly from the Flexprice dashboard under **Integrations → AWS Marketplace**. ### API Request **Endpoint:** `POST /v1/connections` **Headers:** ```http theme={null} Content-Type: application/json x-api-key: your_api_key X-Environment-ID: your_environment_id ``` **Request Body:** ```json theme={null} { "name": "AWS Marketplace", "provider_type": "aws_marketplace", "encrypted_secret_data": { "role_arn": "", "external_id": "" }, "sync_config": { "aws_marketplace": { "region": "us-east-1" } } } ``` | Field | Description | Required | | ------------------------------------ | ------------------------------------------------------------------------ | -------- | | `name` | A friendly name for the connection | ✅ | | `provider_type` | Must be `aws_marketplace` | ✅ | | `role_arn` | The ARN of the cross-account role from Step 2 | ✅ | | `external_id` | The external ID Flexprice provided, matching the role's trust policy | ✅ | | `sync_config.aws_marketplace.region` | The AWS region your product's Marketplace Metering Service is enabled in | ✅ | ### Response ```json theme={null} { "id": "conn_01KX6ABC211T628N0NZHVMK2PZ", "name": "AWS Marketplace", "provider_type": "aws_marketplace", "status": "published", "created_at": "2026-07-15T10:30:00Z", "updated_at": "2026-07-15T10:30:00Z" } ``` Both credentials are stored encrypted. `sync_config.aws_marketplace.region` selects the AWS Marketplace Metering Service endpoint your usage is reported to. ### Connection Verification Creating the connection immediately attempts to assume your role. The connection is only saved if that succeeds, so a successful response confirms your trust policy and external ID actually work rather than failing silently later. If it fails, nothing is saved and you get a validation error. Fix the cause and create the connection again. | Cause | Fix | | ------------------------------------ | ---------------------------------------------------------------------- | | Trust policy names the wrong account | Confirm the principal is the AWS account ID Flexprice showed you | | External ID does not match | Confirm the `sts:ExternalId` condition matches the external ID exactly | | Role ARN typo | Re-copy the ARN from the AWS IAM console | | Policy not attached | Confirm the `BatchMeterUsage` policy is attached to the role | ## Next Steps Link each buyer's AWS agreement to a Flexprice subscription. Understand what gets metered to AWS and when. # AWS Marketplace Usage Reporting Source: https://docs.flexprice.io/integrations/marketplace-integration/aws/usage-reporting What Flexprice meters to AWS Marketplace and when it reports ## Overview Once a buyer's agreement is registered, usage reporting runs automatically. You do not call Flexprice again for that buyer unless their subscription changes. Flexprice computes each mapped subscription's usage for the elapsed period and reports it against your product's single usage dimension. AWS then invoices the buyer and pays you out. ## What Flexprice Meters to AWS The amount reported to AWS is the same billable amount that appears on that subscription's Flexprice invoice for the same period. It is computed with the same rules, so AWS bills what Flexprice bills. That means the reported amount already accounts for: * **Usage-based charges** across every meter on the subscription * **Commitment amounts**, where the subscription has a committed spend * **Overage rates**, where usage exceeds the commitment The amount is reported in cents against your single usage dimension. With the dimension priced at `$0.01` per unit, AWS multiplies it back into the exact amount, down to the cent. See [Create Your AWS Marketplace Listing](/integrations/marketplace-integration/aws/connection-setup#step-1-create-your-aws-marketplace-listing). Flexprice reports usage to AWS on a recurring schedule. Do not expect usage to appear in AWS immediately after it happens in Flexprice. ## Where Usage Appears in AWS Reported usage shows up against your product on the buyer's AWS bill, under the **AWS Marketplace** service provider. Use this to confirm usage is reaching AWS. AWS bill showing Marketplace charges ## Retries and Failures Reporting recovers from transient failures on its own. * **Reports are safe to retry.** Each report carries a fixed timestamp, so a retry is an identical request and AWS de-duplicates it rather than billing twice. * **Unaccepted reports are retried.** If AWS does not accept a report, it stays unreported and is retried on the next run. No action is needed from you. * **A rejected buyer resolves itself.** If AWS reports the buyer has no active agreement, reporting keeps retrying and succeeds once the buyer's agreement is active again. ## Handling Marketplace Lifecycle Events Flexprice does not learn about AWS-side changes to a buyer's subscription. You handle these. | Event on AWS | What you must do in Flexprice | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Buyer cancels or unsubscribes | Cancel the buyer's Flexprice subscription, see [Final Usage When an Agreement Ends](#final-usage-when-an-agreement-ends) | | Buyer's agreement expires | Cancel the buyer's Flexprice subscription | | Buyer re-subscribes with a new agreement | Create a new Flexprice subscription and register the new agreement | If a buyer cancels on AWS and you leave their Flexprice subscription active, Flexprice keeps computing usage for it and attempting to report it to AWS. Cancel the Flexprice subscription to stop this. To detect these events, subscribe to your product's AWS Marketplace notifications in your own application. Flexprice does not subscribe to them. ## Final Usage When an Agreement Ends AWS gives a window of about **one hour** after an agreement ends to submit its final metering record. Use the timestamp from the AWS notification, not the time you process it. Flexprice reports the final usage against `cancel_at`, and a timestamp after the agreement ended falls outside the window AWS will accept. See [When the Marketplace Agreement Ends](/integrations/marketplace-integration/overview#when-the-marketplace-agreement-ends) for how the final usage flush works and how to cancel correctly. ## Troubleshooting | Symptom | Likely cause | Fix | | ---------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | No usage in AWS at all | Agreement never registered | Confirm registration succeeded, see [Agreement Registration](/integrations/marketplace-integration/aws/agreement-registration) | | No usage in AWS for one buyer | Subscription is not in USD | Move the buyer to a USD-denominated plan | | Usage appears late | Expected: reporting is scheduled, not real time | None | | AWS bills the wrong amount | Dimension not priced at `$0.01` per unit | Correct the dimension price on your AWS product | | Usage still reporting after cancellation | Flexprice subscription still active | Cancel the Flexprice subscription | ## Next Steps Set up your AWS listing, IAM role, and Flexprice connection. Link each buyer's AWS agreement to a Flexprice subscription. # Azure Marketplace Agreement Registration Source: https://docs.flexprice.io/integrations/marketplace-integration/azure/agreement-registration Link each buyer Azure Marketplace subscription to a Flexprice subscription so their usage is reported to Azure ## Overview When a buyer subscribes to your offer on Azure Marketplace, Azure creates a SaaS subscription for them. Registering that subscription tells Flexprice which subscription's usage belongs to which buyer on Azure. Registration links three Flexprice entities to their Azure counterparts: | Flexprice entity | Links to | Azure identifier | | ---------------- | ----------------------------- | -------------------------- | | **Customer** | The buyer's Entra tenant | `beneficiary.tenantId` | | **Subscription** | The buyer's SaaS subscription | `id`, sent as `resourceId` | | **Plan** | The plan on your offer | `planId` + dimension | Flexprice does not call the Azure SaaS fulfillment APIs itself. You resolve the subscription's identifiers yourself and pass them to Flexprice. Register the subscription once per buyer, at the point they subscribe. After that, usage reporting runs on its own. ## Prerequisites Before registering an agreement, ensure you have: 1. **A published Azure Marketplace connection** - See [How to Set Up Azure Marketplace](/integrations/marketplace-integration/azure/connection-setup) 2. **The buyer's Azure SaaS subscription, resolved and activated** - Through your own backend, using [Azure's SaaS fulfillment APIs](https://learn.microsoft.com/en-us/partner-center/marketplace-offers/pc-saas-fulfillment-subscription-api). Flexprice is not involved in this step. 3. **Your dimension ID** - The single metered dimension you created on your plan 4. **A Flexprice customer, plan, and active subscription** - For the buyer, see [Step 2](#step-2-make-sure-the-buyer-exists-in-flexprice) ## Step 1: Get the Values Flexprice Needs Registering an agreement needs four values, from the subscription you resolved and your own offer setup. | Field | What it is | Where it comes from | | ---------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Resource ID | The Azure SaaS subscription ID. Sent to Azure as the `resourceId` on every usage report | The subscription's `id`, per buyer | | Plan ID | Azure's identifier for the plan the buyer bought | The subscription's `planId`, per buyer | | Beneficiary account ID | The Entra tenant ID of the buyer's account | The subscription's `beneficiary.tenantId`, per buyer | | Dimension | The metered dimension ID on your plan | Your own offer setup, see [Create Your Azure Marketplace Offer](/integrations/marketplace-integration/azure/connection-setup#step-1-create-your-azure-marketplace-offer) | `offerId` is not part of Azure's usage report payload, so Flexprice does not store it. The plan is identified by `planId` alone. Persist these values on your own side. Flexprice does not look them up again after registration. ## Step 2: Make Sure the Buyer Exists in Flexprice Three things must already exist in Flexprice before you can register the agreement. Flexprice does not create any of them for you. Create one with `POST /v1/customers` if it does not already exist. Usage and billing are attributed to this customer. The plan whose usage-based charge corresponds to the Azure plan the buyer subscribed to. See [Creating a Plan](/docs/product-catalogue/plans/create). Create it with `POST /v1/subscriptions` on that plan if it does not already exist. The subscription must be **active**: registration rejects a subscription in any other state. ## Step 3: Register the Agreement ### Using Flexprice Dashboard You can link a buyer's Azure subscription to a Flexprice subscription directly from the Flexprice dashboard. ### API Request **Endpoint:** `POST /v1/marketplace/agreements` **Headers:** ```http theme={null} Content-Type: application/json x-api-key: your_api_key X-Environment-ID: your_environment_id ``` **Request Body:** ```json theme={null} { "provider": "azure_marketplace", "subscription_id": "subs_01KYAHZ3FMB7MH9V2V0STQXJPK", "customer_id": "cust_01KYAHVFA430GB6HQS556DDA0V", "plan_id": "plan_01KXG2APD7806XMH1V01465Q64", "azure": { "plan_id": "silver", "dimension": "usage_fee", "resource_id": "11111111-2222-3333-4444-555555555555", "beneficiary_account_id": "" } } ``` | Field | Where it comes from | Required | | ------------------------------ | ----------------------------------------------------------------------------- | -------- | | `provider` | Must be `azure_marketplace` | ✅ | | `subscription_id` | The Flexprice subscription for this buyer. Must exist and be active | ✅ | | `customer_id` | The Flexprice customer for this buyer. Must match the subscription's customer | ✅ | | `plan_id` | The Flexprice plan the subscription is on. Must match the subscription's plan | ✅ | | `azure.plan_id` | Azure's `planId` from Step 1, sent to Azure on every usage report | ✅ | | `azure.dimension` | The metered dimension ID on your plan | ✅ | | `azure.resource_id` | The Azure SaaS subscription ID from Step 1 | ✅ | | `azure.beneficiary_account_id` | The buyer's Entra tenant ID from Step 1 | ✅ | `azure.plan_id` is Azure's own plan identifier, sent in the usage report. The top-level `plan_id` is the Flexprice plan. These are two different values and both are required. ### Response ```json theme={null} { "plan_mapping_id": "eim_01KYAJ3SVSCVGQXKHQRY4GBS3V", "subscription_mapping_id": "eim_01KYAJ3T23EBFZHH9ZZ2XCEMJM", "customer_mapping_id": "eim_01KYAHWSV0JM53DZZ9Z84C2H75", "status": "active" } ``` A successful response means the mapping is recorded. Nothing else happens synchronously: usage reporting begins on its own schedule, described in [Usage Reporting](/integrations/marketplace-integration/azure/usage-reporting). ### Validation Rules Registration rejects the request if any of these do not hold: | Rule | Error | | ----------------------------------------------------------------------- | ----------------------------------------------------------------- | | The subscription exists and is **active** | `subscription is not active` | | `customer_id` matches the subscription's customer | `customer_id does not match subscription` | | `plan_id` matches the subscription's plan | `plan_id does not match subscription` | | The `resource_id` is not already registered to a different subscription | `agreement identifier already registered` | | The subscription is not already registered to a different `resource_id` | `subscription already mapped to a different agreement identifier` | Each Azure subscription maps to exactly one Flexprice subscription, and a subscription cannot be re-pointed to a different Azure subscription. If a buyer re-subscribes and Azure issues a new resource ID, create a new Flexprice subscription for them rather than reusing the old one. ## Registering Multiple Buyers on One Plan The plan mapping stores your plan-level Azure configuration, so it is created once and reused. Registering a second buyer on the same plan reuses the existing plan mapping rather than creating another one. This means `azure.plan_id` and `dimension` are set by the **first** agreement registered against a plan. Later registrations for that plan do not change them. Register every buyer for the same Azure plan against the same Flexprice plan, with the same `azure.plan_id` and `dimension`. If your offer has several plans, map each Azure plan to its own Flexprice plan. A buyer who changes plan on Azure moves to the Flexprice plan mapped to their new `planId`. ## Next Steps Set up your Azure offer, Entra app credentials, and Flexprice connection. Understand what gets metered to Azure and when. **API References** * [Create a Customer](/api-reference/customers/create-customer) * [Create a Subscription](/api-reference/subscriptions/create-subscription) # How to Set Up Azure Marketplace Source: https://docs.flexprice.io/integrations/marketplace-integration/azure/connection-setup Complete guide to creating your Azure Marketplace offer and giving Flexprice your Microsoft Entra app credentials ## Overview The Azure Marketplace integration reports a subscription's Flexprice-computed usage to your Azure Marketplace offer, going forward from when it is set up. You sell and manage your offer on Azure Marketplace yourself; Flexprice's role is to compute usage and report it, so Azure can bill the buyer and pay you out. Setting this up is a one-time job with three parts: * **Your Azure offer**: a transactable SaaS offer with a single metered dimension, priced so Azure bills exactly what Flexprice computes * **Entra app credentials**: let Flexprice authenticate as the app registered on your offer and report usage into your Azure account * **A Flexprice connection**: stores those credentials so reporting can run Once this is done, you link each buyer's Azure subscription to a Flexprice subscription. That is covered in [Azure Marketplace Agreement Registration](/integrations/marketplace-integration/azure/agreement-registration). ## Prerequisites Before setting up your Azure Marketplace connection, ensure you have: 1. **A Partner Center account** - Enrolled to publish transactable SaaS offers on Azure Marketplace 2. **Access to your Microsoft Entra ID tenant** - To register an app and generate a client secret ## Step 1: Create Your Azure Marketplace Offer Create your transactable SaaS offer in Partner Center. The integration supports plans priced with **exactly one metered dimension**. On the plan's **Pricing and availability** tab, select **Flat rate**, and for every contract duration and billing term you configure, set the **price per charge** to **`$0`**. All revenue flows through the metered dimension you add next, not the recurring charge. A custom meter dimension is only available on the flat rate model. The per user model does not support it. Add **one** custom meter dimension to the plan (**Add a Custom Meter Dimension**). Azure makes every field immutable once the offer is published. | Field | Value | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | **Dimension count** | Exactly one | | **ID** | Your choice, for example `usage_fee`. This is what Flexprice reports against — copy it, you need it when registering agreements | | **Display Name** | Your choice, shown to the buyer, for example "Usage Fee" | | **Unit of Measure** | Your choice, a free-text description of the billing unit, for example "per cent of usage" | | **Price per unit in USD** | `0.01` | | **Quantity included in base** | `0`, for every contract duration you enable on the plan | The price per unit must be exactly `0.01`, and the price per charge must be `0` for every contract duration and billing term. Any other value scales what Azure bills your buyers by that factor. Billing dimensions are shared across all plans on an offer. If your offer has several plans, each one carries its own plan ID, so register each as a separate Flexprice plan. See [Agreement Registration](/integrations/marketplace-integration/azure/agreement-registration). ## Step 2: Set Up Authentication for Flexprice to Meter on Your Behalf Flexprice reports usage by authenticating as an app registered in your own Microsoft Entra ID tenant. You register the app, generate a client secret, make the Marketplace metering API addressable from your tenant, and enter the app on your offer. Flexprice creates nothing in your Azure tenant and never touches Partner Center. It receives three strings you generate and uses them to request access tokens. In the Azure portal, go to **Microsoft Entra ID**. Finding Microsoft Entra ID from the Azure services grid In the left navigation, select **App registrations**. Microsoft Entra ID overview, with App registrations in the left navigation Select **New registration**. Give it a name, choose **Single tenant**, and register it. Register an application in Microsoft Entra ID From the app's **Overview** page, copy two values: | Value | Where to find it | | --------------------------- | -------------------------------------------------------- | | **Directory (tenant) ID** | App registration overview page. This is your `tenant_id` | | **Application (client) ID** | App registration overview page. This is your `client_id` | The app registration's Overview page, showing the Application (client) ID and Directory (tenant) ID On that app registration, go to **Certificates & secrets → Client secrets → New client secret**. Copy the secret **Value** immediately: Azure shows it only once. This is your `client_secret`. Certificates & secrets page showing a generated client secret Client secrets expire. Note the expiry you choose. When the secret expires, reporting fails until you generate a new one and update your Flexprice connection. Before the app can request a token for the Marketplace metering API, your tenant needs a service principal for it. `20e940b3-4c77-4b0b-9a53-9e16a1b010a7` is Microsoft's fixed application ID for the Marketplace SaaS API, the same value for every publisher. In Azure Cloud Shell, or any terminal signed in with `az login`, run: ```bash theme={null} az ad sp create --id 20e940b3-4c77-4b0b-9a53-9e16a1b010a7 ``` This creates no secret and grants no access on its own. It only makes that Microsoft API addressable from your directory, so the app from the first step can request tokens for it. See step 7 of [Register a SaaS application](https://learn.microsoft.com/en-us/partner-center/marketplace-offers/pc-saas-registration) for Microsoft's own instructions. In Partner Center, open your offer's **Technical Configuration** page and enter this app's **Microsoft Entra tenant ID** and **Microsoft Entra application ID**. Use the same Entra tenant ID and application ID that you specified in the Partner Center Technical Configuration page of each offer you want usage reported for. Azure checks the token's app against the offer on every report, so a mismatch here surfaces later as a failed usage report, not a failed connection. ## Step 3: Create the Azure Marketplace Connection ### Using Flexprice Dashboard You can create an Azure Marketplace connection directly from the Flexprice dashboard under **Integrations → Azure Marketplace**. Paste the tenant ID, client ID, and client secret from Step 2. The Flexprice dashboard's Connect to Azure Marketplace drawer, with Tenant ID, Client ID, and Client secret fields ### API Request **Endpoint:** `POST /v1/connections` **Headers:** ```http theme={null} Content-Type: application/json x-api-key: your_api_key X-Environment-ID: your_environment_id ``` **Request Body:** ```json theme={null} { "name": "Azure Marketplace", "provider_type": "azure_marketplace", "encrypted_secret_data": { "tenant_id": "", "client_id": "", "client_secret": "" } } ``` | Field | Description | Required | | --------------- | --------------------------------------- | -------- | | `name` | A friendly name for the connection | ✅ | | `provider_type` | Must be `azure_marketplace` | ✅ | | `tenant_id` | The directory (tenant) ID from Step 2 | ✅ | | `client_id` | The application (client) ID from Step 2 | ✅ | | `client_secret` | The client secret value from Step 2 | ✅ | ### Response ```json theme={null} { "id": "conn_01KYAKZJWKTMATYXXF4ME2W128", "name": "Azure Marketplace", "provider_type": "azure_marketplace", "status": "published", "created_at": "2026-07-24T17:16:01Z", "updated_at": "2026-07-24T17:16:01Z" } ``` All three credentials are stored encrypted. ### Connection Verification Creating the connection immediately requests an access token with your credentials. The connection is only saved if that succeeds, so a successful response confirms your tenant ID, client ID, and client secret actually work rather than failing silently later. If it fails, nothing is saved and you get a validation error. Fix the cause and create the connection again. | Cause | Fix | | ------------------------------------------------ | ------------------------------------------------------------------------------------- | | Wrong tenant ID, client ID, or client secret | Re-copy all three from the app registration overview and Certificates & secrets pages | | Client secret expired | Generate a new client secret and use its value | | Marketplace API not addressable from your tenant | Run `az ad sp create --id 20e940b3-4c77-4b0b-9a53-9e16a1b010a7` in your tenant | Whether the app is entered on your offer's Technical Configuration is not checked here. Flexprice has no offer to check it against at connection time, so a mismatch there surfaces later as a failed usage report. See [Usage Reporting](/integrations/marketplace-integration/azure/usage-reporting). ## Next Steps Link each buyer's Azure subscription to a Flexprice subscription. Understand what gets metered to Azure and when. # Azure Marketplace Usage Reporting Source: https://docs.flexprice.io/integrations/marketplace-integration/azure/usage-reporting What Flexprice meters to Azure Marketplace and when it reports ## Overview Once a buyer's subscription is registered, Flexprice reports its usage to Azure automatically, going forward. Usage from before the connection was made is not reported, and you do not call Flexprice again for that buyer unless their subscription changes. Flexprice computes each mapped subscription's usage for the elapsed period and reports it against your plan's single metered dimension. Azure then invoices the buyer and pays you out. ## What Flexprice Meters to Azure The amount reported to Azure is the same billable amount that appears on that subscription's Flexprice invoice for the same period. It is computed with the same rules, so Azure bills what Flexprice bills. That means the reported amount already accounts for: * **Usage-based charges** across every meter on the subscription * **Commitment amounts**, where the subscription has a committed spend * **Overage rates**, where usage exceeds the commitment The amount is reported in cents against your single metered dimension. With the dimension priced at `$0.01` per unit, Azure multiplies it back into the exact amount, down to the cent. See [Create Your Azure Marketplace Offer](/integrations/marketplace-integration/azure/connection-setup#step-1-create-your-azure-marketplace-offer). Only subscriptions denominated in **USD** are reported. A subscription in any other currency is left unreported rather than converted. Azure only accepts a usage quantity greater than zero. When a subscription's computed amount for a period rounds to zero cents, Flexprice does not send a usage event for it. This is expected: there is nothing to bill, and the subscription is not left in an error state. Flexprice reports usage to Azure on a recurring schedule. Do not expect usage to appear in Azure immediately after it happens in Flexprice. ## Retries and Failures Reporting recovers from transient failures on its own. * **Reports are safe to retry.** Each report carries a fixed effective time, so a retry is an identical request. Azure rejects it as a duplicate rather than billing twice. * **Unaccepted reports are retried.** If Azure does not accept a report, it stays unreported and is retried on the next run. No action is needed from you. * **A rejected buyer resolves itself.** If Azure reports the subscription is not active, reporting keeps retrying and succeeds once the buyer's subscription is active again. Azure will not accept a report older than 24 hours. A report that keeps failing for a full day stops being accepted after that, and the usage it carries is not billed to the buyer. Act on repeated reporting failures the same day they appear. If the app on your offer's Technical Configuration does not match the credentials on your Flexprice connection, Azure rejects every report with a `401`. Confirm the tenant ID and application ID on the offer match the app whose credentials you gave Flexprice. See [Set Up Authentication](/integrations/marketplace-integration/azure/connection-setup#step-2-set-up-authentication-for-flexprice-to-meter-on-your-behalf). ## Handling Marketplace Lifecycle Events Flexprice does not learn about Azure-side changes to a buyer's subscription. You handle these. | Event on Azure | What you must do in Flexprice | | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | Buyer unsubscribes | Cancel the buyer's Flexprice subscription, see [Final Usage When a Buyer Unsubscribes](#final-usage-when-a-buyer-unsubscribes) | | Buyer's SaaS subscription is suspended or expires | Cancel the buyer's Flexprice subscription | | Buyer re-subscribes with a new SaaS subscription | Create a new Flexprice subscription and register the new one | If a buyer unsubscribes on Azure and you leave their Flexprice subscription active, Flexprice keeps computing usage for it and attempting to report it to Azure. Cancel the Flexprice subscription to stop this. To detect these events, subscribe to your offer's webhook in your own application. Flexprice does not subscribe to it. ## Final Usage When a Buyer Unsubscribes Azure keeps accepting usage for an unsubscribed SaaS subscription as long as the usage covers time **before** the subscription was cancelled, and is submitted inside the 24 hour limit above. See [When the Marketplace Agreement Ends](/integrations/marketplace-integration/overview#when-the-marketplace-agreement-ends) for how the final usage flush works and how to cancel correctly. ## Troubleshooting | Symptom | Likely cause | Fix | | ----------------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | No usage in Azure at all | Agreement never registered | Confirm registration succeeded, see [Agreement Registration](/integrations/marketplace-integration/azure/agreement-registration) | | Every report rejected with a `401` | The app on the offer's Technical Configuration does not match the connected credentials | Confirm the tenant ID and application ID on the offer match your Flexprice connection | | Reports stopped working after months of success | Client secret expired | Generate a new client secret and update your Flexprice connection | | Usage appears late | Expected: reporting is scheduled, not real time | None | | Azure bills the wrong amount | Dimension not priced at `$0.01` per unit, or the plan's recurring price is not `$0` | Correct the pricing on your Azure plan | | Usage still reporting after cancellation | Flexprice subscription still active | Cancel the Flexprice subscription | ## Next Steps Set up your Azure offer, Entra app credentials, and Flexprice connection. Link each buyer's Azure subscription to a Flexprice subscription. # GCP Marketplace Agreement Registration Source: https://docs.flexprice.io/integrations/marketplace-integration/gcp/agreement-registration Link each buyer GCP Marketplace entitlement to a Flexprice subscription so their usage is reported to GCP ## Overview When a buyer subscribes to your product on GCP Marketplace, GCP creates an entitlement for them. Registering that entitlement tells Flexprice which subscription's usage belongs to which buyer on GCP. Registration links three Flexprice entities to their GCP counterparts: | Flexprice entity | Links to | GCP identifier | | ---------------- | ----------------------- | ------------------------------ | | **Customer** | The buyer's GCP account | `account_id` | | **Subscription** | The buyer's entitlement | `usage_reporting_id` | | **Plan** | Your GCP product | `service_name` + `metric_name` | Flexprice does not call the GCP Procurement API itself. You retrieve the entitlement's identifiers yourself and pass them to Flexprice. Register the entitlement once per buyer, at the point they subscribe. After that, usage reporting runs on its own. ## Prerequisites Before registering an agreement, ensure you have: 1. **A published GCP Marketplace connection** - See [How to Set Up GCP Marketplace](/integrations/marketplace-integration/gcp/connection-setup) 2. **Your metric name** - The single usage metric you created on your GCP product 3. **A Flexprice customer, plan, and active subscription** - For the buyer, see [Step 3](#step-3-make-sure-the-buyer-exists-in-flexprice) for what you must check or create before registering. ## Step 1: Handle the Entitlement Creation When a buyer subscribes on GCP Marketplace, GCP creates an entitlement for them in your producer project. Detecting and retrieving that entitlement is done through the GCP Procurement API, in your own backend, using credentials from the GCP project that published the product. This is a call your own backend makes directly to the GCP Procurement API. Flexprice is not involved in this exchange. See [Manage customer entitlements](https://docs.cloud.google.com/marketplace/docs/partners/integrated-saas/manage-entitlements) for how to detect and read a new entitlement. ## Step 2: Get the Values Flexprice Needs Registering an agreement needs four values. Two are per-buyer, from the entitlement your backend retrieved in [Step 1](#step-1-handle-the-entitlement-creation). Two are per-product, fixed for every buyer and already known from setting up your listing. | Field | What it is | Where it comes from | | ------------------ | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Usage reporting ID | The identifier for this buyer's entitlement. Sent to GCP as the consumer ID on every usage report | The entitlement, per buyer | | Account identifier | An identifier for the buyer's GCP account | The entitlement, per buyer | | Service name | Your product's Service Control managed service name | Your own product setup, same for every buyer on this product, see [Create Your GCP Marketplace Listing](/integrations/marketplace-integration/gcp/connection-setup#step-1-create-your-gcp-marketplace-listing) | | Metric name | The fully qualified `/` identifier for your usage metric | Your own product setup, same for every buyer on this product, see [Create Your GCP Marketplace Listing](/integrations/marketplace-integration/gcp/connection-setup#step-1-create-your-gcp-marketplace-listing) | GCP's own docs give this example format: service name `example-messaging-service.gcpmarketplace.example.com`, metric name `example-messaging-service/UsageInGiB`. Your actual values are account-specific, GCP does not publish a fixed value, view yours in Producer Portal's **Technical integration** section, see [Configure usage reporting to Google](https://docs.cloud.google.com/marketplace/docs/partners/integrated-saas/configure-usage-reports). Persist these values on your own side. Flexprice does not look them up again after registration. ## Step 3: Make Sure the Buyer Exists in Flexprice Three things must already exist in Flexprice before you can register the agreement. Flexprice does not create any of them for you. Create one with `POST /v1/customers` if it does not already exist. Usage and billing are attributed to this customer. The plan whose usage-based charge corresponds to the GCP product the buyer subscribed to. See [Creating a Plan](/docs/product-catalogue/plans/create). Create it with `POST /v1/subscriptions` on that plan if it does not already exist. The subscription must be **active**: registration rejects a subscription in any other state. ## Step 4: Register the Agreement ### Using Flexprice Dashboard You can link a buyer's GCP entitlement to a Flexprice subscription directly from the Flexprice dashboard. ### API Request **Endpoint:** `POST /v1/marketplace/agreements` **Headers:** ```http theme={null} Content-Type: application/json x-api-key: your_api_key X-Environment-ID: your_environment_id ``` **Request Body:** ```json theme={null} { "provider": "gcp_marketplace", "subscription_id": "subs_01KY2J08K0196MJYEVXWXGQWND", "customer_id": "cust_01KY2HVFA430GB6HQS556DDA0V", "plan_id": "plan_01KXG2APD7806XMH1V01465Q64", "gcp": { "service_name": "your-service.endpoints.your-project.cloud.goog", "usage_reporting_id": "consumer-identifier-from-entitlement", "metric_name": "your-service.endpoints.your-project.cloud.goog/usage_fee", "account_id": "buyer-account-identifier" } } ``` | Field | Where it comes from | Required | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | -------- | | `provider` | Must be `gcp_marketplace` | ✅ | | `subscription_id` | The Flexprice subscription for this buyer. Must exist and be active | ✅ | | `customer_id` | The Flexprice customer for this buyer. Must match the subscription's customer | ✅ | | `plan_id` | The Flexprice plan the subscription is on. Must match the subscription's plan | ✅ | | `gcp.service_name` | Your product's Service Control managed service name | ✅ | | `gcp.usage_reporting_id` | The consumer ID from Step 2, identifying this buyer's entitlement | ✅ | | `gcp.metric_name` | The fully qualified metric name, `/`, for example `your-service.endpoints.your-project.cloud.goog/usage_fee` | ✅ | | `gcp.account_id` | The buyer's GCP account identifier from Step 2 | ✅ | `gcp.metric_name` is sent to GCP exactly as you provide it, with no prefix added by Flexprice. Pass the fully qualified `/` form, not the bare metric name. ### Response ```json theme={null} { "plan_mapping_id": "eim_01KY2J3SVSCVGQXKHQRY4GBS3V", "subscription_mapping_id": "eim_01KY2J3T23EBFZHH9ZZ2XCEMJM", "customer_mapping_id": "eim_01KY2HWSV0JM53DZZ9Z84C2H75", "status": "active" } ``` A successful response means the mapping is recorded. Nothing else happens synchronously: usage reporting begins on its own schedule, described in [Usage Reporting](/integrations/marketplace-integration/gcp/usage-reporting). ### Validation Rules Registration rejects the request if any of these do not hold: | Rule | Error | | ------------------------------------------------------------------------------ | ----------------------------------------------------------------- | | The subscription exists and is **active** | `subscription is not active` | | `customer_id` matches the subscription's customer | `customer_id does not match subscription` | | `plan_id` matches the subscription's plan | `plan_id does not match subscription` | | The `usage_reporting_id` is not already registered to a different subscription | `agreement identifier already registered` | | The subscription is not already registered to a different `usage_reporting_id` | `subscription already mapped to a different agreement identifier` | Each entitlement maps to exactly one subscription, and a subscription cannot be re-pointed to a different entitlement. If a buyer re-subscribes and GCP issues a new entitlement, create a new Flexprice subscription for them rather than reusing the old one. ## Registering Multiple Buyers on One Plan The plan mapping stores your product-level GCP configuration, so it is created once and reused. Registering a second buyer on the same plan reuses the existing plan mapping rather than creating another one. This means `service_name` and `metric_name` are set by the **first** agreement registered against a plan. Later registrations for that plan do not change them. Register every buyer for the same GCP product against the same Flexprice plan, with the same `service_name` and `metric_name`. ## Next Steps Set up your GCP listing, Workload Identity Federation, and Flexprice connection. Understand what gets metered to GCP and when. **API References** * [Create a Customer](/api-reference/customers/create-customer) * [Create a Subscription](/api-reference/subscriptions/create-subscription) # How to Set Up GCP Marketplace Source: https://docs.flexprice.io/integrations/marketplace-integration/gcp/connection-setup Complete guide to listing your product on GCP Marketplace and connecting your GCP producer project to Flexprice ## Overview The GCP Marketplace integration reports a subscription's Flexprice-computed usage to your GCP Marketplace product, going forward from when it is set up. You sell and manage your product on GCP Marketplace yourself; Flexprice's role is to compute usage and report it, so GCP can bill the buyer and pay you out. Setting this up is a one-time job with three parts: * **Your GCP listing**: a product with a single usage metric, priced so GCP bills exactly what Flexprice computes * **Workload Identity Federation**: lets Flexprice authenticate into your GCP project without a long-lived GCP key * **A Flexprice connection**: stores the federation details so reporting can run Once this is done, you link each buyer's GCP entitlement to a Flexprice subscription. That is covered in [GCP Marketplace Agreement Registration](/integrations/marketplace-integration/gcp/agreement-registration). ## Prerequisites Before setting up your GCP Marketplace connection, ensure you have: 1. **GCP Marketplace Producer Portal access** - Enrolled as a producer, with permission to create and publish products 2. **GCP IAM Access** - To create a workload identity pool, provider, and service account in your producer project ## Step 1: Create Your GCP Marketplace Listing Create your product in the GCP Marketplace Producer Portal. The integration supports products priced with **exactly one usage metric**. Select a usage-based pricing model for your product. Add **one** metered dimension to your product's Service Control managed service. Note its **metric name**: this is what Flexprice reports against. | Field | Value | | ------------------ | ------------------------------------ | | **Metric count** | Exactly one | | **Metric name** | Your choice, for example `usage_fee` | | **Price per unit** | `$0.01` | Price your single usage metric at **`$0.01` per unit**. GCP's Service Control API only accepts whole numbers as a metric value, so Flexprice reports your billable amount in cents. Pricing at this rate means GCP multiplies those cents back into the exact amount Flexprice computed, down to the cent. The price must be exactly `$0.01` per unit. Any other value scales what GCP bills your buyers by that factor. Once your pricing model is submitted, GCP assigns your product a Service Control managed service name and finalizes your metric's identifier. Both are account-specific and visible in Producer Portal's **Technical integration** section, see [Configure usage reporting to Google](https://docs.cloud.google.com/marketplace/docs/partners/integrated-saas/configure-usage-reports). You need both, as `service_name` and `metric_name`, when registering agreements, see [Agreement Registration](/integrations/marketplace-integration/gcp/agreement-registration). ## Step 2: Set Up Workload Identity Federation for Flexprice to Meter on Your Behalf Flexprice reports usage by authenticating into your GCP project through Workload Identity Federation (WIF). Flexprice's own AWS identity is federated into a short-lived GCP token, with no long-lived GCP key stored anywhere. You create a workload identity pool and provider that trust Flexprice's AWS identity, and a service account scoped to report usage only. Go to your GCP Marketplace connection setup in Flexprice to find the **AWS account ID** and **role name** to trust. You need both to run the setup script below. In your GCP project's Cloud Shell (or a terminal with the `gcloud` CLI authenticated against your project), fill in `GCP_PROJECT_ID`, `GCP_PROJECT_NUMBER`, and `SA_ID`, and set `FLEXPRICE_AWS_ACCOUNT_ID` / `FLEXPRICE_AWS_ROLE_NAME` to the values from the previous step. Then run: ```bash theme={null} GCP_PROJECT_ID="your-gcp-project-id" GCP_PROJECT_NUMBER="your-gcp-project-number" SA_ID="flexprice-metering-sa" FLEXPRICE_AWS_ACCOUNT_ID="" FLEXPRICE_AWS_ROLE_NAME="" # Create the workload identity pool gcloud iam workload-identity-pools create "flexprice-pool" \ --location="global" --project="$GCP_PROJECT_ID" # Create the provider, trusting only Flexprice's AWS account and role gcloud iam workload-identity-pools providers create-aws "flexprice-provider" \ --location="global" --workload-identity-pool="flexprice-pool" \ --account-id="$FLEXPRICE_AWS_ACCOUNT_ID" \ --attribute-mapping="google.subject=assertion.arn,attribute.account=assertion.account,attribute.aws_role=assertion.arn.extract('assumed-role/{role}/')" \ --attribute-condition="attribute.aws_role == '$FLEXPRICE_AWS_ROLE_NAME'" \ --project="$GCP_PROJECT_ID" # Create the service account Flexprice will report usage as gcloud iam service-accounts create "$SA_ID" --project="$GCP_PROJECT_ID" # Grant it exactly one permission: report usage gcloud projects add-iam-policy-binding "$GCP_PROJECT_ID" \ --role="roles/servicemanagement.serviceController" \ --member="serviceAccount:$SA_ID@$GCP_PROJECT_ID.iam.gserviceaccount.com" \ --condition=None # Let Flexprice's AWS identity impersonate the service account gcloud iam service-accounts add-iam-policy-binding \ "$SA_ID@$GCP_PROJECT_ID.iam.gserviceaccount.com" \ --role="roles/iam.workloadIdentityUser" \ --member="principalSet://iam.googleapis.com/projects/$GCP_PROJECT_NUMBER/locations/global/workloadIdentityPools/flexprice-pool/attribute.account/$FLEXPRICE_AWS_ACCOUNT_ID" \ --project="$GCP_PROJECT_ID" # Generate the credentials file to paste into Flexprice gcloud iam workload-identity-pools create-cred-config \ "projects/$GCP_PROJECT_NUMBER/locations/global/workloadIdentityPools/flexprice-pool/providers/flexprice-provider" \ --service-account="$SA_ID@$GCP_PROJECT_ID.iam.gserviceaccount.com" \ --aws --output-file="flexprice-gcp-creds.json" ``` Open `flexprice-gcp-creds.json` and copy its full contents. This file contains no private key, only configuration describing how to complete the federation exchange. You need it in the next step. ## Step 3: Create the GCP Marketplace Connection ### Using Flexprice Dashboard You can create a GCP Marketplace connection directly from the Flexprice dashboard under **Integrations → GCP Marketplace**. The dashboard walks you through the same setup script and credentials JSON paste described above, with your Flexprice AWS account ID and role name pre-filled. Connect to GCP Marketplace from the Flexprice dashboard ### API Request **Endpoint:** `POST /v1/connections` **Headers:** ```http theme={null} Content-Type: application/json x-api-key: your_api_key X-Environment-ID: your_environment_id ``` `flexprice-gcp-creds.json` from Step 2 looks like this: ```json theme={null} { "type": "external_account", "audience": "//iam.googleapis.com/projects//locations/global/workloadIdentityPools/flexprice-pool/providers/flexprice-provider", "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request", "token_url": "https://sts.googleapis.com/v1/token", "credential_source": { "environment_id": "aws1", "region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone", "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials", "regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15" }, "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/flexprice-metering-sa@.iam.gserviceaccount.com:generateAccessToken" } ``` `credentials_json` is a **string** field, not a nested object. Its value is that entire file's contents, JSON-encoded as one string: **Request Body:** ```json theme={null} { "name": "GCP Marketplace", "provider_type": "gcp_marketplace", "encrypted_secret_data": { "credentials_json": "{\"type\":\"external_account\",\"audience\":\"//iam.googleapis.com/projects//locations/global/workloadIdentityPools/flexprice-pool/providers/flexprice-provider\",\"subject_token_type\":\"urn:ietf:params:aws:token-type:aws4_request\",\"token_url\":\"https://sts.googleapis.com/v1/token\",\"credential_source\":{\"environment_id\":\"aws1\",\"region_url\":\"http://169.254.169.254/latest/meta-data/placement/availability-zone\",\"url\":\"http://169.254.169.254/latest/meta-data/iam/security-credentials\",\"regional_cred_verification_url\":\"https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15\"},\"service_account_impersonation_url\":\"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/flexprice-metering-sa@.iam.gserviceaccount.com:generateAccessToken\"}" } } ``` If you are building this request programmatically, read the file and pass its raw contents through your language's JSON string encoding rather than constructing the escaped string by hand. | Field | Description | Required | | ------------------ | -------------------------------------------------------------------------------------- | -------- | | `name` | A friendly name for the connection | ✅ | | `provider_type` | Must be `gcp_marketplace` | ✅ | | `credentials_json` | The full contents of the credentials file from Step 2, as a single JSON-encoded string | ✅ | ### Response ```json theme={null} { "id": "conn_01KY2GQY0D0W38S5NAVM5HC9RC", "name": "GCP Marketplace", "provider_type": "gcp_marketplace", "status": "published", "created_at": "2026-07-21T14:19:41Z", "updated_at": "2026-07-21T14:19:41Z" } ``` The credentials are stored encrypted. ### Connection Verification Creating the connection immediately attempts the full Workload Identity Federation exchange: trading your AWS identity for a federated GCP token, then impersonating your service account. The connection is only saved if that succeeds, so a successful response confirms your setup actually works rather than failing silently later. If it fails, nothing is saved and you get a validation error. Fix the cause and create the connection again. | Cause | Fix | | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Provider trusts the wrong AWS account or role | Confirm `--account-id` and `--attribute-condition` in the setup script match the values Flexprice showed you | | Attribute mapping missing `attribute.account` | Re-run the provider creation command exactly as shown; a missing mapping makes the impersonation binding match nothing | | Service account impersonation not granted | Confirm the `iam.workloadIdentityUser` binding was applied to the service account, not the project | | `servicemanagement.serviceController` not granted | Confirm the role was granted to the service account at the project level | | Pasted credentials file is incomplete or edited | Re-copy the full, unmodified contents of `flexprice-gcp-creds.json` | ## Next Steps Link each buyer's GCP entitlement to a Flexprice subscription. Understand what gets metered to GCP and when. # GCP Marketplace Usage Reporting Source: https://docs.flexprice.io/integrations/marketplace-integration/gcp/usage-reporting What Flexprice meters to GCP Marketplace and when it reports ## Overview Once a buyer's entitlement is registered, usage reporting runs automatically. You do not call Flexprice again for that buyer unless their subscription changes. Flexprice reports usage against your product's single usage metric via the Service Control API. GCP then invoices the buyer and pays you out. ## What Flexprice Meters to GCP The amount reported to GCP is the same billable amount that appears on that subscription's Flexprice invoice for the same period. It is computed with the same rules, so GCP bills what Flexprice bills. That means the reported amount already accounts for: * **Usage-based charges** across every meter on the subscription * **Commitment amounts**, where the subscription has a committed spend * **Overage rates**, where usage exceeds the commitment The amount is reported in cents against your single usage metric. With the metric priced at `$0.01` per unit, GCP multiplies it back into the exact amount, down to the cent. See [Create Your GCP Marketplace Listing](/integrations/marketplace-integration/gcp/connection-setup#step-1-create-your-gcp-marketplace-listing). Flexprice reports usage to GCP on a recurring schedule. Do not expect usage to appear in GCP immediately after it happens in Flexprice. ## Retries and Failures Reporting recovers from transient failures on its own. * **Reports are safe to retry.** Each report carries a fixed operation ID, so a retry is an identical request and GCP de-duplicates it rather than billing twice. * **Unaccepted reports are retried.** Service Control's API can return a `200 OK` and still reject a report; Flexprice checks for this and leaves rejected reports unreported so they are retried on the next run. No action is needed from you. * **A rejected buyer resolves itself.** If GCP reports the entitlement is not active, reporting keeps retrying and succeeds once the buyer's entitlement is active again. ## Handling Marketplace Lifecycle Events Flexprice does not learn about GCP-side changes to a buyer's subscription. You handle these. | Event on GCP | What you must do in Flexprice | | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | Buyer cancels or unsubscribes | Cancel the buyer's Flexprice subscription, see [Final Usage When an Entitlement Is Cancelled](#final-usage-when-an-entitlement-is-cancelled) | | Buyer's entitlement expires | Cancel the buyer's Flexprice subscription | | Buyer re-subscribes with a new entitlement | Create a new Flexprice subscription and register the new entitlement | If a buyer cancels on GCP and you leave their Flexprice subscription active, Flexprice keeps computing usage for it and attempting to report it to GCP. Cancel the Flexprice subscription to stop this. To detect these events, subscribe to your product's Procurement API notifications in your own application. Flexprice does not subscribe to them. ## Final Usage When an Entitlement Is Cancelled GCP gives a window of about **one hour** after the cancellation to submit usage for a cancelled entitlement. Use the timestamp from the Procurement API notification, not the time you process it. GCP requires the final report to end **strictly before** the cancellation it recorded, and rejects usage reported at or after that instant. Flexprice reports against `cancel_at`, so a `cancel_at` later than the real cancellation loses the final usage. See [When the Marketplace Agreement Ends](/integrations/marketplace-integration/overview#when-the-marketplace-agreement-ends) for how the final usage flush works and how to cancel correctly. ## Troubleshooting | Symptom | Likely cause | Fix | | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | No usage in GCP at all | Agreement never registered | Confirm registration succeeded, see [Agreement Registration](/integrations/marketplace-integration/gcp/agreement-registration) | | Usage appears late | Expected: reporting is scheduled, not real time | None | | GCP bills the wrong amount | Metric not priced at `$0.01` per unit | Correct the metric price on your GCP product | | Reports rejected with a permission error naming your service | The service name in your plan mapping does not match a real, provisioned Service Control managed service | Confirm the product's pricing has been approved and the managed service is live | ## Next Steps Set up your GCP listing, Workload Identity Federation, and Flexprice connection. Link each buyer's GCP entitlement to a Flexprice subscription. # Overview Source: https://docs.flexprice.io/integrations/marketplace-integration/overview Sell your product through Flexprice supported marketplaces and meter each buyer usage back to them from Flexprice Cloud marketplaces let you list your product where your buyers already have a billing relationship and a committed spend. The buyer subscribes through the marketplace, the marketplace bills them on their existing cloud invoice, and it pays you out. What the marketplace cannot do is work out how much to charge. That comes from your own metering. Flexprice is that metering. You model pricing once as a plan, link each buyer's marketplace agreement to a Flexprice subscription, and Flexprice reports the billable amount to the marketplace on a schedule. That amount matches the invoice for the same subscription and period, so the marketplace bills exactly what you charge. ## How it works Every marketplace integration follows the same three stages. Create your listing on the marketplace, grant Flexprice permission to meter on your behalf, and save the connection in Flexprice. Done once per marketplace. When a buyer subscribes, the marketplace hands your application an identifier for them. Register it against a Flexprice customer, plan, and active subscription. Done once per buyer. Flexprice computes each mapped subscription's usage and reports it on a schedule. You do not call Flexprice again for that buyer unless their subscription changes. ## Supported marketplaces Agreements identified by `LicenseArn`, metered through the AWS Marketplace Metering Service. Entitlements metered through the Service Control API. SaaS subscriptions metered through the Marketplace Metering Service. ## What Flexprice reports The reported amount already accounts for usage-based charges across every meter on the subscription, commitment amounts, and overage rates. It is sent in **cents** against a **single** usage dimension on your marketplace listing. With that dimension priced at `$0.01` per unit, the marketplace multiplies it back into the exact amount, down to the cent. This is why each provider's setup page tells you to create exactly one dimension at that price: your real pricing lives in the Flexprice plan, not in the marketplace listing. Flexprice reports usage to the marketplace on a recurring schedule. ## When the marketplace agreement ends Flexprice is not notified when a buyer cancels or unsubscribes on the marketplace. Subscribe to your product's marketplace notifications in your own application, detect the cancellation there, and cancel the buyer's Flexprice subscription. Each provider's usage reporting page describes the notification source to use. Cancelling the Flexprice subscription triggers a **final usage flush** immediately, rather than on the regular reporting schedule: 1. Any usage still unreported for that buyer is reported. 2. Usage for the final window, from the last reported point up to the cancellation time, is computed and reported. 3. The buyer's marketplace mappings are deactivated, so nothing further is reported for that subscription. The mappings are only deactivated once every report above succeeded. If any report fails, the mappings stay active and the regular reporting schedule keeps retrying the outstanding records. ### Cancel with the correct timestamp Cancel with `cancellation_type: "immediate"` and set `cancel_at` to the timestamp the **marketplace** recorded for the cancellation. ```json theme={null} { "cancellation_type": "immediate", "cancel_at": "2026-08-03T11:47:29Z", "reason": "buyer unsubscribed on marketplace" } ``` Flexprice always reports the final window against `cancel_at`. If you omit it, or set it to the time you process the cancellation rather than the time the marketplace recorded, the final report can fall outside the window the marketplace still accepts. Always pass the marketplace's own cancellation timestamp. Only `immediate` cancellation runs the flush. `end_of_period` and `scheduled_date` leave the subscription active until the scheduled moment and do not report final usage. ## Next steps Listing, IAM role, agreement registration, and usage reporting. Listing, Workload Identity Federation, entitlement registration, and usage reporting. Offer, Entra app credentials, subscription registration, and usage reporting. Cancellation types and how `cancel_at` is interpreted. # 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**. Setup Autopay button on the customer page 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. Flexprice card entry form If the card issuer requires 3DS authentication, the customer is redirected to their bank's verification page and returned automatically after completing it. 3DS verification screen *** ## 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`.)* Moyasar Connection Setup ### 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. Moyasar Connection Setup ## 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**. Stripe Connection Setup ### 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} x-api-key: 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 x-api-key: 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.io/billing/invoices/inv_123", "cancel_url": "https://app.flexprice.io/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) Invoice sync to Nomod **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 Paddle Authentication - API Keys and Client-Side Tokens ## 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 Paddle Default Payment Link ### 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) Paddle Website Approval ### 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 | Paddle Webhook Configuration ## 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. Paddle Connection Setup ```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://us.api.flexprice.io/v1/subscriptions \ -H "x-api-key: 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://us.api.flexprice.io/v1/subscriptions \ -H "x-api-key: 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** QuickBooks Connection Setup 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: Razorpay Connection Setup ### Webhook Configuration After creating the connection, configure webhooks in the connection settings: Razorpay Webhook Configuration ### API Request ### API Request **Endpoint:** `POST /api/v1/connections` **Headers:** ```http theme={null} Content-Type: application/json x-api-key: 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.io](mailto:support@flexprice.io) * **Integration Documentation:** [Flexprice Docs](https://docs.flexprice.io) * **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 x-api-key: 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: Create Payment Link ## 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 x-api-key: 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 "x-api-key: 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: