Product phases
A product phase is a pricing segment with a defined duration. Phases let you model offers like "free for 14 days, then $29/mo for 3 months, then $39/mo forever" without juggling separate products and subscription migrations. The phase carries the price (or a discount off a base price), a count of billing periods it lasts, and an ordinal that sequences phases within a product or subscription.
Frame uses one underlying primitive for two surfaces:
- ProductPhase — phases attached to a Product. Acts as a template: when you create a subscription against the product, the product's phases are copied onto the subscription as SubscriptionPhases. Edit the template; future subscriptions inherit the change; existing ones do not.
- SubscriptionPhase — phases attached to a Subscription. Instance-specific. Edit a subscription's phase to adjust pricing for that one customer; the underlying product is unaffected.
The model is polymorphic (phaseable_id / phaseable_type); the API exposes them as two endpoints with parallel shapes.
Pricing types
A phase carries one of two pricing types:
static— fixed price in cents (amount_cents). Billed at this amount for the phase duration regardless of the product's base price.relative— a percentage discount off the product's base price (discount_percentage). Useful for "50% off for first 3 months" patterns where you want the discount to track the base price if it changes later.
Pick static when the phase amount is a deliberate target (e.g., "$0 trial," "$10 intro price"). Pick relative when the phase is a discount off whatever the current product price is.
Fields
curl --request POST \
--url https://api.framepayments.com/v1/product_phases \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json' \
--data '{
"product_id": "<product_id>",
"name": "Free trial",
"ordinal": 1,
"pricing_type": "static",
"amount_cents": 0,
"period_count": 14
}'
The interesting fields:
ordinal— sequence within the product/subscription. Must be a positive integer (≥1). Phases activate in ordinal order; the lowest ordinal is the first phase the customer sits in. Ordinals must be unique within a phaseable.name— display name. Surfaces in the dashboard and on subscription change logs.pricing_type—staticorrelative(see above).amount_cents— forstaticphases. Required whenpricing_type: static.discount_percentage— forrelativephases. Required whenpricing_type: relative. Range 0-100.period_count— how many billing periods this phase lasts. Must be ≥1; no "open-ended" phase value exists. For "rest of subscription" semantics, setperiod_countto a value larger than the subscription will ever reach (e.g., 999) and treat it as effectively unbounded.started_at— populated by Frame when the phase becomes active on a subscription. Don't set on create.
Phase transitions
A subscription tracks which phase is currently active via current_phase and phase_started_at. When a billing cycle completes:
- Frame checks whether
cycles_completed_in_phase >= period_count. - If yes, transition to the next phase (next-higher ordinal).
- If there is no next phase, stay on the current one indefinitely.
Important caveat: phase transitions don't auto-fire on a background timer in V1. The transition is checked when a subscription event runs (renewal, manual sync). For most subscriptions this happens naturally on the billing cycle; if you're orchestrating subscription state outside the renewal flow, you may need to explicitly trigger the phase check via a subscription update.
Common shapes
Free trial → paid. Two phases: a static $0 phase with period_count: 1 (one billing cycle of free), then a large-period_count phase at the product's base price (e.g., period_count: 999).
Intro pricing → standard. Two phases: a static discounted price for N cycles, then a large-period_count phase at standard pricing.
Annual ramp. Three phases for an enterprise plan: discounted year 1, partial discount year 2, full price year 3+ (set the last phase's period_count high).
Cycle-bounded coupon. A relative 50% phase with period_count: 6 followed by a high-period_count phase at full price. Note: for short-term promotional discounts at checkout, coupons + promotion codes are usually the right surface — phases are more appropriate when the discount is part of the product's offering.
Editing phases on an active subscription
You can edit a subscription's phases via the subscription-phase endpoints. Two patterns:
- Bulk update:
PATCH /v1/subscriptions/<id>/phases/bulk_updateaccepts a full phase array and overwrites. Use when you're restructuring the subscription's ramp end-to-end. - Per-phase update:
PATCH /v1/subscriptions/<id>/phases/<phase_id>for surgical changes.
Editing phases doesn't retroactively re-bill prior cycles. Changes take effect on the next phase transition or the current phase's pricing as of the next renewal.
Relationships
- Product —
phaseable_type = "Product",phaseable_id = <product_id>. Acts as a reusable template. - Subscription —
phaseable_type = "Subscription",phaseable_id = <subscription_id>. Instance-specific.
When a subscription is created against a product, the product's phases are copied (not referenced) onto the subscription as SubscriptionPhases. From that point, the subscription's phases evolve independently — useful, because it means changing a product's phases doesn't disrupt existing customers.
Gotchas
Symptom: you added a new phase to a product, but existing subscriptions didn't pick it up. Why: phases are copied on subscription create, not linked by reference. Existing subscriptions have their own phase set. Fix: this is by design. If you want existing subscriptions to migrate to a new phase structure, update them individually via the subscription-phase endpoints.
Symptom: phase doesn't transition when you expect. Why: phase transitions happen on subscription events (renewal, update), not on a wall-clock timer. If the subscription hasn't had an event since the prior period closed, the transition hasn't fired yet. Fix: trigger a subscription update or wait for the next renewal cycle; the transition will catch up.
Symptom: a relative 50% phase isn't applying the discount you expected. Why: relative discounts are calculated off the product's current default_price, not a frozen historical price. If the product's price has moved since the subscription was created, the discount math has too. Fix: either use a static phase to fix the dollar amount or accept that relative is dynamic — that's its purpose.
Reference
For the full API surface, see POST/v1/products/{product_id}/phases and POST/v1/subscriptions/{subscription_id}/phases.