> ## Documentation Index
> Fetch the complete documentation index at: https://docs.go.aiinsurance.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Concepts

> How transactions, deltas, segments, and versions work in the Policy API

This page explains the core data model behind the transaction-based Policy API. Understanding these concepts will help you predict how the API behaves when you create, endorse, cancel, and reinstate policies.

## Transaction Model

Every change to a policy is recorded as an immutable **transaction**. Transactions are the single source of truth — policy state is always derived from them, never authored directly.

<Steps>
  <Step title="Configure">
    Define your fields, option sets, and exposure types via the [Configuration API](/api-reference/configuration/overview).
  </Step>

  <Step title="Create">
    `POST /transaction/new-business` creates a policy with initial state covering the full term.
  </Step>

  <Step title="Endorse">
    `POST /{policyId}/transaction/endorse` modifies the policy — add exposures, change field values, adjust coverage.
  </Step>

  <Step title="Cancel">
    `POST /{policyId}/transaction/cancel` cancels the policy from a specified date.
  </Step>

  <Step title="Reinstate">
    `POST /{policyId}/transaction/reinstate` reinstates a cancelled policy.
  </Step>

  <Step title="Renew">
    `POST /transaction/renew` starts a new policy term linked to the previous one.
  </Step>
</Steps>

### Transaction Types

