Recurring Payments
Complete guide to subscriptions, seat-based addons, lifecycle states, proration, and renewal calculations.
Recurring Payments & Subscriptions
Donutwork lets you run subscription billing with a predictable model:
- create plan catalog rules,
- attach subscriptions to customers,
- scale seat-based pricing through addons,
- change contracts over time with lifecycle controls,
- keep compatibility with existing legacy integrations.
This page explains how the system behaves in production, with practical examples.
What A Subscription Includes
Renewal Cadence
Day-based or month-based cycles (`renewal_days` / `renewal_months`) with a next renewal date (`next_renew`).
Trial Handling
Optional trial before first paid cycle.
Seat + Addon Pricing
Seat-based pricing uses addon quantity (`addons[].quantity`) and unit price (`addons[].price`).
Discounts & Taxes
Addon discounts, global discount, carryover credit, and tax profile are applied in deterministic order.
Renewal Amount Calculation (Order Matters)
At each renewal boundary, Donutwork computes the due amount in this order:
- Base plan amount.
- Addon charges (
price * quantity) for active addon quantities. - Addon-level discount if configured and still active (
discount_until). - Global discount (
fixedorpercentage, if still active). - Carryover credit (
carryover_credit) from previous downgrade/proration. - Taxes from subscription tax profile.
Formula (simplified)
net_subtotal = base + addons_after_addon_discounts
net_after_global_discount = net_subtotal - global_discount
net_due = max(0, net_after_global_discount - carryover_credit_applied)
vat_due = net_due * tax_rate
gross_due = net_due + vat_dueExample
- Base plan:
99.00 - Seat addon:
12.00x8seats =96.00 - Addon discount 10% active => addon effective
86.40 - Net subtotal =
185.40 - Global discount 15% =>
-27.81 - Carryover credit used:
20.00 - Net due =
137.59 - VAT 22% =
30.27 - Gross due =
167.86
Seat-Based Pricing Model
Seat pricing does not use a separate seat schema. It is intentionally modeled through addons:
- per-seat value:
addons[].price - seats count:
addons[].quantity - final seat amount: included automatically in subscription renewal
This model is stable and backward compatible with existing panel/API flows.
Lifecycle States (Billing Meaning)
| State | Billable | Meaning |
|---|---|---|
active | Yes | Normal recurring billing. |
trialing | Usually no | Trial phase before first paid renewal. |
past_due | Conditional | Payment recovery state after failed attempt. |
paused | No | Billing paused. Renewals are not charged while paused. |
cancel_pending | No new cycle | Scheduled to stop at period end (can be undone before boundary). |
cancelled | No | Closed subscription. |
Pause Semantics
Pausing does not create retroactive debt. If a subscription is resumed after 2 months, the system does not auto-charge the paused months. The next renewal is re-anchored to resume date logic.
Lifecycle Graph
Supported Lifecycle Operations
All lifecycle features are additive (no legacy route removal):
| Operation | Immediate | Period End | Proration | Notes |
|---|---|---|---|---|
| Pause | Yes | No | No | Stops renewal attempts while paused. |
| Resume | Yes | No | No | resume_at can define next boundary. |
| Change Plan | Yes | Yes | Optional | Upgrade/downgrade supported. |
| Change Addons | Yes | Yes | Optional | Seat quantity updates included. |
| Cancel At Period End | Yes (flag) | Effective at boundary | No | Legacy-compatible cancellation behavior. |
| Undo Cancel At Period End | Yes | No | No | Restores active plan before boundary. |
| Next Renewal Preview | Read-only | N/A | N/A | Shows expected next charge breakdown. |
| Amendment History | N/A | N/A | N/A | Full audit trail for lifecycle changes. |
API Examples
Schedule Seat Change At Period End
{
"subscription": {
"action": "change_addons",
"when": "period_end",
"proration": false,
"addons": [
{
"name": "workspace_seat",
"quantity": 25,
"price": 12
}
]
}
}Preview Next Renewal (response excerpt)
{
"status": "success",
"data": {
"subscription_id": "sub_abc123",
"next_renewal": "2026-07-01",
"billable": true,
"pricing": {
"items": [
{ "service": "Subscription Base: Pro", "net": 99, "tax_rate": 22, "tax": 21.78, "total": 120.78 },
{ "service": "Addon: workspace_seat (Qty: 25)", "net": 300, "tax_rate": 22, "tax": 66, "total": 366 }
],
"net_due": 399,
"vat_due": 87.78,
"gross_due": 486.78
}
}
}API FAQ (Common Combinations)
The combinations below refer to Customer API routes documented in /en/docs/api/customer#subscription-management.
How can I upgrade a plan?
Use change-plan with immediate application.
{
"lifecycle": {
"plan_id": "PLAN_ENTERPRISE_V3",
"when": "immediate",
"proration": true
}
}- Endpoint:
PUT /customers/{customerId}/subscriptions/{subscriptionId}/lifecycle/change-plan.json - If proration delta is positive, an immediate adjustment charge is attempted.
- If that immediate charge fails, the lifecycle change is not applied.
How can I downgrade a plan?
Use change-plan and target a lower-priced plan.
{
"lifecycle": {
"plan_id": "PLAN_STARTER_V1",
"when": "immediate",
"proration": true
}
}- Endpoint:
PUT /customers/{customerId}/subscriptions/{subscriptionId}/lifecycle/change-plan.json - With a negative delta, the platform stores credit in
carryover_credit. - That credit is consumed on future renewals before new debit.
How can I schedule plan changes for next renewal?
{
"lifecycle": {
"plan_id": "PLAN_PRO_V2",
"when": "period_end",
"proration": false
}
}- Endpoint:
PUT /customers/{customerId}/subscriptions/{subscriptionId}/lifecycle/change-plan.json - No immediate charge is attempted; change is applied at renewal boundary.
How can I change addon quantities (including seats)?
{
"lifecycle": {
"addons": [
{ "element": "workspace_seat", "quantity": 25 }
],
"when": "immediate",
"proration": true
}
}- Endpoint:
PUT /customers/{customerId}/subscriptions/{subscriptionId}/lifecycle/change-addons.json - In seat-based setups, seat count is
addons[].quantity.
How can I schedule addon quantity changes at period end?
{
"lifecycle": {
"addons": [
{ "element": "workspace_seat", "quantity": 40 }
],
"when": "period_end",
"proration": false
}
}- Endpoint:
PUT /customers/{customerId}/subscriptions/{subscriptionId}/lifecycle/change-addons.json - Useful for commercial changes that should start next cycle.
What happens if I pause and resume after two months?
- Pause:
PUT .../lifecycle/pause.json - Resume:
PUT .../lifecycle/resume.json(optionalresume_at)
Concrete timeline:
- pause on May 1, 2026
- resume on July 1, 2026
- no retroactive billing for May/June; renewal restarts from resume logic.
Can I undo a cancellation already set for period end?
Yes:
- set cancellation:
PUT .../lifecycle/cancel-at-period-end.json - undo cancellation:
PUT .../lifecycle/undo-cancel-at-period-end.json
If undone before renewal boundary, subscription returns to active.
How can I preview financial impact before applying changes?
Use lifecycle preview (no mutation):
{
"lifecycle": {
"action": "change_addons",
"addons": [
{ "element": "workspace_seat", "quantity": 30 }
]
}
}- Endpoint:
POST /customers/{customerId}/subscriptions/{subscriptionId}/lifecycle/preview.json - Response includes
old_due,new_due,delta,direction.
How should I handle safe retries in integrations?
Use lifecycle.idempotency_key on mutating operations.
On retries, this prevents duplicate application of the same change.
Where can I fetch full lifecycle change history?
Use amendment history:
- Endpoint:
GET /customers/{customerId}/subscriptions/{subscriptionId}/amendments.json - Includes action, timing (
immediate/period_end), before/after, and proration metadata.
Quick combination matrix
| Goal | Endpoint | when | proration |
|---|---|---|---|
| Upgrade plan now | .../change-plan.json | immediate | true |
| Downgrade plan now | .../change-plan.json | immediate | true |
| Change plan next cycle | .../change-plan.json | period_end | false |
| Increase/decrease seats now | .../change-addons.json | immediate | true |
| Change seats next cycle | .../change-addons.json | period_end | false |
| Pause service | .../pause.json | N/A | N/A |
| Resume service | .../resume.json | N/A | N/A |
| Cancel at period end | .../cancel-at-period-end.json | boundary | N/A |
| Undo cancellation | .../undo-cancel-at-period-end.json | immediate | N/A |
| Simulate impact | .../preview.json | N/A | N/A |
| Audit changes | .../amendments.json | N/A | N/A |
Practical Scenarios
1) Pause For 2 Months, Then Resume
- Pause on May 1, 2026
- Resume on July 1, 2026
- Result: no back-charge for May/June, renewal continues from resumed schedule.
2) Mid-Cycle Upgrade
- Customer increases seats from 10 to 20 with immediate change.
- Positive proration delta => immediate debit adjustment.
3) Mid-Cycle Downgrade
- Customer reduces seats mid-cycle.
- Negative delta => credit is stored in
carryover_credit. - Next renewal consumes this credit before any new debit.
4) Cancel Pending Then Undo
- Customer requests cancellation at period end.
- State moves to
cancel_pending. - If customer changes mind before boundary, undo operation restores normal lifecycle.
Backward Compatibility (No Break Changes)
Legacy panel and API flows stay valid. New lifecycle capabilities are opt-in.
| Area | Legacy Behavior | Current Behavior |
|---|---|---|
| Attach/detach subscription | Supported | Still supported |
| Legacy addon update route | Supported | Still supported |
| Legacy status field | Supported | Still present |
| Seat modeling | Addon quantity | Same official model |
| Existing integrations | Stable | Can adopt lifecycle incrementally |
Adoption Strategy
If you already use legacy routes, you can keep them unchanged and adopt lifecycle endpoints gradually for pause/resume, proration, and amendment tracking.
Recommended Setup Checklist
Model Seats Via Addons
Set per-seat price on addon and update quantity for seat count.
Use Scheduled Changes
Prefer `period_end` for commercial changes that should start from next cycle.
Enable Audit Visibility
Use amendment history and renewal preview to improve support and billing transparency.