Skip to main content
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.
1

Configure

Define your fields, option sets, and exposure types via the Configuration API.
2

Create

POST /transaction/new-business creates a policy with initial state covering the full term.
3

Endorse

POST /{policyId}/transaction/endorse modifies the policy — add exposures, change field values, adjust coverage.
4

Cancel

POST /{policyId}/transaction/cancel cancels the policy from a specified date.
5

Reinstate

POST /{policyId}/transaction/reinstate reinstates a cancelled policy.
6

Renew

POST /transaction/renew starts a new policy term linked to the previous one.

Transaction Types

NEW_BUSINESS

Creates the policy. The effective date is the policy start date. Produces one segment covering the full term.

ENDORSE

Modifies policy state from a given effective date. Carries one or more of five channels (see Endorsement Channels). May split existing segments or merge them if the change aligns state across periods.

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 fullTermPolicyBillingInfo / fullTermPolicyRatingResult (e.g., short-rate penalties).

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 fullTermPolicyBillingInfo / fullTermPolicyRatingResult (e.g., reinstatement fees).

RENEW

Creates a new policy term linked to the previous via fullTermPolicyInfo.previousPolicyId (a required uuid). Accepts a full fieldModelV1Data 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).

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.

Endorsement Channels

An endorsement carries one or more of five channels. Full-term-ness is membership in a reserved-name container — fullTermPolicyInfo, fullTermPolicyBillingInfo, fullTermPolicyRatingResult, and crossSegmentRatingOutputs. Input channels — deltas XOR fullTermDeltas (exactly one; error if both):
  • deltas — per-segment changes. Each carries its own startDate and endDate within the policy term. The path must not contain any 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 startDates (split that into separate transactions). This binding, and why there is no separate “take-effect” axis, is covered on Effective Dates & the Policy Timeline.
  • fullTermDeltas — endorsements to policy.fullTermPolicyInfo only (term bounds, renewal pointer, primary insured). No dates — the change applies across the whole term because full-term fields are invariant. When present, effectiveDate must equal the policy start date.
A per-segment endorsement and a policy-info endorsement are different transactions, so sending both arrays is a validation error. Derived channels — additive on either input channel:
  • fullTermPolicyBillingInfo — a whole object that overwrites the policy-root billing summary.
  • 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):
FieldTypeDescription
pathstringPredicate path to a field outside any full-term container
actionstringOverwrite, Add, or Remove
valueanyThe new value, value to add, or value to remove
startDatestringStart of the date range this change applies to (must equal the transaction effectiveDate)
endDatestringEnd of the date range this change applies to (must be >= startDate and within the policy term — see Delta Date Ranges)
Full-term-info delta (fullTermDeltas):
FieldTypeDescription
pathstringPath under policy.fullTermPolicyInfo
actionstringOverwrite, Add, or Remove
valueanyThe new value, value to add, or value to remove
Dates are implicit on fullTermDeltas — the delta always spans the whole term, because fullTermPolicyInfo is identical across every segment of a version. Extending or shortening the term is just a fullTermDeltas write to policy.fullTermPolicyInfo.policyEndDate.

Delta Actions

Overwrite

Replace a scalar value or an entire object. This is the most common action.
{
  "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.
{
  "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.
{
  "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 fieldkey[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.
PathTargets
policy.deductibleA scalar field on the policy
policy.additionalExposures[id = 'exp-1'].bedCountA field on a specific exposure (matched by id)
policy.coverages[coverageType = 'GL'].limits[name = 'occurrence']A nested element matched by any field
policy.additionalExposuresThe exposures collection itself (for Add/Remove of entire exposure objects)
policy.policyStatusThe segment-scoped policy status (set by Cancel/Reinstate)
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.
Reserved full-term containers (fullTermPolicyInfo, fullTermPolicyBillingInfo, fullTermPolicyRatingResult, crossSegmentRatingOutputs) must not appear in a deltas path — they have their own channels.

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 billing (the fullTermPolicyBillingInfo channel):
{
  "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"]
      }
    }
  ],
  "fullTermPolicyBillingInfo": {
    "policyPremium": 98000,
    "policyTaxes": 4900,
    "policyFees": 500,
    "policyGrandTotal": 103400
  }
}
The exposure delta applies from April 1 through the end of the term and will split the existing segment at that boundary. fullTermPolicyBillingInfo 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.
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.

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

A new policy starts with one segment covering the full term.
#Date RangeExposures
1Jan 1 – Dec 31Main Hospital (120 beds)
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.
PropertyDescription
policyVersionSequential integer (1, 2, 3, …)
transactionIdThe transaction that produced this version
segmentsComplete set of segments for this version
startDate / endDatePolicy 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. 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.
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.

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.
(fullTermDeltas carry no dates — they always span the whole term — so these range checks do not apply to them.)

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 fullTermDeltas):
  • 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.
