Upgrade or downgrade a subscription

Customers move between plans. A monthly Pro customer upgrades to Annual Pro mid-cycle; a B2B account drops from Enterprise to Standard at renewal. Frame handles both with the same mechanic: PATCH /v1/subscriptions/<id> with a new product, plus a proration_behavior that controls how the mid-cycle math settles.

There's no dedicated "upgrade" endpoint. Every product swap, whether it's a tier up or down, goes through the same PATCH.

Proration behaviors

The proration_behavior field controls how Frame reconciles the unused portion of the old product with the new product's pricing. Three options:

  • create_prorations — Frame calculates a credit (for the unused portion of the old product) and a charge (for the new product) and applies both to the next billing cycle's invoice. The customer doesn't see an immediate charge or refund; the math settles next cycle.
  • always_invoice — Frame generates an immediate proration invoice for the difference, issued right now. The customer is charged (or credited) immediately. Useful when the change should produce a visible event in the customer's billing history.
  • none — no proration. The new product takes effect immediately at full price for the next cycle; the unused portion of the old product is forfeited. Cleanest when you want to keep the math simple and your customer-facing UX makes "no refund / no extra charge" clear.

Pick by intent:

IntentUse
Smooth UX, customer doesn't see an immediate chargecreate_prorations
Immediate billing event, customer sees the charge/credit nowalways_invoice
No proration, simple mathnone

create_prorations is the most common default.

The upgrade flow

curl --request PATCH \
  --url https://api.framepayments.com/v1/subscriptions/<subscription_id> \
  --header "Authorization: Bearer $FRAME_SECRET_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "product": "<new_product_id>",
    "proration_behavior": "create_prorations"
  }'

What Frame does:

  1. Reads the subscription's current product and cycle position.
  2. Calculates the unused portion of the current cycle (in dollars).
  3. Calculates the cost of the new product for the same time window.
  4. The delta is either a credit (downgrade) or a charge (upgrade).
  5. Applies the delta per proration_behavior.
  6. Updates the subscription's product_id to the new product.
  7. Fires the customer.subscription.updated webhook for customer-owned subscriptions. Account-owned subscriptions don't emit an update event — read the change from the change log (step 8) or GET /v1/subscriptions/<id>.
  8. Logs the change in the subscription_change_logs table.

The subscription's status stays active through the swap; no transition through pending or incomplete.

Reading the change log

Frame keeps an audit log of subscription mutations:

curl --request GET \
  --url "https://api.framepayments.com/v1/subscription_change_logs?subscription_id=<subscription_id>" \
  --header "Authorization: Bearer $FRAME_SECRET_KEY"

Each entry records the change type (product swap, payment method update, etc.) and the before/after state. Useful for customer support — "what did the merchant change on this subscription, when?" — and for reconciling billing discrepancies.

Mid-cycle proration math

Example: upgrade from $29/mo to $49/mo at day 10 of a 30-day cycle.

  • Unused portion of old product: $29 × (20 / 30) = $19.33 credit.
  • Cost of new product for the same window: $49 × (20 / 30) = $32.67 charge.
  • Net proration: $32.67 − $19.33 = $13.33 owed.

With proration_behavior: create_prorations, that $13.33 lands on next cycle's invoice as a line item alongside the next cycle's $49 charge → total $62.33 next invoice.

With proration_behavior: always_invoice, an immediate proration invoice for $13.33 is generated + issued now; next cycle bills the full $49.

With proration_behavior: none, no proration line item; next cycle simply bills the new $49.

Downgrade example: $49 to $29 at day 10.

  • Unused portion of old: $49 × (20 / 30) = $32.67 credit.
  • Cost of new for same window: $29 × (20 / 30) = $19.33 charge.
  • Net: $13.34 credit owed to the customer.

With create_prorations, $13.34 credits against next cycle ($29 − $13.34 = $15.66 next invoice). With always_invoice, an immediate credit memo / negative invoice issues. With none, the customer eats the difference; next cycle is $29.

Common variations

Changing payment method. Same PATCH endpoint:

curl --request PATCH \
  --url https://api.framepayments.com/v1/subscriptions/<id> \
  --header "Authorization: Bearer $FRAME_SECRET_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "default_payment_method": "<new_payment_method_id>"
  }'

No proration triggered — payment method changes don't restructure pricing.

Updating description or metadata. Same shape with description or metadata fields. No billing impact.

Changing both product and payment method at once. Single PATCH with both fields. Proration applies based on the product change; payment method change is metadata-only.

Schedule a future swap. Frame doesn't expose a "swap at end of cycle" parameter directly. Pattern: store the requested change in your application, fire the PATCH at the cycle boundary (typically on the *.subscription.renewal.completed webhook for the current cycle).

Webhook signal

The relevant events:

  • customer.subscription.updated — generic update event; fires for any mutation on a customer-owned subscription. Account-owned subscriptions don't emit an update event — poll GET /v1/subscriptions/<id> or use the change log below.
  • The next cycle's invoice events (invoice.created, invoice.issued) reflect the new product + any proration line items.

If you need a fine-grained "the subscription was upgraded" signal, the change log is more reliable than parsing webhook payloads. Listen for the update webhook, then query listSubscriptionChangeLogs for context.

Gotchas

Symptom: you PATCH'd a product change and the customer wasn't charged immediately. Why: default proration is create_prorations — the math settles next cycle. Fix: if you want immediate billing, pass proration_behavior: always_invoice on the PATCH.

Symptom: downgrade resulted in a negative invoice you didn't expect. Why: always_invoice on a downgrade issues a credit invoice for the unused portion of the more-expensive product. Fix: if you don't want immediate refunds, use create_prorations (credits roll forward) or none (no refund).

Symptom: customer says they were charged twice. Why: most often, an always_invoice proration was issued and the customer assumed it replaced the next cycle's normal charge. The proration is additive to the next cycle, not a replacement. Fix: surface the proration in the customer's billing UI so they can see the breakdown. The change log is the source of truth.

Symptom: you swapped to a product with a different recurring_interval (monthly → annual) and the math looks wrong. Why: cross-interval proration is tricky — the math compares "rest of current cycle" against a different cycle length entirely. Frame handles it, but customer expectations vary. Fix: if the cross-interval move is common in your platform, document it in your billing UI carefully, or split the upgrade into two steps (cancel + create new) for cleaner accounting.

Symptom: trying to upgrade a subscription in past_due or unpaid state and getting validation errors. Why: product swaps on non-active subscriptions have undefined behavior — the cycle isn't healthy enough to prorate against. Fix: recover the subscription to active first (pay the outstanding invoice, update the payment method); then PATCH the upgrade.

Next steps