Donutwork Docs
Recurring Payments

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:

  1. Base plan amount.
  2. Addon charges (price * quantity) for active addon quantities.
  3. Addon-level discount if configured and still active (discount_until).
  4. Global discount (fixed or percentage, if still active).
  5. Carryover credit (carryover_credit) from previous downgrade/proration.
  6. 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_due

Example

  • Base plan: 99.00
  • Seat addon: 12.00 x 8 seats = 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)

StateBillableMeaning
activeYesNormal recurring billing.
trialingUsually noTrial phase before first paid renewal.
past_dueConditionalPayment recovery state after failed attempt.
pausedNoBilling paused. Renewals are not charged while paused.
cancel_pendingNo new cycleScheduled to stop at period end (can be undone before boundary).
cancelledNoClosed 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):

OperationImmediatePeriod EndProrationNotes
PauseYesNoNoStops renewal attempts while paused.
ResumeYesNoNoresume_at can define next boundary.
Change PlanYesYesOptionalUpgrade/downgrade supported.
Change AddonsYesYesOptionalSeat quantity updates included.
Cancel At Period EndYes (flag)Effective at boundaryNoLegacy-compatible cancellation behavior.
Undo Cancel At Period EndYesNoNoRestores active plan before boundary.
Next Renewal PreviewRead-onlyN/AN/AShows expected next charge breakdown.
Amendment HistoryN/AN/AN/AFull 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 (optional resume_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

GoalEndpointwhenproration
Upgrade plan now.../change-plan.jsonimmediatetrue
Downgrade plan now.../change-plan.jsonimmediatetrue
Change plan next cycle.../change-plan.jsonperiod_endfalse
Increase/decrease seats now.../change-addons.jsonimmediatetrue
Change seats next cycle.../change-addons.jsonperiod_endfalse
Pause service.../pause.jsonN/AN/A
Resume service.../resume.jsonN/AN/A
Cancel at period end.../cancel-at-period-end.jsonboundaryN/A
Undo cancellation.../undo-cancel-at-period-end.jsonimmediateN/A
Simulate impact.../preview.jsonN/AN/A
Audit changes.../amendments.jsonN/AN/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.

AreaLegacy BehaviorCurrent Behavior
Attach/detach subscriptionSupportedStill supported
Legacy addon update routeSupportedStill supported
Legacy status fieldSupportedStill present
Seat modelingAddon quantitySame official model
Existing integrationsStableCan 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.


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.


On this page