Utilizing Subscription Schedules in Stripe: A Journey of Discovery
Stripe大冒险:利用 Subscription Schedule 更新订阅
Updating subscriptions in Stripe using its API is straightforward, but there are instances where immediate updates are not feasible due to invoice dates or the nature of the subscription product. In such cases, scheduling an update is necessary.
Subscription Schedule is a very powerful feature. However, in my personal development experience, I initially encountered a lot of confusion when using this feature. While Stripe provides detailed API Documentation and Use Cases to describe the Subscription Schedule, as Stripe's API evolves, some new objects are introduced, some fields are deprecated, and the latest documentation doesn't tell us about its evolution. Also, there are some minor gaps between Use Cases and the API documentation, such as Price
replacing Plan
, or the coupon
field that is deprecated in Phase Items.
In this blog, I’ll share an example of scheduling a subscription quantity update at a future time for a subscription with a single item and a coupon. I’ll also discuss the challenges and solutions I encountered along the way.
Scenario Setting
We currently have such a Subscription with a Subscription Item - SaaS Member Fee with a unit price of $10 and a quantity of 5; and it comes with a free coupon for three months. We need to update its quantity to 10 on September 1 i.e. to achieve the following:
TL; DR You can jump straight to the solution section to see the code.
How Stripe itself is implemented
Even with API Documentation and Use Cases, the first time I interfaced with this API was a bit confusing. I know I need to create a Subscription Schedule, but I'm still confused about how to pass the parameters.
For the API POST /v1/subscription_schedules
for creating a Schedule, the documentation gives the following use case:
Stripe::SubscriptionSchedule.create({
customer: 'cus_NcI8FsMbh0OeFs',
start_date: 1680716828,
end_behavior: 'release',
phases: [
{
items: [
{
price: 'price_1Mr3YcLkdIwHu7ixYCFhXHNb',
quantity: 1,
},
],
iterations: 12,
},
],
})
However, it doesn't seem to work for my case, because I need to modify an existing Subscription, so I should at least pass the Subscription ID. Reading the documentation, there is a parameter called from_subscription
which accepts an existing subscription ID, the documentation says:
When using this parameter, other parameters (such as phase values) cannot be set. To create a subscription schedule with other modifications, we recommend making two separate API calls.
This means that if I set from_subscription
then the other parameters can't be set and I have to make two API calls, so what should I do?
We also know that we can do Schedule Update manually in Stripe, which also calls Stripe's own API, so let's open up Chrome's dev tool and see how Stripe does it itself. Stripe makes two API calls (omitting the other parameters):
The first API request:
POST /v1/subscription_schedules
Request Body:
{
from_subscription: "sub_1PeCNNLiQSlGYPd5Wpmvb1n6"
}
Response:
{
id: "sub_sched_1PeH6LLiQSlGYPd599aOVdoW",
# ... other values omitted
}
The second API request:
POST /v1/subscription_schedules/sub_sched_1PeH6LLiQSlGYPd599aOVdoW
Request Body:
{
proration_behavior: "none",
phases: [
{
start_date: 1721378477,
end_date: "now",
iterations: "",
discounts: [
{
coupon: "EDHtWtdk"
}
],
default_tax_rates: "",
automatic_tax: {
enabled: false
},
items: [
{
price: "price_1PeCLRLiQSlGYPd50cFz00Na",
quantity: 5
}
],
collection_method: "charge_automatically"
},
{
start_date: "now",
end_date: 1725217200,
discounts: [
{
coupon: "EDHtWtdk"
}
],
default_tax_rates: "",
items: [
{
quantity: 5,
tax_rates: "",
price: "price_1PeCLRLiQSlGYPd50cFz00Na"
}
]
},
{
start_date: 1725217200,
discounts: [
{
coupon: "EDHtWtdk"
}
],
default_tax_rates: "",
items: [
{
quantity: 10,
tax_rates: "",
price: "price_1PeCLRLiQSlGYPd50cFz00Na"
}
],
proration_behavior: "none",
collection_method: "charge_automatically",
invoice_settings: {
description: "Thank you for your business!"
}
}
],
end_behavior: "release"
}
The first API request creates a Subscription Schedule, passing only the from_subscription parameter; after getting the ID of the created Schedule, the second API modifies the Schedule and passes in a bunch of parameters. This involves a lot of concepts, such as Phase and proration, let's take a look at what they mean.
What is Phase
A Phase represents a specific period of time in a Subscription Schedule during which the Price, Charge, Tax, etc. parameters we specify are applied. In other words, we can specify a set of phases, each with its own billing parameters. A Phase has the following key parameters:
- start_date and end_date: identifies the start date and end date of each phase, a schedule can have more than one phase, but their start_date and end_date must be consistent with each other.
- price and quantity: we can specify different prices and quantities for the subscription items in each phase, so that we can achieve the goal of changing the quantity in the future in this scenario.
- discounts: specific discounts and coupons to be used in this phase.
- tax_rates and automatic_tax: parameters related to tax rates
- collection_method, etc.
From the second request above, Stripe sets up 3 Phases.
- Phase#0, start_date is the start time of the original subscription, end_date is
now
, and discounts contains the original coupon code, and items has an object with price and quantity parameters with the same values as the current subscription. The point of this phase is to terminate the current subscription immediately and keep data separate. - Phase#1, start_date is
now
, end_date is our expected time, and items is the same as Phase#0. - Phase#2, start_date is our expected time, end_date is empty, and quantity in items[0] is updated to 10.
Why do we need to use 3 Phases, can't we use 2? Actually, two is fine in simple cases.
proration and proration_behavior
Proration means adjusting the billing amount to reflect changes such as:
- Upgrading or downgrading a plan.
- Changing the quantity of a subscription item.
- Adding or removing a subscription item.
Proration in Stripe
When we make changes to the Subscription, proration is automatically handled by default. We can specify its behavior using proration_behavior
, a parameter with the following three values
create_prorations
: the default value, which creates prorations when the billing cycle is changednone
: no proration when the billing period changes.always_invoice
: create proration billing immediately
Let's take an example to illustrate. Suppose a customer subscribes to a 100-dollar-per-month Basic Plan, billed in advance, with a billing cycle that begins on the 1st of the month. On the 15th of the month, the customer decides to upgrade to the Premium plan, which costs 200 per month.
If create_prorations is selected, the following calculation occurs:
- Initial subscription cost 100 (charged on the 1st of the month)
- Upgrade on the 15th, number of days remaining in the month: 16 days
- Premium plan daily cost: 200 / 30 ≈ 6.67 / day
- Basic plan daily cost: 100 / 30 ≈ 3.33 days
- Pro-rated difference for the remaining 16 days: (6.67 - 3.33) * 16 = 53.44
- The bill to be paid on the 1st of the next month will be: 200 (Premium plan fee) + 53.44 = 253.44
If none is selected, then yes:
- Initial subscription cost: 100 (charged on the 1st of the month)
- Upgrade on the15th day with no proration
- The next bill (1st of next month) will be 200 for the Premium plan only
end_behavior
end_behavior refers to what the associated Subscription should do when the Schedule ends (i.e. reaches the end_date of the last phase). The enumerated values for end_behavior
are release
or cancel
, which are different:
release
: the default value, when the Schedule ends, the Subscription continues to exist in the same state as at that moment.cancel
: when the Schedule ends, the Subscription is directly terminated (canceled).
Meanwhile, Subscription Schedule also provides 2 corresponding APIs, the meaning is the same:
POST /v1/subscription_schedules/:id/cancel
.POST /v1/subscription_schedules/:id/release
.
This is important because Stripe only allows a Subscription to have a maximum of one Schedule in the active state. If you are debugging code and creating a schedule and encounter this error "You cannot migrate a subscription that is You cannot migrate a subscription that is already attached to a schedule", this means that the current Subscription already has a Schedule for which we can either update or release/cancel. If you choose to cancel at this point, you'll find that the Subscription associated with it simply disappears. This is a bad thing that we should avoid, both from a business perspective and from a debugging perspective.
Why price instead of plan
If we look closely at the results returned from creating a SubscriptionSchedule, we notice that the item is rendered in this way:
{
:billing_thresholds => false,
:discounts => [],
:metadata => {},
:plan => "price_1PU2ARLiQSlGYPd5Agu2y15G",
:price => "price_1PU2ARLiQSlGYPd5Agu2y15G",
:quantity => 5,
:tax_rates => []
}
It makes you wonder why there are both price
and plan
fields with the same price_xxx
value.
Plan is a core concept in earlier versions of the Stripe Subscription system, which is used to define the basic parameters of a Subscription, while Price is an improved concept in the newer versions of Stripe, which provides a more flexible and fine-grained control, and is used in place of Plan. Thus, we prefer to use Price instead of Plan with Schedule feature .
Solution
Given a subscription
, want to modify the quantity
of its item at time
. We are using only 2 phases, the code is as follows:
schedule = Stripe::SubscriptionSchedule.create from_subscription: subscription.id
phase0 = schedule.phases[0]
phase0_item = phase0.items[0]
original_quantity = phase0_item.quantity
price_id = phase0_item.price
discounts = phase0.discounts.collect(&:to_hash)
phases = [
# Phase 0 - Modify an existing subscription phase with an end date of `time`
{
start_date: phase0.start_date,
end_date: time.to_i,
proration_behavior: 'none',
discounts: discounts,
items: [
{
price: price_id,
quantity: original_quantity,
}
]
},
# Phase 1 - Set up a new subscription phase with a start date of `time` and a quantity of `quantity
{
start_date: time.to_i,
end_date: time.to_i + 60,
proration_behavior: 'none',
discounts: discounts,
items: [
{
price: price_id,
quantity: quantity,
}
]
}
]
Stripe::SubscriptionSchedule.update schedule.id, phases: phases, proration_behavior: 'none'
Let me explain the parts that weren't covered before:
- When updating a SubscriptionSchedule, if the
discounts
parameter is not explicitly passed, existing coupons will be canceled. To keep any current discounts or coupons, they need to be included in the updated phase. - The
coupon
parameter in the Item object has been deprecated in favor of passing an array of discounts at the outer level. - It is a good idea to set an end date for the last phase to ensure that the Schedule is automatically released. this avoids hitting the limit we can only have one active Schedule for a Subscription.
Conlusion
Utilizing Stripe's Subscription Schedule feature can greatly simplify the subscription management process. While it may initially be confusing, by carefully reading the documentation and practicing, we can fully harness the potential of this powerful tool. I hope this article helps you understand and use Subscription Schedule more effectively. If you have any questions or need further clarification, please feel free to reach out to me.