Skip to main content
POST
cURL
Moves a customer with an active Stripe-backed monthly subscription to a cheaper paid plan. Send customerId, the target planId and a client-generated Idempotency-Key header. Moving to a free plan isn’t a downgrade: cancel at period end instead. timing says when the cheaper plan starts:
  • period_end (the default): the customer keeps the current plan and its credits until the period ends. Ledger puts a Stripe subscription schedule on the subscription; the renewal bills the cheaper price, Ledger grants its credits and then releases the schedule. Overage from the ending period bills on that renewal at the old plan’s rate. The response is scheduled, with effectiveAt set to the renewal.
  • immediately: the cheaper plan starts now and the renewal date stays. The old plan’s remaining credits end (oldPlanCredits: "keep" leaves them), and the new plan’s credits are prorated to the rest of the period, rounded down. With refund: "prorated", Ledger refunds (old price − new price) × the unused share of the period to the card, plus the tax charged on that amount, as a Stripe credit note on the invoice that paid for it; refundAmountCents (before tax) and creditNoteId report it. The old plan’s unbilled overage bills at its own rate on the next renewal (overageOnPlanChange: "carry_to_renewal") or on an invoice now ("bill_now"). The response is applied.
refund and overageOnPlanChange: "bill_now" only apply now: with period_end they return 400 refund_not_available or 400 overage_option_not_available. Options you leave out come from the workspace’s defaults, then Ledger’s. One change at a time: a pending upgrade returns 409 upgrade_pending, a scheduled cancellation 409 cancellation_scheduled, and another downgrade 409 downgrade_scheduled. While a downgrade is scheduled, upgrades and cancellation return 409 downgrade_scheduled; undo it first. A target that isn’t a cheaper, synced paid plan returns 400 ineligible_downgrade. Reuse the same idempotency key to retry: it returns the same downgrade and never changes Stripe or refunds twice. The same key with a different plan or option returns 409 idempotency_key_mismatch. If a Stripe request fails (502 stripe_downgrade_failed), retry with the same key.

Authorizations

Authorization
string
header
required

Bearer authentication header of the form Bearer <token>, where <token> is your auth token.

Headers

idempotency-key
string
required
Required string length: 1 - 255

Body

application/json
customerId
string
required
Required string length: 1 - 255
planId
string
required
Required string length: 1 - 255
timing
enum<string>

period_end (default) switches at the next renewal; immediately switches now and keeps the billing period.

Available options:
period_end,
immediately
refund
enum<string>

Immediate only. prorated refunds (old price − new price) × the unused share of the period as a Stripe credit note on the invoice that paid for it.

Available options:
none,
prorated
oldPlanCredits
enum<string>

Immediate only. expire (default) ends the old plan's credits now; keep leaves them until their original expiry.

Available options:
keep,
expire
overageOnPlanChange
enum<string>

Immediate only. When the old plan's unbilled overage is billed, at its own rate: on the next renewal (default) or on an invoice now.

Available options:
carry_to_renewal,
bill_now

Response

The downgrade: scheduled until the period ends, or applied

downgradeId
integer
required
status
enum<string>
required
Available options:
pending,
scheduled,
applied,
closed
fromPlanId
string
required
planId
string
required
timing
enum<string>
required
Available options:
period_end,
immediately
refund
enum<string>
required
Available options:
none,
prorated
oldPlanCredits
enum<string>
required
Available options:
keep,
expire
overageOnPlanChange
enum<string>
required
Available options:
carry_to_renewal,
bill_now
effectiveAt
string<date-time> | null
required
refundAmountCents
integer | null
required
creditNoteId
string | null
required