This is the within-transaction counterpart to the collection-vs-element distinction: 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.

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 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

fullTermPolicyBillingInfo is a cross-segment invariant — identical across every segment in a version. It contains the billing summary for the entire policy term: policyPremium, policyTaxes, policyFees, policyGrandTotal. Every endorsement should include a fullTermPolicyBillingInfo channel to keep billing 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.
fullTermPolicyBillingInfo is not necessarily derivable from per-segment rates. Full-term billing 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.
Callers don’t have to pass element-level rating. You could submit endorsements that only modify fullTermPolicyBillingInfo 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.
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 for details.
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 fullTermPolicyBillingInfo update (omitted from the segment tables since it’s the same in every segment).
Create the policy with initial state spanning the full term. Grand total: 89,750.
{
  "fieldModelV1Data": {
    "policy": {
      "policyStatus": "active",
      "fullTermPolicyInfo": {
        "policyStartDate": { "year": 2025, "month": 1, "day": 1, "timezone": "America/New_York" },
        "policyEndDate": { "year": 2025, "month": 12, "day": 31, "timezone": "America/New_York" },
        "primaryInsuredJoin": "exp-1",
        "primaryInsuredName": "Greenfield Main Campus"
      },
      "fullTermPolicyBillingInfo": {
        "policyPremium": 85000, "policyTaxes": 4250,
        "policyFees": 500, "policyGrandTotal": 89750
      },
      "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 RangeExposures
1Jan 1 – Dec 31Main Campus (120 beds, 3 physicians)
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.
{
  "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"]
      }
    }
  ],
  "fullTermPolicyBillingInfo": {
    "policyPremium": 98000, "policyTaxes": 4900,
    "policyFees": 500, "policyGrandTotal": 103400
  }
}
Segments:
#Date RangeExposures
1Jan 1 – Mar 31Main Campus
2Apr 1 – Dec 31Main Campus + West Clinic
The original segment split at April — different exposure count on each side.
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.
{
  "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"
    }
  ],
  "fullTermPolicyBillingInfo": {
    "policyPremium": 106000, "policyTaxes": 5300,
    "policyFees": 500, "policyGrandTotal": 111800
  }
}
Segments:
#Date RangePhysiciansSpecialties
1Jan 1 – Mar 31Patel, Nguyen, HoffmanCardiology, Orthopedics, Surgery
2Apr 1 – May 31Patel, Nguyen, HoffmanCardiology, Orthopedics, Surgery
3Jun 1 – Dec 31Patel, Nguyen, Hoffman, OkaforCardiology, Orthopedics, Surgery, Neurology
The Apr–Dec segment from version 2 split at the June boundary.
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.
{
  "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"
    }
  ],
  "fullTermPolicyBillingInfo": {
    "policyPremium": 101000, "policyTaxes": 5050,
    "policyFees": 500, "policyGrandTotal": 106550
  }
}
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 RangeBedsPhysiciansSpecialties
1Jan 1 – Mar 31120Patel, Nguyen, HoffmanCardiology, Orthopedics, Surgery
2Apr 1 – Dec 31110Patel, Hoffman, OkaforCardiology, Orthopedics, Surgery, Neurology
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. fullTermPolicyBillingInfo didn’t affect the merge — it’s the same in every segment.
For the complete 9-transaction walkthrough — including cancellation, reinstatement, transaction deletion, and both types of segment merging — see the 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 fullTermPolicyBillingInfo and fullTermPolicyRatingResult (e.g., short-rate penalties or reinstatement fees) — never per-segment or element-level rating output.
A cancel followed by a reinstate is invisible in the final segmentscancellationEffectiveOnDate 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.

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 fieldModelV1Data payload as new business, with two extra runtime checks (400, InvalidRequest):
  • fullTermPolicyInfo.previousPolicyId is required and must be a valid uuid. Omitting it or sending a malformed value is rejected: fullTermPolicyInfo.previousPolicyId is required for RENEW (uuid).
  • The new term must not run backward into the term it renews. The new policyStartDate must be >= the previous policy’s policyEndDate; otherwise: fullTermPolicyInfo.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)
Transaction deletion undoes the most recent change. It does not allow arbitrary transaction removal from the middle of the history.

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 RangeRisk ProfiletotalProratedPremiumMerge?
AApr 1 – May 31 (61 days)identical18,700✗ — values differ
BJun 1 – Dec 31 (214 days)identical65,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.