A Change is an operation planned for an instant: publishing a catalog version, cancelling, pausing or resuming a subscription, transforming a subscription onto another plan or version, and more. GET /changes/operations lists the operations this deployment can schedule, with the when kinds each accepts (now, a wall-clock time, the end of the billing period, …) and the schema of its payload.
Author with POST /changes, preview first with POST /changes/preview, move one with PATCH /changes/{id}, cancel one with DELETE /changes/{id}, and read what it did with GET /changes/{id}/outcomes.
Publishing a plan, product, rate table, tax rule or price formula draft is always a Change: operation: catalog.publish_version.
List everything scheduled
One query against one table. This replaces a nine-branch SQL view over eight write models whose own comment admitted a single intent could appear in it twice. Cursor-paginated, sortable and filterable like every other list here. subject narrows it to what is scheduled against named things, in the same kind:uuid spelling each row's own subject field carries; it is repeatable because a subscription's answer includes the subscription_item refs its pins are filed against, and it stays a first-class parameter rather than a filter because it is a union over (kind, id) PAIRS. needs_attention=true finds rows carrying a reason regardless of status — a planned Change whose anchor moved onto a moment another Change already holds is not visible to any status filter.
query Parameters
conflict_classFilter on conflict_class (string). Operators: eq, in — dot grammar, e.g. conflict_class.in=value; a bare conflict_class=value means eq. A bare comma-separated value is in-sugar: conflict_class=a,b means conflict_class.in=a,b.
created_atFilter on created_at (date-time). Operators: eq, gt, gte, lt, lte — dot grammar, e.g. created_at.gt=value; a bare created_at=value means eq.
gateFilter on gate (enum). Operators: eq, in — dot grammar, e.g. gate.in=value; a bare gate=value means eq. A bare comma-separated value is in-sugar: gate=a,b means gate.in=a,b. Legal values: none, approval, after.
needs_attentionFilter on needs_attention (boolean). Derived boolean predicate: only needs_attention=true|false (eq).
operationFilter on operation (string). Operators: eq, in — dot grammar, e.g. operation.in=value; a bare operation=value means eq. A bare comma-separated value is in-sugar: operation=a,b means operation.in=a,b.
plan_idFilter on plan_id (uuid). Operators: eq, in — dot grammar, e.g. plan_id.in=value; a bare plan_id=value means eq. A bare comma-separated value is in-sugar: plan_id=a,b means plan_id.in=a,b.
resolved_atFilter on resolved_at (date-time). Operators: eq, gt, gte, lt, lte — dot grammar, e.g. resolved_at.gt=value; a bare resolved_at=value means eq.
scopeFilter on scope (enum). Operators: eq, in — dot grammar, e.g. scope.in=value; a bare scope=value means eq. A bare comma-separated value is in-sugar: scope=a,b means scope.in=a,b. Legal values: one, selector, cascade.
subject_idFilter on subject_id (uuid). Operators: eq, in — dot grammar, e.g. subject_id.in=value; a bare subject_id=value means eq. A bare comma-separated value is in-sugar: subject_id=a,b means subject_id.in=a,b.
subject_kindFilter on subject_kind (string). Operators: eq, in — dot grammar, e.g. subject_kind.in=value; a bare subject_kind=value means eq. A bare comma-separated value is in-sugar: subject_kind=a,b means subject_kind.in=a,b.
whenFilter on when (string). Operators: eq, in — dot grammar, e.g. when.in=value; a bare when=value means eq. A bare comma-separated value is in-sugar: when=a,b means when.in=a,b.
limitPage size. Values above the server-side cap are clamped (standard default 50, cap 200; a few document-heavy lists use larger windows). Invalid values fall back to the default.
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
searchCase-insensitive substring match over the DATA in the change's title — the customer, plan and product names its arguments carry, e.g. "Acme". Not over the rendered sentence: that is a different sentence in each of the eight locales, so searching it only ever searched the English one.
sortOrdering: resolved_at, created_at, status or operation, each with an optional :asc/:desc suffix (default resolved_at:desc). An unknown value is rejected. Cursors are bound to the ordering that issued them, so changing sort mid-walk needs a fresh first page.
statusFilter by status, comma-separated for several (status=planned,running). Filterable fields (status, operation, subject_kind, subject_id, when, gate, scope, bucket, conflict_class, plan_id, resolved_at, created_at, needs_attention) accept apifilter operator suffixes in the dot grammar, e.g. status.in=planned,running or resolved_at.gte=2026-01-01T00:00:00Z. Repeating a parameter does NOT union: use the comma.
bucketFilter by what has to be DONE about a change, which is not its status: overdue (its instant has passed and it has not concluded — a failed change is here too), upcoming (still ahead), done (applied, cancelled or superseded). Comma-separated for several. Countable via ?counts=bucket.
countsComma-separated countable fields (status, operation, gate, scope, bucket) to include per-value counts for. Counts are self-excluding: the tally for a field ignores the caller's own selection on that field, so the other chips keep their numbers while one is active.
subjectFilter by subject, as kind:uuid (e.g. subscription:3f2a…). Repeatable or comma-separated; results are the union. Naming only unparseable subjects returns an empty feed, never the whole one.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List everything scheduled › Responses
OK
Schedule a change
Authors a Change, PLANNED — scheduling is one act and there is no draft stage. The payload is validated against the schema its operation publishes on GET /v1/changes/operations. gate: "approval" holds it until a second person with change.write releases it from the approvals queue: the request appears there immediately, the engine holds the Change with its reason on the row until somebody decides, and the author cannot decide their own. A refusal retires the Change with the decider's words on it. 501 CHANGE_GATE_NOT_SUPPORTED if this deployment has nothing that can raise one.
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Schedule a change › Request Body
Decision Table
| Variant | Matching Criteria |
|---|---|
| type = object · operation="subscription.transform" · requires: subject_id, when, payload | |
| type = object · operation="subscription.apply_for_a_while" · requires: subject_id, when, payload | |
| type = object · operation="subscription.set_default" · requires: subject_id, when, payload | |
| type = object · operation="subscription.set_status" · requires: subject_id, when, payload | |
| type = object · operation="catalog.publish_version" · requires: subject_id, when, payload |
operationOperation id from GET /v1/changes/operations.
Operation payload; see payload_schema on GET /v1/changes/operations.
subject_idThe thing this Change acts on.
whenWhen kind the operation accepts: now, at, period_end, phase_change, term_end.
atThe WALL CLOCK to act at, required when when=at — NOT an instant. Its calendar fields are read and read and re-anchored in the zone field, so send it with a Z suffix (2027-01-01T00:00:00Z) and name the zone separately. An offset is refused rather than silently discarded.
audience_idThe saved audience a selector-scoped Change is narrowed by. Required when scope=selector and refused otherwise. LINKED, not copied: the population is re-resolved at fire time, because a Change is a single act at one instant and "who is on plan A" must be answered then.
audience_matchedHow many CUSTOMERS the preview reported the audience matching. Recorded and REPORTED, not enforced: a count moves because the world moves — customers sign up, subscriptions cancel — and re-resolving at fire time is the whole point. It is what lets a screen say "you confirmed 1,240; it now matches 1,830".
audience_revisionThe audience.revision the preview reported. Echo it back: any edit to the audience rules bumps it, and a Change scheduled against the old revision is SUPERSEDED at fire time rather than firing at a population nobody previewed. Omit only when scheduling without a preview.
client_tokenCaller's idempotency token.
gatenone (default) runs at its instant. approval holds it until a second person with change.write releases it from the approvals queue — the author cannot release their own.
The notice period for when=after_notice: a calendar duration {count, unit}, counted from the moment of authoring on the calendar in UTC — a month's notice given on 31 January runs out on 28 February.
scopeWho this Change acts on. one (default) is the single subject named by subject_id. selector narrows a saved customer audience to the subjects the operation can act on, and requires audience_id — the subject then names the ENTITY the audience is narrowed through ("everyone in this audience who is on plan A" is subject=plan A, scope=selector).
subject_kindWhich kind of thing subject_id names, when the operation acts on several: product, plan, promotion, rate_table, tax_rule, price_formula.
zoneIANA zone the wall instant is read in.
Schedule a change › Responses
Created
Which anchors one subject has no moment for
The second half of the time picker's contract. whens on the registry is the OPERATION's capability and is true for every subject: subscription.cancel accepts term_end everywhere. Whether a PARTICULAR subscription has a committed term is a different question, and this answers it — returning the anchored kinds this subject has no moment for, each mapped to the reason. It is what lets one tile be greyed out for one subscription with the reason on hover, instead of the operator picking it and learning from a 422. Answered by RESOLVING the anchor through the same resolver authoring uses, so the picker cannot come to disagree with what POST /v1/changes will accept.
query Parameters
subjectThe kind:uuid being scheduled against.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Which anchors one subject has no moment for › Responses
OK
List what this build can schedule
Projects the compiled operation registry: the id, the kind of subject it acts on, the when kinds it accepts, and whether it can be undone. The schedule sheet's form and its time picker are GENERATED from this, so an operation can never be offered a when it cannot honour and the screen can never offer an operation nobody implemented. There is deliberately no way to create an operation through the API.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List what this build can schedule › Responses
OK
Preview a change that has not been authored
Takes the body POST /v1/changes takes and answers what that change WOULD do — who moves, who does NOT and why, and the money delta by currency — without writing anything. The same shape GET /v1/changes/{id}/preview returns, from the same projection. It refuses like POST /v1/changes does: an unknown operation, a payload that fails its schema or an operation's own validation answers the same 4xx, so a confirmation screen finds out the request is impossible before the operator presses the button. It persists nothing, and it nevertheless requires change.write: the body is POST /v1/changes's body, so answering it is the authoring capability minus the commit.
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Preview a change that has not been authored › Request Body
Decision Table
| Variant | Matching Criteria |
|---|---|
| type = object · operation="subscription.transform" · requires: subject_id, when, payload | |
| type = object · operation="subscription.apply_for_a_while" · requires: subject_id, when, payload | |
| type = object · operation="subscription.set_default" · requires: subject_id, when, payload | |
| type = object · operation="subscription.set_status" · requires: subject_id, when, payload | |
| type = object · operation="catalog.publish_version" · requires: subject_id, when, payload |
operationOperation id from GET /v1/changes/operations.
Operation payload; see payload_schema on GET /v1/changes/operations.
subject_idThe thing this Change acts on.
whenWhen kind the operation accepts: now, at, period_end, phase_change, term_end.
atThe WALL CLOCK to act at, required when when=at — NOT an instant. Its calendar fields are read and read and re-anchored in the zone field, so send it with a Z suffix (2027-01-01T00:00:00Z) and name the zone separately. An offset is refused rather than silently discarded.
audience_idThe saved audience a selector-scoped Change is narrowed by. Required when scope=selector and refused otherwise. LINKED, not copied: the population is re-resolved at fire time, because a Change is a single act at one instant and "who is on plan A" must be answered then.
audience_matchedHow many CUSTOMERS the preview reported the audience matching. Recorded and REPORTED, not enforced: a count moves because the world moves — customers sign up, subscriptions cancel — and re-resolving at fire time is the whole point. It is what lets a screen say "you confirmed 1,240; it now matches 1,830".
audience_revisionThe audience.revision the preview reported. Echo it back: any edit to the audience rules bumps it, and a Change scheduled against the old revision is SUPERSEDED at fire time rather than firing at a population nobody previewed. Omit only when scheduling without a preview.
client_tokenCaller's idempotency token.
gatenone (default) runs at its instant. approval holds it until a second person with change.write releases it from the approvals queue — the author cannot release their own.
The notice period for when=after_notice: a calendar duration {count, unit}, counted from the moment of authoring on the calendar in UTC — a month's notice given on 31 January runs out on 28 February.
scopeWho this Change acts on. one (default) is the single subject named by subject_id. selector narrows a saved customer audience to the subjects the operation can act on, and requires audience_id — the subject then names the ENTITY the audience is narrowed through ("everyone in this audience who is on plan A" is subject=plan A, scope=selector).
subject_kindWhich kind of thing subject_id names, when the operation acts on several: product, plan, promotion, rate_table, tax_rule, price_formula.
zoneIANA zone the wall instant is read in.
Preview a change that has not been authored › Responses
OK
Get one scheduled change
path Parameters
idHeaders
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Get one scheduled change › Responses
OK
Cancel a scheduled change
Taking back an intent is deleting it. The surface this replaces spent POST /cancel, POST /unschedule and POST /rollback on three shades of the same idea. Returns 409 CHANGE_NOT_EDITABLE once it has run or been retired. THE SUBJECT'S DRAFT IS KEPT: cancelling a catalog.publish_version cancels the RELEASE, not the edit — the working draft stays staged and can be published later or discarded through the entity's own DELETE /{kind}/{id}/draft.
path Parameters
idHeaders
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Cancel a scheduled change › Responses
OK
dataReschedule a change
Moves a Change to a new when, re-resolving its instant. The when is ALL it moves: scheduling is one act, so a Change is planned from the moment it is created and there is no draft stage to arm out of. Moving one to when: "now" applies it in this request and answers applied. Returns 409 CHANGE_NOT_EDITABLE once it is running or retired.
path Parameters
idHeaders
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Reschedule a change › Request Body
whenNew when kind; must be one the operation accepts.
atThe WALL CLOCK to act at, required when when=at — NOT an instant. Its calendar fields are read and read and re-anchored in the zone field, so send it with a Z suffix (2027-01-01T00:00:00Z) and name the zone separately. An offset is refused rather than silently discarded.
The notice period for when=after_notice: a calendar duration {count, unit}, counted from now.
zoneIANA zone the wall instant is read in.
Reschedule a change › Responses
OK
What happened to each target
One row per subject the change resolved to, with what happened to it. Empty until the change fires — these are a RECORD of what happened, not a prediction of what will.
path Parameters
idHeaders
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
What happened to each target › Responses
OK
Preview what a change will do
Who moves, who does NOT and why, and the money delta by currency. One shape for a one-line price change and a 147-subscription migration.
path Parameters
idHeaders
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Preview what a change will do › Responses
OK