Documentation Index
Fetch the complete documentation index at: https://docs.solvapay.com/llms.txt
Use this file to discover all available pages before exploring further.
SolvaPay automates billing through a combination of purchase lifecycle management and scheduled jobs. Recurring and usage-based plans are billed automatically, with usage costs calculated from the Usage timeseries.
Billing Cycles
Billing cycles determine how often customers are charged and when usage counters reset:
| Cycle | Duration | Use Case |
|---|
weekly | 7 days | High-frequency, short-commitment services |
monthly | 30 days | Standard SaaS subscriptions |
quarterly | 90 days | Discounted longer-term plans |
yearly | 365 days | Annual subscriptions with savings |
custom | 30 days (default) | Custom billing arrangements |
The billing cycle is set on the plan and determines:
- When the next payment is due (
nextBillingDate on the purchase)
- The time window for usage aggregation (
periodStart to periodEnd)
- When usage counters reset
Purchase States
A purchase moves through these states during its lifecycle:
| State | Description |
|---|
trialing | Customer is in a free trial period (set by trialDays on the plan) |
active | Purchase is active and the customer has access |
active (pending cancellation) | Active with cancelledAt set — access continues until period end, can be reactivated |
cancelled | Cancel confirmed at period end |
expired | Purchase has ended (trial expired without payment, cancelled and period ended, or replaced by plan switch) |
past_due | Payment failed; purchase may be suspended |
Reactivation
When a customer cancels a recurring purchase, the purchase enters a “pending cancellation” state — it remains active with cancelledAt set, and the customer keeps access until the current billing period ends.
Before the period ends, the cancellation can be undone:
import { reactivateRenewal } from '@solvapay/next'
await reactivateRenewal(request, { purchaseRef: 'pur_...' })
This clears cancelledAt and restores autoRenew, so the purchase continues renewing as normal. A purchase.updated webhook fires.
Preconditions: the purchase must be active, have cancelledAt set, and endDate must not have passed.
Plan Switching
When a customer wants to change plans on a product, call activatePlan with the new plan reference. If the customer already has an active purchase on a different plan for that product, the system automatically:
- Expires the existing purchase
- Creates a new purchase on the requested plan
import { activatePlan } from '@solvapay/next'
const result = await activatePlan(request, {
productRef: 'prd_myapi',
planRef: 'pln_pro', // new plan
})
This produces two webhook events: purchase.expired for the old purchase and purchase.created for the new one.
Usage Tracking on Purchases
For usage-based and hybrid plans, each purchase maintains a usage subdocument that tracks the current billing period:
{
"usage": {
"used": 0,
"periodStart": "2025-01-01T00:00:00Z",
"periodEnd": "2025-02-01T00:00:00Z",
"resetDate": "2025-02-01T00:00:00Z",
"overageUnits": 0,
"overageCost": 0,
"carriedOverUnits": 0
}
}
The actual usage count comes from the Usage timeseries, not from the usage.used field. The timeseries is the source of truth — limit checks query UsageService.sumForMeter() from periodStart to the current time.
Limit Checking Flow
When a customer makes a request to a protected endpoint, access is determined by:
- Find the active purchase for the customer and product
- Look up the plan to get the
limit
- If
limit is 0 — unlimited access, allow immediately
- Resolve the plan’s auto-assigned requests meter
- Query the Usage timeseries —
UsageService.sumForMeter(providerId, meterName, customerRef, periodStart, now)
- Compare against the hard cap — if
used >= limit, deny with a paywall response
The response includes:
{
"hasAccess": true,
"used": 750,
"remaining": 250,
"limit": 1000,
"freeUnits": 100,
"isExceeded": false,
"meterName": "requests"
}
When access is denied, the response includes a checkout URL so the customer can upgrade.
End-of-Period Billing
A daily cron job (11:00 AM UTC) processes end-of-period billing for usage-based and hybrid plans. For each active recurring purchase whose billing period has ended:
Usage-Based Plans
- Query the Usage timeseries for total usage in the period
- Subtract
freeUnits from the total
- Apply the
creditsPerUnit to the billable amount
- Respect the
limit as a hard cap (usage beyond the cap is not billed unless overage is allowed)
- Create a payment intent for the calculated cost
Example:
Plan: 100 free units, 100 credits/unit, 10,000 limit
Usage: 5,250 records in the period
Billable = max(0, min(5250, 10000) - 100) = 5,150 units
Cost = 5,150 × 100 = 515,000 credits
Hybrid Plans
Hybrid plans combine a recurring base fee with usage-based charges:
- The base price is billed as part of the regular renewal
- Usage beyond the included amount is calculated separately
- Tiered pricing is applied if
usageTiers are defined — each tier has its own creditsPerUnit for a range of usage
- Overage beyond the plan’s
limit is charged at the overagePolicy.overageRate (or creditsPerUnit as fallback)
- The
overagePolicy.maxOverage caps the maximum overage units
Example with tiered pricing:
Base: $49/month, 1,000 included units
Tiers:
0–500: 50 credits/unit
501–2,000: 30 credits/unit
2,001+: 10 credits/unit
Overage: allowed, rate 80 credits/unit, max 5,000
Usage: 3,500 records
Included: 1,000 (covered by base price)
Billable: 2,500 units
Tier 1: 500 × 50 = 25,000 credits
Tier 2: 1,500 × 30 = 45,000 credits
Tier 3: 500 × 10 = 5,000 credits
Usage cost: 75,000 credits
Total: $49.00 (base) + 75,000 credits (usage)
Renewal Processing
A daily cron job (10:00 AM UTC) handles recurring plan renewals:
- Find purchases where
nextBillingDate ≤ now and autoRenew is enabled
- For paid plans, create a payment intent for the plan price
- For free plans, process the renewal directly
- Advance
nextBillingDate to the next billing cycle
Trial Expiration
A daily cron job (8:00 AM UTC) handles trial expirations:
- If
requiresPayment is true: the purchase is suspended until payment is received
- If
requiresPayment is false: the purchase transitions to active
Usage Reset
A periodic job (every 30 minutes) resets usage counters on purchases whose reset date has passed:
- Set
usage.used to 0 (or carry-over amount if rolloverUnusedUnits is enabled)
- Advance
periodStart and periodEnd to the next period
- Calculate the next
resetDate
Since the actual usage is derived from the Usage timeseries using the periodStart window, advancing periodStart effectively “resets” visible usage without deleting any usage records.
Billing Strategies (Hybrid Plans)
Hybrid plans support different billing strategies:
| Strategy | Description |
|---|
recurring_first | Bill the base fee first, then calculate usage charges |
usage_first | Calculate usage first, then add the base fee |
combined | Bill both components together |
Proration (Recurring Plans)
When a customer upgrades or downgrades mid-cycle, proration policies determine how the change is handled:
| Method | Description |
|---|
proportional | Charge/credit proportional to remaining days in the cycle |
full | Charge the full new price immediately |
none | Wait until the next billing cycle to apply the change |
Next Steps