<CardGroup cols={2}>
  <Card title="NEW_BUSINESS">
    Creates the policy. The effective date is the policy start date. Produces one segment covering the full term.
  </Card>

  <Card title="ENDORSE">
    Modifies policy state from a given effective date. Carries one or more of five channels (see [Endorsement Channels](#endorsement-channels)). May split existing segments or merge them if the change aligns state across periods.
  </Card>

  <Card title="CANCEL">
    Flips segment-scoped `policyStatus` to `"cancelled"` from the cancellation date through end of term, and records a single `cancellationEffectiveOnDate` (uniform across the term). `policyStatus` alone marks which side of the boundary a segment is on. Optionally accepts whole-object `fullTermPricingInfo` / `fullTermPolicyRatingResult` (e.g., short-rate penalties).
  </Card>

  <Card title="REINSTATE">
    Flips segment-scoped `policyStatus` back to `"active"` from the reinstatement date and **clears** `cancellationEffectiveOnDate` (no reinstatement date field is added). May not leave a coverage gap. Optionally accepts whole-object `fullTermPricingInfo` / `fullTermPolicyRatingResult` (e.g., reinstatement fees).
  </Card>

  <Card title="RENEW">
    Creates a new policy term linked to the previous via the root `previousPolicy` field (a required `uuid`). Accepts a full `data` payload — the caller provides the complete initial state for the new term. The new term's `policyStartDate` must be **on or after** the previous policy's `policyEndDate` (no backward overlap with the term being renewed).
  </Card>
</CardGroup>

### Effective Date vs Transaction Timestamp

Each transaction carries two dates on independent axes: an **`effectiveDate`** (where on the policy term the change lands — it must fall within `[policyStartDate, policyEndDate]`) and a **`transactionTimestamp`** (the audit / booking axis — when the decision was recorded). `effectiveDate` may backdate or post-date freely within the term; `transactionTimestamp` only moves forward. The full temporal model — the one rule binding a delta's `startDate` to the `effectiveDate`, why there is no third "take-effect" axis, worked backdate / book-ahead examples, and precedence + monotonicity — lives on its own page: **[Effective Dates & the Policy Timeline](/api-reference/policies/effective-dates)**.

## Endorsement Channels

An endorsement carries one or more of **four channels**. Full-term-ness is membership in a reserved-name container — `fullTermPricingInfo`, `fullTermPolicyRatingResult`, and `crossSegmentRatingOutputs`.

**Input channel:**

* **`deltas`** — changes to policy field data. Each delta carries its own `startDate` and `endDate` within the policy term. The path must **not** address a reserved full-term container. Every delta's `startDate` must equal the transaction's `effectiveDate` — the change starts applying exactly when the endorsement takes effect — so a single transaction cannot mix deltas with different `startDate`s (split that into separate transactions). This binding, and why there is no separate "take-effect" axis, is covered on [Effective Dates & the Policy Timeline](/api-reference/policies/effective-dates#the-effective-date-rule). The [whole-term root fields](#whole-term-root-fields) are the one exemption from that binding: they always state the whole term. **This is also the channel that amends a policy term** — see [Amending the Term](#amending-the-term).

There is no second input channel.

**Derived channels — additive on `deltas`:**

* **`fullTermPricingInfo`** — a whole object that overwrites the policy-root pricing contract (`pricingComponents`; the five rollups are computed by the platform from the components).
* **`fullTermPolicyRatingResult`** — a whole object that overwrites the policy-root canonical rating result (the twin of billing).
* **`crossSegmentRatingOutputs`** — element-level rating output, `[{ path, value }]`. Each path terminates at a `crossSegmentRatingOutputs` container on a list element (or the policy); the server derives the write range from the host's presence across segments, so it works on part-term hosts. Not offered on cancel/reinstate.

At least one channel is required.

### Delta Structure

**Per-segment delta (`deltas`):**

| Field       | Type   | Description                                                                                                                                               |
| ----------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `path`      | string | Predicate path to a field outside any full-term container                                                                                                 |
| `action`    | string | `Overwrite`, `Add`, or `Remove`                                                                                                                           |
| `value`     | any    | The new value, value to add, or value to remove                                                                                                           |
| `startDate` | string | Start of the date range this change applies to (must equal the transaction `effectiveDate`, except on a [whole-term root field](#whole-term-root-fields)) |
| `endDate`   | string | End of the date range this change applies to (must be `>=` `startDate` and within the policy term — see [Delta Date Ranges](#delta-date-ranges))          |

### Amending the Term

Shortening a policy term is a `deltas` write to the ROOT `policy.policyEndDate` (or `policy.policyStartDate`), stating the **whole term** as its window:

```json theme={null}
{
  "effectiveDate": "2025-01-01",
  "deltas": [
    {
      "startDate": "2025-01-01",
      "endDate": "2025-12-31",
      "path": "policy.policyEndDate",
      "action": "Overwrite",
      "value": { "year": 2025, "month": 9, "day": 30, "timezone": "America/New_York" }
    }
  ]
}
```

**A term bound may not move outward.** Moving `policyEndDate` later or `policyStartDate` earlier claims days that no endorsement can create, so it is rejected (`400`, problem code `TermLengtheningNotSupported`) — at whatever path depth it is written. Shortening is allowed. A genuine term extension is not yet supported; issue the longer term as a new policy.

### Whole-Term Root Fields

Four policy-root fields hold values that are invariant across the term by definition:

| Path                     | Holds                                           |
| ------------------------ | ----------------------------------------------- |
| `policy.policyNumber`    | The policy number                               |
| `policy.policyStartDate` | Term start                                      |
| `policy.policyEndDate`   | Term end                                        |
| `policy.previousPolicy`  | The renewal pointer to the term this one renews |

Because a part-term value would be meaningless for them, any delta that *touches* one of these paths — exactly, at any depth (`policy.policyEndDate.year`), or via an ancestor such as a whole-`policy` Overwrite — is held to two rules:

* **The window must be the whole term.** `startDate` must equal the policy start date and `endDate` the policy end date, or it is rejected (`400`, `InvalidDelta`): `Delta path "policy.policyEndDate" is invariant across the policy term, so its window must be the whole term […] — got […]`. Because such a delta always states the whole term, these paths are exempt from the "delta `startDate` must equal `effectiveDate`" rule.
* **A term bound may not move outward** (`400`, problem code `TermLengtheningNotSupported`) — see [Amending the Term](#amending-the-term).

<Note>
  These four root fields are the **single source** for the policy number, the term bounds and the renewal pointer — on the write side and on the read side alike. A whole-term delta on a root field is what moves the term. There is no second copy anywhere on a policy.
</Note>

### Delta Actions

#### Overwrite

Replace a scalar value or an entire object. This is the most common action.

```json theme={null}
{
  "path": "policy.additionalExposures[id = 'exp-1'].bedCount",
  "action": "Overwrite",
  "value": 110
}
```

#### Add

Append to a collection. Uses **set semantics** — objects are matched by `id`, primitives by equality. If the value already exists, the delta is a no-op.

```json theme={null}
{
  "path": "policy.additionalExposures[id = 'exp-1'].coveredSpecialties",
  "action": "Add",
  "value": "Neurology"
}
```

#### Remove

Remove from a collection. Same matching rules as Add. If the value is not present, the delta is a no-op.

```json theme={null}
{
  "path": "policy.additionalExposures[id = 'exp-1'].namedPhysicians",
  "action": "Remove",
  "value": "Dr. Nguyen"
}
```

### Path Notation

Paths target fields at any depth. Index into a list by a **predicate on any field** — `key[field = 'value']` — which must resolve to **exactly one** element (the API throws on zero or multiple matches). This uniqueness rule is the cross-segment identity guarantee, and it removes the need for a dedicated `id` field on embedded custom objects.

| Path                                                                | Targets                                                                                      |
| ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `policy.deductible`                                                 | A scalar field on the policy                                                                 |
| `policy.additionalExposures[id = 'exp-1'].bedCount`                 | A field on a specific exposure (matched by `id`)                                             |
| `policy.coverages[coverageType = 'GL'].limits[name = 'occurrence']` | A nested element matched by any field                                                        |
| `policy.additionalExposures`                                        | The exposures collection itself (for Add/Remove of entire exposure objects)                  |
| `policy.policyStatus`                                               | The segment-scoped policy status (set by Cancel/Reinstate)                                   |
| `policy.policyEndDate`                                              | A [whole-term root field](#whole-term-root-fields) (must state the whole term as its window) |

<Note>
  **Overwrite on an indexed path** (e.g., `policy.additionalExposures[id = 'exp-1']`) replaces the entire exposure object. **Add on a collection path** (e.g., `policy.additionalExposures`) appends an exposure. These are different operations targeting different levels.
</Note>

Reserved full-term containers (`fullTermPricingInfo`, `fullTermPolicyRatingResult`, `crossSegmentRatingOutputs`) must not appear in a `deltas` path, at any depth — each has its own channel. A container path in `deltas` is rejected (`400`, `InvalidDelta`): `Invalid deltas path "…" — full-term container paths cannot be written by a caller`.

### Example: Endorsement with Deltas

An endorsement to `POST /v1/policies/{policyId}/transaction/endorse` effective April 1 that adds a new exposure (per-segment `deltas`) and updates pricing (the `fullTermPricingInfo` channel):

```json theme={null}
{
  "effectiveDate": "2025-04-01",
  "deltas": [
    {
      "startDate": "2025-04-01",
      "endDate": "2025-12-31",
      "path": "policy.additionalExposures",
      "action": "Add",
      "value": {
        "id": "exp-2",
        "exposureType": "OutpatientClinic",
        "facilityName": "Greenfield West Clinic",
        "bedCount": 0,
        "coveredSpecialties": ["Dermatology", "Family Medicine"],
        "namedPhysicians": ["Dr. Kim"]
      }
    }
  ],
  "fullTermPricingInfo": {
    "pricingComponents": [
      { "label": "Policy Premium", "group": "Policy Invoice", "kind": "Premium", "value": 98000 },
      { "label": "Taxes", "group": "Policy Invoice", "kind": "Taxes", "value": 4900 },
      { "label": "Policy Fee", "group": "Policy Invoice", "kind": "Fees", "value": 500 }
    ]
  }
}
```

The exposure delta applies from April 1 through the end of the term and will split the existing segment at that boundary. `fullTermPricingInfo` is a whole-object channel applied uniformly across the **full policy term** — no explicit dates, because it must be identical in every segment. (It is additive on `deltas` here, but is never mixed into the `deltas` array itself.)

## Segments

A **segment** is a maximal contiguous date range where the policy state is identical.

<Warning>
  **Segments are NOT one-to-one with transactions.** A single endorsement may split one segment into many. Backdated corrections can merge segments back together. Six transactions can produce two segments — or one.
</Warning>

### Segment Properties

Every version's segments satisfy three invariants:

* **No overlaps** — segments never share a date
* **Full coverage** — segments span the entire policy term with no gaps
* **No adjacent duplicates** — adjacent segments with identical state are automatically merged

### How Segments Change

<Tabs>
  <Tab title="Create">
    A new policy starts with **one segment** covering the full term.

    | # | Date Range     | Exposures                |
    | - | -------------- | ------------------------ |
    | 1 | Jan 1 – Dec 31 | Main Hospital (120 beds) |
  </Tab>

  <Tab title="Endorse (split)">
    An endorsement effective April 1 adds a satellite clinic, splitting the segment:

    | # | Date Range     | Exposures                        |
    | - | -------------- | -------------------------------- |
    | 1 | Jan 1 – Mar 31 | Main Hospital                    |
    | 2 | Apr 1 – Dec 31 | Main Hospital + Satellite Clinic |

    Two segments — different exposure counts on each side of the boundary.
  </Tab>

  <Tab title="Correction (merge)">
    A backdated correction adds the clinic to Jan–Mar too, making it match Apr–Dec:

    | # | Date Range     | Exposures                        |
    | - | -------------- | -------------------------------- |
    | 1 | Jan 1 – Dec 31 | Main Hospital + Satellite Clinic |

    Three transactions, but only **one segment**. The correction converged per-segment state across the full term.
  </Tab>
</Tabs>

**Segments reflect the final state of the policy, not its change history.** The full audit trail is preserved in the transaction history.

## Versions

Each transaction produces a new policy **version**. A version is a complete snapshot — it contains the full set of segments representing the policy at that point in the transaction history.

| Property                | Description                                |
| ----------------------- | ------------------------------------------ |
| `policyVersion`         | Sequential integer (1, 2, 3, ...)          |
| `transactionId`         | The transaction that produced this version |
| `segments`              | Complete set of segments for this version  |
| `startDate` / `endDate` | Policy term boundaries                     |

You can query any historical version to see what the policy looked like after a specific transaction.

## How It Works

When you submit a transaction, the system:

1. **Loads the previous version** — gets the current segments
2. **Applies deltas** — applies each delta to all segments whose date range overlaps the delta's date range
3. **Normalizes** — produces deterministic JSON and computes a hash for each resulting segment
4. **Merges** — collapses adjacent segments with identical hashes into one
5. **Checks term invariance** — refuses the write if it leaves any term-invariant policy field differing between the segments of one term
6. **Persists** — stores the new version with its segments

You don't need to understand the internal algorithm to use the API — just know that the system automatically handles segment splitting and merging based on the deltas you submit.

<Note>
  **No-op deltas are safe.** Adding a value that already exists or removing one that's already gone has no effect. This means a delta that spans a wide date range may change some segments and leave others untouched — the system handles it correctly.
</Note>

## Transaction Validation Rules

Every write transaction is validated before any version is persisted. A violation returns `400` with a descriptive message and **no** new version is created. The rules below are in addition to field-level configuration validation.

### Delta Date Ranges

Each per-segment delta carries its own `startDate` / `endDate`. Two constraints apply (`400`, `InvalidDelta`):

* **`startDate <= endDate`** — a delta whose range is inverted is rejected: `Delta startDate (…) must be <= endDate (…)`.
* **`[startDate, endDate]` ⊆ `[policyStartDate, policyEndDate]`** — a delta range that starts before the policy term or ends after it is rejected: `Delta date range [start, end] falls outside policy period [start, end]`. A delta cannot apply state to dates the policy does not cover.

(A delta on a [whole-term root field](#whole-term-root-fields) satisfies both checks by construction — its window *is* the term — but it has the stricter rule of its own: the window must be exactly the whole term, and it may not lengthen it.)

### Within-Transaction Path Conflicts

Because newest-wins precedence only orders deltas *across* transactions, two deltas in **one** transaction that touch the same place have no ordering between them and are rejected up front (`400`, `InvalidDelta`). This is checked independently within each partition (per-segment deltas and whole-term deltas):

* **Duplicate path** — two deltas sharing the exact same `path` in one transaction: `Two deltas in this transaction share the path "…" — within-transaction conflicts cannot be resolved by insertion order`. Collapse them into the single intended write.
* **Path-prefix conflict** — one delta targets an object and another targets a descendant of it in the same transaction (e.g. `policy.coverages` together with `policy.coverages[0].limit`, or `policy.additionalExposures[id = 'exp-1']` together with `policy.additionalExposures[id = 'exp-1'].bedCount`): `Delta paths "…" and "…" overlap — a delta cannot target both an object and one of its descendants in the same transaction`. The parent would replace the whole subtree while the child mutates one node inside it — ambiguous, so it is rejected. Sibling predicates on the same collection (e.g. two different `additionalExposures[…]` elements) are **not** a conflict.

<Note>
  This is the within-transaction counterpart to the [collection-vs-element distinction](#path-notation): operating on a collection and on one of its elements are different operations at different levels, and they may not be combined in a single transaction.
</Note>

### Transaction Timestamp Monotonicity

When you set `transactionTimestamp` explicitly, it must be **`>=` the largest `transactionTimestamp` already recorded on that policy** (the audit axis only moves forward); effective dates may still backdate freely. An out-of-order timestamp is rejected (`400`, `InvalidRequest`): `transactionTimestamp (…) is earlier than the latest existing transaction on this policy (…)`. This rule, the cross-transaction precedence model, and the two-axis model it constrains are explained in full on the [Effective Dates & the Policy Timeline](/api-reference/policies/effective-dates#precedence-and-monotonicity) page.

## Premiums and Rating

**The API does not calculate premiums.** When you submit a transaction, you supply field values and billing totals yourself. The system stores what you send — it does not rate, pro-rate, or re-aggregate.

Policy financial data lives at two levels:

### Full-Term Billing and Rating

`fullTermPricingInfo` is a **cross-segment invariant** — identical across every segment in a version. It is the pricing contract for the entire policy term: `pricingComponents` (each `{label, group, kind, value[, earningBasis]}`) plus five **server-computed, read-only rollups** — `premium`, `taxes`, `fees`, `brokerCommission`, `programCommission`, each the sum of its kind's components (caller-supplied rollup values are ignored and recomputed). Every endorsement that changes the price should include a `fullTermPricingInfo` channel to keep pricing current.

`fullTermPolicyRatingResult` is its twin — an optional whole-object, policy-level canonical rating result, also invariant across segments. Both are **derived** (a rating byproduct) but **caller-supplied** — the transaction API never rates. Beyond the billing totals, `fullTermPolicyRatingResult` may carry rating factors and more granular pricing detail worth exposing to underwriters.

### Per-Segment and Element-Level Rating

Each segment can carry its own rating data. Element-level rating output attaches to its host via a `crossSegmentRatingOutputs` container — on an exposure (`policy.additionalExposures[id = '…'].crossSegmentRatingOutputs`), a coverage, or the policy. These typically include:

* **`annualPremium`** — the premium as if that segment's state applied for the full year
* **`dailyProratedPremium`** — the daily premium rate for that segment's risk profile

Both values are **time-independent** — they describe the risk characteristics, not the segment's duration. Element-level rating provides visibility into which exposures contribute how much premium over which time periods. A `crossSegmentRatingOutputs` container is uniform across exactly the segments its host spans.

<Note>
  **`fullTermPricingInfo` is not necessarily derivable from per-segment rates.** Full-term pricing can include flat premium minimums, surplus lines taxes, policy fees, or other adjustments that are independent of element-level rating. `fullTermPolicyRatingResult` captures the aggregate policy-level rating detail.
</Note>

<Warning>
  Callers don't *have* to pass element-level rating. You could submit endorsements that only modify `fullTermPricingInfo` and leave per-segment data untouched. However, this reduces the system to **importing bordereau** — you'd know the billing changed, but not what policy details produced the change. Per-segment deltas capture the actual changing characteristics of the policy over time.
</Warning>

<Warning>
  **Time-dependent per-segment values prevent merging.** If a per-segment field's value depends on segment duration (e.g. a total prorated premium for the time slice), segments with identical risk but different lengths will never merge. Use time-independent values like `annualPremium` and `dailyProratedPremium` instead. See [Merging and Time-Dependent Fields](#merging-and-time-dependent-fields) for details.
</Warning>

When using the application UI (not headless) with a rater configured, an aggregation rater automatically computes billing and rating totals across segments. Through the API, this is your responsibility.

## Worked Example

A medical facility policy (Greenfield Medical Center) for Jan 1 – Dec 31 with one exposure. This shows the core segment behaviors — splitting, maintaining, and merging — with the actual payloads. Each endorsement also includes a `fullTermPricingInfo` update (omitted from the segment tables since it's the same in every segment).

<AccordionGroup>
  <Accordion title="1. NEW_BUSINESS — 1 segment">
    Create the policy with initial state spanning the full term. Grand total: 89,750.

    ```json theme={null}
    {
      "data": {
        "policyStatus": "active",
        "policyStartDate": { "year": 2025, "month": 1, "day": 1, "timezone": "America/New_York" },
        "policyEndDate": { "year": 2025, "month": 12, "day": 31, "timezone": "America/New_York" },
        "fullTermPricingInfo": {
          "pricingComponents": [
            { "label": "Policy Premium", "group": "Policy Invoice", "kind": "Premium", "value": 85000 },
            { "label": "Taxes", "group": "Policy Invoice", "kind": "Taxes", "value": 4250 },
            { "label": "Policy Fee", "group": "Policy Invoice", "kind": "Fees", "value": 500 }
          ]
        },
        "additionalExposures": [{
          "id": "exp-1",
          "exposureType": "MedicalFacility",
          "facilityName": "Greenfield Main Campus",
          "bedCount": 120,
          "coveredSpecialties": ["Cardiology", "Orthopedics", "General Surgery"],
          "namedPhysicians": ["Dr. Patel", "Dr. Nguyen", "Dr. Hoffman"]
        }]
      }
    }
    ```

    **Segments:**

    | # | Date Range     | Exposures                            |
    | - | -------------- | ------------------------------------ |
    | 1 | Jan 1 – Dec 31 | Main Campus (120 beds, 3 physicians) |
  </Accordion>

  <Accordion title="2. ENDORSE Apr 1: add satellite clinic — 2 segments">
    A satellite clinic opens. The `Add` action appends a new exposure to the collection. Grand total increases to 103,400 (+13,650) — the additional exposure adds risk for the remaining 9 months.

    ```json theme={null}
    {
      "effectiveDate": "2025-04-01",
      "deltas": [
        {
          "startDate": "2025-04-01", "endDate": "2025-12-31",
          "path": "policy.additionalExposures",
          "action": "Add",
          "value": {
            "id": "exp-2", "exposureType": "OutpatientClinic",
            "facilityName": "Greenfield West Clinic", "bedCount": 0,
            "coveredSpecialties": ["Dermatology", "Family Medicine"],
            "namedPhysicians": ["Dr. Kim"]
          }
        }
      ],
      "fullTermPricingInfo": {
        "pricingComponents": [
          { "label": "Policy Premium", "group": "Policy Invoice", "kind": "Premium", "value": 98000 },
          { "label": "Taxes", "group": "Policy Invoice", "kind": "Taxes", "value": 4900 },
          { "label": "Policy Fee", "group": "Policy Invoice", "kind": "Fees", "value": 500 }
        ]
      }
    }
    ```

    **Segments:**

    | # | Date Range     | Exposures                 |
    | - | -------------- | ------------------------- |
    | 1 | Jan 1 – Mar 31 | Main Campus               |
    | 2 | Apr 1 – Dec 31 | Main Campus + West Clinic |

    The original segment split at April — different exposure count on each side.
  </Accordion>

  <Accordion title="3. ENDORSE Jun 1: add physician + specialty — 3 segments">
    A new surgeon joins the main campus. Neurology added as a covered specialty. Grand total increases to 111,800 (+8,400) — the additional physician and expanded specialty coverage increase risk.

    ```json theme={null}
    {
      "effectiveDate": "2025-06-01",
      "deltas": [
        {
          "startDate": "2025-06-01", "endDate": "2025-12-31",
          "path": "policy.additionalExposures[id = 'exp-1'].namedPhysicians",
          "action": "Add", "value": "Dr. Okafor"
        },
        {
          "startDate": "2025-06-01", "endDate": "2025-12-31",
          "path": "policy.additionalExposures[id = 'exp-1'].coveredSpecialties",
          "action": "Add", "value": "Neurology"
        }
      ],
      "fullTermPricingInfo": {
        "pricingComponents": [
          { "label": "Policy Premium", "group": "Policy Invoice", "kind": "Premium", "value": 106000 },
          { "label": "Taxes", "group": "Policy Invoice", "kind": "Taxes", "value": 5300 },
          { "label": "Policy Fee", "group": "Policy Invoice", "kind": "Fees", "value": 500 }
        ]
      }
    }
    ```

    **Segments:**

    | # | Date Range     | Physicians                         | Specialties                                     |
    | - | -------------- | ---------------------------------- | ----------------------------------------------- |
    | 1 | Jan 1 – Mar 31 | Patel, Nguyen, Hoffman             | Cardiology, Orthopedics, Surgery                |
    | 2 | Apr 1 – May 31 | Patel, Nguyen, Hoffman             | Cardiology, Orthopedics, Surgery                |
    | 3 | Jun 1 – Dec 31 | Patel, Nguyen, Hoffman, **Okafor** | Cardiology, Orthopedics, Surgery, **Neurology** |

    The Apr–Dec segment from version 2 split at the June boundary.
  </Accordion>

  <Accordion title="4. Backdated corrections — merge to 2 segments">
    Internal audit reveals the physician change, bed reduction, and Neurology addition should all have been effective April 1, not June 1. A single endorsement corrects everything retroactively. Grand total decreases to 106,550 (-5,250) — the physician departure and bed reduction reduce risk, partially offset by Neurology covering a longer period.

    ```json theme={null}
    {
      "effectiveDate": "2025-04-01",
      "deltas": [
        {
          "startDate": "2025-04-01", "endDate": "2025-12-31",
          "path": "policy.additionalExposures[id = 'exp-1'].bedCount",
          "action": "Overwrite", "value": 110
        },
        {
          "startDate": "2025-04-01", "endDate": "2025-12-31",
          "path": "policy.additionalExposures[id = 'exp-1'].namedPhysicians",
          "action": "Remove", "value": "Dr. Nguyen"
        },
        {
          "startDate": "2025-04-01", "endDate": "2025-12-31",
          "path": "policy.additionalExposures[id = 'exp-1'].namedPhysicians",
          "action": "Add", "value": "Dr. Okafor"
        },
        {
          "startDate": "2025-04-01", "endDate": "2025-12-31",
          "path": "policy.additionalExposures[id = 'exp-1'].coveredSpecialties",
          "action": "Add", "value": "Neurology"
        }
      ],
      "fullTermPricingInfo": {
        "pricingComponents": [
          { "label": "Policy Premium", "group": "Policy Invoice", "kind": "Premium", "value": 101000 },
          { "label": "Taxes", "group": "Policy Invoice", "kind": "Taxes", "value": 5050 },
          { "label": "Policy Fee", "group": "Policy Invoice", "kind": "Fees", "value": 500 }
        ]
      }
    }
    ```

    All four per-segment deltas span Apr 1 – Dec 31. The Jun–Dec segment **already had** beds=110, Nguyen removed, Okafor present, and Neurology — those deltas are no-ops there. Only Apr–May changes. After applying, Apr–May and Jun–Dec have converged to identical state. The system merges them:

    **Segments:**

    | # | Date Range     | Beds | Physicians             | Specialties                                 |
    | - | -------------- | ---- | ---------------------- | ------------------------------------------- |
    | 1 | Jan 1 – Mar 31 | 120  | Patel, Nguyen, Hoffman | Cardiology, Orthopedics, Surgery            |
    | 2 | Apr 1 – Dec 31 | 110  | Patel, Hoffman, Okafor | Cardiology, Orthopedics, Surgery, Neurology |

    <Info>
      **Four transactions, two segments.** The Apr–May / Jun–Dec boundary vanished — not because a transaction was reversed, but because the correction *converged the per-segment state* on both sides. `fullTermPricingInfo` didn't affect the merge — it's the same in every segment.
    </Info>
  </Accordion>
</AccordionGroup>

For the complete 9-transaction walkthrough — including cancellation, reinstatement, transaction deletion, and both types of segment merging — see the [Lifecycle Walkthrough](/api-reference/policies/lifecycle-walkthrough).

## Cancellation and Reinstatement

Cancel and reinstate are simpler than endorsements but follow the same segment mechanics. The system automatically expands the cancellation/reinstatement date into per-segment status deltas — you only supply the date (and optional billing/rating).

* **Cancel** flips segment-scoped `policyStatus` to `"cancelled"` from the cancellation date through end of term, splitting the existing segment at the boundary. It records a single `cancellationEffectiveOnDate`, written uniformly across the whole term — the same value on both sides of the boundary. `policyStatus` alone tells you which side a segment is on. The date field is technically derivable from the active→cancelled boundary, but it is kept explicit because it makes list/query filtering by cancel date intuitive (you read the date directly instead of reconstructing it from segment boundaries).
* **Reinstate** flips `policyStatus` back to `"active"` from the reinstatement date and **clears** `cancellationEffectiveOnDate` across the term — it removes the cancellation marker rather than recording a parallel reinstatement marker. There is no reinstatement date field.
* **A reinstate may not leave a coverage gap.** A reinstate that would leave a cancelled window between two active periods (e.g. cancel Jun 15, reinstate Jul 1, leaving Jun 15–Jun 30 cancelled) is **not allowed** — the domain models that as a new policy, not a reinstatement, so it is rejected with a `400` pointing at new-business / renew. A valid reinstate restores continuous coverage and fully clears the cancellation.
* Both optionally accept whole-object **`fullTermPricingInfo`** and **`fullTermPolicyRatingResult`** (e.g., short-rate penalties or reinstatement fees) — never per-segment or element-level rating output.

<Note>
  A cancel followed by a reinstate is **invisible in the final segments** — `cancellationEffectiveOnDate` is removed and `policyStatus` returns to `"active"` everywhere, so the derived segments are identical to the pre-cancellation version and merge back together. Both transactions are preserved in the audit trail.
</Note>

## Renewal

`POST /transaction/renew` starts a fresh policy term as its own policy (its own `policyId`), linked to the term it renews. It takes the same whole-state `data` payload as new business, with two extra runtime checks (`400`, `InvalidRequest`):

* **The root `previousPolicy` field is required** and must be a valid `uuid`. Omitting it or sending a malformed value is rejected: `previousPolicy is required for RENEW (uuid)`. It is stamped onto every segment of the new term and written to the `Policy<N:1:previousPolicy>Policy` relationship — the only record of the renewal chain.
* **The new term must not run backward into the term it renews.** The new term's bounds are read from the root `policyStartDate` / `policyEndDate`, exactly as on new business. The new `policyStartDate` must be `>=` the previous policy's `policyEndDate`; otherwise: `policyStartDate (…) must be >= previous policy end date (…)`. The two terms may meet at a shared boundary date but may not overlap.

## Transaction Deletion

Only the **most recent** transaction on a policy can be deleted. Deleting a transaction:

* Rolls back to the prior version — the deleted transaction's segments are removed, and the previous version becomes current (no new version is created)
* Preserves the audit trail — the deleted transaction is archived, not erased
* Is irreversible through the API once deleted (the transaction can be re-created manually)

<Warning>
  Transaction deletion undoes the most recent change. It does not allow arbitrary transaction removal from the middle of the history.
</Warning>

## Merging and Time-Dependent Fields

Segment merging compares the *entire* per-segment state, including rating. If any per-segment field's value depends on the segment's duration, two segments with identical risk profiles but different durations will never merge.

### The Problem

Suppose you store a `totalProratedPremium` that represents the premium for each segment's time slice. After a backdated correction converges two segments' structural data, they still can't merge:

| # | Date Range                | Risk Profile | `totalProratedPremium` | Merge?            |
| - | ------------------------- | ------------ | ---------------------- | ----------------- |
| A | Apr 1 – May 31 (61 days)  | identical    | 18,700                 | ✗ — values differ |
| B | Jun 1 – Dec 31 (214 days) | identical    | 65,500                 |                   |

This is a chicken-and-egg problem: you can't compute the merged segment's prorated premium without knowing the merge will happen, but the merge can't happen while the values differ.

### The Solution Today

Use **time-independent** per-segment values. `annualPremium` and `dailyProratedPremium` describe the risk profile, not the duration. Two segments with the same risk produce the same values regardless of how many days each covers, so merging works naturally.

If you need to know the total premium for a specific segment's time span, derive it from the daily rate and the segment's date range after reading the policy — don't store it as a per-segment field.

### Looking Ahead

We are working on support for **calculated fields** — per-segment fields whose values are automatically derived after segment computation. This will allow fields like `totalProratedPremium` to be stored on segments without blocking merges, because the system will exclude them from the merge comparison and recompute them based on each segment's final date range.
