business | yes | object | - | The business selling the subscription. Carries object (what it is) and id (which one); id is the public identifier a merchant addresses it by. | |
buyer | yes | object | - | - | |
currency | yes | string | - | ISO 4217 3-letter uppercase currency code. One currency for both sides: a plan is only a candidate for a switch when it matches the subscription’s currency, so a plan change never crosses currencies and there is no from/to pair to carry. | |
effective_at | yes | `string | null` | - | When the new plan is expected to bill, derived at write time from the subscription’s next payment date. A PREDICTION, not a confirmation: nothing is scheduled, the renewal simply reads whichever snapshot is current when it runs, so a cycle that moves for an unrelated reason leaves this value stale. The confirmation is stamped internally after Stripe bills the new amount and is deliberately NOT carried here — at emit time it is always null. A consumer that needs the confirmed date reads the change back from the subscriptions API, which renders effective_confirmed_at; this event names no internal attempt-row id for it to resolve, and adding a published reference later is additive. |
event_type | no | const | subscription.plan_changed | Optional on the wire, and normally absent: the event’s type is carried by the CloudEvents envelope as type, so the payload does not repeat it. It is declared here so that if you copy the envelope’s type onto the payload before validating the payload against this schema, that validation still passes. Read the envelope rather than depending on this field — a payload without it is valid. | |
initiated_by | yes | string | buyer, business | Who asked for the switch, and the WHOLE of what this event discloses about the actor. buyer is the buyer changing their own plan; business is the seller changing it on the buyer’s subscription through the API, in which case the buyer is notified rather than asked. Read this before wording anything sent to the buyer: telling someone their seller changed the plan when they changed it themselves, or the reverse, is what the field exists to prevent. No user identifier accompanies it, deliberately: the internal user id subscription.amount_changed publishes is a sequential platform key, and the buyer’s id would be worse — it is the same value at every merchant that person buys from, so publishing it would hand two merchants a key for correlating a shared customer. Which side acted is what a merchant can act on; which person acted is recorded in COPE’s audit trail and answered by the subscriptions API. It deliberately does not reuse the creator value that subscription.amount_changed pins — that value belongs to the older contract and is unchanged by this one. | |
interval | no | `string | null` | day, week, month, year | Billing cadence after the switch. Carried once rather than as a from/to pair because a plan is only a candidate for a switch when its interval and interval_count already match the subscription’s, so the cadence does not move. |
interval_count | no | `integer | null` | - | - |
manage_subscription_url | yes | string | - | - | |
new_plan_id | yes | string | - | Opaque COPE payment-plan token (‘plan_’ + 8 alphanumerics, payment_plans.public_id) of the plan the subscription now runs on. Deliberately not pattern-constrained here: the shape is enforced by a database CHECK on the producing side, and pinning it in a published contract would make a future id-shape change a breaking event change. | |
new_plan_name | yes | `string | null` | - | - |
new_recurring_amount_cents | yes | integer | - | The recurring charge from the next cycle onward, quantity-inclusive. The schema asserts only positivity; the producer enforces the per-currency minimum-charge floor, so the contract does not couple to that constant. | |
old_plan_id | yes | `string | null` | - | Opaque COPE payment-plan token (‘plan_’ + 8 alphanumerics) of the plan the subscription was on. NULL in practice today, and that is a recorded gap rather than data loss: a subscription’s terms live in an immutable payment_plan_snapshots row, and that table carries no payment_plan foreign key, so the plan the old snapshot was built from is not recoverable. old_plan_name below is what identifies the old terms; a later slice that adds the FK populates this field with no version bump. |
old_plan_name | yes | `string | null` | - | Display name frozen on the superseded snapshot — the human-readable identity of the old terms, and today the only identifier of them. Nullable because payment_plan_snapshots.display_name is. |
old_recurring_amount_cents | yes | integer | - | The recurring charge before the switch, quantity-inclusive — the amount Stripe was billing, not the per-unit snapshot figure. | |
order | no | object | - | The order the subscription was bought on. Carries object (what it is) and id (which one); id is the public identifier a merchant addresses it by. | |
product | yes | object | - | The product the subscription is for. Carries object (what it is) and id (which one); id is the public identifier a merchant addresses it by. | |
reason | no | `string | null` | - | - |
subscription | no | object | - | The subscription this event concerns. Carries object (what it is) and id (which one); id is the public identifier a merchant addresses it by. | |
subscription_id | yes | `string | null` | - | The subscription’s public reference (‘sub_’ + 16 alphanumerics, line_items.id), and THE handle for this change: it is the only field naming the subscription, because the line item’s internal primary key is deliberately not published. Nullable because the reference is minted on demand rather than by a database default, so a subscription nobody has addressed publicly does not have one yet; a change made through the public API always does, because that is how the request found the subscription, and that is the only path emitting this event today. A consumer that meets a null here has order_uuid as its correlation anchor. Deliberately replaces the line_item_uuid field subscription.amount_changed declares — that field reads a column line_items does not have and never had, so it has been null on every event ever emitted. |