openapi: 3.0.3 info: title: Velox Billing API description: | Open-source usage-based billing engine. Handles pricing, subscriptions, usage metering, invoice generation, Stripe payments, dunning, and credits. version: "2026-04-07" license: name: MIT url: https://github.com/sagarsuperuser/velox/blob/main/LICENSE contact: name: Velox url: https://github.com/sagarsuperuser/velox servers: - url: http://localhost:8080 description: Local development security: - BearerAuth: [] components: securitySchemes: BearerAuth: type: http scheme: bearer description: | API key authentication. Use `Authorization: Bearer vlx_secret_...` Key types: vlx_platform_ (tenant mgmt), vlx_secret_ (full access), vlx_pub_ (restricted) schemas: Error: type: object properties: error: type: object properties: type: type: string enum: [invalid_request_error, authentication_error, rate_limit_error, api_error] code: type: string message: type: string request_id: type: string Customer: type: object properties: id: type: string example: vlx_cus_42014438d82ddf33 external_id: type: string display_name: type: string email: type: string status: type: string enum: [active, archived] created_at: type: string format: date-time Meter: type: object properties: id: type: string key: type: string name: type: string unit: type: string aggregation: type: string enum: [sum, count, max, last] rating_rule_version_id: type: string Plan: type: object properties: id: type: string code: type: string name: type: string currency: type: string billing_interval: type: string enum: [monthly, yearly] status: type: string enum: [draft, active, archived] base_amount_cents: type: integer format: int64 meter_ids: type: array items: type: string RatingRule: type: object properties: id: type: string rule_key: type: string name: type: string version: type: integer mode: type: string enum: [flat, graduated, package] currency: type: string flat_amount_cents: type: integer format: int64 graduated_tiers: type: array items: type: object properties: up_to: type: integer unit_amount_cents: type: integer Subscription: type: object properties: id: type: string code: type: string display_name: type: string customer_id: type: string plan_id: type: string status: type: string enum: [draft, active, paused, canceled, archived] billing_time: type: string enum: [calendar, anniversary] trial_start_at: type: string format: date-time trial_end_at: type: string format: date-time UsageEvent: type: object properties: id: type: string customer_id: type: string meter_id: type: string quantity: type: integer format: int64 properties: type: object idempotency_key: type: string timestamp: type: string format: date-time InvoiceStatus: type: string enum: [draft, finalized, paid, voided] description: Lifecycle state of the invoice. InvoicePaymentStatus: type: string enum: [pending, processing, succeeded, failed, unknown] description: | Payment-attempt status. `unknown` marks a Stripe charge attempt that returned an ambiguous error (5xx, timeout, connection reset) where we cannot tell whether Stripe processed the charge — a reconciler resolves these by querying Stripe after a cool-off window. InvoiceTaxStatus: type: string enum: [ok, pending, failed] description: | Whether tax has been successfully calculated. Happy path is `ok`. `pending` means a transient calculation failure is awaiting retry; `failed` means retries are exhausted (operator resolves manually). Pending/failed invoices are blocked from finalize so we never send a wrong tax amount. InvoiceBillingReason: type: string enum: [subscription_cycle, subscription_create, manual, threshold] description: | Mirrors Stripe's `invoice.billing_reason`. Classifies the trigger that produced the invoice. Stamped at create time and never mutated. InvoiceLineType: type: string enum: [base_fee, usage, add_on, discount, tax] ItemChangeType: type: string enum: [plan, quantity, add, remove] description: | Classifies per-item proration artifacts so the dedup index can distinguish a plan-change and a quantity-change that happen to share the same `(subscription, item, timestamp)`. Invoice: type: object description: | A Velox invoice. Returned from create, finalize, void, list, get, apply-coupon, rotate-public-token, and collect-payment endpoints. required: - id - customer_id - subscription_id - invoice_number - status - payment_status - currency - subtotal_cents - discount_cents - tax_amount_cents - tax_rate_bp - total_amount_cents - amount_due_cents - amount_paid_cents - credits_applied_cents - billing_period_start - billing_period_end - payment_overdue - net_payment_term_days - created_at - updated_at properties: id: type: string example: vlx_inv_42014438d82ddf33 tenant_id: type: string description: | Owning tenant. Returned for diagnostic clarity but redundant with the API key (which already determines the tenant). Omitted when empty. customer_id: type: string subscription_id: type: string description: Empty for one-off invoices not tied to a subscription. invoice_number: type: string description: Empty until the invoice is finalized. status: $ref: "#/components/schemas/InvoiceStatus" payment_status: $ref: "#/components/schemas/InvoicePaymentStatus" currency: type: string example: USD subtotal_cents: type: integer format: int64 discount_cents: type: integer format: int64 tax_amount_cents: type: integer format: int64 tax_rate_bp: type: integer format: int64 description: Tax rate in basis points (1850 = 18.50%). tax_name: type: string tax_country: type: string tax_id: type: string tax_provider: type: string description: | Durable audit snapshot of the upstream tax engine that ran the calculation (e.g. `stripe`, `none`, `manual`). tax_calculation_id: type: string description: | Provider-side calculation reference captured at build time. For Stripe Tax this expires after 24 h; persisted so finalized invoices remain reconstructable past expiry. tax_transaction_id: type: string description: | Provider-side committed transaction reference, captured at finalize. For Stripe Tax this is `tx_xxx` and is the handle the provider needs for a later reversal when a credit note is issued. tax_reverse_charge: type: boolean tax_exempt_reason: type: string tax_status: $ref: "#/components/schemas/InvoiceTaxStatus" tax_deferred_at: type: string format: date-time nullable: true tax_retry_count: type: integer tax_pending_reason: type: string total_amount_cents: type: integer format: int64 amount_due_cents: type: integer format: int64 amount_paid_cents: type: integer format: int64 credits_applied_cents: type: integer format: int64 billing_period_start: type: string format: date-time billing_period_end: type: string format: date-time issued_at: type: string format: date-time nullable: true description: Set when the invoice transitions out of draft. due_at: type: string format: date-time nullable: true paid_at: type: string format: date-time nullable: true voided_at: type: string format: date-time nullable: true stripe_payment_intent_id: type: string last_payment_error: type: string payment_overdue: type: boolean auto_charge_pending: type: boolean public_token: type: string description: | Hosted-invoice-URL credential (Stripe-parity `hosted_invoice_url`). Generated at finalize; drafts have an empty token. Rotatable via `POST /v1/invoices/{id}/rotate-public-token`. net_payment_term_days: type: integer memo: type: string footer: type: string metadata: type: object additionalProperties: true created_at: type: string format: date-time updated_at: type: string format: date-time source_plan_changed_at: type: string format: date-time nullable: true source_subscription_item_id: type: string source_change_type: $ref: "#/components/schemas/ItemChangeType" billing_reason: $ref: "#/components/schemas/InvoiceBillingReason" stripe_invoice_id: type: string description: | Source Stripe invoice id (`in_xxx`) populated by `velox-import` for invoices imported from a Stripe account. Empty for Velox-native invoices. InvoiceLineItem: type: object description: A single line on an invoice. required: - id - invoice_id - line_type - description - quantity - unit_amount_cents - amount_cents - tax_rate_bp - tax_amount_cents - total_amount_cents - currency - created_at properties: id: type: string invoice_id: type: string tenant_id: type: string description: Owning tenant. Omitted when empty. line_type: $ref: "#/components/schemas/InvoiceLineType" meter_id: type: string description: type: string quantity: type: integer format: int64 unit_amount_cents: type: integer format: int64 amount_cents: type: integer format: int64 tax_rate_bp: type: integer format: int64 tax_amount_cents: type: integer format: int64 tax_jurisdiction: type: string tax_code: type: string tax_reason: type: string description: | Stripe-canonical structured `taxability_reason` (`standard_rated`, `reverse_charge`, `not_collecting`, `customer_exempt`, `product_exempt`, `zero_rated`, etc.). Empty for non-Stripe providers. total_amount_cents: type: integer format: int64 currency: type: string pricing_mode: type: string rating_rule_version_id: type: string billing_period_start: type: string format: date-time nullable: true billing_period_end: type: string format: date-time nullable: true metadata: type: object additionalProperties: true created_at: type: string format: date-time InvoiceWithLineItems: type: object description: | Response shape for `GET /v1/invoices/{id}`. The handler returns the invoice and its line items as separate top-level keys (rather than nesting `line_items` inside the invoice) so the client can render the two arrays independently. required: [invoice, line_items] properties: invoice: $ref: "#/components/schemas/Invoice" line_items: type: array items: $ref: "#/components/schemas/InvoiceLineItem" CreditBalance: type: object properties: customer_id: type: string balance_cents: type: integer format: int64 total_granted: type: integer format: int64 total_used: type: integer format: int64 paths: /health: get: tags: [System] summary: Health check security: [] responses: "200": description: Service is healthy /v1/customers: post: tags: [Customers] summary: Create customer requestBody: required: true content: application/json: schema: type: object required: [external_id, display_name] properties: external_id: type: string display_name: type: string email: type: string responses: "201": description: Customer created content: application/json: schema: $ref: "#/components/schemas/Customer" "409": description: Customer already exists get: tags: [Customers] summary: List customers parameters: - name: status in: query schema: type: string - name: limit in: query schema: type: integer - name: offset in: query schema: type: integer responses: "200": description: Customer list /v1/meters: post: tags: [Pricing] summary: Create meter requestBody: required: true content: application/json: schema: type: object required: [key, name] properties: key: type: string name: type: string unit: type: string default: unit aggregation: type: string enum: [sum, count, max, last] default: sum rating_rule_version_id: type: string responses: "201": description: Meter created get: tags: [Pricing] summary: List meters responses: "200": description: Meter list /v1/plans: post: tags: [Pricing] summary: Create plan requestBody: required: true content: application/json: schema: type: object required: [code, name, currency, billing_interval] properties: code: type: string name: type: string currency: type: string billing_interval: type: string enum: [monthly, yearly] base_amount_cents: type: integer meter_ids: type: array items: type: string responses: "201": description: Plan created get: tags: [Pricing] summary: List plans responses: "200": description: Plan list /v1/rating-rules: post: tags: [Pricing] summary: Create rating rule requestBody: required: true content: application/json: schema: type: object required: [rule_key, name, mode, currency] properties: rule_key: type: string name: type: string mode: type: string enum: [flat, graduated, package] currency: type: string flat_amount_cents: type: integer graduated_tiers: type: array items: type: object properties: up_to: type: integer unit_amount_cents: type: integer package_size: type: integer package_amount_cents: type: integer responses: "201": description: Rating rule created get: tags: [Pricing] summary: List rating rules responses: "200": description: Rating rule list /v1/subscriptions: post: tags: [Subscriptions] summary: Create subscription requestBody: required: true content: application/json: schema: type: object required: [code, display_name, customer_id, plan_id] properties: code: type: string display_name: type: string customer_id: type: string plan_id: type: string billing_time: type: string enum: [calendar, anniversary] default: calendar trial_days: type: integer start_now: type: boolean responses: "201": description: Subscription created get: tags: [Subscriptions] summary: List subscriptions responses: "200": description: Subscription list /v1/subscriptions/{id}/activate: post: tags: [Subscriptions] summary: Activate draft subscription parameters: - name: id in: path required: true schema: type: string responses: "200": description: Subscription activated /v1/subscriptions/{id}/change-plan: post: tags: [Subscriptions] summary: Change subscription plan parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: [new_plan_id] properties: new_plan_id: type: string immediate: type: boolean responses: "200": description: Plan changed /v1/usage-events: post: tags: [Usage] summary: Ingest usage event requestBody: required: true content: application/json: schema: type: object required: [customer_id, meter_id, quantity] properties: customer_id: type: string meter_id: type: string subscription_id: type: string quantity: type: integer format: int64 properties: type: object idempotency_key: type: string timestamp: type: string format: date-time responses: "201": description: Event ingested get: tags: [Usage] summary: List usage events responses: "200": description: Event list /v1/usage-events/batch: post: tags: [Usage] summary: Batch ingest usage events (max 1000) requestBody: required: true content: application/json: schema: type: array items: type: object required: [customer_id, meter_id, quantity] properties: customer_id: type: string meter_id: type: string quantity: type: integer responses: "201": description: All events ingested "206": description: Partial success /v1/invoices: get: tags: [Invoices] summary: List invoices responses: "200": description: Invoice list /v1/invoices/create_preview: post: tags: [Invoices] summary: Preview the next invoice for a customer (Stripe-equivalent) description: | Answers "what is my next bill going to look like?" using the same line-set the cycle scan would emit if billing fired right now — so dashboard projected-bill matches the eventual finalized invoice. Composes across customer / subscription / pricing; registered as a sibling of `/v1/invoices` (chi picks the more specific pattern, otherwise `/{id}` would capture `create_preview` as an invoice ID). See [`docs/design-create-preview.md`](../docs/design-create-preview.md). requestBody: required: true content: application/json: schema: type: object required: [customer_id] properties: customer_id: type: string description: Customer to preview the invoice for. subscription_id: type: string description: | Optional. Defaults to the customer's primary active subscription if not supplied. period: type: object description: | Optional explicit window. Both bounds must be supplied together; partial windows are rejected. Defaults to the resolved subscription's current cycle. required: [from, to] properties: from: type: string format: date-time to: type: string format: date-time responses: "200": description: Preview result "400": description: Invalid request body content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Customer or subscription not found content: application/json: schema: $ref: "#/components/schemas/Error" "422": description: Validation error (missing customer_id, partial period, no active sub, ...) content: application/json: schema: $ref: "#/components/schemas/Error" /v1/invoices/{id}: get: tags: [Invoices] summary: Get invoice with line items operationId: getInvoice parameters: - name: id in: path required: true schema: type: string responses: "200": description: Invoice with its line items. content: application/json: schema: $ref: "#/components/schemas/InvoiceWithLineItems" "404": description: Invoice not found. content: application/json: schema: $ref: "#/components/schemas/Error" /v1/invoices/{id}/pdf: get: tags: [Invoices] summary: Download invoice PDF parameters: - name: id in: path required: true schema: type: string responses: "200": description: PDF file content: application/pdf: {} /v1/invoices/{id}/finalize: post: tags: [Invoices] summary: Finalize draft invoice parameters: - name: id in: path required: true schema: type: string responses: "200": description: Invoice finalized /v1/auth/exchange: post: tags: [Auth] summary: Exchange a pasted API key for an httpOnly session cookie description: | Validates the pasted API key and mints a server-side session row. The response sets `Set-Cookie: velox_session=...; HttpOnly; SameSite=Lax; Path=/`. Subsequent requests authenticate via the cookie. The credential remains the API key; this endpoint just gives the dashboard an httpOnly artefact instead of holding the raw key in JS-readable storage. See ADR-008. requestBody: required: true content: application/json: schema: type: object required: [api_key] properties: api_key: type: string description: A `vlx_secret_…` or `vlx_pub_…` key. responses: "200": description: Session minted; cookie set on response content: application/json: schema: type: object required: [tenant_id, key_id, key_type, livemode, expires_at] properties: tenant_id: type: string key_id: type: string key_type: type: string enum: [platform, secret, publishable] livemode: type: boolean expires_at: type: string format: date-time "400": description: Malformed body content: application/json: schema: $ref: "#/components/schemas/Error" "401": description: Missing, malformed, revoked, or expired API key content: application/json: schema: $ref: "#/components/schemas/Error" "422": description: api_key field missing content: application/json: schema: $ref: "#/components/schemas/Error" /v1/auth/logout: post: tags: [Auth] summary: Revoke the current session and clear the cookie description: | Marks the session row as revoked server-side and clears the cookie on the response. Idempotent — a missing or stale cookie is treated as already-logged-out. responses: "204": description: Session revoked /v1/whoami: get: tags: [Auth] summary: Resolve the current credential to its tenant context description: | Returns the resolved auth context for whichever credential the request carries — `velox_session` cookie (dashboard) or `Authorization: Bearer` (SDK / curl). No specific permission required. responses: "200": description: Resolved auth context content: application/json: schema: type: object required: [tenant_id, key_id, key_type, livemode] properties: tenant_id: type: string key_id: type: string key_type: type: string enum: [platform, secret, publishable] livemode: type: boolean "401": description: Missing, revoked, or expired API key content: application/json: schema: $ref: "#/components/schemas/Error" /v1/billing/run: post: tags: [Billing] summary: Trigger billing cycle responses: "200": description: Billing cycle result content: application/json: schema: type: object properties: invoices_generated: type: integer errors: type: array items: type: string /v1/billing/preview/{subscription_id}: get: tags: [Billing] summary: Invoice preview (dry run) parameters: - name: subscription_id in: path required: true schema: type: string responses: "200": description: Invoice preview /v1/credits/grant: post: tags: [Credits] summary: Grant credits to customer requestBody: required: true content: application/json: schema: type: object required: [customer_id, amount_cents] properties: customer_id: type: string amount_cents: type: integer format: int64 description: type: string responses: "201": description: Credits granted /v1/credits/balance/{customer_id}: get: tags: [Credits] summary: Get customer credit balance parameters: - name: customer_id in: path required: true schema: type: string responses: "200": description: Credit balance content: application/json: schema: $ref: "#/components/schemas/CreditBalance" /v1/dunning/policy: get: tags: [Dunning] summary: Get dunning policy responses: "200": description: Dunning policy put: tags: [Dunning] summary: Configure dunning policy requestBody: required: true content: application/json: schema: type: object properties: name: type: string enabled: type: boolean retry_schedule: type: array items: type: string max_retry_attempts: type: integer final_action: type: string enum: [manual_review, pause, write_off_later] responses: "200": description: Policy updated /v1/webhooks/stripe: post: tags: [Webhooks] summary: Stripe webhook receiver security: [] description: Receives Stripe webhook events. Verified via Stripe-Signature header. responses: "200": description: Event processed /v1/metrics: get: tags: [System] summary: Prometheus metrics security: [] responses: "200": description: Prometheus metrics content: text/plain: {}