Scheduled changes
A scheduled change is a typed, future-dated mutation queued against a billing entity. Use them whenever a change shouldn't apply immediately - a plan switch at renewal, a price increase with mandatory notice, a tax rate rolling over at year-end, a coordinated release across multiple entities.
Two ways to apply a future-dated change:
- Versioned
PUTon a catalog entity -PUT /v1/{entity}/{id}witheffective_at=<future>- the right call when you're publishing a new version of a specific catalog entity at a future time. Available forproducts,plans,prices,promotions,tax-rules,costs. The versioning layer registers a scheduled change behind the scenes so the new version goes live ateffective_at. (The oldPOST /v1/{entity}/{id}/scheduleshims were removed in ADR-0006 - they now return 404.) - Generic endpoint -
POST /v1/scheduled-changes- for cases that don't fit a single entity (cross-entity dependencies, subscription version bumps, custom payloads, or admin tooling that wants the full struct surface).
Both paths produce the same ScheduledChange row and ride the same lifecycle.
Entity types
entity_type decides what the engine does at release time. Seven values:
PRICE · PRODUCT · PLAN · PROMOTION · TAX_RULE · COST · SUBSCRIPTION
For the catalog entities (PRICE / PRODUCT / PLAN / PROMOTION / TAX_RULE / COST), schedules typically publish a new version at the scheduled time - the entity's version timeline gets a new active row. For SUBSCRIPTION, the only scheduled change type is version_bump: a deferred flip of the subscription's plan/product version at the scheduled time. Item adds/removals and quantity changes are not scheduled here - the subscription engine schedules those natively via the effective_at field on the add-item / remove-item / change-quantity requests.
Lifecycle
Code
| Status | Meaning |
|---|---|
pending | Created, scheduled time is far enough out that no notification has fired |
upcoming | Notification window entered - the customer / operator has been told the change is coming |
releasing | Effective time reached, the engine is in the middle of applying it |
released | Successfully applied. Catalog entities now have a new active version; subscription state is changed |
rolled_back | A released change was reverted (terminal) |
cancelled | Removed before release (terminal) |
Webhooks: scheduled_change.created, scheduled_change.upcoming, scheduled_change.reconfirmed, scheduled_change.released, scheduled_change.rolled_back, scheduled_change.cancelled (see Webhook events).
Anatomy
| Field | Notes |
|---|---|
entity_type + entity_id | What's being changed |
change_type | String discriminator that narrows what this scheduled change does within the entity. For SUBSCRIPTION the only value is version_bump. Versioned PUTs on catalog entities set it for you |
change_payload | Typed JSON body the release-time handler consumes, passed as json.RawMessage on the generic POST. For a subscription version_bump the payload is exactly { "target_version": <int> } |
scheduled_at | When the change should release (ISO 8601, UTC) |
release_strategy | AUTOMATIC (default) - engine releases at scheduled_at. MANUAL - sits in upcoming until an operator calls POST /release. These are the only two strategies (CANARY was removed in the scheduler unification) |
notification_config | Optional config blob - recipient list + lead-time per channel (email, webhook). Org default applies when null |
rollback_window | Optional duration string (e.g. "72h") - how long after release the change can be rolled back |
depends_on_change_id | Another scheduled change that must release before this one. Used for ordered multi-entity rollouts |
entity_version_at_schedule | The entity's version when the change was scheduled. The engine compares it against the live version at release time - if they don't match, it logs a warning ("scheduled change targets a stale entity version") and still proceeds with the release; the mismatch does not block release or change the status |
previous_state | Read-only snapshot of the pre-change state, captured at release time. Powers rollback |
Scheduling a catalog entity (edit-as-publish)
For catalog entities, schedule a new version by issuing the entity's own PUT with a future effective_at (the ADR-0006 edit-as-publish flow). The versioning layer registers a scheduled change behind the scenes - there is no separate POST .../schedule endpoint.
Code
Omit effective_at (or send save_as_draft: true) to edit without publishing. Pass expected_version for optimistic-concurrency safety.
This works for products, plans, prices, promotions, tax-rules, and costs. Each call returns the entity with its pending version row.
To act on a specific pending version directly (rather than going through the generic scheduled-change ID), use the per-version sub-routes:
Code
Registered for {plans, prices, promotions, products, tax-rules, costs} (tax-rules has no separate unschedule route). These act on the version timeline of a specific entity, which is friendlier for catalog tooling.
Generic create
For one-off changes (subscriptions, custom payloads, cross-entity dependencies):
Code
Reconfirmation
The engine never moves a change to a separate "stale" state. If the underlying entity is modified after a change was scheduled, the version it captured in entity_version_at_schedule no longer matches the live version - but the release pipeline only logs a warning ("scheduled change targets a stale entity version") and proceeds; the mismatch neither blocks the release nor flips the status.
Reconfirmation is an operator action that pulls a change in the notification window back into the queue:
POST /v1/scheduled-changes/{id}/reconfirm- moves a change fromupcomingback topending(and firesscheduled_change.reconfirmed). Useful when you want to re-stage a change that already entered the notification window.- Cancel the change and re-schedule against the new state if you no longer want it to release.
Reconfirm exists per-entity-version too (POST /{entity}/{id}/versions/{vid}/reconfirm) for the catalog-tool ergonomics. Either form does the same thing.
Release & rollback
AUTOMATIC schedules release themselves at scheduled_at. For MANUAL (or to release early):
Code
Once released, the rollback window starts:
Code
Rollback restores the entity to previous_state going forward - it does not retroactively un-bill. If the released change had already produced invoice lines, a rollback typically issues a credit note for the period billed under the released change. Rollbacks fire scheduled_change.rolled_back.
Cancellation
Code
Valid only before release (pending or upcoming). Terminal released / rolled_back / cancelled reject the call. After release, use rollback instead.
Dependencies
depends_on_change_id makes one scheduled change wait on another's released status before it can release itself. Used for ordered rollouts - e.g. publish the new tax rule first, then transition all subscriptions onto a plan that depends on it. The dependent change shows pending until its dependency releases.
Endpoints
All Scheduled Changes endpoints - generic CRUD + release / rollback / reconfirm / reschedule / diff / bulk-cancel. Catalog scheduling is documented under each entity's own tag (Plans, Prices, Promotions, Products, Tax Rules, Costs) via its versioned PUT and per-version sub-routes.
Related
- Plans -
PUT /plans/{id}witheffective_atand themigrate-subscribersbulk path - Promotions -
PUT /promotions/{id}witheffective_atfor promotion edits - Taxes -
PUT /tax-rules/{id}witheffective_atfor rate rollovers - Subscriptions - schedule a
version_bumpfor a future date - Webhooks → events - full event catalog