Subscription plans are reusable billing templates: they define what to charge, how often to bill, and any trial or contract terms. A plan's line items can reference your catalog (variants or bundles) or be defined ad hoc with a name and unit price, and the plan can add a setup fee, a trial period, a contract term, and an early termination fee. A plan can also offer several intervals and quantities for the buyer to choose from at signup. Every money field on a plan must use the plan's currency.
One plan can be sold through multiple channels: create subscriptions against it directly, or pass its subscription_plan_id to a checkout session or payment link for Flint-hosted signup. To sell subscriptions on any product in a cart instead, use a subscription offer. Plans are archived rather than deleted, so existing subscriptions keep billing against the plan they were created with.
Start with the Subscription billing guide. For selling plans through hosted links, see subscription signup links.
Replace plan line items#
line_items is an owned collection. Create a plan with inline items, then replace the collection with PATCH /v1/subscription-plans/{subscription_plan_id} and the plan's current expected_version.
{
"expected_version": 3,
"line_items": [{
"subscription_plan_line_item_id": "spli_01J00000000000000000000000",
"name": "Monthly support",
"unit_price_money": {"amount": 2500, "currency": "USD"},
"quantity": 1
}]
}
Include an existing subscription_plan_line_item_id to retain that item's identity. Omit the ID for a new item; Flint generates it. Members omitted from the array are removed. Duplicate IDs and IDs belonging to another plan are rejected. Omit line_items to keep the collection unchanged. An empty array or null is invalid because a plan needs at least one item.
Each ad hoc item supplies its name and price. Catalog items supply variant_id or bundle_id instead. A retained catalog item with the same source keeps its recorded price, tax, display, and fulfillment snapshot; omitting modifiers retains their recorded choices. Changing the source requires an explicit modifiers array and creates a fresh catalog snapshot. Existing subscriptions keep the line-item snapshots taken when they were created.
Replacement and scalar changes in the same request commit atomically. A missing version fence returns 400; a stale version returns 409. Archived plans cannot be edited. Every item must use the plan currency and a quantity from 1 through 9999. Use the same Idempotency-Key and body to recover a lost response with the original new item IDs.
Intervals and quantities#
billing_interval_options lists up to 12 distinct pairs of billing_interval and billing_interval_count. It must include the plan's own pair, which is the default. quantity_options lists up to 10 distinct quantities from 1 to 100 and must include 1, the default. Send [] to offer only the plan's own interval or a quantity of 1. On PATCH, an omitted field keeps its value and an array replaces it; changing the plan's billing_interval to a pair the stored options don't include needs the new options in the same request. Responses always return the effective lists, with the plan's own pair first and quantities ascending. Invalid options return INVALID_BILLING_INTERVAL_OPTIONS or INVALID_QUANTITY_OPTIONS. These options apply to every plan, not only plans that ship.
The buyer chooses one interval and one quantity at signup and can change them later among the same options. A quantity multiplies every line on the plan: the subscription reports it as quantity, each subscription line keeps the plan line's per-unit quantity, and each renewal ships the two multiplied together. Multiplied, a line can carry at most 9,999 units. Saving the plan doesn't check quantity_options against that limit, so a subscription or checkout that chooses a quantity putting a line over it, or making the amount too large, fails with INVALID_QUANTITY.
Variant swaps#
A catalog line can list swap_variant_ids: up to 25 other variants of the same product that a subscriber may switch the line to after signup. [] means no swaps. Only variant lines can list swaps; bundle and ad hoc lines can't (INVALID_SWAP_VARIANTS). A subscriber can always swap back to the line's own variant_id. See Change cadence, quantity, and items.
Discounts#
Physical plans can't have a trial. Discount shipments with a promotion applied at signup: recurrence.type once covers the first shipment, repeating the first period_count charged cycles, and forever every cycle. See Discounts and first-shipment offers.
Physical products#
A catalog line whose variant or bundle is physical makes the plan ship every cycle. A catalog item with no kind counts as physical. Each renewal order then carries the subscriber's address, a shipping charge, tax for that address, and a fulfillment. See Ship physical products.
delivery_required is true when at least one line ships. subscription_delivery_method_ids lists the delivery methods subscribers can choose. Empty means the store's checkout default methods. Removing a method changes what new subscribers can choose; current subscribers keep it until it is archived or stops serving their address. delivery_method_subscription_counts reports, for each method that live subscriptions on the plan use, how many are active, paused, and past_due.
Create, line item changes, method changes, and trial changes validate the plan's physical lines:
| Error | Cause |
|---|---|
SUBSCRIPTION_DELIVERY_PROFILE_MISSING | A physical line has no delivery profile. |
SUBSCRIPTION_DELIVERY_PROFILE_ACTION_REQUIRED | A line's delivery profile needs an action before it can be used. |
SUBSCRIPTION_FULFILLMENT_NOT_SUPPORTED | A line's profile allows only pickup. Subscriptions ship with shipment or local_delivery. |
SUBSCRIPTION_DELIVERY_LINES_NOT_COMBINABLE | The physical lines would ship separately, so one method can't cover a renewal. |
SUBSCRIPTION_DELIVERY_METHOD_UNSUPPORTED | No offered method can price every shipment without the buyer present, or a listed method uses caller_supplied pricing or delivery windows. |
SUBSCRIPTION_TRIAL_NOT_SUPPORTED_FOR_PHYSICAL | trial_period_days is above 0. Use a promotion for a first-shipment offer instead. |
Each error names what failed with fields on the error object:
variant_idandproduct_idname a variant line, or the item inside a bundle line that failed.blocking_resourcescan list the line's variant or bundle, withresource_typevariantorbundle. A bundle line is always named this way.delivery_method_idnames a listed method that fails the check. A method that can't be used at all, such as an archived one, is named bydelivery_method_idwith no line fields.
The trial and combination checks, and a method that can't serve the lines, apply to the whole plan, so their line fields name the plan's first physical line. When the error carries blocking_resources or delivery_method_id, error.details has one item with the same fields. Otherwise the error has no details.
Stock source#
inventory_routing_source says where tracked stock for the plan's orders comes from. Its type picks one of three shapes:
type | ID field | Stock comes from |
|---|---|---|
fixed_location | location_id | One Location. |
policy | inventory_allocation_policy_id | An allocation policy. |
policy_version | inventory_allocation_policy_version_id | One immutable version of an allocation policy. |
{
"inventory_routing_source": { "type": "fixed_location", "location_id": "loc_01J00000000000000000000000" }
}
The field is required when any line item tracks inventory. Creating such a plan without it, or replacing line_items with tracked items on a plan that has no source, returns INVENTORY_ROUTING_SOURCE_REQUIRED and saves nothing. A plan whose lines don't track inventory can omit it, and a source you send anyway is validated and stored.
On PATCH, omit the field to keep the stored source. A new object replaces it as a whole, and the check runs against the plan's line items after any line_items replacement in the same request. The source can't be cleared: null returns NULL_NOT_ALLOWED on create and on update. A field from another type, or any other unknown field, returns UNKNOWN_FIELD with its full path, such as inventory_routing_source.location_id. An unknown type, a missing ID, or a malformed ID returns INVENTORY_ROUTING_SOURCE_INVALID.
Responses include inventory_routing_source when the plan has one and omit it otherwise. New subscriptions copy the plan's source when they're created, and new payment links for the plan copy it unless they send their own. Subscriptions and payment links that already exist keep the source they copied.
