cURL
curl --request POST 'https://api.integrateledger.com/v1/billing/downgrade' \
-H "Authorization: Bearer $LEDGER_API_KEY" \
-H 'Idempotency-Key: downgrade_123' \
-H 'Content-Type: application/json' \
--data '{"customerId":"user_123","planId":"starter","timing":"period_end"}'import ledger from "integrateledger";
const client = new ledger({ apiKey: process.env.LEDGER_API_KEY ?? "" });
await client.downgradePaidPlan({ idempotencyKey: "downgrade_123", body: { customerId: "user_123", planId: "starter", timing: "period_end" } });import os
from integrateledger import ledger
client = ledger(api_key=os.environ["LEDGER_API_KEY"])
client.downgrade_paid_plan(customer_id="user_123", plan_id="starter", timing="period_end", idempotency_key="downgrade_123"){
"downgradeId": 123,
"status": "pending",
"fromPlanId": "<string>",
"planId": "<string>",
"timing": "period_end",
"refund": "none",
"oldPlanCredits": "keep",
"overageOnPlanChange": "carry_to_renewal",
"effectiveAt": "2023-11-07T05:31:56Z",
"refundAmountCents": 123,
"creditNoteId": "<string>"
}{
"error": "<string>",
"downgradeId": 123
}{
"error": "<string>",
"downgradeId": 123
}{
"error": "<string>",
"downgradeId": 123
}{
"error": "<string>",
"downgradeId": 123
}{
"error": "payload_too_large"
}{
"error": "rate_limited"
}{
"error": "internal_error",
"request_id": "<string>"
}{
"error": "<string>",
"downgradeId": 123
}{
"error": "<string>",
"downgradeId": 123
}Billing
Downgrade a paid plan
Move a customer to a cheaper paid plan at the end of the period, or now.
POST
/
v1
/
billing
/
downgrade
cURL
curl --request POST 'https://api.integrateledger.com/v1/billing/downgrade' \
-H "Authorization: Bearer $LEDGER_API_KEY" \
-H 'Idempotency-Key: downgrade_123' \
-H 'Content-Type: application/json' \
--data '{"customerId":"user_123","planId":"starter","timing":"period_end"}'import ledger from "integrateledger";
const client = new ledger({ apiKey: process.env.LEDGER_API_KEY ?? "" });
await client.downgradePaidPlan({ idempotencyKey: "downgrade_123", body: { customerId: "user_123", planId: "starter", timing: "period_end" } });import os
from integrateledger import ledger
client = ledger(api_key=os.environ["LEDGER_API_KEY"])
client.downgrade_paid_plan(customer_id="user_123", plan_id="starter", timing="period_end", idempotency_key="downgrade_123"){
"downgradeId": 123,
"status": "pending",
"fromPlanId": "<string>",
"planId": "<string>",
"timing": "period_end",
"refund": "none",
"oldPlanCredits": "keep",
"overageOnPlanChange": "carry_to_renewal",
"effectiveAt": "2023-11-07T05:31:56Z",
"refundAmountCents": 123,
"creditNoteId": "<string>"
}{
"error": "<string>",
"downgradeId": 123
}{
"error": "<string>",
"downgradeId": 123
}{
"error": "<string>",
"downgradeId": 123
}{
"error": "<string>",
"downgradeId": 123
}{
"error": "payload_too_large"
}{
"error": "rate_limited"
}{
"error": "internal_error",
"request_id": "<string>"
}{
"error": "<string>",
"downgradeId": 123
}{
"error": "<string>",
"downgradeId": 123
}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 isscheduled, witheffectiveAtset 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. Withrefund: "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) andcreditNoteIdreport 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 isapplied.
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
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Headers
Required string length:
1 - 255Body
application/json
Required string length:
1 - 255Required string length:
1 - 255period_end (default) switches at the next renewal; immediately switches now and keeps the billing period.
Available options:
period_end, immediately 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 Immediate only. expire (default) ends the old plan's credits now; keep leaves them until their original expiry.
Available options:
keep, expire 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
Available options:
pending, scheduled, applied, closed Available options:
period_end, immediately Available options:
none, prorated Available options:
keep, expire Available options:
carry_to_renewal, bill_now