Scheduled plan change does not re-attach the new product's meter price — usage bills at the old plan's rate indefinitely
# Scheduled plan change effective_at=next_billing_date) does not re-attach the new product's meter price — usage bills at the old plan's rate indefinitely
**Business:** petnow (test mode)
**Feature:** Subscriptions / Change Plan (v1.93.0 "Change Plan on Next Billing Date")
## Summary
When a plan change is **scheduled** effective_at: "next_billing_date", proration_billing_mode: "full_immediately" — the only mode the API accepts with that timing), the subscription's product_id switches at the boundary as expected, but the subscription's attached usage meter keeps the **old** product's price_per_unit. Every subsequent renewal invoice bills usage at the old plan's rate while naming the new product on the line item. An **immediate** change effective_at: "immediately") re-attaches pricing correctly, so the two paths reach the same product with different billing.
## Environment
- Subscription: sub_0NgsRDIOYpACfNHqo5G3u (customer cus_0NgsRDHy8LQ6ZhbqXJLGC)
- Two products sharing one meter mtr_0Nb6cXrBT6IZZ7Ka4nnBW, "Active pets", aggregation last(pet_count), fixed price $0 on both):
- Petify Core (DEV) pdt_0Nb6cwDRX6Qft6tEpHruq — **$0.50**/unit
- Petify Plus (DEV) pdt_0NkHvBH9Y6Se7Qu2rDtco — **$0.75**/unit
## Steps to reproduce (all timestamps 2026-08-06 UTC; reproduced twice on the same subscription)
1. Put the subscription on Plus via change_plan effective_at=immediately, proration_billing_mode=do_not_bill) → meters[0].price_per_unit = "75" ✓
2. Schedule a downgrade: change_plan with product_id=<Core>, proration_billing_mode=full_immediately, effective_at=next_billing_date → scheduled_change created ✓
3. Let the boundary pass. The change applies: product_id → Core ✓, **but meters[0].price_per_unit stays "75"** ✗
4. Ingest usage events and let two further renewals invoice.
## Actual results
| Invoice | Cycle | Line item as printed | Total |
|---|---|---|---|
| inv_0NkmV4TAoiYfHnlVjqDq4 pay_0NkmV7RQIUQEzPgxUpW1N, 04:38:50Z) | 1st cycle spent fully on Core | "Petify Core (DEV)" + "**2222 pets x 0.75 USD**" | $1,833.15 |
| inv_0NkmwSWJL8XkFftGIgP1K pay_0NkmwVU7eJQFrh8T9Hs40, repro run) | 1st cycle on Core | "**1000 pets x 0.75 USD**" | $825.00 |
| inv_0Nkn8VSKgIX3dSdsnkpqM pay_0Nkn8VbKwOFBZFSBzjqVk) | **2nd consecutive cycle on Core — no plan change anywhere near it** | "**4000 pets x 0.75 USD**" | $3,300.00 |
subscriptions.retrieve throughout shows product_id = pdt_0Nb6cwDRX6Qft6tEpHruq (Core) with meters[0].price_per_unit = "75". The state never self-corrects — a "only the transition cycle bills at the old price" interpretation is ruled out by the third invoice. Note the internal contradiction on the invoices themselves: the line names **Petify Core** while billing a unit price that does not exist anywhere on the Core product.
## Expected
After the scheduled change takes effect, usage bills at the new product's meter price ($0.50). Your documentation supports this expectation:
- Localized Pricing: "the new plan's price is resolved with the same rules as checkout … This holds everywhere a plan change can originate: the Change Plan API, the Customer Portal, **and changes scheduled for the end of the billing period**."
- Changelog v1.93.0: customers "retain access to their current plan **until** the end of the billing period."
## Ruled out
- **Not a configuration issue.** No Product Collection is in use, Customer Portal self-service is unused ("Allow Subscription Updates" off); all calls are merchant-API. Under identical settings, the **immediate** path re-prices correctly.
- **Not proration-related.** Both products have a $0 fixed price; the discrepancy is the meter unit price itself.
## Impact
Silent mis-billing on usage-based products after any scheduled plan change — overcharge on downgrades, undercharge on upgrades — invisible to the customer until the invoice arrives.
## Workaround we validated
After the scheduled change applies, calling change_plan again with the **same** product and proration_billing_mode=do_not_bill, effective_at=immediately re-attaches the correct meter price (observed 75 → 50, billing anchor preserved).
## Questions
1. Is this intended behavior or a defect? If a defect, what is the fix timeline?
2. Will a fix retroactively repair subscriptions already in the inconsistent state product_id ≠ attached meter pricing)?
3. Until then, is the no-op immediate change_plan the recommended mitigation?
## Secondary report — product attribution disagrees across surfaces
While investigating, we noticed the **dashboard payment detail page** attributed a payment made on a Core cycle to "Petify Plus" (it appears to render the subscription's current product at view time), while the **invoice PDF** pins the payment-time product, and the **payment API object** carries no product at all product_cart: null on subscription renewals). Three surfaces disagree about which product a payment was for — worth aligning, since it makes billing incidents like the above much harder for merchants to diagnose.
0 Comments
Sign in to comment
No comments yet. Be the first to share your thoughts!
