Billing guide

Contract history

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:

GET /api/v1/contractchanges/{contractChangeId} accepts:

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:

Expansion modes operate solely on the immutable snapshot captured with the change:

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:

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.