Contract history
This guide describes the local ContractChange API profile verified against the vendored Transform OpenAPI and authenticated read-only vendor shape probes. It contains no customer data, contract IDs, or copied business values.
The storage, query, snapshot, projection, mutation-writer, and lifecycle-activation layers have bounded independent approval. The approval covers the local invariants and limits described here; it does not establish complete Transform parity or reconstruct legacy history.
Endpoints and parameters
GET /api/v1/contractchanges accepts:
-
contractId: optional raw 24-character contract ID; -
dateFrom: inclusiveYYYY-MM-DDor RFC 3339 timestamp with an explicit offset; -
dateTo: inclusive RFC 3339 instant, or a complete UTC calendar day whenYYYY-MM-DDis used; -
from: optional raw 24-character ContractChange ID; -
take: 1–500, default 200; -
includeContract: boolean, defaultfalse; -
includePauses:None,Changed, orAll, defaultNone.
GET /api/v1/contractchanges/{contractChangeId} accepts:
-
includeContract: boolean, vendor-confirmed defaulttrue; -
includeComponentSubscriptions:None,Changed, orAll; -
includeDiscountSubscriptions:None,Changed, orAll; -
includePauses:None,Changed, orAll.
Invalid dates return 422 invalid_date_filter. Invalid take returns 422 invalid_pagination. Invalid expansion values return 422 InvalidContractChangeExpansion.
Raw-ID pagination
Results are ordered by Timestamp DESC, Id DESC. To request the next page, pass the last returned change ID as from:
GET /api/v1/contractchanges?contractId=<contract-id>&take=200 GET /api/v1/contractchanges?contractId=<contract-id>&take=200&from=<last-change-id>
The anchor is inclusive, matching the observed vendor behavior. The second response repeats <last-change-id> as its first eligible row; clients should deduplicate by Id when concatenating pages. The response does not provide a Link or next-cursor header.
Pagination is read committed rather than a frozen snapshot. A row committed between page requests can change the visible boundary. A lifecycle activation recorded late uses its effective timestamp and can sort before a cursor already consumed by the caller. Start a fresh traversal to discover such late records; an old cursor is not a live-tail guarantee.
Malformed from values return 400 InvalidCursor. Unknown or foreign-merchant anchors return 404 without revealing another tenant. An anchor outside the selected contract/date filters returns 400 InvalidCursor.
Default and expanded responses
Default list rows contain only available core fields: Id, Type, Timestamp, ContractId, and optional Trigger, OrderId, ChangeDate, NewPlanId, or NewPlanVariantId.
Default detail behaves as includeContract=true. It includes immutable Contract.Before/After state and PricePhases. A Signup has no Contract.Before; its after-state contains the initial phase. The authenticated Annulation shape moves the current phase from Normal to Inactive while preserving the original phase start and inheritance flag.
PricePhases follows includeContract on both endpoints:
-
includeContract=trueincludesContractandPricePhases; -
includeContract=falseomits both, even when component, discount, or pause modes areAll.
Expansion modes operate solely on the immutable snapshot captured with the change:
-
Noneomits that family; -
Changedincludes creates, removals, and rows whose official before/after fields differ; -
Allincludes the complete before/after inventory, including unchanged rows.
Projection never reconstructs historical state from current contracts or subscriptions. Internal reservation status, scheduling identities, raw imported evidence, payment data, and snapshot metadata are excluded from the wire DTO.
New contracts and legacy history
Complete typed history uses HistoryCoverage=CompleteFromSignup and ChangeHistoryVersion=1. It maintains durable phase and pause resources with authoritative current-phase/current-pause pointers. Component reservations with internal Reserved status are excluded until activated with an official status.
Contracts created before complete typed history have, at most, a StateKnownFrom operational baseline. That baseline does not establish prior phases, pauses, or before-state. Generic historical {Event, EffectiveAt, Fields} rows remain immutable and are indexed only to support bounded selection; they are never upgraded into fabricated ContractChange DTOs.
Selecting incomplete legacy history returns 422 contract_change_history_unsupported. Narrower contract/date/raw-ID filters are applied before this check, so unrelated legacy history does not contaminate an otherwise typed page.
Snapshot and response limits
Each immutable change envelope is limited to:
-
1,000 phases;
-
1,000 pauses;
-
5,000 component subscriptions;
-
5,000 discount subscriptions;
-
1,000 price phases;
-
4 MiB canonical envelope size.
Exceeding a capture limit returns 422 ContractChangeSnapshotLimitExceeded and rolls the domain mutation and change record back together.
Expanded list responses preflight persisted envelope sizes before loading full snapshots and are limited to 8 MiB. Exceeding that budget returns 422 ContractChangeResponseLimitExceeded; reduce take or narrow the filters. Default core-only lists use the native core projection and do not load full envelopes.
Scheduled lifecycle activation
Scheduled phase and pause changes are separate durable records. Due transitions are drained before a later DTO-visible foreground mutation captures its before-state. This prevents a late lifecycle worker from constructing a past snapshot after a newer component/subscription mutation. The activation recorder bypasses recursive draining.
Foreground draining is limited to 100 due schedules per contract. A larger backlog returns 422 ContractLifecycleBacklog without committing the activation or later mutation.
The independent lifecycle job runs separately from recurring billing. Its defaults and accepted bounds are:
| Scope | Default | Accepted |
|---|---|---|
| Merchants per run | 25 | 1–100 |
| Contracts per merchant | 100 | 1–500 |
| Schedules per merchant page | 100 | 1–500 |
Invalid job bounds return InvalidLifecycleLimit. Activation uses merchant locking, durable fair cursors, and a unique schedule activation identity. Relevant pre-state checks compare phase/pause pointers and schedule identity; unrelated balances, invoice cadence, payment state, and contract lock-version changes do not invalidate a due transition.
Failed schedules persist a failed claim and bounded audit result while later eligible schedules continue. Writer, activation, snapshot, native entry, schedule status, and outbound event changes share the merchant transaction; failures roll the unit back. These local invariants are covered by the bounded writer/activation approval.
Sources and status
These documents establish the approved local compatibility boundary. They do not claim universal vendor parity, live-tail pagination, reconstructed legacy history, or external-provider behavior.